Commit 1030a916f9

1030a916f95d6fd434574c3fe59be9c9aac0fe21

parent: d99ece54b6

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-03 18:22 UTC

gitutil: end option parsing before every ref

Refs reaching gitutil come from URL segments and command arguments, and
the call sites passed them as bare arguments after a subcommand's
options. A leading-dash ref was only harmless because every caller
happened to glue it to :path or ^{commit} or resolve it first. Each
invocation now passes --end-of-options before the ref (rev-parse with
--verify, since it otherwise echoes the flag), and blame, which has no
such flag, resolves the ref to a sha first.

TestRefsAreNotOptions calls every ref-taking helper with an
option-shaped ref and asserts nothing was parsed as a flag.

Closes #135

Layout: unified · split

internal/gitutil/blame.go +6 −1
@@ -23,8 +23,13 @@ type BlameHunk struct {
23// Blame attributes lines start..end (1-based, inclusive) of path at ref, 23// Blame attributes lines start..end (1-based, inclusive) of path at ref,
24// merging consecutive same-commit lines into hunks. 24// merging consecutive same-commit lines into hunks.
25func Blame(dir, ref, path string, start, end int) ([]BlameHunk, error) { 25func Blame(dir, ref, path string, start, end int) ([]BlameHunk, error) {
26 // blame has no --end-of-options; a resolved sha cannot be an option.
27 sha, err := ResolveRef(dir, ref)
28 if err != nil {
29 return nil, err
30 }
26 cmd := exec.Command("git", "-C", dir, "blame", "--porcelain", 31 cmd := exec.Command("git", "-C", dir, "blame", "--porcelain",
27 fmt.Sprintf("-L%d,%d", start, end), ref, "--", path) 32 fmt.Sprintf("-L%d,%d", start, end), sha, "--", path)
28 out, err := cmd.Output() 33 out, err := cmd.Output()
29 if err != nil { 34 if err != nil {
30 return nil, fmt.Errorf("git blame %s at %s: %w", path, ref, err) 35 return nil, fmt.Errorf("git blame %s at %s: %w", path, ref, err)
internal/gitutil/endofoptions_test.go added +54
@@ -0,0 +1,54 @@
1package gitutil
2
3import (
4 "bytes"
5 "os"
6 "path/filepath"
7 "testing"
8)
9
10// A ref reaching gitutil is user input: a URL segment or an argument. Every
11// git invocation ends option parsing before it, so a ref shaped like an
12// option is a bad revision, never a flag (#135). The payload here is an
13// option several subcommands accept, whose effect would be a file.
14func TestRefsAreNotOptions(t *testing.T) {
15 dir := t.TempDir()
16 git(t, dir, "init", "-q", "-b", "main")
17 write(t, dir, "f.txt", "hi\n")
18 git(t, dir, "add", ".")
19 git(t, dir, "commit", "-q", "-m", "base")
20 pwn := filepath.Join(t.TempDir(), "pwned")
21 ref := "--output=" + pwn
22
23 var sink bytes.Buffer
24 calls := map[string]func() error{
25 "CountCommits": func() error { CountCommits(dir, ref); return nil },
26 "Contributors": func() error { Contributors(dir, ref); return nil },
27 "Languages": func() error { Languages(dir, ref, func(string) string { return "x" }); return nil },
28 "RevList": func() error { _, err := RevList(dir, ref, 1); return err },
29 "RevListPath": func() error { _, err := RevListPath(dir, ref, "f.txt", 1); return err },
30 "PeelToCommit": func() error { _, err := PeelToCommit(dir, ref); return err },
31 "ReadCommit": func() error { _, err := ReadCommit(dir, ref); return err },
32 "ListTree": func() error { _, err := ListTree(dir, ref, ""); return err },
33 "ReadBlob": func() error { _, err := ReadBlob(dir, ref, "f.txt", 1<<20); return err },
34 "ResolveRef": func() error { _, err := ResolveRef(dir, ref); return err },
35 "Archive": func() error { return Archive(dir, ref, "x", &sink) },
36 "Grep": func() error { _, err := Grep(dir, ref, "hi", 10); return err },
37 "MergeBase": func() error { _, err := MergeBase(dir, ref, "main"); return err },
38 "Diff": func() error { _, err := Diff(dir, ref, "main", 1<<20); return err },
39 "DiffFiles": func() error { _, err := DiffFiles(dir, ref, "main"); return err },
40 "RevListRange": func() error { _, err := RevListRange(dir, "main", ref); return err },
41 "Blame": func() error { _, err := Blame(dir, ref, "f.txt", 1, 1); return err },
42 "TipCommit": func() error { TipCommit(dir, ref); return nil },
43 "StatPath": func() error { StatPath(dir, ref, "f.txt"); return nil },
44 }
45 for name, call := range calls {
46 call()
47 if _, err := os.Stat(pwn); err == nil {
48 t.Fatalf("%s: ref %q was parsed as an option and wrote a file", name, ref)
49 }
50 }
51 if _, err := ResolveRef(dir, ref); err == nil {
52 t.Fatal("option-shaped ref resolved")
53 }
54}
internal/gitutil/facts.go +3 −3
@@ -10,7 +10,7 @@ import (
10// CountCommits returns the number of commits reachable from ref, or 0 when 10// CountCommits returns the number of commits reachable from ref, or 0 when
11// the ref does not resolve (an empty repository). 11// the ref does not resolve (an empty repository).
12func CountCommits(dir, ref string) int { 12func CountCommits(dir, ref string) int {
13 out, err := exec.Command("git", "-C", dir, "rev-list", "--count", ref).Output() 13 out, err := exec.Command("git", "-C", dir, "rev-list", "--count", "--end-of-options", ref).Output()
14 if err != nil { 14 if err != nil {
15 return 0 15 return 0
16 } 16 }
@@ -31,7 +31,7 @@ type Contributor struct {
31// say — a bare repo resolves that from HEAD:.mailmap with no config. 31// say — a bare repo resolves that from HEAD:.mailmap with no config.
32func Contributors(dir, ref string) []Contributor { 32func Contributors(dir, ref string) []Contributor {
33 out, err := exec.Command("git", "-C", dir, "log", 33 out, err := exec.Command("git", "-C", dir, "log",
34 "--use-mailmap", "--format=%aN%x01%aE", ref).Output() 34 "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output()
35 if err != nil { 35 if err != nil {
36 return nil 36 return nil
37 } 37 }
@@ -62,7 +62,7 @@ func Contributors(dir, ref string) []Contributor {
62// largest first, keyed by the extension map the caller supplies. Only 62// largest first, keyed by the extension map the caller supplies. Only
63// blobs count; git's own metadata does not. 63// blobs count; git's own metadata does not.
64func Languages(dir, ref string, lang func(path string) string) []Language { 64func Languages(dir, ref string, lang func(path string) string) []Language {
65 out, err := exec.Command("git", "-C", dir, "ls-tree", "-r", "-l", "--full-name", ref).Output() 65 out, err := exec.Command("git", "-C", dir, "ls-tree", "-r", "-l", "--full-name", "--end-of-options", ref).Output()
66 if err != nil { 66 if err != nil {
67 return nil 67 return nil
68 } 68 }
internal/gitutil/gitutil.go +4 −4
@@ -87,7 +87,7 @@ func ZeroSHA(s string) bool {
87 87
88// RevList returns up to limit commit SHAs reachable from ref, newest first. 88// RevList returns up to limit commit SHAs reachable from ref, newest first.
89func RevList(dir, ref string, limit int) ([]string, error) { 89func RevList(dir, ref string, limit int) ([]string, error) {
90 cmd := exec.Command("git", "-C", dir, "rev-list", fmt.Sprintf("--max-count=%d", limit), ref) 90 cmd := exec.Command("git", "-C", dir, "rev-list", fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref)
91 out, err := cmd.Output() 91 out, err := cmd.Output()
92 if err != nil { 92 if err != nil {
93 return nil, fmt.Errorf("rev-list %s: %w", ref, err) 93 return nil, fmt.Errorf("rev-list %s: %w", ref, err)
@@ -106,7 +106,7 @@ func RevList(dir, ref string, limit int) ([]string, error) {
106// read as an option or ref. 106// read as an option or ref.
107func RevListPath(dir, ref, filePath string, limit int) ([]string, error) { 107func RevListPath(dir, ref, filePath string, limit int) ([]string, error) {
108 cmd := exec.Command("git", "-C", dir, "rev-list", 108 cmd := exec.Command("git", "-C", dir, "rev-list",
109 fmt.Sprintf("--max-count=%d", limit), ref, "--", filePath) 109 fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref, "--", filePath)
110 out, err := cmd.Output() 110 out, err := cmd.Output()
111 if err != nil { 111 if err != nil {
112 return nil, fmt.Errorf("rev-list %s -- %s: %w", ref, filePath, err) 112 return nil, fmt.Errorf("rev-list %s -- %s: %w", ref, filePath, err)
@@ -123,7 +123,7 @@ func RevListPath(dir, ref, filePath string, limit int) ([]string, error) {
123// PeelToCommit resolves a ref or object to its commit — annotated tags 123// PeelToCommit resolves a ref or object to its commit — annotated tags
124// peel to the commit they point at. 124// peel to the commit they point at.
125func PeelToCommit(dir, ref string) (string, error) { 125func PeelToCommit(dir, ref string) (string, error) {
126 out, err := exec.Command("git", "-C", dir, "rev-parse", ref+"^{commit}").Output() 126 out, err := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--end-of-options", ref+"^{commit}").Output()
127 if err != nil { 127 if err != nil {
128 return "", fmt.Errorf("rev-parse %s^{commit}: %w", ref, err) 128 return "", fmt.Errorf("rev-parse %s^{commit}: %w", ref, err)
129 } 129 }
@@ -132,7 +132,7 @@ func PeelToCommit(dir, ref string) (string, error) {
132 132
133// ReadCommit returns the raw commit object bytes. 133// ReadCommit returns the raw commit object bytes.
134func ReadCommit(dir, sha string) ([]byte, error) { 134func ReadCommit(dir, sha string) ([]byte, error) {
135 cmd := exec.Command("git", "-C", dir, "cat-file", "commit", sha) 135 cmd := exec.Command("git", "-C", dir, "cat-file", "commit", "--end-of-options", sha)
136 out, err := cmd.Output() 136 out, err := cmd.Output()
137 if err != nil { 137 if err != nil {
138 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err) 138 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err)
internal/gitutil/grep.go +1 −1
@@ -23,7 +23,7 @@ func Grep(dir, ref, query string, max int) ([]GrepMatch, error) {
23 defer cancel() 23 defer cancel()
24 // -z: NUL after the path and the line number, so paths containing 24 // -z: NUL after the path and the line number, so paths containing
25 // ':' parse unambiguously (format: "ref:path\0line\0text\n"). 25 // ':' parse unambiguously (format: "ref:path\0line\0text\n").
26 cmd := exec.CommandContext(ctx, "git", "-C", dir, "grep", "-nIiF", "-z", "-e", query, ref) 26 cmd := exec.CommandContext(ctx, "git", "-C", dir, "grep", "-nIiF", "-z", "-e", query, "--end-of-options", ref)
27 out, err := cmd.Output() 27 out, err := cmd.Output()
28 if err != nil { 28 if err != nil {
29 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 { 29 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
internal/gitutil/lastcommit.go +3 −3
@@ -45,7 +45,7 @@ func LastCommits(dir, ref, path string, names []string) map[string]EntryCommit {
45 } 45 }
46 46
47 args := []string{"-C", dir, "log", "--first-parent", "--name-only", 47 args := []string{"-C", dir, "log", "--first-parent", "--name-only",
48 "--format=%x1e%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "-n", strconv.Itoa(lastCommitScan), ref} 48 "--format=%x1e%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "-n", strconv.Itoa(lastCommitScan), "--end-of-options", ref}
49 if prefix != "" { 49 if prefix != "" {
50 args = append(args, "--", strings.TrimSuffix(prefix, "/")) 50 args = append(args, "--", strings.TrimSuffix(prefix, "/"))
51 } 51 }
@@ -108,7 +108,7 @@ func parseCommitHeader(s string) EntryCommit {
108// answers "who touched this repository last". 108// answers "who touched this repository last".
109func TipCommit(dir, ref string) EntryCommit { 109func TipCommit(dir, ref string) EntryCommit {
110 out, err := exec.Command("git", "-C", dir, "log", "-1", 110 out, err := exec.Command("git", "-C", dir, "log", "-1",
111 "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", ref).Output() 111 "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "--end-of-options", ref).Output()
112 if err != nil { 112 if err != nil {
113 return EntryCommit{} 113 return EntryCommit{}
114 } 114 }
@@ -137,7 +137,7 @@ func entryName(changed, prefix string) (string, bool) {
137// page can report the facts the file listing no longer carries: its size, 137// page can report the facts the file listing no longer carries: its size,
138// and whether it is executable or a symlink. 138// and whether it is executable or a symlink.
139func StatPath(dir, ref, path string) (TreeEntry, bool) { 139func StatPath(dir, ref, path string) (TreeEntry, bool) {
140 out, err := exec.Command("git", "-C", dir, "ls-tree", "-l", ref, "--", path).Output() 140 out, err := exec.Command("git", "-C", dir, "ls-tree", "-l", "--end-of-options", ref, "--", path).Output()
141 if err != nil { 141 if err != nil {
142 return TreeEntry{}, false 142 return TreeEntry{}, false
143 } 143 }
internal/gitutil/merge.go +10 −10
@@ -44,7 +44,7 @@ func DeleteRef(dir, ref string) error {
44 44
45// RevListRange returns commits in old..new, newest first. 45// RevListRange returns commits in old..new, newest first.
46func RevListRange(dir, old, new string) ([]string, error) { 46func RevListRange(dir, old, new string) ([]string, error) {
47 cmd := exec.Command("git", "-C", dir, "rev-list", new, "^"+old) 47 cmd := exec.Command("git", "-C", dir, "rev-list", "--end-of-options", new, "^"+old)
48 out, err := cmd.Output() 48 out, err := cmd.Output()
49 if err != nil { 49 if err != nil {
50 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err) 50 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err)
@@ -61,7 +61,7 @@ func RevListRange(dir, old, new string) ([]string, error) {
61// MergeTree performs a real merge of ours and theirs, returning the merged 61// MergeTree performs a real merge of ours and theirs, returning the merged
62// tree id. conflict=true means the merge cannot be done automatically. 62// tree id. conflict=true means the merge cannot be done automatically.
63func MergeTree(dir, ours, theirs string) (tree string, conflict bool, err error) { 63func MergeTree(dir, ours, theirs string) (tree string, conflict bool, err error) {
64 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", ours, theirs) 64 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", "--end-of-options", ours, theirs)
65 out, runErr := cmd.Output() 65 out, runErr := cmd.Output()
66 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) 66 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
67 if runErr != nil { 67 if runErr != nil {
@@ -95,7 +95,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
95// Diff returns the patch for old..new (three-dot semantics are the caller's 95// Diff returns the patch for old..new (three-dot semantics are the caller's
96// job: pass the merge base as old). 96// job: pass the merge base as old).
97func Diff(dir, old, new string, limit int64) (string, error) { 97func Diff(dir, old, new string, limit int64) (string, error) {
98 cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", old, new) 98 cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new)
99 out, err := cmd.Output() 99 out, err := cmd.Output()
100 if err != nil { 100 if err != nil {
101 return "", fmt.Errorf("diff: %w", err) 101 return "", fmt.Errorf("diff: %w", err)
@@ -108,7 +108,7 @@ func Diff(dir, old, new string, limit int64) (string, error) {
108 108
109// MergeBase returns the best common ancestor, or an error if none exists. 109// MergeBase returns the best common ancestor, or an error if none exists.
110func MergeBase(dir, a, b string) (string, error) { 110func MergeBase(dir, a, b string) (string, error) {
111 cmd := exec.Command("git", "-C", dir, "merge-base", a, b) 111 cmd := exec.Command("git", "-C", dir, "merge-base", "--end-of-options", a, b)
112 out, err := cmd.Output() 112 out, err := cmd.Output()
113 if err != nil { 113 if err != nil {
114 return "", fmt.Errorf("no common history between %s and %s", a, b) 114 return "", fmt.Errorf("no common history between %s and %s", a, b)
@@ -175,7 +175,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
175 175
176// CommitParents returns the parent SHAs of a commit. 176// CommitParents returns the parent SHAs of a commit.
177func CommitParents(dir, sha string) ([]string, error) { 177func CommitParents(dir, sha string) ([]string, error) {
178 out, err := exec.Command("git", "-C", dir, "rev-list", "--parents", "-n1", sha).Output() 178 out, err := exec.Command("git", "-C", dir, "rev-list", "--parents", "-n1", "--end-of-options", sha).Output()
179 if err != nil { 179 if err != nil {
180 return nil, fmt.Errorf("rev-list --parents %s: %w", sha, err) 180 return nil, fmt.Errorf("rev-list --parents %s: %w", sha, err)
181 } 181 }
@@ -188,7 +188,7 @@ func CommitParents(dir, sha string) ([]string, error) {
188 188
189// AuthorIdent returns a commit's author name, email, and ISO date. 189// AuthorIdent returns a commit's author name, email, and ISO date.
190func AuthorIdent(dir, sha string) (name, email, date string, err error) { 190func AuthorIdent(dir, sha string) (name, email, date string, err error) {
191 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%an%x1f%ae%x1f%aI", sha).Output() 191 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%an%x1f%ae%x1f%aI", "--end-of-options", sha).Output()
192 if err != nil { 192 if err != nil {
193 return "", "", "", fmt.Errorf("log %s: %w", sha, err) 193 return "", "", "", fmt.Errorf("log %s: %w", sha, err)
194 } 194 }
@@ -201,7 +201,7 @@ func AuthorIdent(dir, sha string) (name, email, date string, err error) {
201 201
202// CommitMessage returns a commit's full message. 202// CommitMessage returns a commit's full message.
203func CommitMessage(dir, sha string) (string, error) { 203func CommitMessage(dir, sha string) (string, error) {
204 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%B", sha).Output() 204 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%B", "--end-of-options", sha).Output()
205 if err != nil { 205 if err != nil {
206 return "", fmt.Errorf("log %s: %w", sha, err) 206 return "", fmt.Errorf("log %s: %w", sha, err)
207 } 207 }
@@ -211,7 +211,7 @@ func CommitMessage(dir, sha string) (string, error) {
211// MergeTreeOnto replays commit's changes (relative to base) onto onto, 211// MergeTreeOnto replays commit's changes (relative to base) onto onto,
212// returning the resulting tree. conflict=true when it cannot apply cleanly. 212// returning the resulting tree. conflict=true when it cannot apply cleanly.
213func MergeTreeOnto(dir, base, onto, commit string) (tree string, conflict bool, err error) { 213func MergeTreeOnto(dir, base, onto, commit string) (tree string, conflict bool, err error) {
214 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", "--merge-base="+base, onto, commit) 214 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", "--merge-base="+base, "--end-of-options", onto, commit)
215 out, runErr := cmd.Output() 215 out, runErr := cmd.Output()
216 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) 216 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
217 if runErr != nil { 217 if runErr != nil {
@@ -249,7 +249,7 @@ func CommitTreeIdent(dir, tree string, parents []string,
249 249
250// ResolveTree returns the tree id of a commit. 250// ResolveTree returns the tree id of a commit.
251func ResolveTree(dir, sha string) (string, error) { 251func ResolveTree(dir, sha string) (string, error) {
252 out, err := exec.Command("git", "-C", dir, "rev-parse", sha+"^{tree}").Output() 252 out, err := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--end-of-options", sha+"^{tree}").Output()
253 if err != nil { 253 if err != nil {
254 return "", fmt.Errorf("rev-parse %s^{tree}: %w", sha, err) 254 return "", fmt.Errorf("rev-parse %s^{tree}: %w", sha, err)
255 } 255 }
@@ -258,7 +258,7 @@ func ResolveTree(dir, sha string) (string, error) {
258 258
259// DiffFiles lists the paths changed between old and new. 259// DiffFiles lists the paths changed between old and new.
260func DiffFiles(dir, old, new string) ([]string, error) { 260func DiffFiles(dir, old, new string) ([]string, error) {
261 out, err := exec.Command("git", "-C", dir, "diff", "--name-only", old, new).Output() 261 out, err := exec.Command("git", "-C", dir, "diff", "--name-only", "--end-of-options", old, new).Output()
262 if err != nil { 262 if err != nil {
263 return nil, fmt.Errorf("diff --name-only: %w", err) 263 return nil, fmt.Errorf("diff --name-only: %w", err)
264 } 264 }
internal/gitutil/read.go +4 −4
@@ -26,7 +26,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
26 if path != "" { 26 if path != "" {
27 spec = ref + ":" + path 27 spec = ref + ":" + path
28 } 28 }
29 cmd := exec.Command("git", "-C", dir, "ls-tree", "-l", spec) 29 cmd := exec.Command("git", "-C", dir, "ls-tree", "-l", "--end-of-options", spec)
30 out, err := cmd.Output() 30 out, err := cmd.Output()
31 if err != nil { 31 if err != nil {
32 return nil, fmt.Errorf("ls-tree %s: %w", spec, err) 32 return nil, fmt.Errorf("ls-tree %s: %w", spec, err)
@@ -56,7 +56,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
56 56
57// ReadBlob returns the contents of ref:path, capped at limit bytes. 57// ReadBlob returns the contents of ref:path, capped at limit bytes.
58func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) { 58func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
59 cmd := exec.Command("git", "-C", dir, "cat-file", "blob", ref+":"+path) 59 cmd := exec.Command("git", "-C", dir, "cat-file", "blob", "--end-of-options", ref+":"+path)
60 stdout, err := cmd.StdoutPipe() 60 stdout, err := cmd.StdoutPipe()
61 if err != nil { 61 if err != nil {
62 return nil, err 62 return nil, err
@@ -74,7 +74,7 @@ func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
74 74
75// ResolveRef resolves a ref or sha to a full commit sha; errors if absent. 75// ResolveRef resolves a ref or sha to a full commit sha; errors if absent.
76func ResolveRef(dir, ref string) (string, error) { 76func ResolveRef(dir, ref string) (string, error) {
77 cmd := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet", ref+"^{commit}") 77 cmd := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet", "--end-of-options", ref+"^{commit}")
78 out, err := cmd.Output() 78 out, err := cmd.Output()
79 if err != nil { 79 if err != nil {
80 return "", fmt.Errorf("unknown ref %q", ref) 80 return "", fmt.Errorf("unknown ref %q", ref)
@@ -118,7 +118,7 @@ var ErrArchiveTooLarge = errors.New("archive exceeds the size limit")
118func Archive(dir, ref, prefix string, w io.Writer) error { 118func Archive(dir, ref, prefix string, w io.Writer) error {
119 ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout) 119 ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout)
120 defer cancel() 120 defer cancel()
121 cmd := exec.CommandContext(ctx, "git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", ref) 121 cmd := exec.CommandContext(ctx, "git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", "--end-of-options", ref)
122 lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel} 122 lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel}
123 cmd.Stdout = lw 123 cmd.Stdout = lw
124 err := cmd.Run() 124 err := cmd.Run()