krz/gitbay

A CLI-first git forge.

clone: git clone https://gitbay.org/krz/gitbay.git

main: internal/gitutil/read.go · raw

  1package gitutil
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"io"
  7	"os/exec"
  8	"strconv"
  9	"strings"
 10)
 11
 12type TreeEntry struct {
 13	Mode string
 14	Type string // blob | tree
 15	SHA  string
 16	Size int64 // -1 for trees
 17	Name string
 18}
 19
 20// ListTree lists one level of the tree at ref:path.
 21func ListTree(dir, ref, path string) ([]TreeEntry, error) {
 22	spec := ref
 23	if path != "" {
 24		spec = ref + ":" + path
 25	}
 26	cmd := exec.Command("git", "-C", dir, "ls-tree", "-l", spec)
 27	out, err := cmd.Output()
 28	if err != nil {
 29		return nil, fmt.Errorf("ls-tree %s: %w", spec, err)
 30	}
 31	var entries []TreeEntry
 32	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
 33		if line == "" {
 34			continue
 35		}
 36		// <mode> <type> <sha> <size>\t<name>
 37		meta, name, ok := strings.Cut(line, "\t")
 38		if !ok {
 39			continue
 40		}
 41		f := strings.Fields(meta)
 42		if len(f) != 4 {
 43			continue
 44		}
 45		size := int64(-1)
 46		if f[3] != "-" {
 47			size, _ = strconv.ParseInt(f[3], 10, 64)
 48		}
 49		entries = append(entries, TreeEntry{Mode: f[0], Type: f[1], SHA: f[2], Size: size, Name: name})
 50	}
 51	return entries, nil
 52}
 53
 54// ReadBlob returns the contents of ref:path, capped at limit bytes.
 55func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
 56	cmd := exec.Command("git", "-C", dir, "cat-file", "blob", ref+":"+path)
 57	stdout, err := cmd.StdoutPipe()
 58	if err != nil {
 59		return nil, err
 60	}
 61	if err := cmd.Start(); err != nil {
 62		return nil, err
 63	}
 64	data, err := io.ReadAll(io.LimitReader(stdout, limit))
 65	io.Copy(io.Discard, stdout) // drain so git exits cleanly
 66	if werr := cmd.Wait(); werr != nil {
 67		return nil, fmt.Errorf("cat-file blob %s:%s: %w", ref, path, werr)
 68	}
 69	return data, err
 70}
 71
 72// ResolveRef resolves a ref or sha to a full commit sha; errors if absent.
 73func ResolveRef(dir, ref string) (string, error) {
 74	cmd := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet", ref+"^{commit}")
 75	out, err := cmd.Output()
 76	if err != nil {
 77		return "", fmt.Errorf("unknown ref %q", ref)
 78	}
 79	return strings.TrimSpace(string(out)), nil
 80}
 81
 82type Ref struct {
 83	Name string
 84	SHA  string
 85}
 86
 87// Refs lists branches or tags; kind is "heads" or "tags".
 88func Refs(dir, kind string) ([]Ref, error) {
 89	cmd := exec.Command("git", "-C", dir, "for-each-ref",
 90		"--format=%(refname:short) %(objectname)", "refs/"+kind)
 91	out, err := cmd.Output()
 92	if err != nil {
 93		return nil, err
 94	}
 95	var refs []Ref
 96	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
 97		if name, sha, ok := strings.Cut(line, " "); ok {
 98			refs = append(refs, Ref{Name: name, SHA: sha})
 99		}
100	}
101	return refs, nil
102}
103
104// Archive streams a tar.gz of ref to w.
105func Archive(dir, ref, prefix string, w io.Writer) error {
106	cmd := exec.Command("git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", ref)
107	cmd.Stdout = w
108	return cmd.Run()
109}
110
111// ShowPatch returns the stat+patch text for one commit.
112func ShowPatch(dir, sha string, limit int64) (string, error) {
113	cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", sha)
114	stdout, err := cmd.StdoutPipe()
115	if err != nil {
116		return "", err
117	}
118	if err := cmd.Start(); err != nil {
119		return "", err
120	}
121	data, _ := io.ReadAll(io.LimitReader(stdout, limit))
122	io.Copy(io.Discard, stdout)
123	if err := cmd.Wait(); err != nil {
124		return "", fmt.Errorf("show %s: %w", sha, err)
125	}
126	return string(data), nil
127}
128
129// IsBinary reports whether data looks like binary content.
130func IsBinary(data []byte) bool {
131	return bytes.IndexByte(data, 0) >= 0
132}