git.go

  1package config
  2
  3import (
  4	"errors"
  5	"log"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9	"sync"
 10
 11	"github.com/charmbracelet/soft-serve/git"
 12	"github.com/gobwas/glob"
 13	"github.com/golang/groupcache/lru"
 14)
 15
 16// ErrMissingRepo indicates that the requested repository could not be found.
 17var ErrMissingRepo = errors.New("missing repo")
 18
 19// Repo represents a Git repository.
 20type Repo struct {
 21	name        string
 22	description string
 23	path        string
 24	repository  *git.Repository
 25	readme      string
 26	readmePath  string
 27	head        *git.Reference
 28	headCommit  string
 29	refs        []*git.Reference
 30	patchCache  *lru.Cache
 31	private     bool
 32}
 33
 34// open opens a Git repository.
 35func (rs *RepoSource) open(path string) (*Repo, error) {
 36	rg, err := git.Open(path)
 37	if err != nil {
 38		return nil, err
 39	}
 40	r := &Repo{
 41		path:       path,
 42		repository: rg,
 43		patchCache: lru.New(1000),
 44	}
 45	_, err = r.HEAD()
 46	if err != nil {
 47		return nil, err
 48	}
 49	_, err = r.References()
 50	if err != nil {
 51		return nil, err
 52	}
 53	return r, nil
 54}
 55
 56// IsPrivate returns true if the repository is private.
 57func (r *Repo) IsPrivate() bool {
 58	return r.private
 59}
 60
 61// Path returns the path to the repository.
 62func (r *Repo) Path() string {
 63	return r.path
 64}
 65
 66// Repo returns the repository directory name.
 67func (r *Repo) Repo() string {
 68	return filepath.Base(r.path)
 69}
 70
 71// Name returns the name of the repository.
 72func (r *Repo) Name() string {
 73	if r.name == "" {
 74		return strings.TrimSuffix(r.Repo(), ".git")
 75	}
 76	return r.name
 77}
 78
 79// Description returns the description for a repository.
 80func (r *Repo) Description() string {
 81	return r.description
 82}
 83
 84// Readme returns the readme and its path for the repository.
 85func (r *Repo) Readme() (readme string, path string) {
 86	return r.readme, r.readmePath
 87}
 88
 89// SetReadme sets the readme for the repository.
 90func (r *Repo) SetReadme(readme, path string) {
 91	r.readme = readme
 92	r.readmePath = path
 93}
 94
 95// HEAD returns the reference for a repository.
 96func (r *Repo) HEAD() (*git.Reference, error) {
 97	if r.head != nil {
 98		return r.head, nil
 99	}
100	h, err := r.repository.HEAD()
101	if err != nil {
102		return nil, err
103	}
104	r.head = h
105	return h, nil
106}
107
108// GetReferences returns the references for a repository.
109func (r *Repo) References() ([]*git.Reference, error) {
110	if r.refs != nil {
111		return r.refs, nil
112	}
113	refs, err := r.repository.References()
114	if err != nil {
115		return nil, err
116	}
117	r.refs = refs
118	return refs, nil
119}
120
121// Tree returns the git tree for a given path.
122func (r *Repo) Tree(ref *git.Reference, path string) (*git.Tree, error) {
123	return r.repository.TreePath(ref, path)
124}
125
126// Diff returns the diff for a given commit.
127func (r *Repo) Diff(commit *git.Commit) (*git.Diff, error) {
128	hash := commit.Hash.String()
129	c, ok := r.patchCache.Get(hash)
130	if ok {
131		return c.(*git.Diff), nil
132	}
133	diff, err := r.repository.Diff(commit)
134	if err != nil {
135		return nil, err
136	}
137	r.patchCache.Add(hash, diff)
138	return diff, nil
139}
140
141// CountCommits returns the number of commits for a repository.
142func (r *Repo) CountCommits(ref *git.Reference) (int64, error) {
143	tc, err := r.repository.CountCommits(ref)
144	if err != nil {
145		return 0, err
146	}
147	return tc, nil
148}
149
150// Commit returns the commit for a given hash.
151func (r *Repo) Commit(hash string) (*git.Commit, error) {
152	if hash == "HEAD" && r.headCommit != "" {
153		hash = r.headCommit
154	}
155	c, err := r.repository.CatFileCommit(hash)
156	if err != nil {
157		return nil, err
158	}
159	r.headCommit = c.ID.String()
160	return &git.Commit{
161		Commit: c,
162		Hash:   git.Hash(c.ID.String()),
163	}, nil
164}
165
166// CommitsByPage returns the commits for a repository.
167func (r *Repo) CommitsByPage(ref *git.Reference, page, size int) (git.Commits, error) {
168	return r.repository.CommitsByPage(ref, page, size)
169}
170
171// Push pushes the repository to the remote.
172func (r *Repo) Push(remote, branch string) error {
173	return r.repository.Push(remote, branch)
174}
175
176// RepoSource is a reference to an on-disk repositories.
177type RepoSource struct {
178	Path  string
179	mtx   sync.Mutex
180	repos map[string]*Repo
181}
182
183// NewRepoSource creates a new RepoSource.
184func NewRepoSource(repoPath string) *RepoSource {
185	err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
186	if err != nil {
187		log.Fatal(err)
188	}
189	rs := &RepoSource{Path: repoPath}
190	rs.repos = make(map[string]*Repo, 0)
191	return rs
192}
193
194// AllRepos returns all repositories for the given RepoSource.
195func (rs *RepoSource) AllRepos() []*Repo {
196	rs.mtx.Lock()
197	defer rs.mtx.Unlock()
198	repos := make([]*Repo, 0, len(rs.repos))
199	for _, r := range rs.repos {
200		repos = append(repos, r)
201	}
202	return repos
203}
204
205// GetRepo returns a repository by name.
206func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
207	rs.mtx.Lock()
208	defer rs.mtx.Unlock()
209	if strings.HasSuffix(name, ".git") {
210		name = strings.TrimSuffix(name, ".git")
211	}
212	r, ok := rs.repos[name]
213	if !ok {
214		return nil, ErrMissingRepo
215	}
216	return r, nil
217}
218
219// LoadRepo loads a repository from disk.
220func (rs *RepoSource) LoadRepo(name string) error {
221	rs.mtx.Lock()
222	defer rs.mtx.Unlock()
223	if strings.HasSuffix(name, ".git") {
224		name = strings.TrimSuffix(name, ".git")
225	}
226	rp := filepath.Join(rs.Path, name)
227	if _, err := os.Stat(rp); os.IsNotExist(err) {
228		rp += ".git"
229	} else {
230		log.Printf("warning: %q should be renamed to %q", rp, rp+".git")
231	}
232	r, err := rs.open(rp)
233	if err != nil {
234		log.Printf("error opening repository %q: %s", rp, err)
235		return err
236	}
237	rs.repos[name] = r
238	return nil
239}
240
241// LoadRepos opens Git repositories.
242func (rs *RepoSource) LoadRepos() error {
243	rd, err := os.ReadDir(rs.Path)
244	if err != nil {
245		return err
246	}
247	for _, de := range rd {
248		if !de.IsDir() {
249			log.Printf("warning: %q is not a directory", filepath.Join(rs.Path, de.Name()))
250			continue
251		}
252		err = rs.LoadRepo(de.Name())
253		if err == git.ErrNotAGitRepository {
254			continue
255		}
256		if err != nil {
257			return err
258		}
259	}
260	return nil
261}
262
263// LatestFile returns the contents of the latest file at the specified path in
264// the repository and its file path.
265func (r *Repo) LatestFile(pattern string) (string, string, error) {
266	g := glob.MustCompile(pattern)
267	dir := filepath.Dir(pattern)
268	t, err := r.repository.TreePath(r.head, dir)
269	if err != nil {
270		return "", "", err
271	}
272	ents, err := t.Entries()
273	if err != nil {
274		return "", "", err
275	}
276	for _, e := range ents {
277		fp := filepath.Join(dir, e.Name())
278		if e.IsTree() {
279			continue
280		}
281		if g.Match(fp) {
282			bts, err := e.Contents()
283			if err != nil {
284				return "", "", err
285			}
286			return string(bts), fp, nil
287		}
288	}
289	return "", "", git.ErrFileNotFound
290}
291
292// UpdateServerInfo updates the server info for the repository.
293func (r *Repo) UpdateServerInfo() error {
294	return r.repository.UpdateServerInfo()
295}