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