internal/gitutil/batch.go
81 lines · 2113 bytes
1package gitutil
2
3import (
4 "bufio"
5 "fmt"
6 "io"
7 "os/exec"
8 "strconv"
9 "strings"
10
11 "gitbay.org/gitbay/internal/toolpath"
12)
13
14// BlobBatch reads blobs by object id through one `git cat-file --batch`
15// process, for a caller reading many blobs in one request.
16type BlobBatch struct {
17 cmd *exec.Cmd
18 in io.WriteCloser
19 out *bufio.Reader
20}
21
22// NewBlobBatch starts the cat-file process in dir. Close ends it.
23func NewBlobBatch(dir string) (*BlobBatch, error) {
24 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "--batch")
25 in, err := cmd.StdinPipe()
26 if err != nil {
27 return nil, err
28 }
29 out, err := cmd.StdoutPipe()
30 if err != nil {
31 return nil, err
32 }
33 if err := cmd.Start(); err != nil {
34 return nil, err
35 }
36 return &BlobBatch{cmd: cmd, in: in, out: bufio.NewReader(out)}, nil
37}
38
39// Read returns the blob oid, refusing one larger than limit bytes.
40func (b *BlobBatch) Read(oid string, limit int64) ([]byte, error) {
41 if strings.ContainsAny(oid, " \n") {
42 return nil, fmt.Errorf("bad object id %q", oid)
43 }
44 if _, err := io.WriteString(b.in, oid+"\n"); err != nil {
45 return nil, err
46 }
47 head, err := b.out.ReadString('\n')
48 if err != nil {
49 return nil, err
50 }
51 f := strings.Fields(head)
52 if len(f) != 3 {
53 return nil, fmt.Errorf("cat-file %s: %s", oid, strings.TrimSpace(head))
54 }
55 size, err := strconv.ParseInt(f[2], 10, 64)
56 if err != nil {
57 return nil, fmt.Errorf("cat-file %s: %s", oid, strings.TrimSpace(head))
58 }
59 // A refused object is still on the stream, with its trailing
60 // newline; skipping it keeps the next Read in step.
61 if f[1] != "blob" || size > limit {
62 if _, err := io.CopyN(io.Discard, b.out, size+1); err != nil {
63 return nil, err
64 }
65 if f[1] != "blob" {
66 return nil, fmt.Errorf("%s is a %s, not a blob", oid, f[1])
67 }
68 return nil, fmt.Errorf("%s is larger than %d bytes", oid, limit)
69 }
70 data := make([]byte, size+1) // the object and its trailing newline
71 if _, err := io.ReadFull(b.out, data); err != nil {
72 return nil, err
73 }
74 return data[:size], nil
75}
76
77// Close ends the process.
78func (b *BlobBatch) Close() error {
79 b.in.Close()
80 return b.cmd.Wait()
81}