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
 19var ErrMissingRepo = errors.New("missing repo")
 20
 21type Repo struct {
 22	Name        string
 23	Repository  *git.Repository
 24	Readme      string
 25	LastUpdated *time.Time
 26}
 27
 28type RepoCommit struct {
 29	Name   string
 30	Commit *object.Commit
 31}
 32
 33type CommitLog []RepoCommit
 34
 35func (cl CommitLog) Len() int      { return len(cl) }
 36func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
 37func (cl CommitLog) Less(i, j int) bool {
 38	return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When)
 39}
 40
 41type RepoSource struct {
 42	Path    string
 43	mtx     sync.Mutex
 44	repos   []*Repo
 45	commits CommitLog
 46}
 47
 48func NewRepoSource(repoPath string) *RepoSource {
 49	err := os.MkdirAll(repoPath, os.ModeDir|os.FileMode(0700))
 50	if err != nil {
 51		log.Fatal(err)
 52	}
 53	rs := &RepoSource{Path: repoPath}
 54	return rs
 55}
 56
 57func (rs *RepoSource) AllRepos() []*Repo {
 58	rs.mtx.Lock()
 59	defer rs.mtx.Unlock()
 60	return rs.repos
 61}
 62
 63func (rs *RepoSource) GetRepo(name string) (*Repo, error) {
 64	rs.mtx.Lock()
 65	defer rs.mtx.Unlock()
 66	for _, r := range rs.repos {
 67		if r.Name == name {
 68			return r, nil
 69		}
 70	}
 71	return nil, ErrMissingRepo
 72}
 73
 74func (rs *RepoSource) InitRepo(name string, bare bool) (*Repo, error) {
 75	rs.mtx.Lock()
 76	defer rs.mtx.Unlock()
 77	rp := filepath.Join(rs.Path, name)
 78	rg, err := git.PlainInit(rp, bare)
 79	if err != nil {
 80		return nil, err
 81	}
 82	if bare {
 83		// Clone repo into memory storage
 84		ar, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
 85			URL: rp,
 86		})
 87		if err != nil && err != transport.ErrEmptyRemoteRepository {
 88			return nil, err
 89		}
 90		rg = ar
 91	}
 92	r := &Repo{
 93		Name:       name,
 94		Repository: rg,
 95	}
 96	rs.repos = append(rs.repos, r)
 97	return r, nil
 98}
 99
100func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
101	rs.mtx.Lock()
102	defer rs.mtx.Unlock()
103	if limit > len(rs.commits) {
104		limit = len(rs.commits)
105	}
106	return rs.commits[:limit]
107}
108
109func (rs *RepoSource) LoadRepos() error {
110	rs.mtx.Lock()
111	defer rs.mtx.Unlock()
112	rd, err := os.ReadDir(rs.Path)
113	if err != nil {
114		return err
115	}
116	rs.repos = make([]*Repo, 0)
117	rs.commits = make([]RepoCommit, 0)
118	for _, de := range rd {
119		rn := de.Name()
120		rg, err := git.PlainOpen(filepath.Join(rs.Path, rn))
121		if err != nil {
122			return err
123		}
124		r, err := rs.loadRepo(rn, rg)
125		if err != nil {
126			return err
127		}
128		rs.repos = append(rs.repos, r)
129	}
130	return nil
131}
132
133func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
134	r := &Repo{Name: name}
135	r.Repository = rg
136	l, err := rg.Log(&git.LogOptions{All: true})
137	if err != nil {
138		return nil, err
139	}
140	err = l.ForEach(func(c *object.Commit) error {
141		if r.LastUpdated == nil {
142			r.LastUpdated = &c.Author.When
143			rf, err := c.File("README.md")
144			if err == nil {
145				rmd, err := rf.Contents()
146				if err == nil {
147					r.Readme = rmd
148				}
149			}
150		}
151		rs.commits = append(rs.commits, RepoCommit{Name: name, Commit: c})
152		return nil
153	})
154	if err != nil {
155		return nil, err
156	}
157	sort.Sort(rs.commits)
158	return r, nil
159}
160
161func (r *Repo) LatestFile(path string) (string, error) {
162	lg, err := r.Repository.Log(&git.LogOptions{})
163	if err != nil {
164		return "", err
165	}
166	c, err := lg.Next()
167	if err != nil {
168		return "", err
169	}
170	f, err := c.File(path)
171	if err != nil {
172		return "", err
173	}
174	content, err := f.Contents()
175	if err != nil {
176		return "", err
177	}
178	return content, nil
179}