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