git/git.go (view raw)
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
17type GitRepo struct {
18 r *git.Repository
19 h plumbing.Hash
20}
21
22type TagList []*object.Tag
23
24// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
25// to tar WriteHeader
26type infoWrapper struct {
27 name string
28 size int64
29 mode fs.FileMode
30 modTime time.Time
31 isDir bool
32}
33
34func (self TagList) Len() int {
35 return len(self)
36}
37
38func (self TagList) Swap(i, j int) {
39 self[i], self[j] = self[j], self[i]
40}
41
42// sorting tags in reverse chronological order
43func (self TagList) Less(i, j int) bool {
44 return self[i].Tagger.When.After(self[j].Tagger.When)
45}
46
47func Open(path string, ref string) (*GitRepo, error) {
48 var err error
49 g := GitRepo{}
50 g.r, err = git.PlainOpen(path)
51 if err != nil {
52 return nil, fmt.Errorf("opening %s: %w", path, err)
53 }
54
55 if ref == "" {
56 head, err := g.r.Head()
57 if err != nil {
58 return nil, fmt.Errorf("getting head of %s: %w", path, err)
59 }
60 g.h = head.Hash()
61 } else {
62 hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
63 if err != nil {
64 return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
65 }
66 g.h = *hash
67 }
68 return &g, nil
69}
70
71func (g *GitRepo) Commits() ([]*object.Commit, error) {
72 ci, err := g.r.Log(&git.LogOptions{From: g.h})
73 if err != nil {
74 return nil, fmt.Errorf("commits from ref: %w", err)
75 }
76
77 commits := []*object.Commit{}
78 ci.ForEach(func(c *object.Commit) error {
79 commits = append(commits, c)
80 return nil
81 })
82
83 return commits, nil
84}
85
86func (g *GitRepo) LastCommit() (*object.Commit, error) {
87 c, err := g.r.CommitObject(g.h)
88 if err != nil {
89 return nil, fmt.Errorf("last commit: %w", err)
90 }
91 return c, nil
92}
93
94func (g *GitRepo) FileContent(path string) (string, error) {
95 c, err := g.r.CommitObject(g.h)
96 if err != nil {
97 return "", fmt.Errorf("commit object: %w", err)
98 }
99
100 tree, err := c.Tree()
101 if err != nil {
102 return "", fmt.Errorf("file tree: %w", err)
103 }
104
105 file, err := tree.File(path)
106 if err != nil {
107 return "", err
108 }
109
110 isbin, _ := file.IsBinary()
111
112 if !isbin {
113 return file.Contents()
114 } else {
115 return "Not displaying binary file", nil
116 }
117}
118
119func (g *GitRepo) Tags() ([]*object.Tag, error) {
120 ti, err := g.r.TagObjects()
121 if err != nil {
122 return nil, fmt.Errorf("tag objects: %w", err)
123 }
124
125 tags := []*object.Tag{}
126
127 _ = ti.ForEach(func(t *object.Tag) error {
128 for i, existing := range tags {
129 if existing.Name == t.Name {
130 if t.Tagger.When.After(existing.Tagger.When) {
131 tags[i] = t
132 }
133 return nil
134 }
135 }
136 tags = append(tags, t)
137 return nil
138 })
139
140 var tagList TagList
141 tagList = tags
142 sort.Sort(tagList)
143
144 return tags, nil
145}
146
147func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
148 bi, err := g.r.Branches()
149 if err != nil {
150 return nil, fmt.Errorf("branchs: %w", err)
151 }
152
153 branches := []*plumbing.Reference{}
154
155 _ = bi.ForEach(func(ref *plumbing.Reference) error {
156 branches = append(branches, ref)
157 return nil
158 })
159
160 return branches, nil
161}
162
163func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
164 for _, b := range branches {
165 _, err := g.r.ResolveRevision(plumbing.Revision(b))
166 if err == nil {
167 return b, nil
168 }
169 }
170 return "", fmt.Errorf("unable to find main branch")
171}
172
173// WriteTar writes itself from a tree into a binary tar file format.
174// prefix is root folder to be appended.
175func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
176 tw := tar.NewWriter(w)
177 defer tw.Close()
178
179 c, err := g.r.CommitObject(g.h)
180 if err != nil {
181 return fmt.Errorf("commit object: %w", err)
182 }
183
184 tree, err := c.Tree()
185 if err != nil {
186 return err
187 }
188
189 walker := object.NewTreeWalker(tree, true, nil)
190 defer walker.Close()
191
192 name, entry, err := walker.Next()
193 for ; err == nil; name, entry, err = walker.Next() {
194 info, err := newInfoWrapper(name, prefix, &entry, tree)
195 if err != nil {
196 return err
197 }
198
199 header, err := tar.FileInfoHeader(info, "")
200 if err != nil {
201 return err
202 }
203
204 err = tw.WriteHeader(header)
205 if err != nil {
206 return err
207 }
208
209 if !info.IsDir() {
210 file, err := tree.File(name)
211 if err != nil {
212 return err
213 }
214
215 reader, err := file.Blob.Reader()
216 if err != nil {
217 return err
218 }
219
220 _, err = io.Copy(tw, reader)
221 if err != nil {
222 reader.Close()
223 return err
224 }
225 reader.Close()
226 }
227 }
228
229 return nil
230}
231
232func newInfoWrapper(
233 name string,
234 prefix string,
235 entry *object.TreeEntry,
236 tree *object.Tree,
237) (*infoWrapper, error) {
238 var (
239 size int64
240 mode fs.FileMode
241 isDir bool
242 )
243
244 if entry.Mode.IsFile() {
245 file, err := tree.TreeEntryFile(entry)
246 if err != nil {
247 return nil, err
248 }
249 mode = fs.FileMode(file.Mode)
250
251 size, err = tree.Size(name)
252 if err != nil {
253 return nil, err
254 }
255 } else {
256 isDir = true
257 mode = fs.ModeDir | fs.ModePerm
258 }
259
260 fullname := path.Join(prefix, name)
261 return &infoWrapper{
262 name: fullname,
263 size: size,
264 mode: mode,
265 modTime: time.Unix(0, 0),
266 isDir: isDir,
267 }, nil
268}
269
270func (i *infoWrapper) Name() string {
271 return i.name
272}
273
274func (i *infoWrapper) Size() int64 {
275 return i.size
276}
277
278func (i *infoWrapper) Mode() fs.FileMode {
279 return i.mode
280}
281
282func (i *infoWrapper) ModTime() time.Time {
283 return i.modTime
284}
285
286func (i *infoWrapper) IsDir() bool {
287 return i.isDir
288}
289
290func (i *infoWrapper) Sys() any {
291 return nil
292}