internal/gitutil/treeblobs.go

v1.41.0
gitbay/internal/gitutil/treeblobs.go history · blame · raw

117 lines · 2999 bytes

  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")