1package git
  2
  3import (
  4	"errors"
  5	"log"
  6	"os"
  7	"path/filepath"
  8	"sort"
  9	"sync"
 10	"time"
 11
 12	"github.com/go-git/go-billy/v5/memfs"
 13	"github.com/go-git/go-git/v5"
 14	"github.com/go-git/go-git/v5/plumbing/object"
 15	"github.com/go-git/go-git/v5/plumbing/transport"
 16	"github.com/go-git/go-git/v5/storage/memory"
 17)
 18
 19// ErrMissingRepo indicates that the requested repository could not be found.
 20var ErrMissingRepo = errors.New("missing repo")
 21
 22// Repo represents a Git repository.
 23type Repo struct {
 24	Name        string
 25	Repository  *git.Repository
 26	Readme      string
 27	LastUpdated *time.Time
 28	commits     CommitLog
 29}
 30
 31// RepoCommit contains metadata for a Git commit.
 32type RepoCommit struct {
 33	Name   string
 34	Commit *object.Commit
 35}
 36
 37// CommitLog is a series of Git commits.
 38type CommitLog []RepoCommit
 39
 40func (cl CommitLog) Len() int      { return len(cl) }
 41func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 42func (cl CommitLog) Less(i, j int) bool {
 43	return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When)
 44}
 45
 46// RepoSource is a reference to an on-disk repositories.
 47type RepoSource struct {
 48	Path  string
 49	mtx   sync.Mutex
 50	repos []*Repo
 51}
 52
 53// NewRepoSource creates a new RepoSource.
 54func NewRepoSource(repoPath string) *RepoSource {
 55	err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
 56	if err != nil {
 57		log.Fatal(err)
 58	}
 59	rs := &RepoSource{Path: repoPath}
 60	return rs
 61}
 62
 63// AllRepos returns all repositories for the given RepoSource.
 64func (rs *RepoSource) AllRepos() []*Repo {
 65	rs.mtx.Lock()
 66	defer rs.mtx.Unlock()
 67	return rs.repos
 68}
 69
 70// GetRepo returns a repository by name.
 71func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
 72	rs.mtx.Lock()
 73	defer rs.mtx.Unlock()
 74	for _, r := range rs.repos {
 75		if r.Name == name {
 76			return r, nil
 77		}
 78	}
 79	return nil, ErrMissingRepo
 80}
 81
 82// InitRepo initializes a new Git repository.
 83func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
 84	rs.mtx.Lock()
 85	defer rs.mtx.Unlock()
 86	rp := filepath.Join(rs.Path, name)
 87	rg, err := git.PlainInit(rp, bare)
 88	if err != nil {
 89		return nil, err
 90	}
 91	if bare {
 92		// Clone repo into memory storage
 93		ar, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
 94			URL: rp,
 95		})
 96		if err != nil && err != transport.ErrEmptyRemoteRepository {
 97			return nil, err
 98		}
 99		rg = ar
100	}
101	r := &Repo{
102		Name:       name,
103		Repository: rg,
104	}
105	rs.repos = append(rs.repos, r)
106	return r, nil
107}
108
109func (r *Repo) GetCommits(limit int) CommitLog {
110	if limit <= 0 {
111		return r.commits
112	}
113	if limit > len(r.commits) {
114		limit = len(r.commits)
115	}
116	return r.commits[:limit]
117}
118
119// LoadRepos opens Git repositories.
120func (rs *RepoSource) LoadRepos() error {
121	rs.mtx.Lock()
122	defer rs.mtx.Unlock()
123	rd, err := os.ReadDir(rs.Path)
124	if err != nil {
125		return err
126	}
127	rs.repos = make([]*Repo, 0)
128	for _, de := range rd {
129		rn := de.Name()
130		rg, err := git.PlainOpen(filepath.Join(rs.Path, rn))
131		if err != nil {
132			return err
133		}
134		r, err := rs.loadRepo(rn, rg)
135		if err != nil {
136			return err
137		}
138		rs.repos = append(rs.repos, r)
139	}
140	return nil
141}
142
143func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
144	r := &Repo{Name: name}
145	r.commits = make([]RepoCommit, 0)
146	r.Repository = rg
147	l, err := rg.Log(&git.LogOptions{All: true})
148	if err != nil {
149		return nil, err
150	}
151	err = l.ForEach(func(c *object.Commit) error {
152		if r.LastUpdated == nil {
153			r.LastUpdated = &c.Author.When
154			rf, err := c.File("README.md")
155			if err == nil {
156				rmd, err := rf.Contents()
157				if err == nil {
158					r.Readme = rmd
159				}
160			}
161		}
162		r.commits = append(r.commits, RepoCommit{Name: name, Commit: c})
163		return nil
164	})
165	if err != nil {
166		return nil, err
167	}
168	sort.Sort(r.commits)
169	return r, nil
170}
171
172// LatestFile returns the latest file at the specified path in the repository.
173func (r *Repo) LatestFile(path string) (string, error) {
174	lg, err := r.Repository.Log(&git.LogOptions{})
175	if err != nil {
176		return "", err
177	}
178	c, err := lg.Next()
179	if err != nil {
180		return "", err
181	}
182	f, err := c.File(path)
183	if err != nil {
184		return "", err
185	}
186	content, err := f.Contents()
187	if err != nil {
188		return "", err
189	}
190	return content, nil
191}