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/v6/osfs"
 12	git "github.com/go-git/go-git/v6"
 13	"github.com/go-git/go-git/v6/plumbing"
 14	"github.com/go-git/go-git/v6/plumbing/cache"
 15	"github.com/go-git/go-git/v6/plumbing/object"
 16	"github.com/go-git/go-git/v6/storage"
 17	"github.com/go-git/go-git/v6/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	configLoaded   bool
 32	config         RepoConfiguration
 33}
 34
 35func (m *Manager) NewGitRootRepository(name string, withCache *GitRootRepository) (*GitRootRepository, error) {
 36	errorHandler := oops.Code("NewGitRootRepository").With("name", name)
 37
 38	unlock := m.repoLocks.Read(name)
 39	defer unlock()
 40
 41	if withCache == nil {
 42		storerCache := cache.NewObjectLRUDefault()
 43		bfs := filesystem.NewStorageWithOptions(osfs.New(m.conf.GetDirPathForRepo(name), osfs.WithBoundOS()), storerCache, filesystem.Options{
 44			HighMemoryMode: true,
 45			UseInMemoryIdx: true,
 46		})
 47		repo, err := git.Open(bfs, nil)
 48		if err != nil {
 49			return nil, errorHandler.Wrapf(err, "can't open")
 50		}
 51		return &GitRootRepository{
 52			name:           name,
 53			logger:         m.logger.NewSubLogger(name),
 54			errorHandler:   errorHandler,
 55			repo:           repo,
 56			storer:         bfs,
 57			storerCache:    storerCache,
 58			manager:        m,
 59			hasBecomeWrite: nil,
 60		}, nil
 61	} else {
 62		return &GitRootRepository{
 63			name:           name,
 64			logger:         m.logger.NewSubLogger(name),
 65			errorHandler:   errorHandler,
 66			repo:           withCache.repo,
 67			storer:         withCache.storer,
 68			storerCache:    withCache.storerCache,
 69			manager:        m,
 70			hasBecomeWrite: nil,
 71		}, nil
 72	}
 73}
 74
 75func (repo *GitRootRepository) Name() string {
 76	return repo.name
 77}
 78
 79func (repo *GitRootRepository) WillWrite(branch plumbing.ReferenceName) (*GitRootRepositoryWrite, error) {
 80	lock := repo.manager.repoLocks.WillWrite(repo.name)
 81	if repo.hasBecomeWrite != nil {
 82		return nil, repo.errorHandler.Errorf("WillWrite called multiple times before close")
 83	}
 84	hasBecomeWrite, err := NewGitRootRepositoryWrite(repo, branch, lock)
 85	repo.hasBecomeWrite = hasBecomeWrite
 86	return repo.hasBecomeWrite, err
 87}
 88
 89func (repo *GitRootRepository) StopWrite() {
 90	if repo.hasBecomeWrite != nil {
 91		repo.hasBecomeWrite.close()
 92	}
 93	repo.hasBecomeWrite = nil
 94	repo.storerCache.Clear()
 95}
 96
 97func (repo *GitRootRepository) Commit(hash plumbing.Hash) (*object.Commit, error) {
 98	return repo.repo.CommitObject(hash)
 99}
100
101func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) {
102	if !repo.configLoaded {
103		hash, err := repo.DefaultBranchCurrentHash()
104		if err != nil {
105			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't get default hash")
106		}
107		content, err := repo.ContentAtHash(repo.manager.conf.PathFileRepoConfigurationName(), hash)
108		if err != nil {
109			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file")
110		}
111		conf, err := readRepoConfiguration(content)
112		if err != nil {
113			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read repo conf")
114		}
115		repo.configLoaded = true
116		repo.config = conf
117	}
118	return repo.config, nil
119}
120
121func (repo *GitRootRepository) Branches() ([]plumbing.ReferenceName, error) {
122	unlock := repo.manager.repoLocks.Read(repo.name)
123	defer unlock()
124	branches, err := repo.repo.Branches()
125	if err != nil {
126		return nil, repo.errorHandler.Wrapf(err, "can't load branches")
127	}
128	b := []plumbing.ReferenceName{}
129	branches.ForEach(func(r *plumbing.Reference) error {
130		b = append(b, r.Name())
131		return nil
132	})
133	return b, nil
134}
135
136func (repo *GitRootRepository) DefaultBranch() (plumbing.ReferenceName, error) {
137	unlock := repo.manager.repoLocks.Read(repo.name)
138	defer unlock()
139	conf, err := repo.repo.Config()
140	if err != nil {
141		return plumbing.NewBranchReferenceName(""), repo.errorHandler.Wrapf(err, "can't read conf git")
142	}
143	return plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), nil
144}
145
146func (repo *GitRootRepository) DefaultBranchCurrentHash() (plumbing.Hash, error) {
147	unlock := repo.manager.repoLocks.Read(repo.name)
148	defer unlock()
149	conf, err := repo.repo.Config()
150	if err != nil {
151		return plumbing.ZeroHash, repo.errorHandler.With("conf", conf).Wrapf(err, "can't read conf git")
152	}
153	ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), true)
154	if err != nil {
155		return plumbing.ZeroHash, repo.errorHandler.With("DefaultBranch", conf.Init.DefaultBranch).Wrapf(err, "can't read conf git")
156	}
157	return ref.Hash(), nil
158}
159
160func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) {
161	hash, err := repo.DefaultBranchCurrentHash()
162	if err != nil {
163		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
164	}
165	return repo.ContentAtHash(repo.manager.conf.PathFileRepositories(), hash)
166}
167
168func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) {
169	hash, err := repo.DefaultBranchCurrentHash()
170	if err != nil {
171		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
172	}
173	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
174}
175
176func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) {
177	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
178}
179
180func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) {
181	return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name)
182}
183
184func (repo *GitRootRepository) ContentUserAtDefaultBranch() ([]byte, error) {
185	hash, err := repo.DefaultBranchCurrentHash()
186	if err != nil {
187		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
188	}
189	return repo.ContentAtHash(repo.manager.conf.PathFileUsers(), hash)
190}
191
192func (repo *GitRootRepository) ContentUserAtRef(name plumbing.ReferenceName) ([]byte, error) {
193	return repo.ContentAtRef(repo.manager.conf.PathFileUsers(), name)
194}
195
196func (repo *GitRootRepository) PathDataWeb(path string) string {
197	return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
198}
199
200func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
201	hash, err := repo.DefaultBranchCurrentHash()
202	if err != nil {
203		return nil, oops.Wrapf(err, "Head")
204	}
205	repo.logger.Debug("head", logger.NewLoggerPair("hash", hash.String()))
206	return repo.ContentAtHash(filepath, hash)
207}
208
209func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
210	unlock := repo.manager.repoLocks.Read(repo.name)
211	ref, err := repo.repo.Reference(name, false)
212	if err != nil {
213		unlock()
214		return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
215	}
216	unlock()
217	return repo.ContentAtHash(filepath, ref.Hash())
218}
219
220func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
221	unlock := repo.manager.repoLocks.Read(repo.name)
222	defer unlock()
223	c, err := repo.repo.CommitObject(hash)
224	if err != nil {
225		return nil, err
226	}
227	tree, err := c.Tree()
228	if err != nil {
229		return nil, err
230	}
231	file, err := tree.File(filepath)
232	if err != nil {
233		return nil, err
234	}
235	fileContent, err := file.Contents()
236	if err != nil {
237		return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
238	}
239	return []byte(fileContent), nil
240}
241
242func (repo *GitRootRepository) Exists(filepath string) bool {
243	hash, err := repo.DefaultBranchCurrentHash()
244	if err != nil {
245		return false
246	}
247	return repo.ExistsAtHash(filepath, hash)
248}
249
250func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
251	unlock := repo.manager.repoLocks.Read(repo.name)
252	defer unlock()
253	c, err := repo.repo.CommitObject(hash)
254	if err != nil {
255		return false
256	}
257	tree, err := c.Tree()
258	if err != nil {
259		return false
260	}
261	f, _ := tree.FindEntry(filepath)
262	return f != nil
263}
264
265func (repo *GitRootRepository) Worktree(path string, branch string) error {
266	hash, err := repo.DefaultBranchCurrentHash()
267	if err != nil {
268		return repo.errorHandler.Wrapf(err, "can't get repoConf for worktree on osfs")
269	}
270	unlock := repo.manager.repoLocks.Read(repo.name)
271	defer unlock()
272	ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(branch), false)
273	if err != nil {
274		return oops.With("ref", branch).Wrapf(err, "Worktree")
275	}
276	commit, err := repo.repo.CommitObject(ref.Hash())
277	if err != nil {
278		return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get CommitObject for worktree on osfs")
279	}
280	tree, err := repo.repo.TreeObject(commit.TreeHash)
281	if err != nil {
282		return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get TreeObject for worktree on osfs")
283	}
284	files := tree.Files()
285	files.ForEach(func(file *object.File) error {
286		content, err := file.Contents()
287		if err != nil {
288			repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name))
289			return nil
290		}
291		dir, _ := filepath.Split(file.Name)
292		err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm)
293		if err != nil {
294			repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir))
295		}
296		if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil {
297			repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name))
298		}
299		return nil
300	})
301	return nil
302}
303
304func (repo *GitRootRepository) Close() error {
305	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
306	if repo.hasBecomeWrite != nil {
307		repo.hasBecomeWrite.close()
308		repo.hasBecomeWrite = nil
309	}
310	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
311		repo.logger.Debug("project closed", logger.NewLoggerPair("name", repo.name))
312		return repo.manager.cacheRepos.Delete(repo.name, func() error {
313			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
314			defer repoLock.Close()
315			unwrite := repoLock.Write()
316			defer unwrite()
317
318			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
319			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
320			// } else {
321			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
322			// }
323
324			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
325			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
326			// } else {
327			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
328			// }
329
330			if sto, ok := repo.storer.(*filesystem.Storage); ok {
331				sto.Close()
332			}
333			repo.storerCache.Clear()
334			return nil
335		})
336	} else {
337		repo.logger.Debug("project not closed", logger.NewLoggerPair("name", repo.name), logger.NewLoggerPair("nb", nb))
338	}
339
340	return nil
341}