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