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// IsBare returns true if the repository is a bare repository.
 57func (r *Repo) IsBare() bool {
 58	return r.repository.IsBare
 59}
 60
 61// IsPrivate returns true if the repository is private.
 62func (r *Repo) IsPrivate() bool {
 63	return r.private
 64}
 65
 66// Path returns the path to the repository.
 67func (r *Repo) Path() string {
 68	return r.path
 69}
 70
 71// Repo returns the repository directory name.
 72func (r *Repo) Repo() string {
 73	return strings.TrimSuffix(filepath.Base(r.path), ".git")
 74}
 75
 76// Name returns the name of the repository.
 77func (r *Repo) Name() string {
 78	if r.name == "" {
 79		return r.Repo()
 80	}
 81	return r.name
 82}
 83
 84// Description returns the description for a repository.
 85func (r *Repo) Description() string {
 86	return r.description
 87}
 88
 89// Readme returns the readme and its path for the repository.
 90func (r *Repo) Readme() (readme string, path string) {
 91	return r.readme, r.readmePath
 92}
 93
 94// SetReadme sets the readme for the repository.
 95func (r *Repo) SetReadme(readme, path string) {
 96	r.readme = readme
 97	r.readmePath = path
 98}
 99
100// HEAD returns the reference for a repository.
101func (r *Repo) HEAD() (*git.Reference, error) {
102	if r.head != nil {
103		return r.head, nil
104	}
105	h, err := r.repository.HEAD()
106	if err != nil {
107		return nil, err
108	}
109	r.head = h
110	return h, nil
111}
112
113// GetReferences returns the references for a repository.
114func (r *Repo) References() ([]*git.Reference, error) {
115	if r.refs != nil {
116		return r.refs, nil
117	}
118	refs, err := r.repository.References()
119	if err != nil {
120		return nil, err
121	}
122	r.refs = refs
123	return refs, nil
124}
125
126// Tree returns the git tree for a given path.
127func (r *Repo) Tree(ref *git.Reference, path string) (*git.Tree, error) {
128	return r.repository.TreePath(ref, path)
129}
130
131// Diff returns the diff for a given commit.
132func (r *Repo) Diff(commit *git.Commit) (*git.Diff, error) {
133	hash := commit.Hash.String()
134	c, ok := r.patchCache.Get(hash)
135	if ok {
136		return c.(*git.Diff), nil
137	}
138	diff, err := r.repository.Diff(commit)
139	if err != nil {
140		return nil, err
141	}
142	r.patchCache.Add(hash, diff)
143	return diff, nil
144}
145
146// CountCommits returns the number of commits for a repository.
147func (r *Repo) CountCommits(ref *git.Reference) (int64, error) {
148	tc, err := r.repository.CountCommits(ref)
149	if err != nil {
150		return 0, err
151	}
152	return tc, nil
153}
154
155// Commit returns the commit for a given hash.
156func (r *Repo) Commit(hash string) (*git.Commit, error) {
157	if hash == "HEAD" && r.headCommit != "" {
158		hash = r.headCommit
159	}
160	c, err := r.repository.CatFileCommit(hash)
161	if err != nil {
162		return nil, err
163	}
164	r.headCommit = c.ID.String()
165	return &git.Commit{
166		Commit: c,
167		Hash:   git.Hash(c.ID.String()),
168	}, nil
169}
170
171// CommitsByPage returns the commits for a repository.
172func (r *Repo) CommitsByPage(ref *git.Reference, page, size int) (git.Commits, error) {
173	return r.repository.CommitsByPage(ref, page, size)
174}
175
176// Push pushes the repository to the remote.
177func (r *Repo) Push(remote, branch string) error {
178	return r.repository.Push(remote, branch)
179}
180
181// RepoSource is a reference to an on-disk repositories.
182type RepoSource struct {
183	Path  string
184	mtx   sync.Mutex
185	repos map[string]*Repo
186}
187
188// NewRepoSource creates a new RepoSource.
189func NewRepoSource(repoPath string) *RepoSource {
190	err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
191	if err != nil {
192		log.Fatal(err)
193	}
194	rs := &RepoSource{Path: repoPath}
195	rs.repos = make(map[string]*Repo, 0)
196	return rs
197}
198
199// AllRepos returns all repositories for the given RepoSource.
200func (rs *RepoSource) AllRepos() []*Repo {
201	rs.mtx.Lock()
202	defer rs.mtx.Unlock()
203	repos := make([]*Repo, 0, len(rs.repos))
204	for _, r := range rs.repos {
205		repos = append(repos, r)
206	}
207	return repos
208}
209
210// GetRepo returns a repository by name.
211func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
212	rs.mtx.Lock()
213	defer rs.mtx.Unlock()
214	if strings.HasSuffix(name, ".git") {
215		name = strings.TrimSuffix(name, ".git")
216	}
217	r, ok := rs.repos[name]
218	if !ok {
219		return nil, ErrMissingRepo
220	}
221	return r, nil
222}
223
224// LoadRepo loads a repository from disk.
225func (rs *RepoSource) LoadRepo(name string) error {
226	rs.mtx.Lock()
227	defer rs.mtx.Unlock()
228	if strings.HasSuffix(name, ".git") {
229		name = strings.TrimSuffix(name, ".git")
230	}
231	rp := filepath.Join(rs.Path, name)
232	if _, err := os.Stat(rp); os.IsNotExist(err) {
233		rp += ".git"
234	}
235	r, err := rs.open(rp)
236	if err != nil {
237		return err
238	}
239	if !r.IsBare() {
240		log.Printf("warning: %q is not a bare repository", rp)
241	} else if r.IsBare() && !strings.HasSuffix(rp, ".git") {
242		log.Printf("warning: %q should be renamed to %q", rp, rp+".git")
243	}
244	rs.repos[name] = r
245	return nil
246}
247
248// LoadRepos opens Git repositories.
249func (rs *RepoSource) LoadRepos() error {
250	rd, err := os.ReadDir(rs.Path)
251	if err != nil {
252		return err
253	}
254	for _, de := range rd {
255		if !de.IsDir() {
256			log.Printf("warning: %q is not a directory", filepath.Join(rs.Path, de.Name()))
257			continue
258		}
259		if err := rs.LoadRepo(de.Name()); err != nil {
260			log.Printf("error opening repository %q: %s", de.Name(), err)
261			continue
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}