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) Tree(path string) (*object.Tree, error) {
183 err := g.validateRef()
184 if err != nil {
185 return nil, err
186 }
187
188 c, err := g.repository.CommitObject(g.ref)
189 if err != nil {
190 return nil, err
191 }
192
193 tree, err := c.Tree()
194 if err != nil {
195 return nil, err
196 }
197
198 if path == "" {
199 return tree, nil
200 } else {
201 o, err := tree.FindEntry(path)
202 if err != nil {
203 return nil, err
204 }
205
206 if !o.Mode.IsFile() {
207 subtree, err := tree.Tree(path)
208 if err != nil {
209 return nil, err
210 }
211 return subtree, nil
212 } else {
213 return nil, TreeForFileErr
214 }
215 }
216}
217
218func (g *GitRepository) validateRef() error {
219 if !g.setRef {
220 return g.SetRef("")
221 }
222 return nil
223}
224
225func (g *GitRepository) IsBinary(path string) (bool, error) {
226 tree, err := g.Tree("")
227 if err != nil {
228 return false, err
229 }
230
231 file, err := tree.File(path)
232 if err != nil {
233 return false, err
234 }
235
236 return file.IsBinary()
237}
238
239func (g *GitRepository) FileContent(path string) ([]byte, error) {
240 err := g.validateRef()
241 if err != nil {
242 return nil, err
243 }
244
245 c, err := g.repository.CommitObject(g.ref)
246 if err != nil {
247 return nil, err
248 }
249
250 tree, err := c.Tree()
251 if err != nil {
252 return nil, err
253 }
254
255 file, err := tree.File(path)
256 if err != nil {
257 return nil, err
258 }
259
260 r, err := file.Blob.Reader()
261 if err != nil {
262 return nil, err
263 }
264 defer r.Close()
265
266 var buf bytes.Buffer
267 _, err = io.Copy(&buf, r)
268 if err != nil {
269 return nil, err
270 }
271
272 return buf.Bytes(), nil
273}
274
275func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
276 tw := tar.NewWriter(w)
277 defer tw.Close()
278
279 tree, err := g.Tree("")
280 if err != nil {
281 return err
282 }
283
284 walker := object.NewTreeWalker(tree, true, nil)
285 defer walker.Close()
286
287 name, entry, err := walker.Next()
288 for ; err == nil; name, entry, err = walker.Next() {
289 info, err := newInfoWrapper(name, prefix, &entry, tree)
290 if err != nil {
291 return err
292 }
293
294 header, err := tar.FileInfoHeader(info, "")
295 if err != nil {
296 return err
297 }
298
299 err = tw.WriteHeader(header)
300 if err != nil {
301 return err
302 }
303
304 if !info.IsDir() {
305 file, err := tree.File(name)
306 if err != nil {
307 return err
308 }
309
310 reader, err := file.Blob.Reader()
311 if err != nil {
312 return err
313 }
314
315 _, err = io.Copy(tw, reader)
316 if err != nil {
317 reader.Close()
318 return err
319 }
320 reader.Close()
321 }
322 }
323
324 return nil
325}
326
327func newInfoWrapper(
328 filename string,
329 prefix string,
330 entry *object.TreeEntry,
331 tree *object.Tree,
332) (*infoWrapper, error) {
333 var (
334 size int64
335 mode fs.FileMode
336 isDir bool
337 )
338
339 if entry.Mode.IsFile() {
340 file, err := tree.TreeEntryFile(entry)
341 if err != nil {
342 return nil, err
343 }
344 mode = fs.FileMode(file.Mode)
345
346 size, err = tree.Size(filename)
347 if err != nil {
348 return nil, err
349 }
350 } else {
351 isDir = true
352 mode = fs.ModeDir | fs.ModePerm
353 }
354
355 fullname := path.Join(prefix, filename)
356 return &infoWrapper{
357 name: fullname,
358 size: size,
359 mode: mode,
360 modTime: time.Unix(0, 0),
361 isDir: isDir,
362 }, nil
363}
364
365func (i *infoWrapper) Name() string {
366 return i.name
367}
368
369func (i *infoWrapper) Size() int64 {
370 return i.size
371}
372
373func (i *infoWrapper) Mode() fs.FileMode {
374 return i.mode
375}
376
377func (i *infoWrapper) ModTime() time.Time {
378 return i.modTime
379}
380
381func (i *infoWrapper) IsDir() bool {
382 return i.isDir
383}
384
385func (i *infoWrapper) Sys() any {
386 return nil
387}
388
389func (t *TagReference) HashString() string {
390 return t.ref.Hash().String()
391}
392
393func (t *TagReference) ShortName() string {
394 return t.ref.Name().Short()
395}
396
397func (t *TagReference) Message() string {
398 if t.tag != nil {
399 return t.tag.Message
400 }
401 return ""
402
403}
404
405func (self *tagList) Len() int {
406 return len(self.refs)
407}
408
409func (self *tagList) Swap(i, j int) {
410 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
411}
412
413func (self *tagList) Less(i, j int) bool {
414 var dateI time.Time
415 var dateJ time.Time
416
417 if self.refs[i].tag != nil {
418 dateI = self.refs[i].tag.Tagger.When
419 } else {
420 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
421 if err != nil {
422 dateI = time.Now()
423 } else {
424 dateI = c.Committer.When
425 }
426 }
427
428 if self.refs[j].tag != nil {
429 dateJ = self.refs[j].tag.Tagger.When
430 } else {
431 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
432 if err != nil {
433 dateJ = time.Now()
434 } else {
435 dateJ = c.Committer.When
436 }
437 }
438
439 return dateI.After(dateJ)
440}