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