git.go

  1package config
  2
  3import (
  4	"errors"
  5	"os"
  6	"path/filepath"
  7	"sync"
  8
  9	"github.com/charmbracelet/log"
 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 r.Repo()
 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	r, ok := rs.repos[name]
210	if !ok {
211		return nil, ErrMissingRepo
212	}
213	return r, nil
214}
215
216// InitRepo initializes a new Git repository.
217func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
218	rs.mtx.Lock()
219	defer rs.mtx.Unlock()
220	rp := filepath.Join(rs.Path, name)
221	rg, err := git.Init(rp, bare)
222	if err != nil {
223		return nil, err
224	}
225	r := &Repo{
226		path:       rp,
227		repository: rg,
228		refs: []*git.Reference{
229			git.NewReference(rp, git.RefsHeads+"master"),
230		},
231	}
232	rs.repos[name] = r
233	return r, nil
234}
235
236// LoadRepo loads a repository from disk.
237func (rs *RepoSource) LoadRepo(name string) error {
238	rs.mtx.Lock()
239	defer rs.mtx.Unlock()
240	rp := filepath.Join(rs.Path, name)
241	r, err := rs.open(rp)
242	if err != nil {
243		log.Error("error opening repository", "path", rp, "err", err)
244		return err
245	}
246	rs.repos[name] = r
247	return nil
248}
249
250// LoadRepos opens Git repositories.
251func (rs *RepoSource) LoadRepos() error {
252	rd, err := os.ReadDir(rs.Path)
253	if err != nil {
254		return err
255	}
256	for _, de := range rd {
257		if !de.IsDir() {
258			log.Warn("not a directory", "path", filepath.Join(rs.Path, de.Name()))
259			continue
260		}
261		err = rs.LoadRepo(de.Name())
262		if err == git.ErrNotAGitRepository {
263			continue
264		}
265		if err != nil {
266			return err
267		}
268	}
269	return nil
270}
271
272// LatestFile returns the contents of the latest file at the specified path in
273// the repository and its file path.
274func (r *Repo) LatestFile(pattern string) (string, string, error) {
275	g := glob.MustCompile(pattern)
276	dir := filepath.Dir(pattern)
277	t, err := r.repository.TreePath(r.head, dir)
278	if err != nil {
279		return "", "", err
280	}
281	ents, err := t.Entries()
282	if err != nil {
283		return "", "", err
284	}
285	for _, e := range ents {
286		fp := filepath.Join(dir, e.Name())
287		if e.IsTree() {
288			continue
289		}
290		if g.Match(fp) {
291			bts, err := e.Contents()
292			if err != nil {
293				return "", "", err
294			}
295			return string(bts), fp, nil
296		}
297	}
298	return "", "", git.ErrFileNotFound
299}
300
301// UpdateServerInfo updates the server info for the repository.
302func (r *Repo) UpdateServerInfo() error {
303	return r.repository.UpdateServerInfo()
304}