forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package git
2
3import (
4 "archive/tar"
5 "fmt"
6 "io"
7 "io/fs"
8 "path"
9 "sort"
10 "time"
11
12 "github.com/go-git/go-git/v5"
13 "github.com/go-git/go-git/v5/plumbing"
14 "github.com/go-git/go-git/v5/plumbing/object"
15)
16
17var (
18 ErrBinaryFile = fmt.Errorf("binary file")
19)
20
21type GitRepo struct {
22 r *git.Repository
23 h plumbing.Hash
24}
25
26type TagList struct {
27 refs []*TagReference
28 r *git.Repository
29}
30
31// TagReference is used to list both tag and non-annotated tags.
32// Non-annotated tags should only contains a reference.
33// Annotated tags should contain its reference and its tag information.
34type TagReference struct {
35 ref *plumbing.Reference
36 tag *object.Tag
37}
38
39// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
40// to tar WriteHeader
41type infoWrapper struct {
42 name string
43 size int64
44 mode fs.FileMode
45 modTime time.Time
46 isDir bool
47}
48
49func (self *TagList) Len() int {
50 return len(self.refs)
51}
52
53func (self *TagList) Swap(i, j int) {
54 self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
55}
56
57// sorting tags in reverse chronological order
58func (self *TagList) Less(i, j int) bool {
59 var dateI time.Time
60 var dateJ time.Time
61
62 if self.refs[i].tag != nil {
63 dateI = self.refs[i].tag.Tagger.When
64 } else {
65 c, err := self.r.CommitObject(self.refs[i].ref.Hash())
66 if err != nil {
67 dateI = time.Now()
68 } else {
69 dateI = c.Committer.When
70 }
71 }
72
73 if self.refs[j].tag != nil {
74 dateJ = self.refs[j].tag.Tagger.When
75 } else {
76 c, err := self.r.CommitObject(self.refs[j].ref.Hash())
77 if err != nil {
78 dateJ = time.Now()
79 } else {
80 dateJ = c.Committer.When
81 }
82 }
83
84 return dateI.After(dateJ)
85}
86
87func Open(path string, ref string) (*GitRepo, error) {
88 var err error
89 g := GitRepo{}
90 g.r, err = git.PlainOpen(path)
91 if err != nil {
92 return nil, fmt.Errorf("opening %s: %w", path, err)
93 }
94
95 if ref == "" {
96 head, err := g.r.Head()
97 if err != nil {
98 return nil, fmt.Errorf("getting head of %s: %w", path, err)
99 }
100 g.h = head.Hash()
101 } else {
102 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
103 if err != nil {
104 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
105 }
106 g.h = *hash
107 }
108 return &g, nil
109}
110
111func (g *GitRepo) Commits() ([]*object.Commit, error) {
112 ci, err := g.r.Log(&git.LogOptions{From: g.h})
113 if err != nil {
114 return nil, fmt.Errorf("commits from ref: %w", err)
115 }
116
117 commits := []*object.Commit{}
118 ci.ForEach(func(c *object.Commit) error {
119 commits = append(commits, c)
120 return nil
121 })
122
123 return commits, nil
124}
125
126func (g *GitRepo) LastCommit() (*object.Commit, error) {
127 c, err := g.r.CommitObject(g.h)
128 if err != nil {
129 return nil, fmt.Errorf("last commit: %w", err)
130 }
131 return c, nil
132}
133
134func (g *GitRepo) FileContent(path string) (string, error) {
135 c, err := g.r.CommitObject(g.h)
136 if err != nil {
137 return "", fmt.Errorf("commit object: %w", err)
138 }
139
140 tree, err := c.Tree()
141 if err != nil {
142 return "", fmt.Errorf("file tree: %w", err)
143 }
144
145 file, err := tree.File(path)
146 if err != nil {
147 return "", err
148 }
149
150 isbin, _ := file.IsBinary()
151
152 if !isbin {
153 return file.Contents()
154 } else {
155 return "", ErrBinaryFile
156 }
157}
158
159func (g *GitRepo) Tags() ([]*TagReference, error) {
160 iter, err := g.r.Tags()
161 if err != nil {
162 return nil, fmt.Errorf("tag objects: %w", err)
163 }
164
165 tags := make([]*TagReference, 0)
166
167 if err := iter.ForEach(func(ref *plumbing.Reference) error {
168 obj, err := g.r.TagObject(ref.Hash())
169 switch err {
170 case nil:
171 tags = append(tags, &TagReference{
172 ref: ref,
173 tag: obj,
174 })
175 case plumbing.ErrObjectNotFound:
176 tags = append(tags, &TagReference{
177 ref: ref,
178 })
179 default:
180 return err
181 }
182 return nil
183 }); err != nil {
184 return nil, err
185 }
186
187 tagList := &TagList{r: g.r, refs: tags}
188 sort.Sort(tagList)
189 return tags, nil
190}
191
192func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
193 bi, err := g.r.Branches()
194 if err != nil {
195 return nil, fmt.Errorf("branchs: %w", err)
196 }
197
198 branches := []*plumbing.Reference{}
199
200 _ = bi.ForEach(func(ref *plumbing.Reference) error {
201 branches = append(branches, ref)
202 return nil
203 })
204
205 return branches, nil
206}
207
208func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
209 branches = append(branches, []string{
210 "main",
211 "master",
212 "trunk",
213 }...)
214 for _, b := range branches {
215 _, err := g.r.ResolveRevision(plumbing.Revision(b))
216 if err == nil {
217 return b, nil
218 }
219 }
220 return "", fmt.Errorf("unable to find main branch")
221}
222
223// WriteTar writes itself from a tree into a binary tar file format.
224// prefix is root folder to be appended.
225func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
226 tw := tar.NewWriter(w)
227 defer tw.Close()
228
229 c, err := g.r.CommitObject(g.h)
230 if err != nil {
231 return fmt.Errorf("commit object: %w", err)
232 }
233
234 tree, err := c.Tree()
235 if err != nil {
236 return err
237 }
238
239 walker := object.NewTreeWalker(tree, true, nil)
240 defer walker.Close()
241
242 name, entry, err := walker.Next()
243 for ; err == nil; name, entry, err = walker.Next() {
244 info, err := newInfoWrapper(name, prefix, &entry, tree)
245 if err != nil {
246 return err
247 }
248
249 header, err := tar.FileInfoHeader(info, "")
250 if err != nil {
251 return err
252 }
253
254 err = tw.WriteHeader(header)
255 if err != nil {
256 return err
257 }
258
259 if !info.IsDir() {
260 file, err := tree.File(name)
261 if err != nil {
262 return err
263 }
264
265 reader, err := file.Blob.Reader()
266 if err != nil {
267 return err
268 }
269
270 _, err = io.Copy(tw, reader)
271 if err != nil {
272 reader.Close()
273 return err
274 }
275 reader.Close()
276 }
277 }
278
279 return nil
280}
281
282func newInfoWrapper(
283 name string,
284 prefix string,
285 entry *object.TreeEntry,
286 tree *object.Tree,
287) (*infoWrapper, error) {
288 var (
289 size int64
290 mode fs.FileMode
291 isDir bool
292 )
293
294 if entry.Mode.IsFile() {
295 file, err := tree.TreeEntryFile(entry)
296 if err != nil {
297 return nil, err
298 }
299 mode = fs.FileMode(file.Mode)
300
301 size, err = tree.Size(name)
302 if err != nil {
303 return nil, err
304 }
305 } else {
306 isDir = true
307 mode = fs.ModeDir | fs.ModePerm
308 }
309
310 fullname := path.Join(prefix, name)
311 return &infoWrapper{
312 name: fullname,
313 size: size,
314 mode: mode,
315 modTime: time.Unix(0, 0),
316 isDir: isDir,
317 }, nil
318}
319
320func (i *infoWrapper) Name() string {
321 return i.name
322}
323
324func (i *infoWrapper) Size() int64 {
325 return i.size
326}
327
328func (i *infoWrapper) Mode() fs.FileMode {
329 return i.mode
330}
331
332func (i *infoWrapper) ModTime() time.Time {
333 return i.modTime
334}
335
336func (i *infoWrapper) IsDir() bool {
337 return i.isDir
338}
339
340func (i *infoWrapper) Sys() any {
341 return nil
342}
343
344func (t *TagReference) Name() string {
345 return t.ref.Name().Short()
346}
347
348func (t *TagReference) Message() string {
349 if t.tag != nil {
350 return t.tag.Message
351 }
352 return ""
353}
354
355func (t *TagReference) TagObject() *object.Tag {
356 return t.tag
357}
358
359func (t *TagReference) Hash() plumbing.Hash {
360 return t.ref.Hash()
361}