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