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 "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() ([]*object.Commit, error) {
146 ci, err := g.r.Log(&git.LogOptions{From: g.h})
147 if err != nil {
148 return nil, fmt.Errorf("commits from ref: %w", err)
149 }
150
151 commits := []*object.Commit{}
152 ci.ForEach(func(c *object.Commit) error {
153 commits = append(commits, c)
154 return nil
155 })
156
157 return commits, nil
158}
159
160func (g *GitRepo) Commit(h plumbing.Hash) (*object.Commit, error) {
161 return g.r.CommitObject(h)
162}
163
164func (g *GitRepo) LastCommit() (*object.Commit, error) {
165 c, err := g.r.CommitObject(g.h)
166 if err != nil {
167 return nil, fmt.Errorf("last commit: %w", err)
168 }
169 return c, nil
170}
171
172func (g *GitRepo) FileContentN(path string, cap int64) ([]byte, error) {
173 buf := []byte{}
174
175 c, err := g.r.CommitObject(g.h)
176 if err != nil {
177 return nil, fmt.Errorf("commit object: %w", err)
178 }
179
180 tree, err := c.Tree()
181 if err != nil {
182 return nil, fmt.Errorf("file tree: %w", err)
183 }
184
185 file, err := tree.File(path)
186 if err != nil {
187 return nil, err
188 }
189
190 isbin, _ := file.IsBinary()
191
192 if !isbin {
193 reader, err := file.Reader()
194 if err != nil {
195 return nil, err
196 }
197 bufReader := io.LimitReader(reader, cap)
198 _, err = bufReader.Read(buf)
199 if err != nil {
200 return nil, err
201 }
202 return buf, nil
203 } else {
204 return nil, ErrBinaryFile
205 }
206}
207
208func (g *GitRepo) FileContent(path string) (string, error) {
209 c, err := g.r.CommitObject(g.h)
210 if err != nil {
211 return "", fmt.Errorf("commit object: %w", err)
212 }
213
214 tree, err := c.Tree()
215 if err != nil {
216 return "", fmt.Errorf("file tree: %w", err)
217 }
218
219 file, err := tree.File(path)
220 if err != nil {
221 return "", err
222 }
223
224 isbin, _ := file.IsBinary()
225
226 if !isbin {
227 return file.Contents()
228 } else {
229 return "", ErrBinaryFile
230 }
231}
232
233func (g *GitRepo) RawContent(path string) ([]byte, error) {
234 c, err := g.r.CommitObject(g.h)
235 if err != nil {
236 return nil, fmt.Errorf("commit object: %w", err)
237 }
238
239 tree, err := c.Tree()
240 if err != nil {
241 return nil, fmt.Errorf("file tree: %w", err)
242 }
243
244 file, err := tree.File(path)
245 if err != nil {
246 return nil, err
247 }
248
249 reader, err := file.Reader()
250 if err != nil {
251 return nil, fmt.Errorf("opening file reader: %w", err)
252 }
253 defer reader.Close()
254
255 return io.ReadAll(reader)
256}
257
258func (g *GitRepo) Tags() ([]*TagReference, error) {
259 iter, err := g.r.Tags()
260 if err != nil {
261 return nil, fmt.Errorf("tag objects: %w", err)
262 }
263
264 tags := make([]*TagReference, 0)
265
266 if err := iter.ForEach(func(ref *plumbing.Reference) error {
267 obj, err := g.r.TagObject(ref.Hash())
268 switch err {
269 case nil:
270 tags = append(tags, &TagReference{
271 ref: ref,
272 tag: obj,
273 })
274 case plumbing.ErrObjectNotFound:
275 tags = append(tags, &TagReference{
276 ref: ref,
277 })
278 default:
279 return err
280 }
281 return nil
282 }); err != nil {
283 return nil, err
284 }
285
286 tagList := &TagList{r: g.r, refs: tags}
287 sort.Sort(tagList)
288 return tags, nil
289}
290
291func (g *GitRepo) Branches() ([]types.Branch, error) {
292 bi, err := g.r.Branches()
293 if err != nil {
294 return nil, fmt.Errorf("branchs: %w", err)
295 }
296
297 branches := []types.Branch{}
298
299 defaultBranch, err := g.FindMainBranch()
300
301 _ = bi.ForEach(func(ref *plumbing.Reference) error {
302 b := types.Branch{}
303 b.Hash = ref.Hash().String()
304 b.Name = ref.Name().Short()
305
306 // resolve commit that this branch points to
307 commit, _ := g.Commit(ref.Hash())
308 if commit != nil {
309 b.Commit = commit
310 }
311
312 if defaultBranch != "" && defaultBranch == b.Name {
313 b.IsDefault = true
314 }
315
316 branches = append(branches, b)
317
318 return nil
319 })
320
321 return branches, nil
322}
323
324func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
325 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
326 if err != nil {
327 return nil, fmt.Errorf("branch: %w", err)
328 }
329
330 if !ref.Name().IsBranch() {
331 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
332 }
333
334 return ref, nil
335}
336
337func (g *GitRepo) SetDefaultBranch(branch string) error {
338 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
339 return g.r.Storer.SetReference(ref)
340}
341
342func (g *GitRepo) FindMainBranch() (string, error) {
343 ref, err := g.r.Head()
344 if err != nil {
345 return "", fmt.Errorf("unable to find main branch: %w", err)
346 }
347 if ref.Name().IsBranch() {
348 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
349 }
350
351 return "", fmt.Errorf("unable to find main branch: %w", err)
352}
353
354// WriteTar writes itself from a tree into a binary tar file format.
355// prefix is root folder to be appended.
356func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
357 tw := tar.NewWriter(w)
358 defer tw.Close()
359
360 c, err := g.r.CommitObject(g.h)
361 if err != nil {
362 return fmt.Errorf("commit object: %w", err)
363 }
364
365 tree, err := c.Tree()
366 if err != nil {
367 return err
368 }
369
370 walker := object.NewTreeWalker(tree, true, nil)
371 defer walker.Close()
372
373 name, entry, err := walker.Next()
374 for ; err == nil; name, entry, err = walker.Next() {
375 info, err := newInfoWrapper(name, prefix, &entry, tree)
376 if err != nil {
377 return err
378 }
379
380 header, err := tar.FileInfoHeader(info, "")
381 if err != nil {
382 return err
383 }
384
385 err = tw.WriteHeader(header)
386 if err != nil {
387 return err
388 }
389
390 if !info.IsDir() {
391 file, err := tree.File(name)
392 if err != nil {
393 return err
394 }
395
396 reader, err := file.Blob.Reader()
397 if err != nil {
398 return err
399 }
400
401 _, err = io.Copy(tw, reader)
402 if err != nil {
403 reader.Close()
404 return err
405 }
406 reader.Close()
407 }
408 }
409
410 return nil
411}
412
413func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) {
414 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path)
415 cacheMu.RLock()
416 if commitInfo, found := commitCache.Get(cacheKey); found {
417 cacheMu.RUnlock()
418 return commitInfo.(*types.LastCommitInfo), nil
419 }
420 cacheMu.RUnlock()
421
422 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path)
423
424 var out bytes.Buffer
425 cmd.Stdout = &out
426 cmd.Stderr = &out
427
428 if err := cmd.Run(); err != nil {
429 return nil, fmt.Errorf("failed to get commit hash: %w", err)
430 }
431
432 output := strings.TrimSpace(out.String())
433 if output == "" {
434 return nil, fmt.Errorf("no commits found for path: %s", path)
435 }
436
437 parts := strings.SplitN(output, " ", 2)
438 if len(parts) < 2 {
439 return nil, fmt.Errorf("unexpected commit log format")
440 }
441
442 commitHash := parts[0]
443 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64)
444 if err != nil {
445 return nil, fmt.Errorf("parsing commit time: %w", err)
446 }
447 commitTime := time.Unix(commitTimeUnix, 0)
448
449 hash := plumbing.NewHash(commitHash)
450
451 commitInfo := &types.LastCommitInfo{
452 Hash: hash,
453 Message: "",
454 When: commitTime,
455 }
456
457 cacheMu.Lock()
458 commitCache.Set(cacheKey, commitInfo, 1)
459 cacheMu.Unlock()
460
461 return commitInfo, nil
462}
463
464func newInfoWrapper(
465 name string,
466 prefix string,
467 entry *object.TreeEntry,
468 tree *object.Tree,
469) (*infoWrapper, error) {
470 var (
471 size int64
472 mode fs.FileMode
473 isDir bool
474 )
475
476 if entry.Mode.IsFile() {
477 file, err := tree.TreeEntryFile(entry)
478 if err != nil {
479 return nil, err
480 }
481 mode = fs.FileMode(file.Mode)
482
483 size, err = tree.Size(name)
484 if err != nil {
485 return nil, err
486 }
487 } else {
488 isDir = true
489 mode = fs.ModeDir | fs.ModePerm
490 }
491
492 fullname := path.Join(prefix, name)
493 return &infoWrapper{
494 name: fullname,
495 size: size,
496 mode: mode,
497 modTime: time.Unix(0, 0),
498 isDir: isDir,
499 }, nil
500}
501
502func (i *infoWrapper) Name() string {
503 return i.name
504}
505
506func (i *infoWrapper) Size() int64 {
507 return i.size
508}
509
510func (i *infoWrapper) Mode() fs.FileMode {
511 return i.mode
512}
513
514func (i *infoWrapper) ModTime() time.Time {
515 return i.modTime
516}
517
518func (i *infoWrapper) IsDir() bool {
519 return i.isDir
520}
521
522func (i *infoWrapper) Sys() any {
523 return nil
524}
525
526func (t *TagReference) Name() string {
527 return t.ref.Name().Short()
528}
529
530func (t *TagReference) Message() string {
531 if t.tag != nil {
532 return t.tag.Message
533 }
534 return ""
535}
536
537func (t *TagReference) TagObject() *object.Tag {
538 return t.tag
539}
540
541func (t *TagReference) Hash() plumbing.Hash {
542 return t.ref.Hash()
543}