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}
29
30// RepoCommit contains metadata for a Git commit.
31type RepoCommit struct {
32 Name string
33 Commit *object.Commit
34}
35
36// CommitLog is a series of Git commits.
37type CommitLog []RepoCommit
38
39func (cl CommitLog) Len() int { return len(cl) }
40func (cl CommitLog) Swap(i, j int) { cl[i], cl[j] = cl[j], cl[i] }
41func (cl CommitLog) Less(i, j int) bool {
42 return cl[i].Commit.Author.When.After(cl[j].Commit.Author.When)
43}
44
45// RepoSource is a reference to an on-disk repositories.
46type RepoSource struct {
47 Path string
48 mtx sync.Mutex
49 repos []*Repo
50 commits CommitLog
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
109// GetCommits returns commits for the repository.
110func (rs *RepoSource) GetCommits(limit int) []RepoCommit {
111 rs.mtx.Lock()
112 defer rs.mtx.Unlock()
113 if limit > len(rs.commits) {
114 limit = len(rs.commits)
115 }
116 return rs.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 rs.commits = make([]RepoCommit, 0)
129 for _, de := range rd {
130 rn := de.Name()
131 rg, err := git.PlainOpen(filepath.Join(rs.Path, rn))
132 if err != nil {
133 return err
134 }
135 r, err := rs.loadRepo(rn, rg)
136 if err != nil {
137 return err
138 }
139 rs.repos = append(rs.repos, r)
140 }
141 return nil
142}
143
144func (rs *RepoSource) loadRepo(name string, rg *git.Repository) (*Repo, error) {
145 r := &Repo{Name: name}
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 rs.commits = append(rs.commits, RepoCommit{Name: name, Commit: c})
163 return nil
164 })
165 if err != nil {
166 return nil, err
167 }
168 sort.Sort(rs.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}