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