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