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)
42
43type GitRepo struct {
44 path string
45 r *git.Repository
46 h plumbing.Hash
47}
48
49type TagList struct {
50 refs []*TagReference
51 r *git.Repository
52}
53
54// TagReference is used to list both tag and non-annotated tags.
55// Non-annotated tags should only contains a reference.
56// Annotated tags should contain its reference and its tag information.
57type TagReference struct {
58 ref *plumbing.Reference
59 tag *object.Tag
60}
61
62// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
63// to tar WriteHeader
64type infoWrapper struct {
65 name string
66 size int64
67 mode fs.FileMode
68 modTime time.Time
69 isDir bool
70}
71
72func (self *TagList) Len() int {
73 return len(self.refs)
74}
75
76func (self *TagList) Swap(i, j int) {
77 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
78}
79
80// sorting tags in reverse chronological order
81func (self *TagList) Less(i, j int) bool {
82 var dateI time.Time
83 var dateJ time.Time
84
85 if self.refs[i].tag != nil {
86 dateI = self.refs[i].tag.Tagger.When
87 } else {
88 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
89 if err != nil {
90 dateI = time.Now()
91 } else {
92 dateI = c.Committer.When
93 }
94 }
95
96 if self.refs[j].tag != nil {
97 dateJ = self.refs[j].tag.Tagger.When
98 } else {
99 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
100 if err != nil {
101 dateJ = time.Now()
102 } else {
103 dateJ = c.Committer.When
104 }
105 }
106
107 return dateI.After(dateJ)
108}
109
110func Open(path string, ref string) (*GitRepo, error) {
111 var err error
112 g := GitRepo{path: path}
113 g.r, err = git.PlainOpen(path)
114 if err != nil {
115 return nil, fmt.Errorf("opening %s: %w", path, err)
116 }
117
118 if ref == "" {
119 head, err := g.r.Head()
120 if err != nil {
121 return nil, fmt.Errorf("getting head of %s: %w", path, err)
122 }
123 g.h = head.Hash()
124 } else {
125 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
126 if err != nil {
127 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
128 }
129 g.h = *hash
130 }
131 return &g, nil
132}
133
134func PlainOpen(path string) (*GitRepo, error) {
135 var err error
136 g := GitRepo{path: path}
137 g.r, err = git.PlainOpen(path)
138 if err != nil {
139 return nil, fmt.Errorf("opening %s: %w", path, err)
140 }
141 return &g, nil
142}
143
144func (g *GitRepo) Commits() ([]*object.Commit, error) {
145 ci, err := g.r.Log(&git.LogOptions{From: g.h})
146 if err != nil {
147 return nil, fmt.Errorf("commits from ref: %w", err)
148 }
149
150 commits := []*object.Commit{}
151 ci.ForEach(func(c *object.Commit) error {
152 commits = append(commits, c)
153 return nil
154 })
155
156 return commits, nil
157}
158
159func (g *GitRepo) LastCommit() (*object.Commit, error) {
160 c, err := g.r.CommitObject(g.h)
161 if err != nil {
162 return nil, fmt.Errorf("last commit: %w", err)
163 }
164 return c, nil
165}
166
167func (g *GitRepo) FileContent(path string) (string, error) {
168 c, err := g.r.CommitObject(g.h)
169 if err != nil {
170 return "", fmt.Errorf("commit object: %w", err)
171 }
172
173 tree, err := c.Tree()
174 if err != nil {
175 return "", fmt.Errorf("file tree: %w", err)
176 }
177
178 file, err := tree.File(path)
179 if err != nil {
180 return "", err
181 }
182
183 isbin, _ := file.IsBinary()
184
185 if !isbin {
186 return file.Contents()
187 } else {
188 return "", ErrBinaryFile
189 }
190}
191
192func (g *GitRepo) Tags() ([]*TagReference, error) {
193 iter, err := g.r.Tags()
194 if err != nil {
195 return nil, fmt.Errorf("tag objects: %w", err)
196 }
197
198 tags := make([]*TagReference, 0)
199
200 if err := iter.ForEach(func(ref *plumbing.Reference) error {
201 obj, err := g.r.TagObject(ref.Hash())
202 switch err {
203 case nil:
204 tags = append(tags, &TagReference{
205 ref: ref,
206 tag: obj,
207 })
208 case plumbing.ErrObjectNotFound:
209 tags = append(tags, &TagReference{
210 ref: ref,
211 })
212 default:
213 return err
214 }
215 return nil
216 }); err != nil {
217 return nil, err
218 }
219
220 tagList := &TagList{r: g.r, refs: tags}
221 sort.Sort(tagList)
222 return tags, nil
223}
224
225func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
226 bi, err := g.r.Branches()
227 if err != nil {
228 return nil, fmt.Errorf("branchs: %w", err)
229 }
230
231 branches := []*plumbing.Reference{}
232
233 _ = bi.ForEach(func(ref *plumbing.Reference) error {
234 branches = append(branches, ref)
235 return nil
236 })
237
238 return branches, nil
239}
240
241func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
242 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
243 if err != nil {
244 return nil, fmt.Errorf("branch: %w", err)
245 }
246
247 if !ref.Name().IsBranch() {
248 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
249 }
250
251 return ref, nil
252}
253
254func (g *GitRepo) SetDefaultBranch(branch string) error {
255 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
256 return g.r.Storer.SetReference(ref)
257}
258
259func (g *GitRepo) FindMainBranch() (string, error) {
260 ref, err := g.r.Head()
261 if err != nil {
262 return "", fmt.Errorf("unable to find main branch: %w", err)
263 }
264 if ref.Name().IsBranch() {
265 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
266 }
267
268 return "", fmt.Errorf("unable to find main branch: %w", err)
269}
270
271// WriteTar writes itself from a tree into a binary tar file format.
272// prefix is root folder to be appended.
273func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
274 tw := tar.NewWriter(w)
275 defer tw.Close()
276
277 c, err := g.r.CommitObject(g.h)
278 if err != nil {
279 return fmt.Errorf("commit object: %w", err)
280 }
281
282 tree, err := c.Tree()
283 if err != nil {
284 return err
285 }
286
287 walker := object.NewTreeWalker(tree, true, nil)
288 defer walker.Close()
289
290 name, entry, err := walker.Next()
291 for ; err == nil; name, entry, err = walker.Next() {
292 info, err := newInfoWrapper(name, prefix, &entry, tree)
293 if err != nil {
294 return err
295 }
296
297 header, err := tar.FileInfoHeader(info, "")
298 if err != nil {
299 return err
300 }
301
302 err = tw.WriteHeader(header)
303 if err != nil {
304 return err
305 }
306
307 if !info.IsDir() {
308 file, err := tree.File(name)
309 if err != nil {
310 return err
311 }
312
313 reader, err := file.Blob.Reader()
314 if err != nil {
315 return err
316 }
317
318 _, err = io.Copy(tw, reader)
319 if err != nil {
320 reader.Close()
321 return err
322 }
323 reader.Close()
324 }
325 }
326
327 return nil
328}
329
330func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) {
331 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path)
332 cacheMu.RLock()
333 if commitInfo, found := commitCache.Get(cacheKey); found {
334 cacheMu.RUnlock()
335 return commitInfo.(*types.LastCommitInfo), nil
336 }
337 cacheMu.RUnlock()
338
339 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path)
340
341 var out bytes.Buffer
342 cmd.Stdout = &out
343 cmd.Stderr = &out
344
345 if err := cmd.Run(); err != nil {
346 return nil, fmt.Errorf("failed to get commit hash: %w", err)
347 }
348
349 output := strings.TrimSpace(out.String())
350 if output == "" {
351 return nil, fmt.Errorf("no commits found for path: %s", path)
352 }
353
354 parts := strings.SplitN(output, " ", 2)
355 if len(parts) < 2 {
356 return nil, fmt.Errorf("unexpected commit log format")
357 }
358
359 commitHash := parts[0]
360 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64)
361 if err != nil {
362 return nil, fmt.Errorf("parsing commit time: %w", err)
363 }
364 commitTime := time.Unix(commitTimeUnix, 0)
365
366 hash := plumbing.NewHash(commitHash)
367
368 commitInfo := &types.LastCommitInfo{
369 Hash: hash,
370 Message: "",
371 When: commitTime,
372 }
373
374 cacheMu.Lock()
375 commitCache.Set(cacheKey, commitInfo, 1)
376 cacheMu.Unlock()
377
378 return commitInfo, nil
379}
380
381func newInfoWrapper(
382 name string,
383 prefix string,
384 entry *object.TreeEntry,
385 tree *object.Tree,
386) (*infoWrapper, error) {
387 var (
388 size int64
389 mode fs.FileMode
390 isDir bool
391 )
392
393 if entry.Mode.IsFile() {
394 file, err := tree.TreeEntryFile(entry)
395 if err != nil {
396 return nil, err
397 }
398 mode = fs.FileMode(file.Mode)
399
400 size, err = tree.Size(name)
401 if err != nil {
402 return nil, err
403 }
404 } else {
405 isDir = true
406 mode = fs.ModeDir | fs.ModePerm
407 }
408
409 fullname := path.Join(prefix, name)
410 return &infoWrapper{
411 name: fullname,
412 size: size,
413 mode: mode,
414 modTime: time.Unix(0, 0),
415 isDir: isDir,
416 }, nil
417}
418
419func (i *infoWrapper) Name() string {
420 return i.name
421}
422
423func (i *infoWrapper) Size() int64 {
424 return i.size
425}
426
427func (i *infoWrapper) Mode() fs.FileMode {
428 return i.mode
429}
430
431func (i *infoWrapper) ModTime() time.Time {
432 return i.modTime
433}
434
435func (i *infoWrapper) IsDir() bool {
436 return i.isDir
437}
438
439func (i *infoWrapper) Sys() any {
440 return nil
441}
442
443func (t *TagReference) Name() string {
444 return t.ref.Name().Short()
445}
446
447func (t *TagReference) Message() string {
448 if t.tag != nil {
449 return t.tag.Message
450 }
451 return ""
452}
453
454func (t *TagReference) TagObject() *object.Tag {
455 return t.tag
456}
457
458func (t *TagReference) Hash() plumbing.Hash {
459 return t.ref.Hash()
460}