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