1package git
2
3import (
4 "archive/tar"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "path"
10 "time"
11
12 "github.com/go-git/go-git/v5"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/object"
15)
16
17var ()
18
19var (
20 MissingRefErr = errors.New("Reference not found")
21 TreeForFileErr = errors.New("Trying to get tree of a file")
22)
23
24type (
25 GitRepository struct {
26 path string
27 repository *git.Repository
28
29 ref plumbing.Hash
30 // this is setRef when ref is setRef
31 setRef bool
32 }
33 infoWrapper struct {
34 name string
35 size int64
36 mode fs.FileMode
37 modTime time.Time
38 isDir bool
39 }
40)
41
42func OpenRepository(dir string) (*GitRepository, error) {
43 g := &GitRepository{
44 path: dir,
45 }
46
47 repo, err := git.PlainOpen(dir)
48 if err != nil {
49 return nil, err
50 }
51 g.repository = repo
52
53 return g, nil
54}
55
56func (g *GitRepository) SetRef(ref string) error {
57 if ref == "" {
58 head, err := g.repository.Head()
59 if err != nil {
60 return errors.Join(MissingRefErr, err)
61 }
62 g.ref = head.Hash()
63 } else {
64 hash, err := g.repository.ResolveRevision(plumbing.Revision(ref))
65 if err != nil {
66 return errors.Join(MissingRefErr, err)
67 }
68 g.ref = *hash
69 }
70 g.setRef = true
71 return nil
72}
73
74func (g *GitRepository) Path() string {
75 return g.path
76}
77
78func (g *GitRepository) LastCommit() (*object.Commit, error) {
79 err := g.validateRef()
80 if err != nil {
81 return nil, err
82 }
83
84 c, err := g.repository.CommitObject(g.ref)
85 if err != nil {
86 return nil, err
87 }
88 return c, nil
89}
90
91func (g *GitRepository) Commits(count int) ([]*object.Commit, error) {
92 err := g.validateRef()
93 if err != nil {
94 return nil, err
95 }
96
97 ci, err := g.repository.Log(&git.LogOptions{From: g.ref})
98 if err != nil {
99 return nil, fmt.Errorf("commits from ref: %w", err)
100 }
101
102 commits := []*object.Commit{}
103 // TODO: for now only load first 1000
104 for x := 0; x < count; x++ {
105 c, err := ci.Next()
106 if err != nil && errors.Is(err, io.EOF) {
107 break
108 } else if err != nil {
109 return nil, err
110 }
111 commits = append(commits, c)
112 }
113
114 return commits, nil
115}
116
117func (g *GitRepository) Head() (*plumbing.Reference, error) {
118 return g.repository.Head()
119}
120
121func (g *GitRepository) Tags() ([]*plumbing.Reference, error) {
122 ti, err := g.repository.Tags()
123 if err != nil {
124 return nil, err
125 }
126
127 tags := []*plumbing.Reference{}
128 err = ti.ForEach(func(t *plumbing.Reference) error {
129 tags = append(tags, t)
130 return nil
131 })
132 if err != nil {
133 return nil, err
134 }
135
136 return tags, nil
137}
138
139func (g *GitRepository) Branches() ([]*plumbing.Reference, error) {
140 bs, err := g.repository.Branches()
141 if err != nil {
142 return nil, err
143 }
144
145 branches := []*plumbing.Reference{}
146 err = bs.ForEach(func(ref *plumbing.Reference) error {
147 branches = append(branches, ref)
148 return nil
149 })
150 if err != nil {
151 return nil, err
152 }
153
154 return branches, nil
155}
156
157func (g *GitRepository) Tree(path string) (*object.Tree, error) {
158 err := g.validateRef()
159 if err != nil {
160 return nil, err
161 }
162
163 c, err := g.repository.CommitObject(g.ref)
164 if err != nil {
165 return nil, err
166 }
167
168 tree, err := c.Tree()
169 if err != nil {
170 return nil, err
171 }
172
173 if path == "" {
174 return tree, nil
175 } else {
176 o, err := tree.FindEntry(path)
177 if err != nil {
178 return nil, err
179 }
180
181 if !o.Mode.IsFile() {
182 subtree, err := tree.Tree(path)
183 if err != nil {
184 return nil, err
185 }
186 return subtree, nil
187 } else {
188 return nil, TreeForFileErr
189 }
190 }
191}
192
193func (g *GitRepository) validateRef() error {
194 if !g.setRef {
195 return g.SetRef("")
196 }
197 return nil
198}
199
200func (g *GitRepository) FileContent(path string) (string, error) {
201 c, err := g.repository.CommitObject(g.ref)
202 if err != nil {
203 return "", err
204 }
205
206 tree, err := c.Tree()
207 if err != nil {
208 return "", err
209 }
210
211 file, err := tree.File(path)
212 if err != nil {
213 return "", err
214 }
215
216 isbin, err := file.IsBinary()
217 if err != nil {
218 return "", err
219 }
220
221 if !isbin {
222 return file.Contents()
223 } else {
224 return "Binary file", nil
225 }
226}
227
228func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
229 tw := tar.NewWriter(w)
230 defer tw.Close()
231
232 tree, err := g.Tree("")
233 if err != nil {
234 return err
235 }
236
237 walker := object.NewTreeWalker(tree, true, nil)
238 defer walker.Close()
239
240 name, entry, err := walker.Next()
241 for ; err == nil; name, entry, err = walker.Next() {
242 info, err := newInfoWrapper(name, prefix, &entry, tree)
243 if err != nil {
244 return err
245 }
246
247 header, err := tar.FileInfoHeader(info, "")
248 if err != nil {
249 return err
250 }
251
252 err = tw.WriteHeader(header)
253 if err != nil {
254 return err
255 }
256
257 if !info.IsDir() {
258 c, err := g.FileContent(name)
259 if err != nil {
260 return err
261 }
262
263 _, err = tw.Write([]byte(c))
264 if err != nil {
265 return err
266 }
267 }
268 }
269
270 return nil
271}
272
273func newInfoWrapper(
274 filename string,
275 prefix string,
276 entry *object.TreeEntry,
277 tree *object.Tree,
278) (*infoWrapper, error) {
279 var (
280 size int64
281 mode fs.FileMode
282 isDir bool
283 )
284
285 if entry.Mode.IsFile() {
286 file, err := tree.TreeEntryFile(entry)
287 if err != nil {
288 return nil, err
289 }
290 mode = fs.FileMode(file.Mode)
291
292 size, err = tree.Size(filename)
293 if err != nil {
294 return nil, err
295 }
296 } else {
297 isDir = true
298 mode = fs.ModeDir | fs.ModePerm
299 }
300
301 fullname := path.Join(prefix, filename)
302 return &infoWrapper{
303 name: fullname,
304 size: size,
305 mode: mode,
306 modTime: time.Unix(0, 0),
307 isDir: isDir,
308 }, nil
309}
310
311func (i *infoWrapper) Name() string {
312 return i.name
313}
314
315func (i *infoWrapper) Size() int64 {
316 return i.size
317}
318
319func (i *infoWrapper) Mode() fs.FileMode {
320 return i.mode
321}
322
323func (i *infoWrapper) ModTime() time.Time {
324 return i.modTime
325}
326
327func (i *infoWrapper) IsDir() bool {
328 return i.isDir
329}
330
331func (i *infoWrapper) Sys() any {
332 return nil
333}