GitRoot

craft your forge, build your project, grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package repository
  6
  7import (
  8	"os"
  9	"path/filepath"
 10
 11	"github.com/go-git/go-billy/v5/osfs"
 12	git "github.com/go-git/go-git/v5"
 13	"github.com/go-git/go-git/v5/plumbing"
 14	"github.com/go-git/go-git/v5/plumbing/cache"
 15	"github.com/go-git/go-git/v5/plumbing/object"
 16	"github.com/go-git/go-git/v5/storage"
 17	"github.com/go-git/go-git/v5/storage/filesystem"
 18	"github.com/samber/oops"
 19	"gitroot.dev/server/logger"
 20)
 21
 22type GitRootRepository struct {
 23	name           string
 24	logger         *logger.Logger
 25	errorHandler   oops.OopsErrorBuilder
 26	repo           *git.Repository
 27	storer         storage.Storer
 28	storerCache    cache.Object
 29	manager        *Manager
 30	hasBecomeWrite *GitRootRepositoryWrite
 31}
 32
 33func (m *Manager) NewGitRootRepository(name string, withCache *GitRootRepository) (*GitRootRepository, error) {
 34	errorHandler := oops.Code("NewGitRootRepository").With("name", name)
 35
 36	unlock := m.repoLocks.Read(name)
 37	defer unlock()
 38
 39	if withCache == nil {
 40		storerCache := cache.NewObjectLRUDefault()
 41		bfs := filesystem.NewStorage(osfs.New(m.conf.GetDirPathForRepo(name), osfs.WithBoundOS()), storerCache)
 42		repo, err := git.Open(bfs, nil)
 43		if err != nil {
 44			return nil, errorHandler.Wrapf(err, "can't open")
 45		}
 46		return &GitRootRepository{
 47			name:           name,
 48			logger:         m.logger.NewSubLogger(name),
 49			errorHandler:   errorHandler,
 50			repo:           repo,
 51			storer:         bfs,
 52			storerCache:    storerCache,
 53			manager:        m,
 54			hasBecomeWrite: nil,
 55		}, nil
 56	} else {
 57		return &GitRootRepository{
 58			name:           name,
 59			logger:         m.logger.NewSubLogger(name),
 60			errorHandler:   errorHandler,
 61			repo:           withCache.repo,
 62			storer:         withCache.storer,
 63			storerCache:    withCache.storerCache,
 64			manager:        m,
 65			hasBecomeWrite: nil,
 66		}, nil
 67	}
 68}
 69
 70func (repo *GitRootRepository) Name() string {
 71	return repo.name
 72}
 73
 74func (repo *GitRootRepository) WillWrite(branch plumbing.ReferenceName) (*GitRootRepositoryWrite, error) {
 75	lock := repo.manager.repoLocks.WillWrite(repo.name)
 76	if repo.hasBecomeWrite != nil {
 77		return nil, repo.errorHandler.Errorf("WillWrite called multiple times before close")
 78	}
 79	hasBecomeWrite, err := NewGitRootRepositoryWrite(repo, branch, lock)
 80	repo.hasBecomeWrite = hasBecomeWrite
 81	return repo.hasBecomeWrite, err
 82}
 83
 84func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) {
 85	content, err := repo.Content(repo.manager.conf.PathFileRepoConfigurationName())
 86	if err != nil {
 87		return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file")
 88	}
 89	return readRepoConfiguration(content)
 90}
 91
 92func (repo *GitRootRepository) Status() (*plumbing.Reference, error) {
 93	unlock := repo.manager.repoLocks.Read(repo.name)
 94	defer unlock()
 95	return repo.repo.Head()
 96}
 97
 98func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) {
 99	return repo.Content(repo.manager.conf.PathFileRepositories())
100}
101
102func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) {
103	return repo.Content(repo.manager.conf.PathFilePlugins())
104}
105
106func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) {
107	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
108}
109
110func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) {
111	return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name)
112}
113
114func (repo *GitRootRepository) ContentUserAtDefaultBranch() ([]byte, error) {
115	conf, err := repo.Configuration()
116	if err != nil {
117		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
118	}
119	return repo.ContentAtRef(repo.manager.conf.PathFileUsers(), conf.DefaultBranch)
120}
121
122func (repo *GitRootRepository) PathDataWeb(path string) string {
123	return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
124}
125
126func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
127	head, err := repo.Status()
128	if err != nil {
129		return nil, oops.Wrapf(err, "Head")
130	}
131	repo.logger.Debug("head", logger.NewLoggerPair("hash", head.Hash().String()))
132	return repo.ContentAtHash(filepath, head.Hash())
133}
134
135func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
136	unlock := repo.manager.repoLocks.Read(repo.name)
137	ref, err := repo.repo.Reference(name, false)
138	if err != nil {
139		unlock()
140		return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
141	}
142	unlock()
143	return repo.ContentAtHash(filepath, ref.Hash())
144}
145
146func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
147	unlock := repo.manager.repoLocks.Read(repo.name)
148	defer unlock()
149	c, err := repo.repo.CommitObject(hash)
150	if err != nil {
151		return nil, err
152	}
153	tree, err := c.Tree()
154	if err != nil {
155		return nil, err
156	}
157	file, err := tree.File(filepath)
158	if err != nil {
159		return nil, err
160	}
161	fileContent, err := file.Contents()
162	if err != nil {
163		return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
164	}
165	return []byte(fileContent), nil
166}
167
168func (repo *GitRootRepository) Exists(filepath string) bool {
169	head, err := repo.Status()
170	if err != nil {
171		return false
172	}
173	return repo.ExistsAtHash(filepath, head.Hash())
174}
175
176func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
177	unlock := repo.manager.repoLocks.Read(repo.name)
178	defer unlock()
179	c, err := repo.repo.CommitObject(hash)
180	if err != nil {
181		return false
182	}
183	tree, err := c.Tree()
184	if err != nil {
185		return false
186	}
187	f, _ := tree.FindEntry(filepath)
188	return f != nil
189}
190
191func (repo *GitRootRepository) CurrentBranch() (plumbing.ReferenceName, error) {
192	unlock := repo.manager.repoLocks.Read(repo.name)
193	defer unlock()
194	head, err := repo.repo.Head()
195	if err != nil {
196		return "", oops.Wrapf(err, "Head")
197	}
198	return head.Name(), nil
199}
200
201func (repo *GitRootRepository) Worktree(path string) error {
202	repoConf, err := repo.Status()
203	if err != nil {
204		return repo.errorHandler.Wrapf(err, "can't get repoConf for worktree on osfs")
205	}
206	unlock := repo.manager.repoLocks.Read(repo.name)
207	defer unlock()
208	commit, err := repo.repo.CommitObject(repoConf.Hash())
209	if err != nil {
210		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get CommitObject for worktree on osfs")
211	}
212	tree, err := repo.repo.TreeObject(commit.TreeHash)
213	if err != nil {
214		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get TreeObject for worktree on osfs")
215	}
216	files := tree.Files()
217	files.ForEach(func(file *object.File) error {
218		content, err := file.Contents()
219		if err != nil {
220			repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name))
221			return nil
222		}
223		dir, _ := filepath.Split(file.Name)
224		err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm)
225		if err != nil {
226			repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir))
227		}
228		if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil {
229			repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name))
230		}
231		return nil
232	})
233	return nil
234}
235
236func (repo *GitRootRepository) Close() error {
237	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
238	if repo.hasBecomeWrite != nil {
239		repo.hasBecomeWrite.close()
240		repo.hasBecomeWrite = nil
241	}
242	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
243		return repo.manager.cacheRepos.Delete(repo.name, func() error {
244			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
245			defer repoLock.Close()
246			unwrite := repoLock.Write()
247			defer unwrite()
248
249			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
250			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
251			// } else {
252			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
253			// }
254
255			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
256			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
257			// } else {
258			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
259			// }
260
261			if sto, ok := repo.storer.(*filesystem.Storage); ok {
262				sto.Close()
263			}
264			repo.storerCache.Clear()
265			return nil
266		})
267	}
268
269	return nil
270}