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