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}