1package git
2
3import (
4 "archive/tar"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "io/fs"
10 "path"
11 "sort"
12 "time"
13
14 "github.com/go-git/go-git/v5"
15 "github.com/go-git/go-git/v5/plumbing"
16 "github.com/go-git/go-git/v5/plumbing/object"
17)
18
19var ()
20
21var (
22 MissingRefErr = errors.New("Reference not found")
23 TreeForFileErr = errors.New("Trying to get tree of a file")
24)
25
26type (
27 GitRepository struct {
28 path string
29 repository *git.Repository
30 ref plumbing.Hash
31 setRef bool
32 }
33 TagReference struct {
34 ref *plumbing.Reference
35 tag *object.Tag
36 }
37 infoWrapper struct {
38 name string
39 size int64
40 mode fs.FileMode
41 modTime time.Time
42 isDir bool
43 }
44 tagList struct {
45 refs []*TagReference
46 r *git.Repository
47 }
48)
49
50func OpenRepository(dir string) (*GitRepository, error) {
51 g := &GitRepository{
52 path: dir,
53 }
54
55 repo, err := git.PlainOpen(dir)
56 if err != nil {
57 return nil, err
58 }
59 g.repository = repo
60
61 return g, nil
62}
63
64func (g *GitRepository) SetRef(ref string) error {
65 if ref == "" {
66 head, err := g.repository.Head()
67 if err != nil {
68 return errors.Join(MissingRefErr, err)
69 }
70 g.ref = head.Hash()
71 } else {
72 hash, err := g.repository.ResolveRevision(plumbing.Revision(ref))
73 if err != nil {
74 return errors.Join(MissingRefErr, err)
75 }
76 g.ref = *hash
77 }
78 g.setRef = true
79 return nil
80}
81
82func (g *GitRepository) Path() string {
83 return g.path
84}
85
86func (g *GitRepository) LastCommit() (*object.Commit, error) {
87 err := g.validateRef()
88 if err != nil {
89 return nil, err
90 }
91
92 c, err := g.repository.CommitObject(g.ref)
93 if err != nil {
94 return nil, err
95 }
96 return c, nil
97}
98
99func (g *GitRepository) Commits(count int) ([]*object.Commit, error) {
100 err := g.validateRef()
101 if err != nil {
102 return nil, err
103 }
104
105 ci, err := g.repository.Log(&git.LogOptions{From: g.ref})
106 if err != nil {
107 return nil, fmt.Errorf("commits from ref: %w", err)
108 }
109
110 commits := []*object.Commit{}
111 // TODO: for now only load first 1000
112 for x := 0; x < count; x++ {
113 c, err := ci.Next()
114 if err != nil && errors.Is(err, io.EOF) {
115 break
116 } else if err != nil {
117 return nil, err
118 }
119 commits = append(commits, c)
120 }
121
122 return commits, nil
123}
124
125func (g *GitRepository) Head() (*plumbing.Reference, error) {
126 return g.repository.Head()
127}
128
129func (g *GitRepository) Tags() ([]*TagReference, error) {
130 iter, err := g.repository.Tags()
131 if err != nil {
132 return nil, err
133 }
134
135 tags := make([]*TagReference, 0)
136
137 if err := iter.ForEach(func(ref *plumbing.Reference) error {
138 obj, err := g.repository.TagObject(ref.Hash())
139 switch err {
140 case nil:
141 tags = append(tags, &TagReference{
142 ref: ref,
143 tag: obj,
144 })
145 case plumbing.ErrObjectNotFound:
146 tags = append(tags, &TagReference{
147 ref: ref,
148 })
149 default:
150 return err
151 }
152 return nil
153 }); err != nil {
154 return nil, err
155 }
156
157 // tagList modify the underlying tag list.
158 tagList := &tagList{r: g.repository, refs: tags}
159 sort.Sort(tagList)
160
161 return tags, nil
162}
163
164func (g *GitRepository) Branches() ([]*plumbing.Reference, error) {
165 bs, err := g.repository.Branches()
166 if err != nil {
167 return nil, err
168 }
169
170 branches := []*plumbing.Reference{}
171 err = bs.ForEach(func(ref *plumbing.Reference) error {
172 branches = append(branches, ref)
173 return nil
174 })
175 if err != nil {
176 return nil, err
177 }
178
179 return branches, nil
180}
181
182func (g *GitRepository) Diff() (string, error) {
183 err := g.validateRef()
184 if err != nil {
185 return "", err
186 }
187
188 c, err := g.repository.CommitObject(g.ref)
189 if err != nil {
190 return "", err
191 }
192
193 commitTree, err := c.Tree()
194 if err != nil {
195 return "", err
196 }
197
198 patch := &object.Patch{}
199 parentTree := &object.Tree{}
200 if c.NumParents() != 0 {
201 parent, err := c.Parents().Next()
202 if err == nil {
203 parentTree, err = parent.Tree()
204 if err == nil {
205 patch, err = parentTree.Patch(commitTree)
206 if err != nil {
207 return "", err
208 }
209 }
210 }
211 } else {
212 patch, err = parentTree.Patch(commitTree)
213 if err != nil {
214 return "", err
215 }
216 }
217
218 return patch.String(), nil
219}
220
221func (g *GitRepository) Tree(path string) (*object.Tree, error) {
222 err := g.validateRef()
223 if err != nil {
224 return nil, err
225 }
226
227 c, err := g.repository.CommitObject(g.ref)
228 if err != nil {
229 return nil, err
230 }
231
232 tree, err := c.Tree()
233 if err != nil {
234 return nil, err
235 }
236
237 if path == "" {
238 return tree, nil
239 } else {
240 o, err := tree.FindEntry(path)
241 if err != nil {
242 return nil, err
243 }
244
245 if !o.Mode.IsFile() {
246 subtree, err := tree.Tree(path)
247 if err != nil {
248 return nil, err
249 }
250 return subtree, nil
251 } else {
252 return nil, TreeForFileErr
253 }
254 }
255}
256
257func (g *GitRepository) validateRef() error {
258 if !g.setRef {
259 return g.SetRef("")
260 }
261 return nil
262}
263
264func (g *GitRepository) IsBinary(path string) (bool, error) {
265 tree, err := g.Tree("")
266 if err != nil {
267 return false, err
268 }
269
270 file, err := tree.File(path)
271 if err != nil {
272 return false, err
273 }
274
275 return file.IsBinary()
276}
277
278func (g *GitRepository) FileContent(path string) ([]byte, error) {
279 err := g.validateRef()
280 if err != nil {
281 return nil, err
282 }
283
284 c, err := g.repository.CommitObject(g.ref)
285 if err != nil {
286 return nil, err
287 }
288
289 tree, err := c.Tree()
290 if err != nil {
291 return nil, err
292 }
293
294 file, err := tree.File(path)
295 if err != nil {
296 return nil, err
297 }
298
299 r, err := file.Blob.Reader()
300 if err != nil {
301 return nil, err
302 }
303 defer r.Close()
304
305 var buf bytes.Buffer
306 _, err = io.Copy(&buf, r)
307 if err != nil {
308 return nil, err
309 }
310
311 return buf.Bytes(), nil
312}
313
314func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
315 tw := tar.NewWriter(w)
316 defer tw.Close()
317
318 tree, err := g.Tree("")
319 if err != nil {
320 return err
321 }
322
323 walker := object.NewTreeWalker(tree, true, nil)
324 defer walker.Close()
325
326 name, entry, err := walker.Next()
327 for ; err == nil; name, entry, err = walker.Next() {
328 info, err := newInfoWrapper(name, prefix, &entry, tree)
329 if err != nil {
330 return err
331 }
332
333 header, err := tar.FileInfoHeader(info, "")
334 if err != nil {
335 return err
336 }
337
338 err = tw.WriteHeader(header)
339 if err != nil {
340 return err
341 }
342
343 if !info.IsDir() {
344 file, err := tree.File(name)
345 if err != nil {
346 return err
347 }
348
349 reader, err := file.Blob.Reader()
350 if err != nil {
351 return err
352 }
353
354 _, err = io.Copy(tw, reader)
355 if err != nil {
356 reader.Close()
357 return err
358 }
359 reader.Close()
360 }
361 }
362
363 return nil
364}
365
366func newInfoWrapper(
367 filename string,
368 prefix string,
369 entry *object.TreeEntry,
370 tree *object.Tree,
371) (*infoWrapper, error) {
372 var (
373 size int64
374 mode fs.FileMode
375 isDir bool
376 )
377
378 if entry.Mode.IsFile() {
379 file, err := tree.TreeEntryFile(entry)
380 if err != nil {
381 return nil, err
382 }
383 mode = fs.FileMode(file.Mode)
384
385 size, err = tree.Size(filename)
386 if err != nil {
387 return nil, err
388 }
389 } else {
390 isDir = true
391 mode = fs.ModeDir | fs.ModePerm
392 }
393
394 fullname := path.Join(prefix, filename)
395 return &infoWrapper{
396 name: fullname,
397 size: size,
398 mode: mode,
399 modTime: time.Unix(0, 0),
400 isDir: isDir,
401 }, nil
402}
403
404func (i *infoWrapper) Name() string {
405 return i.name
406}
407
408func (i *infoWrapper) Size() int64 {
409 return i.size
410}
411
412func (i *infoWrapper) Mode() fs.FileMode {
413 return i.mode
414}
415
416func (i *infoWrapper) ModTime() time.Time {
417 return i.modTime
418}
419
420func (i *infoWrapper) IsDir() bool {
421 return i.isDir
422}
423
424func (i *infoWrapper) Sys() any {
425 return nil
426}
427
428func (t *TagReference) HashString() string {
429 return t.ref.Hash().String()
430}
431
432func (t *TagReference) ShortName() string {
433 return t.ref.Name().Short()
434}
435
436func (t *TagReference) Message() string {
437 if t.tag != nil {
438 return t.tag.Message
439 }
440 return ""
441
442}
443
444func (self *tagList) Len() int {
445 return len(self.refs)
446}
447
448func (self *tagList) Swap(i, j int) {
449 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
450}
451
452func (self *tagList) Less(i, j int) bool {
453 var dateI time.Time
454 var dateJ time.Time
455
456 if self.refs[i].tag != nil {
457 dateI = self.refs[i].tag.Tagger.When
458 } else {
459 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
460 if err != nil {
461 dateI = time.Now()
462 } else {
463 dateI = c.Committer.When
464 }
465 }
466
467 if self.refs[j].tag != nil {
468 dateJ = self.refs[j].tag.Tagger.When
469 } else {
470 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
471 if err != nil {
472 dateJ = time.Now()
473 } else {
474 dateJ = c.Committer.When
475 }
476 }
477
478 return dateI.After(dateJ)
479}