forked from
tangled.org/core
fork
Configure Feed
Select the types of activity you want to include in your feed.
this repo has no description
fork
Configure Feed
Select the types of activity you want to include in your feed.
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}