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