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