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