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