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