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}