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 %s: %s", name, 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		err = rs.LoadRepo(de.Name())
257		if err == git.ErrNotAGitRepository {
258			continue
259		}
260		if err != nil {
261			return err
262		}
263	}
264	return nil
265}
266
267// LatestFile returns the contents of the latest file at the specified path in
268// the repository and its file path.
269func (r *Repo) LatestFile(pattern string) (string, string, error) {
270	g := glob.MustCompile(pattern)
271	dir := filepath.Dir(pattern)
272	t, err := r.repository.TreePath(r.head, dir)
273	if err != nil {
274		return "", "", err
275	}
276	ents, err := t.Entries()
277	if err != nil {
278		return "", "", err
279	}
280	for _, e := range ents {
281		fp := filepath.Join(dir, e.Name())
282		if e.IsTree() {
283			continue
284		}
285		if g.Match(fp) {
286			bts, err := e.Contents()
287			if err != nil {
288				return "", "", err
289			}
290			return string(bts), fp, nil
291		}
292	}
293	return "", "", git.ErrFileNotFound
294}
295
296// UpdateServerInfo updates the server info for the repository.
297func (r *Repo) UpdateServerInfo() error {
298	return r.repository.UpdateServerInfo()
299}