krz/gitbay
A CLI-first git forge.
clone: git clone https://gitbay.org/krz/gitbay.git
repo-descriptions: 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}