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}