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 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) FindMainBranch() (string, error) {
242 ref, err := g.r.Head()
243 if err != nil {
244 return "", fmt.Errorf("unable to find main branch: %w", err)
245 }
246 if ref.Name().IsBranch() {
247 return strings.TrimPrefix(string(ref.Name()), "refs/heads/"), nil
248 }
249
250 return "", fmt.Errorf("unable to find main branch: %w", err)
251}
252
253// WriteTar writes itself from a tree into a binary tar file format.
254// prefix is root folder to be appended.
255func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
256 tw := tar.NewWriter(w)
257 defer tw.Close()
258
259 c, err := g.r.CommitObject(g.h)
260 if err != nil {
261 return fmt.Errorf("commit object: %w", err)
262 }
263
264 tree, err := c.Tree()
265 if err != nil {
266 return err
267 }
268
269 walker := object.NewTreeWalker(tree, true, nil)
270 defer walker.Close()
271
272 name, entry, err := walker.Next()
273 for ; err == nil; name, entry, err = walker.Next() {
274 info, err := newInfoWrapper(name, prefix, &entry, tree)
275 if err != nil {
276 return err
277 }
278
279 header, err := tar.FileInfoHeader(info, "")
280 if err != nil {
281 return err
282 }
283
284 err = tw.WriteHeader(header)
285 if err != nil {
286 return err
287 }
288
289 if !info.IsDir() {
290 file, err := tree.File(name)
291 if err != nil {
292 return err
293 }
294
295 reader, err := file.Blob.Reader()
296 if err != nil {
297 return err
298 }
299
300 _, err = io.Copy(tw, reader)
301 if err != nil {
302 reader.Close()
303 return err
304 }
305 reader.Close()
306 }
307 }
308
309 return nil
310}
311
312func (g *GitRepo) LastCommitForPath(path string) (*types.LastCommitInfo, error) {
313 cacheKey := fmt.Sprintf("%s:%s", g.h.String(), path)
314 cacheMu.RLock()
315 if commitInfo, found := commitCache.Get(cacheKey); found {
316 cacheMu.RUnlock()
317 return commitInfo.(*types.LastCommitInfo), nil
318 }
319 cacheMu.RUnlock()
320
321 cmd := exec.Command("git", "-C", g.path, "log", g.h.String(), "-1", "--format=%H %ct", "--", path)
322
323 var out bytes.Buffer
324 cmd.Stdout = &out
325 cmd.Stderr = &out
326
327 if err := cmd.Run(); err != nil {
328 return nil, fmt.Errorf("failed to get commit hash: %w", err)
329 }
330
331 output := strings.TrimSpace(out.String())
332 if output == "" {
333 return nil, fmt.Errorf("no commits found for path: %s", path)
334 }
335
336 parts := strings.SplitN(output, " ", 2)
337 if len(parts) < 2 {
338 return nil, fmt.Errorf("unexpected commit log format")
339 }
340
341 commitHash := parts[0]
342 commitTimeUnix, err := strconv.ParseInt(parts[1], 10, 64)
343 if err != nil {
344 return nil, fmt.Errorf("parsing commit time: %w", err)
345 }
346 commitTime := time.Unix(commitTimeUnix, 0)
347
348 hash := plumbing.NewHash(commitHash)
349
350 commitInfo := &types.LastCommitInfo{
351 Hash: hash,
352 Message: "",
353 When: commitTime,
354 }
355
356 cacheMu.Lock()
357 commitCache.Set(cacheKey, commitInfo, 1)
358 cacheMu.Unlock()
359
360 return commitInfo, nil
361}
362
363func newInfoWrapper(
364 name string,
365 prefix string,
366 entry *object.TreeEntry,
367 tree *object.Tree,
368) (*infoWrapper, error) {
369 var (
370 size int64
371 mode fs.FileMode
372 isDir bool
373 )
374
375 if entry.Mode.IsFile() {
376 file, err := tree.TreeEntryFile(entry)
377 if err != nil {
378 return nil, err
379 }
380 mode = fs.FileMode(file.Mode)
381
382 size, err = tree.Size(name)
383 if err != nil {
384 return nil, err
385 }
386 } else {
387 isDir = true
388 mode = fs.ModeDir | fs.ModePerm
389 }
390
391 fullname := path.Join(prefix, name)
392 return &infoWrapper{
393 name: fullname,
394 size: size,
395 mode: mode,
396 modTime: time.Unix(0, 0),
397 isDir: isDir,
398 }, nil
399}
400
401func (i *infoWrapper) Name() string {
402 return i.name
403}
404
405func (i *infoWrapper) Size() int64 {
406 return i.size
407}
408
409func (i *infoWrapper) Mode() fs.FileMode {
410 return i.mode
411}
412
413func (i *infoWrapper) ModTime() time.Time {
414 return i.modTime
415}
416
417func (i *infoWrapper) IsDir() bool {
418 return i.isDir
419}
420
421func (i *infoWrapper) Sys() any {
422 return nil
423}
424
425func (t *TagReference) Name() string {
426 return t.ref.Name().Short()
427}
428
429func (t *TagReference) Message() string {
430 if t.tag != nil {
431 return t.tag.Message
432 }
433 return ""
434}
435
436func (t *TagReference) TagObject() *object.Tag {
437 return t.tag
438}
439
440func (t *TagReference) Hash() plumbing.Hash {
441 return t.ref.Hash()
442}