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) FileContentN(path string, cap int64) ([]byte, error) {
124 c, err := g.r.CommitObject(g.h)
125 if err != nil {
126 return nil, fmt.Errorf("commit object: %w", err)
127 }
128
129 tree, err := c.Tree()
130 if err != nil {
131 return nil, fmt.Errorf("file tree: %w", err)
132 }
133
134 file, err := tree.File(path)
135 if err != nil {
136 return nil, err
137 }
138
139 isbin, _ := file.IsBinary()
140 if isbin {
141 return nil, ErrBinaryFile
142 }
143
144 reader, err := file.Reader()
145 if err != nil {
146 return nil, err
147 }
148
149 buf := new(bytes.Buffer)
150 if _, err = buf.ReadFrom(io.LimitReader(reader, cap)); err != nil {
151 return nil, err
152 }
153
154 return buf.Bytes(), nil
155}
156
157func (g *GitRepo) RawContent(path string) ([]byte, error) {
158 c, err := g.r.CommitObject(g.h)
159 if err != nil {
160 return nil, fmt.Errorf("commit object: %w", err)
161 }
162
163 tree, err := c.Tree()
164 if err != nil {
165 return nil, fmt.Errorf("file tree: %w", err)
166 }
167
168 file, err := tree.File(path)
169 if err != nil {
170 return nil, err
171 }
172
173 reader, err := file.Reader()
174 if err != nil {
175 return nil, fmt.Errorf("opening file reader: %w", err)
176 }
177 defer reader.Close()
178
179 return io.ReadAll(reader)
180}
181
182func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
183 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
184 if err != nil {
185 return nil, fmt.Errorf("branch: %w", err)
186 }
187
188 if !ref.Name().IsBranch() {
189 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
190 }
191
192 return ref, nil
193}
194
195func (g *GitRepo) SetDefaultBranch(branch string) error {
196 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
197 return g.r.Storer.SetReference(ref)
198}
199
200func (g *GitRepo) FindMainBranch() (string, error) {
201 output, err := g.revParse("--abbrev-ref", "HEAD")
202 if err != nil {
203 return "", fmt.Errorf("failed to find main branch: %w", err)
204 }
205
206 return strings.TrimSpace(string(output)), nil
207}
208
209// WriteTar writes itself from a tree into a binary tar file format.
210// prefix is root folder to be appended.
211func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
212 tw := tar.NewWriter(w)
213 defer tw.Close()
214
215 c, err := g.r.CommitObject(g.h)
216 if err != nil {
217 return fmt.Errorf("commit object: %w", err)
218 }
219
220 tree, err := c.Tree()
221 if err != nil {
222 return err
223 }
224
225 walker := object.NewTreeWalker(tree, true, nil)
226 defer walker.Close()
227
228 name, entry, err := walker.Next()
229 for ; err == nil; name, entry, err = walker.Next() {
230 info, err := newInfoWrapper(name, prefix, &entry, tree)
231 if err != nil {
232 return err
233 }
234
235 header, err := tar.FileInfoHeader(info, "")
236 if err != nil {
237 return err
238 }
239
240 err = tw.WriteHeader(header)
241 if err != nil {
242 return err
243 }
244
245 if !info.IsDir() {
246 file, err := tree.File(name)
247 if err != nil {
248 return err
249 }
250
251 reader, err := file.Blob.Reader()
252 if err != nil {
253 return err
254 }
255
256 _, err = io.Copy(tw, reader)
257 if err != nil {
258 reader.Close()
259 return err
260 }
261 reader.Close()
262 }
263 }
264
265 return nil
266}
267
268func newInfoWrapper(
269 name string,
270 prefix string,
271 entry *object.TreeEntry,
272 tree *object.Tree,
273) (*infoWrapper, error) {
274 var (
275 size int64
276 mode fs.FileMode
277 isDir bool
278 )
279
280 if entry.Mode.IsFile() {
281 file, err := tree.TreeEntryFile(entry)
282 if err != nil {
283 return nil, err
284 }
285 mode = fs.FileMode(file.Mode)
286
287 size, err = tree.Size(name)
288 if err != nil {
289 return nil, err
290 }
291 } else {
292 isDir = true
293 mode = fs.ModeDir | fs.ModePerm
294 }
295
296 fullname := path.Join(prefix, name)
297 return &infoWrapper{
298 name: fullname,
299 size: size,
300 mode: mode,
301 modTime: time.Unix(0, 0),
302 isDir: isDir,
303 }, nil
304}
305
306func (i *infoWrapper) Name() string {
307 return i.name
308}
309
310func (i *infoWrapper) Size() int64 {
311 return i.size
312}
313
314func (i *infoWrapper) Mode() fs.FileMode {
315 return i.mode
316}
317
318func (i *infoWrapper) ModTime() time.Time {
319 return i.modTime
320}
321
322func (i *infoWrapper) IsDir() bool {
323 return i.isDir
324}
325
326func (i *infoWrapper) Sys() any {
327 return nil
328}