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