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 "path/filepath"
9
10 "github.com/go-git/go-billy/v5/osfs"
11 git "github.com/go-git/go-git/v5"
12 "github.com/go-git/go-git/v5/plumbing"
13 "github.com/go-git/go-git/v5/plumbing/cache"
14 "github.com/go-git/go-git/v5/storage"
15 "github.com/go-git/go-git/v5/storage/filesystem"
16 "github.com/samber/oops"
17 "gitroot.dev/server/logger"
18)
19
20type GitRootRepository struct {
21 name string
22 logger *logger.Logger
23 errorHandler oops.OopsErrorBuilder
24 repo *git.Repository
25 storer storage.Storer
26 storerCache cache.Object
27 manager *Manager
28 hasBecomeWrite *GitRootRepositoryWrite
29}
30
31func (m *Manager) NewGitRootRepository(name string, withCache *GitRootRepository) (*GitRootRepository, error) {
32 errorHandler := oops.Code("NewGitRootRepository").With("name", name)
33
34 unlock := m.repoLocks.Read(name)
35 defer unlock()
36
37 if withCache == nil {
38 storerCache := cache.NewObjectLRUDefault()
39 bfs := filesystem.NewStorage(osfs.New(m.conf.GetDirPathForRepo(name), osfs.WithBoundOS()), storerCache)
40 repo, err := git.Open(bfs, nil)
41 if err != nil {
42 return nil, errorHandler.Wrapf(err, "can't open")
43 }
44 return &GitRootRepository{
45 name: name,
46 logger: m.logger.NewSubLogger(name),
47 errorHandler: errorHandler,
48 repo: repo,
49 storer: bfs,
50 storerCache: storerCache,
51 manager: m,
52 hasBecomeWrite: nil,
53 }, nil
54 } else {
55 return &GitRootRepository{
56 name: name,
57 logger: m.logger.NewSubLogger(name),
58 errorHandler: errorHandler,
59 repo: withCache.repo,
60 storer: withCache.storer,
61 storerCache: withCache.storerCache,
62 manager: m,
63 hasBecomeWrite: nil,
64 }, nil
65 }
66}
67
68func (repo *GitRootRepository) Name() string {
69 return repo.name
70}
71
72func (repo *GitRootRepository) WillWrite(branch plumbing.ReferenceName) (*GitRootRepositoryWrite, error) {
73 lock := repo.manager.repoLocks.WillWrite(repo.name)
74 if repo.hasBecomeWrite != nil {
75 return nil, repo.errorHandler.Errorf("WillWrite called multiple times before close")
76 }
77 hasBecomeWrite, err := NewGitRootRepositoryWrite(repo, branch, lock)
78 repo.hasBecomeWrite = hasBecomeWrite
79 return repo.hasBecomeWrite, err
80}
81
82func (repo *GitRootRepository) Configuration() (RepoConfiguration, error) {
83 content, err := repo.Content(repo.manager.conf.PathFileRepoConfigurationName())
84 if err != nil {
85 return RepoConfiguration{}, repo.errorHandler.Wrapf(err, "can't read conf file")
86 }
87 return readRepoConfiguration(content)
88}
89
90func (repo *GitRootRepository) Status() (*plumbing.Reference, error) {
91 unlock := repo.manager.repoLocks.Read(repo.name)
92 defer unlock()
93 return repo.repo.Head()
94}
95
96func (repo *GitRootRepository) ContentRepositoriesConf() ([]byte, error) {
97 return repo.Content(repo.manager.conf.PathFileRepositories())
98}
99
100func (repo *GitRootRepository) ContentPluginsConf() ([]byte, error) {
101 return repo.Content(repo.manager.conf.PathFilePlugins())
102}
103
104func (repo *GitRootRepository) ContentPluginsConfAtHash(hash plumbing.Hash) ([]byte, error) {
105 return repo.ContentAtHash(repo.manager.conf.PathFilePlugins(), hash)
106}
107
108func (repo *GitRootRepository) ContentPluginsConfAtRef(name plumbing.ReferenceName) ([]byte, error) {
109 return repo.ContentAtRef(repo.manager.conf.PathFilePlugins(), name)
110}
111
112func (repo *GitRootRepository) PathDataWeb(path string) string {
113 return filepath.Join(repo.manager.conf.PathDataWeb(repo.name), path)
114}
115
116func (repo *GitRootRepository) Content(filepath string) ([]byte, error) {
117 head, err := repo.Status()
118 if err != nil {
119 return nil, oops.Wrapf(err, "Head")
120 }
121 repo.logger.Debug("head", logger.NewLoggerPair("hash", head.Hash().String()))
122 return repo.ContentAtHash(filepath, head.Hash())
123}
124
125func (repo *GitRootRepository) ContentAtRef(filepath string, name plumbing.ReferenceName) ([]byte, error) {
126 unlock := repo.manager.repoLocks.Read(repo.name)
127 ref, err := repo.repo.Reference(name, false)
128 if err != nil {
129 unlock()
130 return nil, oops.With("ref", name).Wrapf(err, "ContentPluginsConfAtRef")
131 }
132 unlock()
133 return repo.ContentAtHash(filepath, ref.Hash())
134}
135
136func (repo *GitRootRepository) ContentAtHash(filepath string, hash plumbing.Hash) ([]byte, error) {
137 unlock := repo.manager.repoLocks.Read(repo.name)
138 defer unlock()
139 c, err := repo.repo.CommitObject(hash)
140 if err != nil {
141 return nil, err
142 }
143 tree, err := c.Tree()
144 if err != nil {
145 return nil, err
146 }
147 file, err := tree.File(filepath)
148 if err != nil {
149 return nil, err
150 }
151 fileContent, err := file.Contents()
152 if err != nil {
153 return nil, repo.errorHandler.With("filepath", filepath).Wrapf(err, "can't read file")
154 }
155 return []byte(fileContent), nil
156}
157
158func (repo *GitRootRepository) Exists(filepath string) bool {
159 head, err := repo.Status()
160 if err != nil {
161 return false
162 }
163 return repo.ExistsAtHash(filepath, head.Hash())
164}
165
166func (repo *GitRootRepository) ExistsAtHash(filepath string, hash plumbing.Hash) bool {
167 unlock := repo.manager.repoLocks.Read(repo.name)
168 defer unlock()
169 c, err := repo.repo.CommitObject(hash)
170 if err != nil {
171 return false
172 }
173 tree, err := c.Tree()
174 if err != nil {
175 return false
176 }
177 f, _ := tree.FindEntry(filepath)
178 return f != nil
179}
180
181func (repo *GitRootRepository) CurrentBranch() (plumbing.ReferenceName, error) {
182 unlock := repo.manager.repoLocks.Read(repo.name)
183 defer unlock()
184 head, err := repo.repo.Head()
185 if err != nil {
186 return "", oops.Wrapf(err, "Head")
187 }
188 return head.Name(), nil
189}
190
191func (repo *GitRootRepository) Close() error {
192 repo.logger.Debug("Close", logger.NewLoggerPair("hasBecomeWrite", repo.hasBecomeWrite != nil))
193 if repo.hasBecomeWrite != nil {
194 repo.hasBecomeWrite.close()
195 repo.hasBecomeWrite = nil
196 }
197 if nb := repo.manager.cacheRepos.Remove(repo.name); nb == 0 {
198 return repo.manager.cacheRepos.Delete(repo.name, func() error {
199 repoLock := repo.manager.repoLocks.WillWrite(repo.name)
200 defer repoLock.Close()
201 unwrite := repoLock.Write()
202 defer unwrite()
203
204 // if err := repo.repo.Prune(git.PruneOptions{Handler: repo.repo.DeleteObject}); err != nil && !errors.Is(err, git.ErrLooseObjectsNotSupported) {
205 // repo.logger.Error("close prune", oops.Wrapf(err, "Prune"), logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
206 // } else {
207 // repo.logger.Debug("close prune ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
208 // }
209
210 // if err := repo.repo.RepackObjects(&git.RepackConfig{UseRefDeltas: true}); err != nil && !errors.Is(err, git.ErrPackedObjectsNotSupported) {
211 // repo.logger.Error("close RepackObjects", oops.Wrapf(err, "RepackObjects"))
212 // } else {
213 // repo.logger.Debug("close RepackObjects ok", logger.NewLoggerPair("type storer", fmt.Sprintf("%T", repo.repo.Storer)))
214 // }
215
216 if sto, ok := repo.storer.(*filesystem.Storage); ok {
217 sto.Close()
218 }
219 repo.storerCache.Clear()
220 return nil
221 })
222 }
223
224 return nil
225}