forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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 (g *GitRepo) Commits() ([]*object.Commit, error) {
135 ci, err := g.r.Log(&git.LogOptions{From: g.h})
136 if err != nil {
137 return nil, fmt.Errorf("commits from ref: %w", err)
138 }
139
140 commits := []*object.Commit{}
141 ci.ForEach(func(c *object.Commit) error {
142 commits = append(commits, c)
143 return nil
144 })
145
146 return commits, nil
147}
148
149func (g *GitRepo) LastCommit() (*object.Commit, error) {
150 c, err := g.r.CommitObject(g.h)
151 if err != nil {
152 return nil, fmt.Errorf("last commit: %w", err)
153 }
154 return c, nil
155}
156
157func (g *GitRepo) FileContent(path string) (string, error) {
158 c, err := g.r.CommitObject(g.h)
159 if err != nil {
160 return "", fmt.Errorf("commit object: %w", err)
161 }
162
163 tree, err := c.Tree()
164 if err != nil {
165 return "", fmt.Errorf("file tree: %w", err)
166 }
167
168 file, err := tree.File(path)
169 if err != nil {
170 return "", err
171 }
172
173 isbin, _ := file.IsBinary()
174
175 if !isbin {
176 return file.Contents()
177 } else {
178 return "", ErrBinaryFile
179 }
180}
181
182func (g *GitRepo) Tags() ([]*TagReference, error) {
183 iter, err := g.r.Tags()
184 if err != nil {
185 return nil, fmt.Errorf("tag objects: %w", err)
186 }
187
188 tags := make([]*TagReference, 0)
189
190 if err := iter.ForEach(func(ref *plumbing.Reference) error {
191 obj, err := g.r.TagObject(ref.Hash())
192 switch err {
193 case nil:
194 tags = append(tags, &TagReference{
195 ref: ref,
196 tag: obj,
197 })
198 case plumbing.ErrObjectNotFound:
199 tags = append(tags, &TagReference{
200 ref: ref,
201 })
202 default:
203 return err
204 }
205 return nil
206 }); err != nil {
207 return nil, err
208 }
209
210 tagList := &TagList{r: g.r, refs: tags}
211 sort.Sort(tagList)
212 return tags, nil
213}
214
215func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
216 bi, err := g.r.Branches()
217 if err != nil {
218 return nil, fmt.Errorf("branchs: %w", err)
219 }
220
221 branches := []*plumbing.Reference{}
222
223 _ = bi.ForEach(func(ref *plumbing.Reference) error {
224 branches = append(branches, ref)
225 return nil
226 })
227
228 return branches, nil
229}
230
231func (g *GitRepo) SetDefaultBranch(branch string) error {
232 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
233 return g.r.Storer.SetReference(ref)
234}
235
236func (g *GitRepo) FindMainBranch() (string, error) {
237 ref, err := g.r.Head()
238 if err != nil {
239 return "", fmt.Errorf("unable to find main branch: %w", err)
240 }
241 if ref.Name().IsBranch() {
242 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
243 }
244
245 return "", fmt.Errorf("unable to find main branch: %w", err)
246}
247
248// WriteTar writes itself from a tree into a binary tar file format.
249// prefix is root folder to be appended.
250func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
251 tw := tar.NewWriter(w)
252 defer tw.Close()
253
254 c, err := g.r.CommitObject(g.h)
255 if err != nil {
256 return fmt.Errorf("commit object: %w", err)
257 }
258
259 tree, err := c.Tree()
260 if err != nil {
261 return err
262 }
263
264 walker := object.NewTreeWalker(tree, true, nil)
265 defer walker.Close()
266
267 name, entry, err := walker.Next()
268 for ; err == nil; name, entry, err = walker.Next() {
269 info, err := newInfoWrapper(name, prefix, &entry, tree)
270 if err != nil {
271 return err
272 }
273
274 header, err := tar.FileInfoHeader(info, "")
275 if err != nil {
276 return err
277 }
278
279 err = tw.WriteHeader(header)
280 if err != nil {
281 return err
282 }
283
284 if !info.IsDir() {
285 file, err := tree.File(name)
286 if err != nil {
287 return err
288 }
289
290 reader, err := file.Blob.Reader()
291 if err != nil {
292 return err
293 }
294
295 _, err = io.Copy(tw, reader)
296 if err != nil {
297 reader.Close()
298 return err
299 }
300 reader.Close()
301 }
302 }
303
304 return nil
305}
306
307func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) {
308 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path)
309 cacheMu.RLock()
310 if commitInfo, found := commitCache.Get(cacheKey); found {
311 cacheMu.RUnlock()
312 return commitInfo.(*types.LastCommitInfo), nil
313 }
314 cacheMu.RUnlock()
315
316 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path)
317
318 var out bytes.Buffer
319 cmd.Stdout = &out
320 cmd.Stderr = &out
321
322 if err := cmd.Run(); err != nil {
323 return nil, fmt.Errorf("failed to get commit hash: %w", err)
324 }
325
326 output := strings.TrimSpace(out.String())
327 if output == "" {
328 return nil, fmt.Errorf("no commits found for path: %s", path)
329 }
330
331 parts := strings.SplitN(output, " ", 2)
332 if len(parts) < 2 {
333 return nil, fmt.Errorf("unexpected commit log format")
334 }
335
336 commitHash := parts[0]
337 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64)
338 if err != nil {
339 return nil, fmt.Errorf("parsing commit time: %w", err)
340 }
341 commitTime := time.Unix(commitTimeUnix, 0)
342
343 hash := plumbing.NewHash(commitHash)
344
345 commitInfo := &types.LastCommitInfo{
346 Hash: hash,
347 Message: "",
348 When: commitTime,
349 }
350
351 cacheMu.Lock()
352 commitCache.Set(cacheKey, commitInfo, 1)
353 cacheMu.Unlock()
354
355 return commitInfo, nil
356}
357
358func newInfoWrapper(
359 name string,
360 prefix string,
361 entry *object.TreeEntry,
362 tree *object.Tree,
363) (*infoWrapper, error) {
364 var (
365 size int64
366 mode fs.FileMode
367 isDir bool
368 )
369
370 if entry.Mode.IsFile() {
371 file, err := tree.TreeEntryFile(entry)
372 if err != nil {
373 return nil, err
374 }
375 mode = fs.FileMode(file.Mode)
376
377 size, err = tree.Size(name)
378 if err != nil {
379 return nil, err
380 }
381 } else {
382 isDir = true
383 mode = fs.ModeDir | fs.ModePerm
384 }
385
386 fullname := path.Join(prefix, name)
387 return &infoWrapper{
388 name: fullname,
389 size: size,
390 mode: mode,
391 modTime: time.Unix(0, 0),
392 isDir: isDir,
393 }, nil
394}
395
396func (i *infoWrapper) Name() string {
397 return i.name
398}
399
400func (i *infoWrapper) Size() int64 {
401 return i.size
402}
403
404func (i *infoWrapper) Mode() fs.FileMode {
405 return i.mode
406}
407
408func (i *infoWrapper) ModTime() time.Time {
409 return i.modTime
410}
411
412func (i *infoWrapper) IsDir() bool {
413 return i.isDir
414}
415
416func (i *infoWrapper) Sys() any {
417 return nil
418}
419
420func (t *TagReference) Name() string {
421 return t.ref.Name().Short()
422}
423
424func (t *TagReference) Message() string {
425 if t.tag != nil {
426 return t.tag.Message
427 }
428 return ""
429}
430
431func (t *TagReference) TagObject() *object.Tag {
432 return t.tag
433}
434
435func (t *TagReference) Hash() plumbing.Hash {
436 return t.ref.Hash()
437}