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