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