git.go

  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}