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