internal/gitutil/treeblobs.go
117 lines · 2999 bytes
3 symbols in this file
1package gitutil
2
3import (
4 "bufio"
5 "bytes"
6 "context"
7 "errors"
8 "fmt"
9 "io"
10 "os/exec"
11 "strconv"
12 "strings"
13
14 "gitbay.org/gitbay/internal/toolpath"
15)
16
17// ListBlobs lists every blob in a tree, recursively, with its size. Name
18// is the path from the tree's root. Paths are read NUL-separated, so no
19// file name is quoted or split.
20func ListBlobs(ctx context.Context, dir, tree string) ([]TreeEntry, error) {
21 out, err := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir,
22 "ls-tree", "-r", "-l", "-z", "--end-of-options", tree).Output()
23 if err != nil {
24 return nil, fmt.Errorf("ls-tree %s: %w", tree, err)
25 }
26 var entries []TreeEntry
27 for _, rec := range bytes.Split(out, []byte{0}) {
28 meta, name, ok := strings.Cut(string(rec), "\t")
29 if !ok {
30 continue
31 }
32 f := strings.Fields(meta)
33 if len(f) != 4 || f[1] != "blob" {
34 continue
35 }
36 // ls-tree prints a size of BAD for a blob it cannot read.
37 size, err := strconv.ParseInt(f[3], 10, 64)
38 if err != nil {
39 return nil, fmt.Errorf("ls-tree %s: cannot read %s", tree, name)
40 }
41 entries = append(entries, TreeEntry{Mode: f[0], Type: f[1], SHA: f[2], Size: size, Name: name})
42 }
43 return entries, nil
44}
45
46// CatBlobs reads blobs by id through one `git cat-file --batch`, calling
47// fn with each one's index in shas and its contents, in order. fn
48// returning false stops the read. Cancelling parent stops it too, and is
49// reported as parent's error.
50func CatBlobs(parent context.Context, dir string, shas []string, fn func(i int, data []byte) bool) error {
51 ctx, cancel := context.WithCancel(parent)
52 defer cancel()
53 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir, "cat-file", "--batch")
54 stdin, err := cmd.StdinPipe()
55 if err != nil {
56 return err
57 }
58 stdout, err := cmd.StdoutPipe()
59 if err != nil {
60 return err
61 }
62 if err := cmd.Start(); err != nil {
63 return err
64 }
65 go func() {
66 w := bufio.NewWriter(stdin)
67 for _, s := range shas {
68 if _, err := fmt.Fprintln(w, s); err != nil {
69 break
70 }
71 }
72 w.Flush()
73 stdin.Close()
74 }()
75 r := bufio.NewReader(stdout)
76 readErr := func() error {
77 for i := range shas {
78 header, err := r.ReadString('\n')
79 if err != nil {
80 return err
81 }
82 // <sha> <type> <size>, or <sha> missing
83 f := strings.Fields(header)
84 if len(f) != 3 {
85 return fmt.Errorf("cat-file: %s", strings.TrimSpace(header))
86 }
87 size, err := strconv.ParseInt(f[2], 10, 64)
88 if err != nil {
89 return fmt.Errorf("cat-file: %s", strings.TrimSpace(header))
90 }
91 data := make([]byte, size+1) // the object and its trailing newline
92 if _, err := io.ReadFull(r, data); err != nil {
93 return err
94 }
95 if !fn(i, data[:size]) {
96 return errStopped
97 }
98 }
99 return nil
100 }()
101 if readErr != nil {
102 cancel()
103 }
104 io.Copy(io.Discard, stdout)
105 waitErr := cmd.Wait()
106 switch {
107 case readErr == errStopped:
108 return nil
109 case parent.Err() != nil:
110 return parent.Err()
111 case readErr != nil:
112 return readErr
113 }
114 return waitErr
115}
116
117var errStopped = errors.New("stopped")