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, branch 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	ref, err := repo.repo.Reference(plumbing.NewBranchReferenceName(branch), false)
209	if err != nil {
210		return oops.With("ref", branch).Wrapf(err, "Worktree")
211	}
212	commit, err := repo.repo.CommitObject(ref.Hash())
213	if err != nil {
214		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get CommitObject for worktree on osfs")
215	}
216	tree, err := repo.repo.TreeObject(commit.TreeHash)
217	if err != nil {
218		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get TreeObject for worktree on osfs")
219	}
220	files := tree.Files()
221	files.ForEach(func(file *object.File) error {
222		content, err := file.Contents()
223		if err != nil {
224			repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name))
225			return nil
226		}
227		dir, _ := filepath.Split(file.Name)
228		err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm)
229		if err != nil {
230			repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir))
231		}
232		if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil {
233			repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name))
234		}
235		return nil
236	})
237	return nil
238}
239
240func (repo *GitRootRepository) Close() error {
241	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
242	if repo.hasBecomeWrite != nil {
243		repo.hasBecomeWrite.close()
244		repo.hasBecomeWrite = nil
245	}
246	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
247		return repo.manager.cacheRepos.Delete(repo.name, func() error {
248			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
249			defer repoLock.Close()
250			unwrite := repoLock.Write()
251			defer unwrite()
252
253			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
254			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
255			// } else {
256			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
257			// }
258
259			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
260			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
261			// } else {
262			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
263			// }
264
265			if sto, ok := repo.storer.(*filesystem.Storage); ok {
266				sto.Close()
267			}
268			repo.storerCache.Clear()
269			return nil
270		})
271	}
272
273	return nil
274}