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}
 95
 96func (repo *GitRootRepository) Commit(hash plumbing.Hash) (*object.Commit, error) {
 97	return repo.repo.CommitObject(hash)
 98}
 99
100func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) {
101	if !repo.configLoaded {
102		hash, err := repo.DefaultBranchCurrentHash()
103		if err != nil {
104			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't get default hash")
105		}
106		content, err := repo.ContentAtHash(repo.manager.conf.PathFileRepoConfigurationName(), hash)
107		if err != nil {
108			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file")
109		}
110		conf, err := readRepoConfiguration(content)
111		if err != nil {
112			return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read repo conf")
113		}
114		repo.configLoaded = true
115		repo.config = conf
116	}
117	return repo.config, nil
118}
119
120func (repo *GitRootRepository) DefaultBranch() (plumbing.ReferenceName, error) {
121	unlock := repo.manager.repoLocks.Read(repo.name)
122	defer unlock()
123	conf, err := repo.repo.Config()
124	if err != nil {
125		return plumbing.NewBranchReferenceName(""), repo.errorHandler.Wrapf(err, "can't read conf git")
126	}
127	return plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), nil
128}
129
130func (repo *GitRootRepository) DefaultBranchCurrentHash() (plumbing.Hash, error) {
131	unlock := repo.manager.repoLocks.Read(repo.name)
132	defer unlock()
133	conf, err := repo.repo.Config()
134	if err != nil {
135		return plumbing.ZeroHash, repo.errorHandler.Wrapf(err, "can't read conf git")
136	}
137	ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(conf.Init.DefaultBranch), true)
138	if err != nil {
139		return plumbing.ZeroHash, repo.errorHandler.Wrapf(err, "can't read conf git")
140	}
141	return ref.Hash(), nil
142}
143
144func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) {
145	hash, err := repo.DefaultBranchCurrentHash()
146	if err != nil {
147		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
148	}
149	return repo.ContentAtHash(repo.manager.conf.PathFileRepositories(), hash)
150}
151
152func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) {
153	hash, err := repo.DefaultBranchCurrentHash()
154	if err != nil {
155		return nil, oops.With("repo", repo.Name()).Wrapf(err, "can't get conf")
156	}
157	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
158}
159
160func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) {
161	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
162}
163
164func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) {
165	return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name)
166}
167
168func (repo *GitRootRepository) ContentUserAtDefaultBranch() ([]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.PathFileUsers(), hash)
174}
175
176func (repo *GitRootRepository) PathDataWeb(path string) string {
177	return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
178}
179
180func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
181	hash, err := repo.DefaultBranchCurrentHash()
182	if err != nil {
183		return nil, oops.Wrapf(err, "Head")
184	}
185	repo.logger.Debug("head", logger.NewLoggerPair("hash", hash.String()))
186	return repo.ContentAtHash(filepath, hash)
187}
188
189func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
190	unlock := repo.manager.repoLocks.Read(repo.name)
191	ref, err := repo.repo.Reference(name, false)
192	if err != nil {
193		unlock()
194		return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
195	}
196	unlock()
197	return repo.ContentAtHash(filepath, ref.Hash())
198}
199
200func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
201	unlock := repo.manager.repoLocks.Read(repo.name)
202	defer unlock()
203	c, err := repo.repo.CommitObject(hash)
204	if err != nil {
205		return nil, err
206	}
207	tree, err := c.Tree()
208	if err != nil {
209		return nil, err
210	}
211	file, err := tree.File(filepath)
212	if err != nil {
213		return nil, err
214	}
215	fileContent, err := file.Contents()
216	if err != nil {
217		return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
218	}
219	return []byte(fileContent), nil
220}
221
222func (repo *GitRootRepository) Exists(filepath string) bool {
223	hash, err := repo.DefaultBranchCurrentHash()
224	if err != nil {
225		return false
226	}
227	return repo.ExistsAtHash(filepath, hash)
228}
229
230func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
231	unlock := repo.manager.repoLocks.Read(repo.name)
232	defer unlock()
233	c, err := repo.repo.CommitObject(hash)
234	if err != nil {
235		return false
236	}
237	tree, err := c.Tree()
238	if err != nil {
239		return false
240	}
241	f, _ := tree.FindEntry(filepath)
242	return f != nil
243}
244
245func (repo *GitRootRepository) Worktree(path string, branch string) error {
246	hash, err := repo.DefaultBranchCurrentHash()
247	if err != nil {
248		return repo.errorHandler.Wrapf(err, "can't get repoConf for worktree on osfs")
249	}
250	unlock := repo.manager.repoLocks.Read(repo.name)
251	defer unlock()
252	ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(branch), false)
253	if err != nil {
254		return oops.With("ref", branch).Wrapf(err, "Worktree")
255	}
256	commit, err := repo.repo.CommitObject(ref.Hash())
257	if err != nil {
258		return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get CommitObject for worktree on osfs")
259	}
260	tree, err := repo.repo.TreeObject(commit.TreeHash)
261	if err != nil {
262		return repo.errorHandler.With("hash", hash).Wrapf(err, "can't get TreeObject for worktree on osfs")
263	}
264	files := tree.Files()
265	files.ForEach(func(file *object.File) error {
266		content, err := file.Contents()
267		if err != nil {
268			repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name))
269			return nil
270		}
271		dir, _ := filepath.Split(file.Name)
272		err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm)
273		if err != nil {
274			repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir))
275		}
276		if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil {
277			repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name))
278		}
279		return nil
280	})
281	return nil
282}
283
284func (repo *GitRootRepository) Close() error {
285	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
286	if repo.hasBecomeWrite != nil {
287		repo.hasBecomeWrite.close()
288		repo.hasBecomeWrite = nil
289	}
290	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
291		return repo.manager.cacheRepos.Delete(repo.name, func() error {
292			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
293			defer repoLock.Close()
294			unwrite := repoLock.Write()
295			defer unwrite()
296
297			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
298			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
299			// } else {
300			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
301			// }
302
303			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
304			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
305			// } else {
306			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
307			// }
308
309			if sto, ok := repo.storer.(*filesystem.Storage); ok {
310				sto.Close()
311			}
312			repo.storerCache.Clear()
313			return nil
314		})
315	}
316
317	return nil
318}