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