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