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