internal/gitutil/batch.go

81 lines · 2113 bytes

4 symbols in this file
 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}