this repo has no description
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) Tags() ([]*TagReference, error) {
268 iter, err := g.r.Tags()
269 if err != nil {
270 return nil, fmt.Errorf("tag objects: %w", err)
271 }
272
273 tags := make([]*TagReference, 0)
274
275 if err := iter.ForEach(func(ref *plumbing.Reference) error {
276 obj, err := g.r.TagObject(ref.Hash())
277 switch err {
278 case nil:
279 tags = append(tags, &TagReference{
280 ref: ref,
281 tag: obj,
282 })
283 case plumbing.ErrObjectNotFound:
284 tags = append(tags, &TagReference{
285 ref: ref,
286 })
287 default:
288 return err
289 }
290 return nil
291 }); err != nil {
292 return nil, err
293 }
294
295 tagList := &TagList{r: g.r, refs: tags}
296 sort.Sort(tagList)
297 return tags, nil
298}
299
300func (g *GitRepo) Branches() ([]types.Branch, error) {
301 bi, err := g.r.Branches()
302 if err != nil {
303 return nil, fmt.Errorf("branchs: %w", err)
304 }
305
306 branches := []types.Branch{}
307
308 defaultBranch, err := g.FindMainBranch()
309
310 _ = bi.ForEach(func(ref *plumbing.Reference) error {
311 b := types.Branch{}
312 b.Hash = ref.Hash().String()
313 b.Name = ref.Name().Short()
314
315 // resolve commit that this branch points to
316 commit, _ := g.Commit(ref.Hash())
317 if commit != nil {
318 b.Commit = commit
319 }
320
321 if defaultBranch != "" && defaultBranch == b.Name {
322 b.IsDefault = true
323 }
324
325 branches = append(branches, b)
326
327 return nil
328 })
329
330 return branches, nil
331}
332
333func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
334 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
335 if err != nil {
336 return nil, fmt.Errorf("branch: %w", err)
337 }
338
339 if !ref.Name().IsBranch() {
340 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
341 }
342
343 return ref, nil
344}
345
346func (g *GitRepo) SetDefaultBranch(branch string) error {
347 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
348 return g.r.Storer.SetReference(ref)
349}
350
351func (g *GitRepo) FindMainBranch() (string, error) {
352 ref, err := g.r.Head()
353 if err != nil {
354 return "", fmt.Errorf("unable to find main branch: %w", err)
355 }
356 if ref.Name().IsBranch() {
357 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
358 }
359
360 return "", fmt.Errorf("unable to find main branch: %w", err)
361}
362
363// WriteTar writes itself from a tree into a binary tar file format.
364// prefix is root folder to be appended.
365func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
366 tw := tar.NewWriter(w)
367 defer tw.Close()
368
369 c, err := g.r.CommitObject(g.h)
370 if err != nil {
371 return fmt.Errorf("commit object: %w", err)
372 }
373
374 tree, err := c.Tree()
375 if err != nil {
376 return err
377 }
378
379 walker := object.NewTreeWalker(tree, true, nil)
380 defer walker.Close()
381
382 name, entry, err := walker.Next()
383 for ; err == nil; name, entry, err = walker.Next() {
384 info, err := newInfoWrapper(name, prefix, &entry, tree)
385 if err != nil {
386 return err
387 }
388
389 header, err := tar.FileInfoHeader(info, "")
390 if err != nil {
391 return err
392 }
393
394 err = tw.WriteHeader(header)
395 if err != nil {
396 return err
397 }
398
399 if !info.IsDir() {
400 file, err := tree.File(name)
401 if err != nil {
402 return err
403 }
404
405 reader, err := file.Blob.Reader()
406 if err != nil {
407 return err
408 }
409
410 _, err = io.Copy(tw, reader)
411 if err != nil {
412 reader.Close()
413 return err
414 }
415 reader.Close()
416 }
417 }
418
419 return nil
420}
421
422func newInfoWrapper(
423 name string,
424 prefix string,
425 entry *object.TreeEntry,
426 tree *object.Tree,
427) (*infoWrapper, error) {
428 var (
429 size int64
430 mode fs.FileMode
431 isDir bool
432 )
433
434 if entry.Mode.IsFile() {
435 file, err := tree.TreeEntryFile(entry)
436 if err != nil {
437 return nil, err
438 }
439 mode = fs.FileMode(file.Mode)
440
441 size, err = tree.Size(name)
442 if err != nil {
443 return nil, err
444 }
445 } else {
446 isDir = true
447 mode = fs.ModeDir | fs.ModePerm
448 }
449
450 fullname := path.Join(prefix, name)
451 return &infoWrapper{
452 name: fullname,
453 size: size,
454 mode: mode,
455 modTime: time.Unix(0, 0),
456 isDir: isDir,
457 }, nil
458}
459
460func (i *infoWrapper) Name() string {
461 return i.name
462}
463
464func (i *infoWrapper) Size() int64 {
465 return i.size
466}
467
468func (i *infoWrapper) Mode() fs.FileMode {
469 return i.mode
470}
471
472func (i *infoWrapper) ModTime() time.Time {
473 return i.modTime
474}
475
476func (i *infoWrapper) IsDir() bool {
477 return i.isDir
478}
479
480func (i *infoWrapper) Sys() any {
481 return nil
482}
483
484func (t *TagReference) Name() string {
485 return t.ref.Name().Short()
486}
487
488func (t *TagReference) Message() string {
489 if t.tag != nil {
490 return t.tag.Message
491 }
492 return ""
493}
494
495func (t *TagReference) TagObject() *object.Tag {
496 return t.tag
497}
498
499func (t *TagReference) Hash() plumbing.Hash {
500 return t.ref.Hash()
501}