1package config
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "sync"
8
9 "github.com/charmbracelet/log"
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 r.Repo()
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 r, ok := rs.repos[name]
210 if !ok {
211 return nil, ErrMissingRepo
212 }
213 return r, nil
214}
215
216// InitRepo initializes a new Git repository.
217func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
218 rs.mtx.Lock()
219 defer rs.mtx.Unlock()
220 rp := filepath.Join(rs.Path, name)
221 rg, err := git.Init(rp, bare)
222 if err != nil {
223 return nil, err
224 }
225 r := &Repo{
226 path: rp,
227 repository: rg,
228 refs: []*git.Reference{
229 git.NewReference(rp, git.RefsHeads+"master"),
230 },
231 }
232 rs.repos[name] = r
233 return r, nil
234}
235
236// LoadRepo loads a repository from disk.
237func (rs *RepoSource) LoadRepo(name string) error {
238 rs.mtx.Lock()
239 defer rs.mtx.Unlock()
240 rp := filepath.Join(rs.Path, name)
241 r, err := rs.open(rp)
242 if err != nil {
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.Warn("not a directory", "path", 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 log.Warn("error loading repository", "path", filepath.Join(rs.Path, de.Name()), "err", err)
266 continue
267 }
268 }
269 return nil
270}
271
272// LatestFile returns the contents of the latest file at the specified path in
273// the repository and its file path.
274func (r *Repo) LatestFile(pattern string) (string, string, error) {
275 g := glob.MustCompile(pattern)
276 dir := filepath.Dir(pattern)
277 t, err := r.repository.TreePath(r.head, dir)
278 if err != nil {
279 return "", "", err
280 }
281 ents, err := t.Entries()
282 if err != nil {
283 return "", "", err
284 }
285 for _, e := range ents {
286 fp := filepath.Join(dir, e.Name())
287 if e.IsTree() {
288 continue
289 }
290 if g.Match(fp) {
291 bts, err := e.Contents()
292 if err != nil {
293 return "", "", err
294 }
295 return string(bts), fp, nil
296 }
297 }
298 return "", "", git.ErrFileNotFound
299}
300
301// UpdateServerInfo updates the server info for the repository.
302func (r *Repo) UpdateServerInfo() error {
303 return r.repository.UpdateServerInfo()
304}