1package config
2
3import (
4 "errors"
5 "log"
6 "os"
7 "path/filepath"
8 "strings"
9 "sync"
10
11 "github.com/charmbracelet/soft-serve/git"
12 "github.com/gobwas/glob"
13 "github.com/golang/groupcache/lru"
14)
15
16// ErrMissingRepo indicates that the requested repository could not be found.
17var ErrMissingRepo = errors.New("missing repo")
18
19// Repo represents a Git repository.
20type Repo struct {
21 name string
22 description string
23 path string
24 repository *git.Repository
25 readme string
26 readmePath string
27 head *git.Reference
28 headCommit string
29 refs []*git.Reference
30 patchCache *lru.Cache
31 private bool
32}
33
34// open opens a Git repository.
35func (rs *RepoSource) open(path string) (*Repo, error) {
36 rg, err := git.Open(path)
37 if err != nil {
38 return nil, err
39 }
40 r := &Repo{
41 path: path,
42 repository: rg,
43 patchCache: lru.New(1000),
44 }
45 _, err = r.HEAD()
46 if err != nil {
47 return nil, err
48 }
49 _, err = r.References()
50 if err != nil {
51 return nil, err
52 }
53 return r, nil
54}
55
56// IsBare returns true if the repository is a bare repository.
57func (r *Repo) IsBare() bool {
58 return r.repository.IsBare
59}
60
61// IsPrivate returns true if the repository is private.
62func (r *Repo) IsPrivate() bool {
63 return r.private
64}
65
66// Path returns the path to the repository.
67func (r *Repo) Path() string {
68 return r.path
69}
70
71// Repo returns the repository directory name.
72func (r *Repo) Repo() string {
73 return strings.TrimSuffix(filepath.Base(r.path), ".git")
74}
75
76// Name returns the name of the repository.
77func (r *Repo) Name() string {
78 if r.name == "" {
79 return r.Repo()
80 }
81 return r.name
82}
83
84// Description returns the description for a repository.
85func (r *Repo) Description() string {
86 return r.description
87}
88
89// Readme returns the readme and its path for the repository.
90func (r *Repo) Readme() (readme string, path string) {
91 return r.readme, r.readmePath
92}
93
94// SetReadme sets the readme for the repository.
95func (r *Repo) SetReadme(readme, path string) {
96 r.readme = readme
97 r.readmePath = path
98}
99
100// HEAD returns the reference for a repository.
101func (r *Repo) HEAD() (*git.Reference, error) {
102 if r.head != nil {
103 return r.head, nil
104 }
105 h, err := r.repository.HEAD()
106 if err != nil {
107 return nil, err
108 }
109 r.head = h
110 return h, nil
111}
112
113// GetReferences returns the references for a repository.
114func (r *Repo) References() ([]*git.Reference, error) {
115 if r.refs != nil {
116 return r.refs, nil
117 }
118 refs, err := r.repository.References()
119 if err != nil {
120 return nil, err
121 }
122 r.refs = refs
123 return refs, nil
124}
125
126// Tree returns the git tree for a given path.
127func (r *Repo) Tree(ref *git.Reference, path string) (*git.Tree, error) {
128 return r.repository.TreePath(ref, path)
129}
130
131// Diff returns the diff for a given commit.
132func (r *Repo) Diff(commit *git.Commit) (*git.Diff, error) {
133 hash := commit.Hash.String()
134 c, ok := r.patchCache.Get(hash)
135 if ok {
136 return c.(*git.Diff), nil
137 }
138 diff, err := r.repository.Diff(commit)
139 if err != nil {
140 return nil, err
141 }
142 r.patchCache.Add(hash, diff)
143 return diff, nil
144}
145
146// CountCommits returns the number of commits for a repository.
147func (r *Repo) CountCommits(ref *git.Reference) (int64, error) {
148 tc, err := r.repository.CountCommits(ref)
149 if err != nil {
150 return 0, err
151 }
152 return tc, nil
153}
154
155// Commit returns the commit for a given hash.
156func (r *Repo) Commit(hash string) (*git.Commit, error) {
157 if hash == "HEAD" && r.headCommit != "" {
158 hash = r.headCommit
159 }
160 c, err := r.repository.CatFileCommit(hash)
161 if err != nil {
162 return nil, err
163 }
164 r.headCommit = c.ID.String()
165 return &git.Commit{
166 Commit: c,
167 Hash: git.Hash(c.ID.String()),
168 }, nil
169}
170
171// CommitsByPage returns the commits for a repository.
172func (r *Repo) CommitsByPage(ref *git.Reference, page, size int) (git.Commits, error) {
173 return r.repository.CommitsByPage(ref, page, size)
174}
175
176// Push pushes the repository to the remote.
177func (r *Repo) Push(remote, branch string) error {
178 return r.repository.Push(remote, branch)
179}
180
181// RepoSource is a reference to an on-disk repositories.
182type RepoSource struct {
183 Path string
184 mtx sync.Mutex
185 repos map[string]*Repo
186}
187
188// NewRepoSource creates a new RepoSource.
189func NewRepoSource(repoPath string) *RepoSource {
190 err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
191 if err != nil {
192 log.Fatal(err)
193 }
194 rs := &RepoSource{Path: repoPath}
195 rs.repos = make(map[string]*Repo, 0)
196 return rs
197}
198
199// AllRepos returns all repositories for the given RepoSource.
200func (rs *RepoSource) AllRepos() []*Repo {
201 rs.mtx.Lock()
202 defer rs.mtx.Unlock()
203 repos := make([]*Repo, 0, len(rs.repos))
204 for _, r := range rs.repos {
205 repos = append(repos, r)
206 }
207 return repos
208}
209
210// GetRepo returns a repository by name.
211func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
212 rs.mtx.Lock()
213 defer rs.mtx.Unlock()
214 if strings.HasSuffix(name, ".git") {
215 name = strings.TrimSuffix(name, ".git")
216 }
217 r, ok := rs.repos[name]
218 if !ok {
219 return nil, ErrMissingRepo
220 }
221 return r, nil
222}
223
224// LoadRepo loads a repository from disk.
225func (rs *RepoSource) LoadRepo(name string) error {
226 rs.mtx.Lock()
227 defer rs.mtx.Unlock()
228 if strings.HasSuffix(name, ".git") {
229 name = strings.TrimSuffix(name, ".git")
230 }
231 rp := filepath.Join(rs.Path, name)
232 if _, err := os.Stat(rp); os.IsNotExist(err) {
233 rp += ".git"
234 }
235 r, err := rs.open(rp)
236 if err != nil {
237 return err
238 }
239 if !r.IsBare() {
240 log.Printf("warning: %q is not a bare repository", rp)
241 } else if r.IsBare() && !strings.HasSuffix(rp, ".git") {
242 log.Printf("warning: %q should be renamed to %q", rp, rp+".git")
243 }
244 rs.repos[name] = r
245 return nil
246}
247
248// LoadRepos opens Git repositories.
249func (rs *RepoSource) LoadRepos() error {
250 rd, err := os.ReadDir(rs.Path)
251 if err != nil {
252 return err
253 }
254 for _, de := range rd {
255 if !de.IsDir() {
256 log.Printf("warning: %q is not a directory", filepath.Join(rs.Path, de.Name()))
257 continue
258 }
259 if err := rs.LoadRepo(de.Name()); err != nil {
260 log.Printf("error opening repository %q: %s", de.Name(), err)
261 continue
262 }
263 }
264 return nil
265}
266
267// LatestFile returns the contents of the latest file at the specified path in
268// the repository and its file path.
269func (r *Repo) LatestFile(pattern string) (string, string, error) {
270 g := glob.MustCompile(pattern)
271 dir := filepath.Dir(pattern)
272 t, err := r.repository.TreePath(r.head, dir)
273 if err != nil {
274 return "", "", err
275 }
276 ents, err := t.Entries()
277 if err != nil {
278 return "", "", err
279 }
280 for _, e := range ents {
281 fp := filepath.Join(dir, e.Name())
282 if e.IsTree() {
283 continue
284 }
285 if g.Match(fp) {
286 bts, err := e.Contents()
287 if err != nil {
288 return "", "", err
289 }
290 return string(bts), fp, nil
291 }
292 }
293 return "", "", git.ErrFileNotFound
294}
295
296// UpdateServerInfo updates the server info for the repository.
297func (r *Repo) UpdateServerInfo() error {
298 return r.repository.UpdateServerInfo()
299}