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	"path/filepath"
  9
 10	"github.com/go-git/go-billy/v5/osfs"
 11	git "github.com/go-git/go-git/v5"
 12	"github.com/go-git/go-git/v5/plumbing"
 13	"github.com/go-git/go-git/v5/plumbing/cache"
 14	"github.com/go-git/go-git/v5/storage"
 15	"github.com/go-git/go-git/v5/storage/filesystem"
 16	"github.com/samber/oops"
 17	"gitroot.dev/server/logger"
 18)
 19
 20type GitRootRepository struct {
 21	name           string
 22	logger         *logger.Logger
 23	errorHandler   oops.OopsErrorBuilder
 24	repo           *git.Repository
 25	storer         storage.Storer
 26	storerCache    cache.Object
 27	manager        *Manager
 28	hasBecomeWrite *GitRootRepositoryWrite
 29}
 30
 31func (m *Manager) NewGitRootRepository(name string, withCache *GitRootRepository) (*GitRootRepository, error) {
 32	errorHandler := oops.Code("NewGitRootRepository").With("name", name)
 33
 34	unlock := m.repoLocks.Read(name)
 35	defer unlock()
 36
 37	if withCache == nil {
 38		storerCache := cache.NewObjectLRUDefault()
 39		bfs := filesystem.NewStorage(osfs.New(m.conf.GetDirPathForRepo(name), osfs.WithBoundOS()), storerCache)
 40		repo, err := git.Open(bfs, nil)
 41		if err != nil {
 42			return nil, errorHandler.Wrapf(err, "can't open")
 43		}
 44		return &GitRootRepository{
 45			name:           name,
 46			logger:         m.logger.NewSubLogger(name),
 47			errorHandler:   errorHandler,
 48			repo:           repo,
 49			storer:         bfs,
 50			storerCache:    storerCache,
 51			manager:        m,
 52			hasBecomeWrite: nil,
 53		}, nil
 54	} else {
 55		return &GitRootRepository{
 56			name:           name,
 57			logger:         m.logger.NewSubLogger(name),
 58			errorHandler:   errorHandler,
 59			repo:           withCache.repo,
 60			storer:         withCache.storer,
 61			storerCache:    withCache.storerCache,
 62			manager:        m,
 63			hasBecomeWrite: nil,
 64		}, nil
 65	}
 66}
 67
 68func (repo *GitRootRepository) Name() string {
 69	return repo.name
 70}
 71
 72func (repo *GitRootRepository) WillWrite(branch plumbing.ReferenceName) (*GitRootRepositoryWrite, error) {
 73	lock := repo.manager.repoLocks.WillWrite(repo.name)
 74	if repo.hasBecomeWrite != nil {
 75		return nil, repo.errorHandler.Errorf("WillWrite called multiple times before close")
 76	}
 77	hasBecomeWrite, err := NewGitRootRepositoryWrite(repo, branch, lock)
 78	repo.hasBecomeWrite = hasBecomeWrite
 79	return repo.hasBecomeWrite, err
 80}
 81
 82func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) {
 83	content, err := repo.Content(repo.manager.conf.PathFileRepoConfigurationName())
 84	if err != nil {
 85		return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file")
 86	}
 87	return readRepoConfiguration(content)
 88}
 89
 90func (repo *GitRootRepository) Status() (*plumbing.Reference, error) {
 91	unlock := repo.manager.repoLocks.Read(repo.name)
 92	defer unlock()
 93	return repo.repo.Head()
 94}
 95
 96func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) {
 97	return repo.Content(repo.manager.conf.PathFileRepositories())
 98}
 99
100func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) {
101	return repo.Content(repo.manager.conf.PathFilePlugins())
102}
103
104func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) {
105	return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
106}
107
108func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) {
109	return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name)
110}
111
112func (repo *GitRootRepository) PathDataWeb(path string) string {
113	return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
114}
115
116func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
117	head, err := repo.Status()
118	if err != nil {
119		return nil, oops.Wrapf(err, "Head")
120	}
121	repo.logger.Debug("head", logger.NewLoggerPair("hash", head.Hash().String()))
122	return repo.ContentAtHash(filepath, head.Hash())
123}
124
125func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
126	unlock := repo.manager.repoLocks.Read(repo.name)
127	ref, err := repo.repo.Reference(name, false)
128	if err != nil {
129		unlock()
130		return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
131	}
132	unlock()
133	return repo.ContentAtHash(filepath, ref.Hash())
134}
135
136func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
137	unlock := repo.manager.repoLocks.Read(repo.name)
138	defer unlock()
139	c, err := repo.repo.CommitObject(hash)
140	if err != nil {
141		return nil, err
142	}
143	tree, err := c.Tree()
144	if err != nil {
145		return nil, err
146	}
147	file, err := tree.File(filepath)
148	if err != nil {
149		return nil, err
150	}
151	fileContent, err := file.Contents()
152	if err != nil {
153		return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
154	}
155	return []byte(fileContent), nil
156}
157
158func (repo *GitRootRepository) Exists(filepath string) bool {
159	head, err := repo.Status()
160	if err != nil {
161		return false
162	}
163	return repo.ExistsAtHash(filepath, head.Hash())
164}
165
166func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
167	unlock := repo.manager.repoLocks.Read(repo.name)
168	defer unlock()
169	c, err := repo.repo.CommitObject(hash)
170	if err != nil {
171		return false
172	}
173	tree, err := c.Tree()
174	if err != nil {
175		return false
176	}
177	f, _ := tree.FindEntry(filepath)
178	return f != nil
179}
180
181func (repo *GitRootRepository) CurrentBranch() (plumbing.ReferenceName, error) {
182	unlock := repo.manager.repoLocks.Read(repo.name)
183	defer unlock()
184	head, err := repo.repo.Head()
185	if err != nil {
186		return "", oops.Wrapf(err, "Head")
187	}
188	return head.Name(), nil
189}
190
191func (repo *GitRootRepository) Close() error {
192	repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
193	if repo.hasBecomeWrite != nil {
194		repo.hasBecomeWrite.close()
195		repo.hasBecomeWrite = nil
196	}
197	if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
198		return repo.manager.cacheRepos.Delete(repo.name, func() error {
199			repoLock := repo.manager.repoLocks.WillWrite(repo.name)
200			defer repoLock.Close()
201			unwrite := repoLock.Write()
202			defer unwrite()
203
204			// if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
205			// 	repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
206			// } else {
207			// 	repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
208			// }
209
210			// if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
211			// 	repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
212			// } else {
213			// 	repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
214			// }
215
216			if sto, ok := repo.storer.(*filesystem.Storage); ok {
217				sto.Close()
218			}
219			repo.storerCache.Clear()
220			return nil
221		})
222	}
223
224	return nil
225}