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}