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) PathDataWeb(path string) string {
115	return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
116}
117
118func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
119	head, err := repo.Status()
120	if err != nil {
121		return nil, oops.Wrapf(err, "Head")
122	}
123	repo.logger.Debug("head", logger.NewLoggerPair("hash", head.Hash().String()))
124	return repo.ContentAtHash(filepath, head.Hash())
125}
126
127func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
128	unlock := repo.manager.repoLocks.Read(repo.name)
129	ref, err := repo.repo.Reference(name, false)
130	if err != nil {
131		unlock()
132		return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
133	}
134	unlock()
135	return repo.ContentAtHash(filepath, ref.Hash())
136}
137
138func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
139	unlock := repo.manager.repoLocks.Read(repo.name)
140	defer unlock()
141	c, err := repo.repo.CommitObject(hash)
142	if err != nil {
143		return nil, err
144	}
145	tree, err := c.Tree()
146	if err != nil {
147		return nil, err
148	}
149	file, err := tree.File(filepath)
150	if err != nil {
151		return nil, err
152	}
153	fileContent, err := file.Contents()
154	if err != nil {
155		return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
156	}
157	return []byte(fileContent), nil
158}
159
160func (repo *GitRootRepository) Exists(filepath string) bool {
161	head, err := repo.Status()
162	if err != nil {
163		return false
164	}
165	return repo.ExistsAtHash(filepath, head.Hash())
166}
167
168func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
169	unlock := repo.manager.repoLocks.Read(repo.name)
170	defer unlock()
171	c, err := repo.repo.CommitObject(hash)
172	if err != nil {
173		return false
174	}
175	tree, err := c.Tree()
176	if err != nil {
177		return false
178	}
179	f, _ := tree.FindEntry(filepath)
180	return f != nil
181}
182
183func (repo *GitRootRepository) CurrentBranch() (plumbing.ReferenceName, error) {
184	unlock := repo.manager.repoLocks.Read(repo.name)
185	defer unlock()
186	head, err := repo.repo.Head()
187	if err != nil {
188		return "", oops.Wrapf(err, "Head")
189	}
190	return head.Name(), nil
191}
192
193func (repo *GitRootRepository) Worktree(path string) error {
194	repoConf, err := repo.Status()
195	if err != nil {
196		return repo.errorHandler.Wrapf(err, "can't get repoConf for worktree on osfs")
197	}
198	unlock := repo.manager.repoLocks.Read(repo.name)
199	defer unlock()
200	commit, err := repo.repo.CommitObject(repoConf.Hash())
201	if err != nil {
202		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get CommitObject for worktree on osfs")
203	}
204	tree, err := repo.repo.TreeObject(commit.TreeHash)
205	if err != nil {
206		return repo.errorHandler.With("hash", repoConf.Hash()).Wrapf(err, "can't get TreeObject for worktree on osfs")
207	}
208	files := tree.Files()
209	files.ForEach(func(file *object.File) error {
210		content, err := file.Contents()
211		if err != nil {
212			repo.logger.Error("can't get content", err, logger.NewLoggerPair("file", file.Name))
213			return nil
214		}
215		dir, _ := filepath.Split(file.Name)
216		err = os.MkdirAll(filepath.Join(path, dir), os.ModePerm)
217		if err != nil {
218			repo.logger.Error("can't mkdirall", err, logger.NewLoggerPair("dir", dir))
219		}
220		if err := os.WriteFile(filepath.Join(path, file.Name), []byte(content), os.ModePerm); err != nil {
221			repo.logger.Error("can't WriteFile", err, logger.NewLoggerPair("file", file.Name))
222		}
223		return nil
224	})
225	return nil
226}
227
228func (repo *GitRootRepository) Close() error {
229	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
230	if repo.hasBecomeWrite != nil {
231		repo.hasBecomeWrite.close()
232		repo.hasBecomeWrite = nil
233	}
234	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
235		return repo.manager.cacheRepos.Delete(repo.name, func() error {
236			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
237			defer repoLock.Close()
238			unwrite := repoLock.Write()
239			defer unwrite()
240
241			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
242			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
243			// } else {
244			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
245			// }
246
247			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
248			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
249			// } else {
250			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
251			// }
252
253			if sto, ok := repo.storer.(*filesystem.Storage); ok {
254				sto.Close()
255			}
256			repo.storerCache.Clear()
257			return nil
258		})
259	}
260
261	return nil
262}