1package git
2
3import (
4 "archive/tar"
5 "bytes"
6 "fmt"
7 "io"
8 "io/fs"
9 "path"
10 "strconv"
11 "strings"
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 "tangled.sh/tangled.sh/core/types"
18)
19
20var (
21 ErrBinaryFile = fmt.Errorf("binary file")
22 ErrNotBinaryFile = fmt.Errorf("not binary file")
23)
24
25type GitRepo struct {
26 path string
27 r *git.Repository
28 h plumbing.Hash
29}
30
31type TagList struct {
32 refs []*TagReference
33 r *git.Repository
34}
35
36// TagReference is used to list both tag and non-annotated tags.
37// Non-annotated tags should only contains a reference.
38// Annotated tags should contain its reference and its tag information.
39type TagReference struct {
40 ref *plumbing.Reference
41 tag *object.Tag
42}
43
44// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
45// to tar WriteHeader
46type infoWrapper struct {
47 name string
48 size int64
49 mode fs.FileMode
50 modTime time.Time
51 isDir bool
52}
53
54func (self *TagList) Len() int {
55 return len(self.refs)
56}
57
58func (self *TagList) Swap(i, j int) {
59 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
60}
61
62// sorting tags in reverse chronological order
63func (self *TagList) Less(i, j int) bool {
64 var dateI time.Time
65 var dateJ time.Time
66
67 if self.refs[i].tag != nil {
68 dateI = self.refs[i].tag.Tagger.When
69 } else {
70 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
71 if err != nil {
72 dateI = time.Now()
73 } else {
74 dateI = c.Committer.When
75 }
76 }
77
78 if self.refs[j].tag != nil {
79 dateJ = self.refs[j].tag.Tagger.When
80 } else {
81 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
82 if err != nil {
83 dateJ = time.Now()
84 } else {
85 dateJ = c.Committer.When
86 }
87 }
88
89 return dateI.After(dateJ)
90}
91
92func Open(path string, ref string) (*GitRepo, error) {
93 var err error
94 g := GitRepo{path: path}
95 g.r, err = git.PlainOpen(path)
96 if err != nil {
97 return nil, fmt.Errorf("opening %s: %w", path, err)
98 }
99
100 if ref == "" {
101 head, err := g.r.Head()
102 if err != nil {
103 return nil, fmt.Errorf("getting head of %s: %w", path, err)
104 }
105 g.h = head.Hash()
106 } else {
107 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
108 if err != nil {
109 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
110 }
111 g.h = *hash
112 }
113 return &g, nil
114}
115
116func PlainOpen(path string) (*GitRepo, error) {
117 var err error
118 g := GitRepo{path: path}
119 g.r, err = git.PlainOpen(path)
120 if err != nil {
121 return nil, fmt.Errorf("opening %s: %w", path, err)
122 }
123 return &g, nil
124}
125
126func (g *GitRepo) Commits(offset, limit int) ([]*object.Commit, error) {
127 commits := []*object.Commit{}
128
129 output, err := g.revList(
130 g.h.String(),
131 fmt.Sprintf("--skip=%d", offset),
132 fmt.Sprintf("--max-count=%d", limit),
133 )
134 if err != nil {
135 return nil, fmt.Errorf("commits from ref: %w", err)
136 }
137
138 lines := strings.Split(strings.TrimSpace(string(output)), "\n")
139 if len(lines) == 1 && lines[0] == "" {
140 return commits, nil
141 }
142
143 for _, item := range lines {
144 obj, err := g.r.CommitObject(plumbing.NewHash(item))
145 if err != nil {
146 continue
147 }
148 commits = append(commits, obj)
149 }
150
151 return commits, nil
152}
153
154func (g *GitRepo) TotalCommits() (int, error) {
155 output, err := g.revList(
156 g.h.String(),
157 fmt.Sprintf("--count"),
158 )
159 if err != nil {
160 return 0, fmt.Errorf("failed to run rev-list: %w", err)
161 }
162
163 count, err := strconv.Atoi(strings.TrimSpace(string(output)))
164 if err != nil {
165 return 0, err
166 }
167
168 return count, nil
169}
170
171func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) {
172 return g.r.CommitObject(h)
173}
174
175func (g *GitRepo) LastCommit() (*object.Commit, error) {
176 c, err := g.r.CommitObject(g.h)
177 if err != nil {
178 return nil, fmt.Errorf("last commit: %w", err)
179 }
180 return c, nil
181}
182
183func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
184 c, err := g.r.CommitObject(g.h)
185 if err != nil {
186 return nil, fmt.Errorf("commit object: %w", err)
187 }
188
189 tree, err := c.Tree()
190 if err != nil {
191 return nil, fmt.Errorf("file tree: %w", err)
192 }
193
194 file, err := tree.File(path)
195 if err != nil {
196 return nil, err
197 }
198
199 isbin, _ := file.IsBinary()
200 if isbin {
201 return nil, ErrBinaryFile
202 }
203
204 reader, err := file.Reader()
205 if err != nil {
206 return nil, err
207 }
208
209 buf := new(bytes.Buffer)
210 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil {
211 return nil, err
212 }
213
214 return buf.Bytes(), nil
215}
216
217func (g *GitRepo) FileContent(path string) (string, error) {
218 c, err := g.r.CommitObject(g.h)
219 if err != nil {
220 return "", fmt.Errorf("commit object: %w", err)
221 }
222
223 tree, err := c.Tree()
224 if err != nil {
225 return "", fmt.Errorf("file tree: %w", err)
226 }
227
228 file, err := tree.File(path)
229 if err != nil {
230 return "", err
231 }
232
233 isbin, _ := file.IsBinary()
234
235 if !isbin {
236 return file.Contents()
237 } else {
238 return "", ErrBinaryFile
239 }
240}
241
242func (g *GitRepo) RawContent(path string) ([]byte, error) {
243 c, err := g.r.CommitObject(g.h)
244 if err != nil {
245 return nil, fmt.Errorf("commit object: %w", err)
246 }
247
248 tree, err := c.Tree()
249 if err != nil {
250 return nil, fmt.Errorf("file tree: %w", err)
251 }
252
253 file, err := tree.File(path)
254 if err != nil {
255 return nil, err
256 }
257
258 reader, err := file.Reader()
259 if err != nil {
260 return nil, fmt.Errorf("opening file reader: %w", err)
261 }
262 defer reader.Close()
263
264 return io.ReadAll(reader)
265}
266
267func (g *GitRepo) Branches() ([]types.Branch, error) {
268 bi, err := g.r.Branches()
269 if err != nil {
270 return nil, fmt.Errorf("branchs: %w", err)
271 }
272
273 branches := []types.Branch{}
274
275 defaultBranch, err := g.FindMainBranch()
276
277 _ = bi.ForEach(func(ref *plumbing.Reference) error {
278 b := types.Branch{}
279 b.Hash = ref.Hash().String()
280 b.Name = ref.Name().Short()
281
282 // resolve commit that this branch points to
283 commit, _ := g.Commit(ref.Hash())
284 if commit != nil {
285 b.Commit = commit
286 }
287
288 if defaultBranch != "" && defaultBranch == b.Name {
289 b.IsDefault = true
290 }
291
292 branches = append(branches, b)
293
294 return nil
295 })
296
297 return branches, nil
298}
299
300func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
301 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
302 if err != nil {
303 return nil, fmt.Errorf("branch: %w", err)
304 }
305
306 if !ref.Name().IsBranch() {
307 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
308 }
309
310 return ref, nil
311}
312
313func (g *GitRepo) SetDefaultBranch(branch string) error {
314 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
315 return g.r.Storer.SetReference(ref)
316}
317
318func (g *GitRepo) FindMainBranch() (string, error) {
319 ref, err := g.r.Head()
320 if err != nil {
321 return "", fmt.Errorf("unable to find main branch: %w", err)
322 }
323 if ref.Name().IsBranch() {
324 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
325 }
326
327 return "", fmt.Errorf("unable to find main branch: %w", err)
328}
329
330// WriteTar writes itself from a tree into a binary tar file format.
331// prefix is root folder to be appended.
332func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
333 tw := tar.NewWriter(w)
334 defer tw.Close()
335
336 c, err := g.r.CommitObject(g.h)
337 if err != nil {
338 return fmt.Errorf("commit object: %w", err)
339 }
340
341 tree, err := c.Tree()
342 if err != nil {
343 return err
344 }
345
346 walker := object.NewTreeWalker(tree, true, nil)
347 defer walker.Close()
348
349 name, entry, err := walker.Next()
350 for ; err == nil; name, entry, err = walker.Next() {
351 info, err := newInfoWrapper(name, prefix, &entry, tree)
352 if err != nil {
353 return err
354 }
355
356 header, err := tar.FileInfoHeader(info, "")
357 if err != nil {
358 return err
359 }
360
361 err = tw.WriteHeader(header)
362 if err != nil {
363 return err
364 }
365
366 if !info.IsDir() {
367 file, err := tree.File(name)
368 if err != nil {
369 return err
370 }
371
372 reader, err := file.Blob.Reader()
373 if err != nil {
374 return err
375 }
376
377 _, err = io.Copy(tw, reader)
378 if err != nil {
379 reader.Close()
380 return err
381 }
382 reader.Close()
383 }
384 }
385
386 return nil
387}
388
389func newInfoWrapper(
390 name string,
391 prefix string,
392 entry *object.TreeEntry,
393 tree *object.Tree,
394) (*infoWrapper, error) {
395 var (
396 size int64
397 mode fs.FileMode
398 isDir bool
399 )
400
401 if entry.Mode.IsFile() {
402 file, err := tree.TreeEntryFile(entry)
403 if err != nil {
404 return nil, err
405 }
406 mode = fs.FileMode(file.Mode)
407
408 size, err = tree.Size(name)
409 if err != nil {
410 return nil, err
411 }
412 } else {
413 isDir = true
414 mode = fs.ModeDir | fs.ModePerm
415 }
416
417 fullname := path.Join(prefix, name)
418 return &infoWrapper{
419 name: fullname,
420 size: size,
421 mode: mode,
422 modTime: time.Unix(0, 0),
423 isDir: isDir,
424 }, nil
425}
426
427func (i *infoWrapper) Name() string {
428 return i.name
429}
430
431func (i *infoWrapper) Size() int64 {
432 return i.size
433}
434
435func (i *infoWrapper) Mode() fs.FileMode {
436 return i.mode
437}
438
439func (i *infoWrapper) ModTime() time.Time {
440 return i.modTime
441}
442
443func (i *infoWrapper) IsDir() bool {
444 return i.isDir
445}
446
447func (i *infoWrapper) Sys() any {
448 return nil
449}
450
451func (t *TagReference) Name() string {
452 return t.ref.Name().Short()
453}
454
455func (t *TagReference) Message() string {
456 if t.tag != nil {
457 return t.tag.Message
458 }
459 return ""
460}
461
462func (t *TagReference) TagObject() *object.Tag {
463 return t.tag
464}
465
466func (t *TagReference) Hash() plumbing.Hash {
467 return t.ref.Hash()
468}