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