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}