exec: resolve git and ssh once, at start-up !255

merged merged by cmc on 2026-09-05 00:02 UTC · krz/gitbay:sonar-toolpath into main

20 files changed, +216 −72

Layout: unified · split

cmd/gitbay-runner/main.go +5 −4
@@ -24,6 +24,7 @@ import (
24 "time" 24 "time"
25 25
26 "gitbay.org/gitbay/internal/buildinfo" 26 "gitbay.org/gitbay/internal/buildinfo"
27 "gitbay.org/gitbay/internal/toolpath"
27) 28)
28 29
29type job struct { 30type job struct {
@@ -203,7 +204,7 @@ func (r *runner) run(j job) bool {
203 defer os.RemoveAll(dir) 204 defer os.RemoveAll(dir)
204 205
205 // One long-lived `runner log` session receives the whole stream. 206 // One long-lived `runner log` session receives the whole stream.
206 logCmd := exec.Command("ssh", append(r.sshOpts, r.remote, "runner", "log", fmt.Sprint(j.ID))...) 207 logCmd := exec.Command(toolpath.Look("ssh"), append(r.sshOpts, r.remote, "runner", "log", fmt.Sprint(j.ID))...)
207 pipe, err := logCmd.StdinPipe() 208 pipe, err := logCmd.StdinPipe()
208 if err != nil { 209 if err != nil {
209 log.Printf("build %d: log pipe: %v", j.ID, err) 210 log.Printf("build %d: log pipe: %v", j.ID, err)
@@ -294,7 +295,7 @@ func (r *runner) run(j job) bool {
294 } 295 }
295 steps = append(steps, []string{"-C", dir, "checkout", "-q", j.SHA}) 296 steps = append(steps, []string{"-C", dir, "checkout", "-q", j.SHA})
296 for _, args := range steps { 297 for _, args := range steps {
297 cmd := exec.Command("git", args...) 298 cmd := exec.Command(toolpath.Look("git"), args...)
298 cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0") 299 cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0")
299 cmd.Stdout, cmd.Stderr = sink, sink 300 cmd.Stdout, cmd.Stderr = sink, sink
300 if ok, why := runStep(cmd, deadline); !ok { 301 if ok, why := runStep(cmd, deadline); !ok {
@@ -305,7 +306,7 @@ func (r *runner) run(j job) bool {
305 306
306 for _, step := range j.Steps { 307 for _, step := range j.Steps {
307 fmt.Fprintf(sink, "$ %s\n", step) 308 fmt.Fprintf(sink, "$ %s\n", step)
308 cmd := exec.Command("sh", "-c", step) 309 cmd := exec.Command(toolpath.Look("sh"), "-c", step)
309 cmd.Dir = dir 310 cmd.Dir = dir
310 cmd.Env = append(os.Environ(), 311 cmd.Env = append(os.Environ(),
311 "GITBAY_REPO="+j.Repo, "GITBAY_SHA="+j.SHA, "GITBAY_REF="+j.Ref, "GITBAY_JOB="+j.Job, "CI=true") 312 "GITBAY_REPO="+j.Repo, "GITBAY_SHA="+j.SHA, "GITBAY_REF="+j.Ref, "GITBAY_JOB="+j.Job, "CI=true")
@@ -323,7 +324,7 @@ func (r *runner) run(j job) bool {
323 324
324// ssh runs one control command against the server and returns stdout. 325// ssh runs one control command against the server and returns stdout.
325func (r *runner) ssh(stdin io.Reader, args ...string) (string, error) { 326func (r *runner) ssh(stdin io.Reader, args ...string) (string, error) {
326 cmd := exec.Command("ssh", append(append(r.sshOpts, r.remote), args...)...) 327 cmd := exec.Command(toolpath.Look("ssh"), append(append(r.sshOpts, r.remote), args...)...)
327 if stdin != nil { 328 if stdin != nil {
328 cmd.Stdin = stdin 329 cmd.Stdin = stdin
329 } 330 }
cmd/gitbay/local.go +5 −4
@@ -11,6 +11,7 @@ import (
11 11
12 "gitbay.org/gitbay/internal/cliconfig" 12 "gitbay.org/gitbay/internal/cliconfig"
13 "gitbay.org/gitbay/internal/protocol" 13 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/toolpath"
14) 15)
15 16
16// hasBodyFlag reports whether args already carry body/message input. 17// hasBodyFlag reports whether args already carry body/message input.
@@ -50,7 +51,7 @@ func maybeEditor(args []string, kind string, prefill func() string) ([]string, *
50 fmt.Fprintf(f, "\n# Write the %s body above. Lines starting with '#' are ignored.\n# Save an empty file to skip the body.\n", kind) 51 fmt.Fprintf(f, "\n# Write the %s body above. Lines starting with '#' are ignored.\n# Save an empty file to skip the body.\n", kind)
51 f.Close() 52 f.Close()
52 53
53 ed := exec.Command("sh", "-c", editor+" "+shellQuote(f.Name())) 54 ed := exec.Command(toolpath.Look("sh"), "-c", editor+" "+shellQuote(f.Name()))
54 ed.Stdin, ed.Stdout, ed.Stderr = os.Stdin, os.Stdout, os.Stderr 55 ed.Stdin, ed.Stdout, ed.Stderr = os.Stdin, os.Stdout, os.Stderr
55 if err := ed.Run(); err != nil { 56 if err := ed.Run(); err != nil {
56 return nil, nil, false, fmt.Errorf("editor: %w", err) 57 return nil, nil, false, fmt.Errorf("editor: %w", err)
@@ -74,7 +75,7 @@ func maybeEditor(args []string, kind string, prefill func() string) ([]string, *
74} 75}
75 76
76func runGitLocal(args ...string) int { 77func runGitLocal(args ...string) int {
77 cmd := exec.Command("git", args...) 78 cmd := exec.Command(toolpath.Look("git"), args...)
78 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 79 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
79 if err := cmd.Run(); err != nil { 80 if err := cmd.Run(); err != nil {
80 if ee, ok := err.(*exec.ExitError); ok { 81 if ee, ok := err.(*exec.ExitError); ok {
@@ -190,7 +191,7 @@ func captureSSH(t target, serverArgv []string) (string, int) {
190 args := sshArgs(t.inst) 191 args := sshArgs(t.inst)
191 quoted := quoteAll(serverArgv) 192 quoted := quoteAll(serverArgv)
192 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " ")) 193 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " "))
193 cmd := exec.Command("ssh", args...) 194 cmd := exec.Command(toolpath.Look("ssh"), args...)
194 cmd.Stderr = os.Stderr 195 cmd.Stderr = os.Stderr
195 out, err := cmd.Output() 196 out, err := cmd.Output()
196 if err != nil { 197 if err != nil {
@@ -301,7 +302,7 @@ func cmdRemoteList() int {
301// currentBranch is the checked-out branch of the working directory's 302// currentBranch is the checked-out branch of the working directory's
302// clone, or "" outside a clone or on a detached HEAD. 303// clone, or "" outside a clone or on a detached HEAD.
303func currentBranch() string { 304func currentBranch() string {
304 out, err := exec.Command("git", "symbolic-ref", "--quiet", "--short", "HEAD").Output() 305 out, err := exec.Command(toolpath.Look("git"), "symbolic-ref", "--quiet", "--short", "HEAD").Output()
305 if err != nil { 306 if err != nil {
306 return "" 307 return ""
307 } 308 }
cmd/gitbay/migrate.go +4 −3
@@ -11,6 +11,7 @@ import (
11 "github.com/spf13/cobra" 11 "github.com/spf13/cobra"
12 12
13 "gitbay.org/gitbay/internal/protocol" 13 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/toolpath"
14) 15)
15 16
16func migrateCmd() *cobra.Command { 17func migrateCmd() *cobra.Command {
@@ -43,7 +44,7 @@ func sourceSSH(host string, port int, extra ...string) *exec.Cmd {
43 } 44 }
44 args = append(args, "git@"+host, "--") 45 args = append(args, "git@"+host, "--")
45 args = append(args, extra...) 46 args = append(args, extra...)
46 return exec.Command("ssh", args...) 47 return exec.Command(toolpath.Look("ssh"), args...)
47} 48}
48 49
49func runMigrate(from string, fromPort int) int { 50func runMigrate(from string, fromPort int) int {
@@ -99,14 +100,14 @@ func runMigrate(from string, fromPort int) int {
99 if fromPort != 0 && fromPort != 22 { 100 if fromPort != 0 && fromPort != 22 {
100 srcURL = fmt.Sprintf("ssh://git@%s:%d/%s.git", from, fromPort, repoPath) 101 srcURL = fmt.Sprintf("ssh://git@%s:%d/%s.git", from, fromPort, repoPath)
101 } 102 }
102 clone := exec.Command("git", "clone", "--quiet", "--mirror", srcURL, tmp+"/r") 103 clone := exec.Command(toolpath.Look("git"), "clone", "--quiet", "--mirror", srcURL, tmp+"/r")
103 clone.Stderr = os.Stderr 104 clone.Stderr = os.Stderr
104 if err := clone.Run(); err != nil { 105 if err := clone.Run(); err != nil {
105 fmt.Fprintf(os.Stderr, "gitbay: cloning %s failed; fix and re-run migrate (it resumes)\n", repoPath) 106 fmt.Fprintf(os.Stderr, "gitbay: cloning %s failed; fix and re-run migrate (it resumes)\n", repoPath)
106 os.RemoveAll(tmp) 107 os.RemoveAll(tmp)
107 return protocol.ExitFailure 108 return protocol.ExitFailure
108 } 109 }
109 push := exec.Command("git", "-C", tmp+"/r", "push", "--quiet", t.inst.CloneURL(repoPath), 110 push := exec.Command(toolpath.Look("git"), "-C", tmp+"/r", "push", "--quiet", t.inst.CloneURL(repoPath),
110 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*") 111 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*")
111 if len(t.inst.SSHOptions) > 0 { 112 if len(t.inst.SSHOptions) > 0 {
112 push.Env = append(os.Environ(), "GIT_SSH_COMMAND=ssh "+strings.Join(quoteAll(t.inst.SSHOptions), " ")) 113 push.Env = append(os.Environ(), "GIT_SSH_COMMAND=ssh "+strings.Join(quoteAll(t.inst.SSHOptions), " "))
cmd/gitbay/ssh.go +4 −3
@@ -13,6 +13,7 @@ import (
13 13
14 "gitbay.org/gitbay/internal/cliconfig" 14 "gitbay.org/gitbay/internal/cliconfig"
15 "gitbay.org/gitbay/internal/protocol" 15 "gitbay.org/gitbay/internal/protocol"
16 "gitbay.org/gitbay/internal/toolpath"
16) 17)
17 18
18// context is the resolved target for a command: which instance to talk to 19// context is the resolved target for a command: which instance to talk to
@@ -59,7 +60,7 @@ func resolveTarget() (target, error) {
59} 60}
60 61
61func originURL() string { 62func originURL() string {
62 out, err := exec.Command("git", "remote", "get-url", "origin").Output() 63 out, err := exec.Command(toolpath.Look("git"), "remote", "get-url", "origin").Output()
63 if err != nil { 64 if err != nil {
64 return "" 65 return ""
65 } 66 }
@@ -115,7 +116,7 @@ func runSSH(t target, serverArgv []string, stdin io.Reader) int {
115 } 116 }
116 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " ")) 117 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " "))
117 118
118 cmd := exec.Command("ssh", args...) 119 cmd := exec.Command(toolpath.Look("ssh"), args...)
119 cmd.Stdin = stdin 120 cmd.Stdin = stdin
120 cmd.Stdout = os.Stdout 121 cmd.Stdout = os.Stdout
121 cmd.Stderr = os.Stderr 122 cmd.Stderr = os.Stderr
@@ -145,7 +146,7 @@ func sshCapture(t target, serverArgv []string) (string, int) {
145 quoted[i] = shellQuote(a) 146 quoted[i] = shellQuote(a)
146 } 147 }
147 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " ")) 148 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " "))
148 out, err := exec.Command("ssh", args...).Output() 149 out, err := exec.Command(toolpath.Look("ssh"), args...).Output()
149 if err != nil { 150 if err != nil {
150 code := protocol.ExitProtocol 151 code := protocol.ExitProtocol
151 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() != 255 { 152 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() != 255 {
cmd/gitbayd/hook.go +3 −2
@@ -15,6 +15,7 @@ import (
15 "gitbay.org/gitbay/internal/gitutil" 15 "gitbay.org/gitbay/internal/gitutil"
16 "gitbay.org/gitbay/internal/hookd" 16 "gitbay.org/gitbay/internal/hookd"
17 "gitbay.org/gitbay/internal/policy" 17 "gitbay.org/gitbay/internal/policy"
18 "gitbay.org/gitbay/internal/toolpath"
18) 19)
19 20
20// incomingSHAs lists the commits this push introduces, in order, without 21// incomingSHAs lists the commits this push introduces, in order, without
@@ -28,7 +29,7 @@ func incomingSHAs(updates []policy.RefUpdate) ([]string, error) {
28 continue 29 continue
29 } 30 }
30 // Everything reachable from the new tip that no existing ref has. 31 // Everything reachable from the new tip that no existing ref has.
31 raw, err := exec.Command("git", "rev-list", u.New, "--not", "--all").Output() 32 raw, err := exec.Command(toolpath.Look("git"), "rev-list", u.New, "--not", "--all").Output()
32 if err != nil { 33 if err != nil {
33 return nil, fmt.Errorf("rev-list %s: %w", u.New, err) 34 return nil, fmt.Errorf("rev-list %s: %w", u.New, err)
34 } 35 }
@@ -62,7 +63,7 @@ func streamIncomingCommits(updates []policy.RefUpdate, emit func(hookd.RawCommit
62 // part-way through a large push leaves git blocked writing into a 63 // part-way through a large push leaves git blocked writing into a
63 // pipe nobody is reading and Wait blocked on git. 64 // pipe nobody is reading and Wait blocked on git.
64 ctx, cancel := context.WithCancel(context.Background()) 65 ctx, cancel := context.WithCancel(context.Background())
65 cmd := exec.CommandContext(ctx, "git", "cat-file", "--batch") 66 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "cat-file", "--batch")
66 stdin, err := cmd.StdinPipe() 67 stdin, err := cmd.StdinPipe()
67 if err != nil { 68 if err != nil {
68 cancel() 69 cancel()
cmd/gitbayd/main.go +7
@@ -32,6 +32,7 @@ import (
32 "gitbay.org/gitbay/internal/notify" 32 "gitbay.org/gitbay/internal/notify"
33 "gitbay.org/gitbay/internal/sshd" 33 "gitbay.org/gitbay/internal/sshd"
34 "gitbay.org/gitbay/internal/store" 34 "gitbay.org/gitbay/internal/store"
35 "gitbay.org/gitbay/internal/toolpath"
35 "gitbay.org/gitbay/internal/webhook" 36 "gitbay.org/gitbay/internal/webhook"
36) 37)
37 38
@@ -122,6 +123,12 @@ func serveCmd() *cobra.Command {
122 // First line of every run: the journal then says which commit is 123 // First line of every run: the journal then says which commit is
123 // serving, without rebuilding the binary to find out. 124 // serving, without rebuilding the binary to find out.
124 logBuild() 125 logBuild()
126 // The tools this daemon shells out to are resolved once, at
127 // package init. Say so now rather than failing on whichever
128 // request first needed git.
129 if err := toolpath.Verify(); err != nil {
130 return fmt.Errorf("required tools missing: %w", err)
131 }
125 cfg, err := config.Load(configPath) 132 cfg, err := config.Load(configPath)
126 if err != nil { 133 if err != nil {
127 return err 134 return err
cmd/gitbayd/maint.go +2 −1
@@ -13,6 +13,7 @@ import (
13 "gitbay.org/gitbay/internal/gitutil" 13 "gitbay.org/gitbay/internal/gitutil"
14 "gitbay.org/gitbay/internal/lfs" 14 "gitbay.org/gitbay/internal/lfs"
15 "gitbay.org/gitbay/internal/store" 15 "gitbay.org/gitbay/internal/store"
16 "gitbay.org/gitbay/internal/toolpath"
16) 17)
17 18
18func gcCmd() *cobra.Command { 19func gcCmd() *cobra.Command {
@@ -51,7 +52,7 @@ func gcCmd() *cobra.Command {
51 if aggressive { 52 if aggressive {
52 gcArgs = append(gcArgs, "--aggressive") 53 gcArgs = append(gcArgs, "--aggressive")
53 } 54 }
54 if out, err := exec.Command("git", gcArgs...).CombinedOutput(); err != nil { 55 if out, err := exec.Command(toolpath.Look("git"), gcArgs...).CombinedOutput(); err != nil {
55 fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out) 56 fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out)
56 continue 57 continue
57 } 58 }
internal/gitd/gitd.go +2 −1
@@ -16,6 +16,7 @@ import (
16 "gitbay.org/gitbay/internal/config" 16 "gitbay.org/gitbay/internal/config"
17 "gitbay.org/gitbay/internal/control" 17 "gitbay.org/gitbay/internal/control"
18 "gitbay.org/gitbay/internal/store" 18 "gitbay.org/gitbay/internal/store"
19 "gitbay.org/gitbay/internal/toolpath"
19) 20)
20 21
21type Server struct { 22type Server struct {
@@ -68,7 +69,7 @@ func (s *Server) handle(conn net.Conn) {
68 } 69 }
69 70
70 dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name) 71 dir := control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)
71 cmd := exec.Command("git", "upload-pack", dir) 72 cmd := exec.Command(toolpath.Look("git"), "upload-pack", dir)
72 cmd.Env = append(os.Environ(), protoEnv...) 73 cmd.Env = append(os.Environ(), protoEnv...)
73 cmd.Stdin = conn 74 cmd.Stdin = conn
74 cmd.Stdout = conn 75 cmd.Stdout = conn
internal/gitutil/blame.go +3 −1
@@ -7,6 +7,8 @@ import (
7 "os/exec" 7 "os/exec"
8 "strconv" 8 "strconv"
9 "strings" 9 "strings"
10
11 "gitbay.org/gitbay/internal/toolpath"
10) 12)
11 13
12// BlameHunk is a run of consecutive lines attributed to one commit. 14// BlameHunk is a run of consecutive lines attributed to one commit.
@@ -28,7 +30,7 @@ func Blame(dir, ref, path string, start, end int) ([]BlameHunk, error) {
28 if err != nil { 30 if err != nil {
29 return nil, err 31 return nil, err
30 } 32 }
31 cmd := exec.Command("git", "-C", dir, "blame", "--porcelain", 33 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "blame", "--porcelain",
32 fmt.Sprintf("-L%d,%d", start, end), sha, "--", path) 34 fmt.Sprintf("-L%d,%d", start, end), sha, "--", path)
33 out, err := cmd.Output() 35 out, err := cmd.Output()
34 if err != nil { 36 if err != nil {
internal/gitutil/facts.go +5 −3
@@ -5,12 +5,14 @@ import (
5 "sort" 5 "sort"
6 "strconv" 6 "strconv"
7 "strings" 7 "strings"
8
9 "gitbay.org/gitbay/internal/toolpath"
8) 10)
9 11
10// CountCommits returns the number of commits reachable from ref, or 0 when 12// CountCommits returns the number of commits reachable from ref, or 0 when
11// the ref does not resolve (an empty repository). 13// the ref does not resolve (an empty repository).
12func CountCommits(dir, ref string) int { 14func CountCommits(dir, ref string) int {
13 out, err := exec.Command("git", "-C", dir, "rev-list", "--count", "--end-of-options", ref).Output() 15 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", "--count", "--end-of-options", ref).Output()
14 if err != nil { 16 if err != nil {
15 return 0 17 return 0
16 } 18 }
@@ -30,7 +32,7 @@ type Contributor struct {
30// tie back to an account, after the repository's own .mailmap has had its 32// tie back to an account, after the repository's own .mailmap has had its
31// say — a bare repo resolves that from HEAD:.mailmap with no config. 33// say — a bare repo resolves that from HEAD:.mailmap with no config.
32func Contributors(dir, ref string) []Contributor { 34func Contributors(dir, ref string) []Contributor {
33 out, err := exec.Command("git", "-C", dir, "log", 35 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log",
34 "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output() 36 "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output()
35 if err != nil { 37 if err != nil {
36 return nil 38 return nil
@@ -62,7 +64,7 @@ func Contributors(dir, ref string) []Contributor {
62// largest first, keyed by the extension map the caller supplies. Only 64// largest first, keyed by the extension map the caller supplies. Only
63// blobs count; git's own metadata does not. 65// blobs count; git's own metadata does not.
64func Languages(dir, ref string, lang func(path string) string) []Language { 66func Languages(dir, ref string, lang func(path string) string) []Language {
65 out, err := exec.Command("git", "-C", dir, "ls-tree", "-r", "-l", "--full-name", "--end-of-options", ref).Output() 67 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "ls-tree", "-r", "-l", "--full-name", "--end-of-options", ref).Output()
66 if err != nil { 68 if err != nil {
67 return nil 69 return nil
68 } 70 }
internal/gitutil/gitutil.go +14 −12
@@ -11,6 +11,8 @@ import (
11 "os/exec" 11 "os/exec"
12 "path/filepath" 12 "path/filepath"
13 "strings" 13 "strings"
14
15 "gitbay.org/gitbay/internal/toolpath"
14) 16)
15 17
16// InitBare creates a bare repository with the shared hooks directory wired 18// InitBare creates a bare repository with the shared hooks directory wired
@@ -19,14 +21,14 @@ func InitBare(path, defaultBranch, hooksPath string) error {
19 if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { 21 if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
20 return err 22 return err
21 } 23 }
22 cmd := exec.Command("git", "init", "--bare", "--initial-branch="+defaultBranch, path) 24 cmd := exec.Command(toolpath.Look("git"), "init", "--bare", "--initial-branch="+defaultBranch, path)
23 if out, err := cmd.CombinedOutput(); err != nil { 25 if out, err := cmd.CombinedOutput(); err != nil {
24 return fmt.Errorf("git init: %v\n%s", err, out) 26 return fmt.Errorf("git init: %v\n%s", err, out)
25 } 27 }
26 // An empty hooksPath leaves the bare repo with no hooks — used for 28 // An empty hooksPath leaves the bare repo with no hooks — used for
27 // companion repos (wikis) that carry no ref policy. 29 // companion repos (wikis) that carry no ref policy.
28 if hooksPath != "" { 30 if hooksPath != "" {
29 cmd = exec.Command("git", "-C", path, "config", "core.hooksPath", hooksPath) 31 cmd = exec.Command(toolpath.Look("git"), "-C", path, "config", "core.hooksPath", hooksPath)
30 if out, err := cmd.CombinedOutput(); err != nil { 32 if out, err := cmd.CombinedOutput(); err != nil {
31 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out) 33 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out)
32 } 34 }
@@ -49,7 +51,7 @@ func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer
49 default: 51 default:
50 return fmt.Errorf("unknown service %q", service) 52 return fmt.Errorf("unknown service %q", service)
51 } 53 }
52 cmd := exec.Command("git", args...) 54 cmd := exec.Command(toolpath.Look("git"), args...)
53 cmd.Env = append(os.Environ(), extraEnv...) 55 cmd.Env = append(os.Environ(), extraEnv...)
54 cmd.Stdin = stdin 56 cmd.Stdin = stdin
55 cmd.Stdout = stdout 57 cmd.Stdout = stdout
@@ -61,7 +63,7 @@ func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer
61// dir. It must run with the caller's environment intact so that quarantined 63// dir. It must run with the caller's environment intact so that quarantined
62// objects during pre-receive remain visible. 64// objects during pre-receive remain visible.
63func IsAncestor(dir, old, new string) (bool, error) { 65func IsAncestor(dir, old, new string) (bool, error) {
64 cmd := exec.Command("git", "-C", dir, "merge-base", "--is-ancestor", old, new) 66 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "merge-base", "--is-ancestor", old, new)
65 err := cmd.Run() 67 err := cmd.Run()
66 if err == nil { 68 if err == nil {
67 return true, nil 69 return true, nil
@@ -87,7 +89,7 @@ func ZeroSHA(s string) bool {
87 89
88// RevList returns up to limit commit SHAs reachable from ref, newest first. 90// RevList returns up to limit commit SHAs reachable from ref, newest first.
89func RevList(dir, ref string, limit int) ([]string, error) { 91func RevList(dir, ref string, limit int) ([]string, error) {
90 cmd := exec.Command("git", "-C", dir, "rev-list", fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref) 92 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref)
91 out, err := cmd.Output() 93 out, err := cmd.Output()
92 if err != nil { 94 if err != nil {
93 return nil, fmt.Errorf("rev-list %s: %w", ref, err) 95 return nil, fmt.Errorf("rev-list %s: %w", ref, err)
@@ -105,7 +107,7 @@ func RevList(dir, ref string, limit int) ([]string, error) {
105// touch filePath, newest first. The "--" keeps the path from ever being 107// touch filePath, newest first. The "--" keeps the path from ever being
106// read as an option or ref. 108// read as an option or ref.
107func RevListPath(dir, ref, filePath string, limit int) ([]string, error) { 109func RevListPath(dir, ref, filePath string, limit int) ([]string, error) {
108 cmd := exec.Command("git", "-C", dir, "rev-list", 110 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list",
109 fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref, "--", filePath) 111 fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref, "--", filePath)
110 out, err := cmd.Output() 112 out, err := cmd.Output()
111 if err != nil { 113 if err != nil {
@@ -123,7 +125,7 @@ func RevListPath(dir, ref, filePath string, limit int) ([]string, error) {
123// PeelToCommit resolves a ref or object to its commit — annotated tags 125// PeelToCommit resolves a ref or object to its commit — annotated tags
124// peel to the commit they point at. 126// peel to the commit they point at.
125func PeelToCommit(dir, ref string) (string, error) { 127func PeelToCommit(dir, ref string) (string, error) {
126 out, err := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--end-of-options", ref+"^{commit}").Output() 128 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-parse", "--verify", "--end-of-options", ref+"^{commit}").Output()
127 if err != nil { 129 if err != nil {
128 return "", fmt.Errorf("rev-parse %s^{commit}: %w", ref, err) 130 return "", fmt.Errorf("rev-parse %s^{commit}: %w", ref, err)
129 } 131 }
@@ -132,7 +134,7 @@ func PeelToCommit(dir, ref string) (string, error) {
132 134
133// ReadCommit returns the raw commit object bytes. 135// ReadCommit returns the raw commit object bytes.
134func ReadCommit(dir, sha string) ([]byte, error) { 136func ReadCommit(dir, sha string) ([]byte, error) {
135 cmd := exec.Command("git", "-C", dir, "cat-file", "commit", "--end-of-options", sha) 137 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "commit", "--end-of-options", sha)
136 out, err := cmd.Output() 138 out, err := cmd.Output()
137 if err != nil { 139 if err != nil {
138 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err) 140 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err)
@@ -145,7 +147,7 @@ func ReadCommit(dir, sha string) ([]byte, error) {
145// an interactive caller can watch. extraEnv carries credentials via 147// an interactive caller can watch. extraEnv carries credentials via
146// GIT_ASKPASS; the URL itself must never contain them. 148// GIT_ASKPASS; the URL itself must never contain them.
147func FetchMirror(ctx context.Context, dir, url string, errW io.Writer, extraEnv []string) error { 149func FetchMirror(ctx context.Context, dir, url string, errW io.Writer, extraEnv []string) error {
148 cmd := exec.CommandContext(ctx, "git", "-C", dir, "fetch", "--progress", "--no-write-fetch-head", url, 150 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir, "fetch", "--progress", "--no-write-fetch-head", url,
149 "+refs/heads/*:refs/heads/*", 151 "+refs/heads/*:refs/heads/*",
150 "+refs/tags/*:refs/tags/*", 152 "+refs/tags/*:refs/tags/*",
151 "+refs/notes/*:refs/notes/*") 153 "+refs/notes/*:refs/notes/*")
@@ -168,7 +170,7 @@ func FetchMirror(ctx context.Context, dir, url string, errW io.Writer, extraEnv
168// handshakes. extraEnv carries credentials via GIT_ASKPASS; the URL must 170// handshakes. extraEnv carries credentials via GIT_ASKPASS; the URL must
169// never contain them. 171// never contain them.
170func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraEnv []string) error { 172func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraEnv []string) error {
171 cmd := exec.CommandContext(ctx, "git", "-C", dir, "fetch", "--no-write-fetch-head", "--no-tags", 173 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir, "fetch", "--no-write-fetch-head", "--no-tags",
172 url, "+refs/pull/*/head:refs/gh-pull/*") 174 url, "+refs/pull/*/head:refs/gh-pull/*")
173 cmd.Env = append(os.Environ(), extraEnv...) 175 cmd.Env = append(os.Environ(), extraEnv...)
174 cmd.Stderr = errW 176 cmd.Stderr = errW
@@ -180,7 +182,7 @@ func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraE
180 182
181// RemoteDefaultBranch asks the remote which branch HEAD points at. 183// RemoteDefaultBranch asks the remote which branch HEAD points at.
182func RemoteDefaultBranch(ctx context.Context, url string, extraEnv []string) (string, error) { 184func RemoteDefaultBranch(ctx context.Context, url string, extraEnv []string) (string, error) {
183 cmd := exec.CommandContext(ctx, "git", "ls-remote", "--symref", url, "HEAD") 185 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "ls-remote", "--symref", url, "HEAD")
184 cmd.Env = append(os.Environ(), extraEnv...) 186 cmd.Env = append(os.Environ(), extraEnv...)
185 out, err := cmd.Output() 187 out, err := cmd.Output()
186 if err != nil { 188 if err != nil {
@@ -199,7 +201,7 @@ func RemoteDefaultBranch(ctx context.Context, url string, extraEnv []string) (st
199 201
200// SetHead points the bare repo's HEAD at a branch. 202// SetHead points the bare repo's HEAD at a branch.
201func SetHead(dir, branch string) error { 203func SetHead(dir, branch string) error {
202 cmd := exec.Command("git", "-C", dir, "symbolic-ref", "HEAD", "refs/heads/"+branch) 204 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "symbolic-ref", "HEAD", "refs/heads/"+branch)
203 if out, err := cmd.CombinedOutput(); err != nil { 205 if out, err := cmd.CombinedOutput(); err != nil {
204 return fmt.Errorf("symbolic-ref: %v\n%s", err, out) 206 return fmt.Errorf("symbolic-ref: %v\n%s", err, out)
205 } 207 }
internal/gitutil/grep.go +3 −1
@@ -7,6 +7,8 @@ import (
7 "strconv" 7 "strconv"
8 "strings" 8 "strings"
9 "time" 9 "time"
10
11 "gitbay.org/gitbay/internal/toolpath"
10) 12)
11 13
12type GrepMatch struct { 14type GrepMatch struct {
@@ -23,7 +25,7 @@ func Grep(dir, ref, query string, max int) ([]GrepMatch, error) {
23 defer cancel() 25 defer cancel()
24 // -z: NUL after the path and the line number, so paths containing 26 // -z: NUL after the path and the line number, so paths containing
25 // ':' parse unambiguously (format: "ref:path\0line\0text\n"). 27 // ':' parse unambiguously (format: "ref:path\0line\0text\n").
26 cmd := exec.CommandContext(ctx, "git", "-C", dir, "grep", "-nIiF", "-z", "-e", query, "--end-of-options", ref) 28 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir, "grep", "-nIiF", "-z", "-e", query, "--end-of-options", ref)
27 out, err := cmd.Output() 29 out, err := cmd.Output()
28 if err != nil { 30 if err != nil {
29 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 { 31 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
internal/gitutil/lastcommit.go +5 −3
@@ -6,6 +6,8 @@ import (
6 "strconv" 6 "strconv"
7 "strings" 7 "strings"
8 "time" 8 "time"
9
10 "gitbay.org/gitbay/internal/toolpath"
9) 11)
10 12
11// EntryCommit is the newest commit touching one entry of a tree listing. 13// EntryCommit is the newest commit touching one entry of a tree listing.
@@ -49,7 +51,7 @@ func LastCommits(dir, ref, path string, names []string) map[string]EntryCommit {
49 if prefix != "" { 51 if prefix != "" {
50 args = append(args, "--", strings.TrimSuffix(prefix, "/")) 52 args = append(args, "--", strings.TrimSuffix(prefix, "/"))
51 } 53 }
52 cmd := exec.Command("git", args...) 54 cmd := exec.Command(toolpath.Look("git"), args...)
53 stdout, err := cmd.StdoutPipe() 55 stdout, err := cmd.StdoutPipe()
54 if err != nil { 56 if err != nil {
55 return nil 57 return nil
@@ -107,7 +109,7 @@ func parseCommitHeader(s string) EntryCommit {
107// TipCommit is the commit at ref, for the bar above a tree listing that 109// TipCommit is the commit at ref, for the bar above a tree listing that
108// answers "who touched this repository last". 110// answers "who touched this repository last".
109func TipCommit(dir, ref string) EntryCommit { 111func TipCommit(dir, ref string) EntryCommit {
110 out, err := exec.Command("git", "-C", dir, "log", "-1", 112 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1",
111 "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "--end-of-options", ref).Output() 113 "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "--end-of-options", ref).Output()
112 if err != nil { 114 if err != nil {
113 return EntryCommit{} 115 return EntryCommit{}
@@ -137,7 +139,7 @@ func entryName(changed, prefix string) (string, bool) {
137// page can report the facts the file listing no longer carries: its size, 139// page can report the facts the file listing no longer carries: its size,
138// and whether it is executable or a symlink. 140// and whether it is executable or a symlink.
139func StatPath(dir, ref, path string) (TreeEntry, bool) { 141func StatPath(dir, ref, path string) (TreeEntry, bool) {
140 out, err := exec.Command("git", "-C", dir, "ls-tree", "-l", "--end-of-options", ref, "--", path).Output() 142 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "ls-tree", "-l", "--end-of-options", ref, "--", path).Output()
141 if err != nil { 143 if err != nil {
142 return TreeEntry{}, false 144 return TreeEntry{}, false
143 } 145 }
internal/gitutil/lfs.go +4 −2
@@ -7,6 +7,8 @@ import (
7 "regexp" 7 "regexp"
8 "strconv" 8 "strconv"
9 "strings" 9 "strings"
10
11 "gitbay.org/gitbay/internal/toolpath"
10) 12)
11 13
12// lfsPointerMax bounds a pointer file; real ones are around 130 bytes. 14// lfsPointerMax bounds a pointer file; real ones are around 130 bytes.
@@ -18,7 +20,7 @@ var lfsOIDLine = regexp.MustCompile(`(?m)^oid sha256:([0-9a-f]{64})$`)
18// anywhere in the repository: every object, not just the reachable ones, 20// anywhere in the repository: every object, not just the reachable ones,
19// since an unreachable blob is still an object gc has not removed. 21// since an unreachable blob is still an object gc has not removed.
20func LFSPointerOIDs(dir string) ([]string, error) { 22func LFSPointerOIDs(dir string) ([]string, error) {
21 list := exec.Command("git", "-C", dir, "cat-file", "--batch-all-objects", 23 list := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "--batch-all-objects",
22 "--batch-check=%(objecttype) %(objectsize) %(objectname)") 24 "--batch-check=%(objecttype) %(objectsize) %(objectname)")
23 out, err := list.Output() 25 out, err := list.Output()
24 if err != nil { 26 if err != nil {
@@ -38,7 +40,7 @@ func LFSPointerOIDs(dir string) ([]string, error) {
38 if len(small) == 0 { 40 if len(small) == 0 {
39 return nil, nil 41 return nil, nil
40 } 42 }
41 cat := exec.Command("git", "-C", dir, "cat-file", "--batch") 43 cat := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "--batch")
42 cat.Stdin = strings.NewReader(strings.Join(small, "\n") + "\n") 44 cat.Stdin = strings.NewReader(strings.Join(small, "\n") + "\n")
43 out, err = cat.Output() 45 out, err = cat.Output()
44 if err != nil { 46 if err != nil {
internal/gitutil/merge.go +22 −20
@@ -6,13 +6,15 @@ import (
6 "os" 6 "os"
7 "os/exec" 7 "os/exec"
8 "strings" 8 "strings"
9
10 "gitbay.org/gitbay/internal/toolpath"
9) 11)
10 12
11// FetchInto copies srcRef from srcDir into dstDir as dstRef, forcing the 13// FetchInto copies srcRef from srcDir into dstDir as dstRef, forcing the
12// update. Objects are copied, not shared — the destination owns everything 14// update. Objects are copied, not shared — the destination owns everything
13// afterward, which is what keeps MRs alive when their fork is deleted. 15// afterward, which is what keeps MRs alive when their fork is deleted.
14func FetchInto(dstDir, srcDir, srcRef, dstRef string) error { 16func FetchInto(dstDir, srcDir, srcRef, dstRef string) error {
15 cmd := exec.Command("git", "-C", dstDir, "fetch", "--quiet", "--no-write-fetch-head", 17 cmd := exec.Command(toolpath.Look("git"), "-C", dstDir, "fetch", "--quiet", "--no-write-fetch-head",
16 srcDir, "+"+srcRef+":"+dstRef) 18 srcDir, "+"+srcRef+":"+dstRef)
17 if out, err := cmd.CombinedOutput(); err != nil { 19 if out, err := cmd.CombinedOutput(); err != nil {
18 return fmt.Errorf("fetch %s from %s: %v\n%s", srcRef, srcDir, err, out) 20 return fmt.Errorf("fetch %s from %s: %v\n%s", srcRef, srcDir, err, out)
@@ -28,7 +30,7 @@ func UpdateRefCAS(dir, ref, newSHA, oldSHA string) error {
28 if oldSHA != "" { 30 if oldSHA != "" {
29 args = append(args, oldSHA) 31 args = append(args, oldSHA)
30 } 32 }
31 cmd := exec.Command("git", args...) 33 cmd := exec.Command(toolpath.Look("git"), args...)
32 if out, err := cmd.CombinedOutput(); err != nil { 34 if out, err := cmd.CombinedOutput(); err != nil {
33 return fmt.Errorf("update-ref %s: %v\n%s", ref, err, out) 35 return fmt.Errorf("update-ref %s: %v\n%s", ref, err, out)
34 } 36 }
@@ -36,7 +38,7 @@ func UpdateRefCAS(dir, ref, newSHA, oldSHA string) error {
36} 38}
37 39
38func DeleteRef(dir, ref string) error { 40func DeleteRef(dir, ref string) error {
39 cmd := exec.Command("git", "-C", dir, "update-ref", "-d", ref) 41 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "update-ref", "-d", ref)
40 if out, err := cmd.CombinedOutput(); err != nil { 42 if out, err := cmd.CombinedOutput(); err != nil {
41 return fmt.Errorf("delete-ref %s: %v\n%s", ref, err, out) 43 return fmt.Errorf("delete-ref %s: %v\n%s", ref, err, out)
42 } 44 }
@@ -45,7 +47,7 @@ func DeleteRef(dir, ref string) error {
45 47
46// RevListRange returns commits in old..new, newest first. 48// RevListRange returns commits in old..new, newest first.
47func RevListRange(dir, old, new string) ([]string, error) { 49func RevListRange(dir, old, new string) ([]string, error) {
48 cmd := exec.Command("git", "-C", dir, "rev-list", "--end-of-options", new, "^"+old) 50 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", "--end-of-options", new, "^"+old)
49 out, err := cmd.Output() 51 out, err := cmd.Output()
50 if err != nil { 52 if err != nil {
51 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err) 53 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err)
@@ -62,7 +64,7 @@ func RevListRange(dir, old, new string) ([]string, error) {
62// MergeTree performs a real merge of ours and theirs, returning the merged 64// MergeTree performs a real merge of ours and theirs, returning the merged
63// tree id. conflict=true means the merge cannot be done automatically. 65// tree id. conflict=true means the merge cannot be done automatically.
64func MergeTree(dir, ours, theirs string) (tree string, conflict bool, err error) { 66func MergeTree(dir, ours, theirs string) (tree string, conflict bool, err error) {
65 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", "--end-of-options", ours, theirs) 67 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "merge-tree", "--write-tree", "--end-of-options", ours, theirs)
66 out, runErr := cmd.Output() 68 out, runErr := cmd.Output()
67 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) 69 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
68 if runErr != nil { 70 if runErr != nil {
@@ -81,7 +83,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
81 for _, p := range parents { 83 for _, p := range parents {
82 args = append(args, "-p", p) 84 args = append(args, "-p", p)
83 } 85 }
84 cmd := exec.Command("git", args...) 86 cmd := exec.Command(toolpath.Look("git"), args...)
85 cmd.Env = append(os.Environ(), 87 cmd.Env = append(os.Environ(),
86 "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email, 88 "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email,
87 "GIT_COMMITTER_NAME="+name, "GIT_COMMITTER_EMAIL="+email, 89 "GIT_COMMITTER_NAME="+name, "GIT_COMMITTER_EMAIL="+email,
@@ -98,7 +100,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
98// truncated says whether it was cut, so the caller can say so instead of 100// truncated says whether it was cut, so the caller can say so instead of
99// rendering a hunk that ends mid-line (#117). 101// rendering a hunk that ends mid-line (#117).
100func Diff(dir, old, new string, limit int64) (patch string, truncated bool, err error) { 102func Diff(dir, old, new string, limit int64) (patch string, truncated bool, err error) {
101 cmd := exec.Command("git", "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new) 103 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "diff", "--stat", "--patch", "--end-of-options", old, new)
102 out, err := cmd.Output() 104 out, err := cmd.Output()
103 if err != nil { 105 if err != nil {
104 return "", false, fmt.Errorf("diff: %w", err) 106 return "", false, fmt.Errorf("diff: %w", err)
@@ -122,7 +124,7 @@ func cutAtLine(out []byte, limit int64) ([]byte, bool) {
122 124
123// MergeBase returns the best common ancestor, or an error if none exists. 125// MergeBase returns the best common ancestor, or an error if none exists.
124func MergeBase(dir, a, b string) (string, error) { 126func MergeBase(dir, a, b string) (string, error) {
125 cmd := exec.Command("git", "-C", dir, "merge-base", "--end-of-options", a, b) 127 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "merge-base", "--end-of-options", a, b)
126 out, err := cmd.Output() 128 out, err := cmd.Output()
127 if err != nil { 129 if err != nil {
128 return "", fmt.Errorf("no common history between %s and %s", a, b) 130 return "", fmt.Errorf("no common history between %s and %s", a, b)
@@ -141,7 +143,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
141 } 143 }
142 144
143 // Hash the new blob. 145 // Hash the new blob.
144 hb := exec.Command("git", "-C", dir, "hash-object", "-w", "--stdin") 146 hb := exec.Command(toolpath.Look("git"), "-C", dir, "hash-object", "-w", "--stdin")
145 hb.Stdin = strings.NewReader(string(content)) 147 hb.Stdin = strings.NewReader(string(content))
146 out, err := hb.Output() 148 out, err := hb.Output()
147 if err != nil { 149 if err != nil {
@@ -159,17 +161,17 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
159 defer os.Remove(idx.Name()) 161 defer os.Remove(idx.Name())
160 env := append(os.Environ(), "GIT_INDEX_FILE="+idx.Name()) 162 env := append(os.Environ(), "GIT_INDEX_FILE="+idx.Name())
161 163
162 rt := exec.Command("git", "-C", dir, "read-tree", parent+"^{tree}") 164 rt := exec.Command(toolpath.Look("git"), "-C", dir, "read-tree", parent+"^{tree}")
163 rt.Env = env 165 rt.Env = env
164 if out, err := rt.CombinedOutput(); err != nil { 166 if out, err := rt.CombinedOutput(); err != nil {
165 return "", fmt.Errorf("read-tree: %v\n%s", err, out) 167 return "", fmt.Errorf("read-tree: %v\n%s", err, out)
166 } 168 }
167 ui := exec.Command("git", "-C", dir, "update-index", "--add", "--cacheinfo", "100644,"+blob+","+path) 169 ui := exec.Command(toolpath.Look("git"), "-C", dir, "update-index", "--add", "--cacheinfo", "100644,"+blob+","+path)
168 ui.Env = env 170 ui.Env = env
169 if out, err := ui.CombinedOutput(); err != nil { 171 if out, err := ui.CombinedOutput(); err != nil {
170 return "", fmt.Errorf("update-index: %v\n%s", err, out) 172 return "", fmt.Errorf("update-index: %v\n%s", err, out)
171 } 173 }
172 wt := exec.Command("git", "-C", dir, "write-tree") 174 wt := exec.Command(toolpath.Look("git"), "-C", dir, "write-tree")
173 wt.Env = env 175 wt.Env = env
174 out, err = wt.Output() 176 out, err = wt.Output()
175 if err != nil { 177 if err != nil {
@@ -189,7 +191,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
189 191
190// CommitParents returns the parent SHAs of a commit. 192// CommitParents returns the parent SHAs of a commit.
191func CommitParents(dir, sha string) ([]string, error) { 193func CommitParents(dir, sha string) ([]string, error) {
192 out, err := exec.Command("git", "-C", dir, "rev-list", "--parents", "-n1", "--end-of-options", sha).Output() 194 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-list", "--parents", "-n1", "--end-of-options", sha).Output()
193 if err != nil { 195 if err != nil {
194 return nil, fmt.Errorf("rev-list --parents %s: %w", sha, err) 196 return nil, fmt.Errorf("rev-list --parents %s: %w", sha, err)
195 } 197 }
@@ -202,7 +204,7 @@ func CommitParents(dir, sha string) ([]string, error) {
202 204
203// AuthorIdent returns a commit's author name, email, and ISO date. 205// AuthorIdent returns a commit's author name, email, and ISO date.
204func AuthorIdent(dir, sha string) (name, email, date string, err error) { 206func AuthorIdent(dir, sha string) (name, email, date string, err error) {
205 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%an%x1f%ae%x1f%aI", "--end-of-options", sha).Output() 207 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%an%x1f%ae%x1f%aI", "--end-of-options", sha).Output()
206 if err != nil { 208 if err != nil {
207 return "", "", "", fmt.Errorf("log %s: %w", sha, err) 209 return "", "", "", fmt.Errorf("log %s: %w", sha, err)
208 } 210 }
@@ -215,7 +217,7 @@ func AuthorIdent(dir, sha string) (name, email, date string, err error) {
215 217
216// CommitMessage returns a commit's full message. 218// CommitMessage returns a commit's full message.
217func CommitMessage(dir, sha string) (string, error) { 219func CommitMessage(dir, sha string) (string, error) {
218 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%B", "--end-of-options", sha).Output() 220 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%B", "--end-of-options", sha).Output()
219 if err != nil { 221 if err != nil {
220 return "", fmt.Errorf("log %s: %w", sha, err) 222 return "", fmt.Errorf("log %s: %w", sha, err)
221 } 223 }
@@ -225,7 +227,7 @@ func CommitMessage(dir, sha string) (string, error) {
225// MergeTreeOnto replays commit's changes (relative to base) onto onto, 227// MergeTreeOnto replays commit's changes (relative to base) onto onto,
226// returning the resulting tree. conflict=true when it cannot apply cleanly. 228// returning the resulting tree. conflict=true when it cannot apply cleanly.
227func MergeTreeOnto(dir, base, onto, commit string) (tree string, conflict bool, err error) { 229func MergeTreeOnto(dir, base, onto, commit string) (tree string, conflict bool, err error) {
228 cmd := exec.Command("git", "-C", dir, "merge-tree", "--write-tree", "--merge-base="+base, "--end-of-options", onto, commit) 230 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "merge-tree", "--write-tree", "--merge-base="+base, "--end-of-options", onto, commit)
229 out, runErr := cmd.Output() 231 out, runErr := cmd.Output()
230 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) 232 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
231 if runErr != nil { 233 if runErr != nil {
@@ -245,7 +247,7 @@ func CommitTreeIdent(dir, tree string, parents []string,
245 for _, p := range parents { 247 for _, p := range parents {
246 args = append(args, "-p", p) 248 args = append(args, "-p", p)
247 } 249 }
248 cmd := exec.Command("git", args...) 250 cmd := exec.Command(toolpath.Look("git"), args...)
249 env := append(os.Environ(), 251 env := append(os.Environ(),
250 "GIT_AUTHOR_NAME="+authorName, "GIT_AUTHOR_EMAIL="+authorEmail, 252 "GIT_AUTHOR_NAME="+authorName, "GIT_AUTHOR_EMAIL="+authorEmail,
251 "GIT_COMMITTER_NAME="+committerName, "GIT_COMMITTER_EMAIL="+committerEmail, 253 "GIT_COMMITTER_NAME="+committerName, "GIT_COMMITTER_EMAIL="+committerEmail,
@@ -263,7 +265,7 @@ func CommitTreeIdent(dir, tree string, parents []string,
263 265
264// ResolveTree returns the tree id of a commit. 266// ResolveTree returns the tree id of a commit.
265func ResolveTree(dir, sha string) (string, error) { 267func ResolveTree(dir, sha string) (string, error) {
266 out, err := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--end-of-options", sha+"^{tree}").Output() 268 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "rev-parse", "--verify", "--end-of-options", sha+"^{tree}").Output()
267 if err != nil { 269 if err != nil {
268 return "", fmt.Errorf("rev-parse %s^{tree}: %w", sha, err) 270 return "", fmt.Errorf("rev-parse %s^{tree}: %w", sha, err)
269 } 271 }
@@ -272,7 +274,7 @@ func ResolveTree(dir, sha string) (string, error) {
272 274
273// DiffFiles lists the paths changed between old and new. 275// DiffFiles lists the paths changed between old and new.
274func DiffFiles(dir, old, new string) ([]string, error) { 276func DiffFiles(dir, old, new string) ([]string, error) {
275 out, err := exec.Command("git", "-C", dir, "diff", "--name-only", "--end-of-options", old, new).Output() 277 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "diff", "--name-only", "--end-of-options", old, new).Output()
276 if err != nil { 278 if err != nil {
277 return nil, fmt.Errorf("diff --name-only: %w", err) 279 return nil, fmt.Errorf("diff --name-only: %w", err)
278 } 280 }
@@ -304,7 +306,7 @@ func DiffFiles(dir, old, new string) ([]string, error) {
304// tells a reviewer nothing. It pairs at 80, and two genuinely unrelated 306// tells a reviewer nothing. It pairs at 80, and two genuinely unrelated
305// commits are still left unpaired there; both measured. 307// commits are still left unpaired there; both measured.
306func RangeDiff(dir, oldBase, oldHead, newBase, newHead string, limit int64) (patch string, truncated bool, err error) { 308func RangeDiff(dir, oldBase, oldHead, newBase, newHead string, limit int64) (patch string, truncated bool, err error) {
307 cmd := exec.Command("git", "-C", dir, "range-diff", "--creation-factor=80", "--end-of-options", 309 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "range-diff", "--creation-factor=80", "--end-of-options",
308 oldBase+".."+oldHead, newBase+".."+newHead) 310 oldBase+".."+oldHead, newBase+".."+newHead)
309 out, err := cmd.Output() 311 out, err := cmd.Output()
310 if err != nil { 312 if err != nil {
internal/gitutil/messages.go +7 −5
@@ -4,6 +4,8 @@ import (
4 "fmt" 4 "fmt"
5 "os/exec" 5 "os/exec"
6 "strings" 6 "strings"
7
8 "gitbay.org/gitbay/internal/toolpath"
7) 9)
8 10
9const zeroSHA = "0000000000000000000000000000000000000000" 11const zeroSHA = "0000000000000000000000000000000000000000"
@@ -22,7 +24,7 @@ func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) {
22 if old != "" && old != zeroSHA { 24 if old != "" && old != zeroSHA {
23 args = append(args, "^"+old) 25 args = append(args, "^"+old)
24 } 26 }
25 out, err := exec.Command("git", args...).Output() 27 out, err := exec.Command(toolpath.Look("git"), args...).Output()
26 if err != nil { 28 if err != nil {
27 return nil, fmt.Errorf("rev-list authors: %w", err) 29 return nil, fmt.Errorf("rev-list authors: %w", err)
28 } 30 }
@@ -39,7 +41,7 @@ func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) {
39 41
40// Parents returns a commit's parent shas. 42// Parents returns a commit's parent shas.
41func Parents(dir, sha string) []string { 43func Parents(dir, sha string) []string {
42 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%P", sha).Output() 44 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%P", sha).Output()
43 if err != nil { 45 if err != nil {
44 return nil 46 return nil
45 } 47 }
@@ -49,7 +51,7 @@ func Parents(dir, sha string) []string {
49// LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip, 51// LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip,
50// or "" for empty repos. 52// or "" for empty repos.
51func LastCommitDate(dir, ref string) string { 53func LastCommitDate(dir, ref string) string {
52 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%cs", ref).Output() 54 out, err := exec.Command(toolpath.Look("git"), "-C", dir, "log", "-1", "--format=%cs", ref).Output()
53 if err != nil { 55 if err != nil {
54 return "" 56 return ""
55 } 57 }
@@ -58,7 +60,7 @@ func LastCommitDate(dir, ref string) string {
58 60
59// HasCommit reports whether sha names a commit object present in dir. 61// HasCommit reports whether sha names a commit object present in dir.
60func HasCommit(dir, sha string) bool { 62func HasCommit(dir, sha string) bool {
61 return exec.Command("git", "-C", dir, "cat-file", "-e", sha+"^{commit}").Run() == nil 63 return exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "-e", sha+"^{commit}").Run() == nil
62} 64}
63 65
64type CommitMsg struct { 66type CommitMsg struct {
@@ -76,7 +78,7 @@ func RevListMessages(dir, old, new string, max int) ([]CommitMsg, error) {
76 if old != "" && old != zeroSHA { 78 if old != "" && old != zeroSHA {
77 args = append(args, "^"+old) 79 args = append(args, "^"+old)
78 } 80 }
79 out, err := exec.Command("git", args...).Output() 81 out, err := exec.Command(toolpath.Look("git"), args...).Output()
80 if err != nil { 82 if err != nil {
81 return nil, fmt.Errorf("rev-list messages: %w", err) 83 return nil, fmt.Errorf("rev-list messages: %w", err)
82 } 84 }
internal/gitutil/read.go +8 −6
@@ -10,6 +10,8 @@ import (
10 "strconv" 10 "strconv"
11 "strings" 11 "strings"
12 "time" 12 "time"
13
14 "gitbay.org/gitbay/internal/toolpath"
13) 15)
14 16
15type TreeEntry struct { 17type TreeEntry struct {
@@ -26,7 +28,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
26 if path != "" { 28 if path != "" {
27 spec = ref + ":" + path 29 spec = ref + ":" + path
28 } 30 }
29 cmd := exec.Command("git", "-C", dir, "ls-tree", "-l", "--end-of-options", spec) 31 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "ls-tree", "-l", "--end-of-options", spec)
30 out, err := cmd.Output() 32 out, err := cmd.Output()
31 if err != nil { 33 if err != nil {
32 return nil, fmt.Errorf("ls-tree %s: %w", spec, err) 34 return nil, fmt.Errorf("ls-tree %s: %w", spec, err)
@@ -56,7 +58,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
56 58
57// ReadBlob returns the contents of ref:path, capped at limit bytes. 59// ReadBlob returns the contents of ref:path, capped at limit bytes.
58func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) { 60func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
59 cmd := exec.Command("git", "-C", dir, "cat-file", "blob", "--end-of-options", ref+":"+path) 61 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "blob", "--end-of-options", ref+":"+path)
60 stdout, err := cmd.StdoutPipe() 62 stdout, err := cmd.StdoutPipe()
61 if err != nil { 63 if err != nil {
62 return nil, err 64 return nil, err
@@ -74,7 +76,7 @@ func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
74 76
75// ResolveRef resolves a ref or sha to a full commit sha; errors if absent. 77// ResolveRef resolves a ref or sha to a full commit sha; errors if absent.
76func ResolveRef(dir, ref string) (string, error) { 78func ResolveRef(dir, ref string) (string, error) {
77 cmd := exec.Command("git", "-C", dir, "rev-parse", "--verify", "--quiet", "--end-of-options", ref+"^{commit}") 79 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "rev-parse", "--verify", "--quiet", "--end-of-options", ref+"^{commit}")
78 out, err := cmd.Output() 80 out, err := cmd.Output()
79 if err != nil { 81 if err != nil {
80 return "", fmt.Errorf("unknown ref %q", ref) 82 return "", fmt.Errorf("unknown ref %q", ref)
@@ -89,7 +91,7 @@ type Ref struct {
89 91
90// Refs lists branches or tags; kind is "heads" or "tags". 92// Refs lists branches or tags; kind is "heads" or "tags".
91func Refs(dir, kind string) ([]Ref, error) { 93func Refs(dir, kind string) ([]Ref, error) {
92 cmd := exec.Command("git", "-C", dir, "for-each-ref", 94 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "for-each-ref",
93 "--format=%(refname:short) %(objectname)", "refs/"+kind) 95 "--format=%(refname:short) %(objectname)", "refs/"+kind)
94 out, err := cmd.Output() 96 out, err := cmd.Output()
95 if err != nil { 97 if err != nil {
@@ -118,7 +120,7 @@ var ErrArchiveTooLarge = errors.New("archive exceeds the size limit")
118func Archive(dir, ref, prefix string, w io.Writer) error { 120func Archive(dir, ref, prefix string, w io.Writer) error {
119 ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout) 121 ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout)
120 defer cancel() 122 defer cancel()
121 cmd := exec.CommandContext(ctx, "git", "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", "--end-of-options", ref) 123 cmd := exec.CommandContext(ctx, toolpath.Look("git"), "-C", dir, "archive", "--format=tar.gz", "--prefix="+prefix+"/", "--end-of-options", ref)
122 lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel} 124 lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel}
123 cmd.Stdout = lw 125 cmd.Stdout = lw
124 err := cmd.Run() 126 err := cmd.Run()
@@ -152,7 +154,7 @@ func (c *cappedWriter) Write(p []byte) (int, error) {
152 154
153// ShowPatch returns the stat+patch text for one commit. 155// ShowPatch returns the stat+patch text for one commit.
154func ShowPatch(dir, sha string, limit int64) (patch string, truncated bool, err error) { 156func ShowPatch(dir, sha string, limit int64) (patch string, truncated bool, err error) {
155 cmd := exec.Command("git", "-C", dir, "show", "--stat", "--patch", "--format=", "--end-of-options", sha) 157 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "show", "--stat", "--patch", "--format=", "--end-of-options", sha)
156 stdout, err := cmd.StdoutPipe() 158 stdout, err := cmd.StdoutPipe()
157 if err != nil { 159 if err != nil {
158 return "", false, err 160 return "", false, err
internal/mirror/mirror.go +2 −1
@@ -17,6 +17,7 @@ import (
17 "gitbay.org/gitbay/internal/config" 17 "gitbay.org/gitbay/internal/config"
18 "gitbay.org/gitbay/internal/control" 18 "gitbay.org/gitbay/internal/control"
19 "gitbay.org/gitbay/internal/store" 19 "gitbay.org/gitbay/internal/store"
20 "gitbay.org/gitbay/internal/toolpath"
20) 21)
21 22
22const askpassScript = `#!/bin/sh 23const askpassScript = `#!/bin/sh
@@ -106,7 +107,7 @@ func (w *Worker) sync(m store.Mirror) error {
106 args = []string{"-C", dir, "fetch", "--prune", m.URL, 107 args = []string{"-C", dir, "fetch", "--prune", m.URL,
107 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"} 108 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"}
108 } 109 }
109 cmd := exec.CommandContext(ctx, "git", args...) 110 cmd := exec.CommandContext(ctx, toolpath.Look("git"), args...)
110 cmd.Env = env 111 cmd.Env = env
111 if out, err := cmd.CombinedOutput(); err != nil { 112 if out, err := cmd.CombinedOutput(); err != nil {
112 return fmt.Errorf("git %s: %v: %.300s", m.Direction, err, out) 113 return fmt.Errorf("git %s: %v: %.300s", m.Direction, err, out)
internal/toolpath/toolpath.go added +64
@@ -0,0 +1,64 @@
1// Package toolpath resolves the external programs gitbay runs — git, ssh,
2// sh — to absolute paths once, when the process starts, instead of
3// letting the kernel search PATH on every spawn.
4//
5// Three things it buys, in the order they matter.
6//
7// A missing tool becomes one legible failure at start-up rather than an
8// opaque one at the first push, on whichever request happened to need it.
9//
10// The command a long-lived daemon runs is then fixed for its lifetime,
11// decided from the environment it was started with rather than resolved
12// afresh each time. gitbayd and gitbay-runner both run under systemd with
13// a root-owned PATH and a read-only /usr, so a search was never
14// attacker-influenced in a shipped configuration — but a spawn that
15// cannot be redirected is one fewer thing to reason about, and it is what
16// the scanner asks for (go:S4036).
17//
18// And the lookup leaves the hot path: a repository page can spawn several
19// git processes, and each was searching PATH from scratch.
20package toolpath
21
22import (
23 "fmt"
24 "os/exec"
25 "strings"
26 "sync"
27)
28
29var (
30 mu sync.Mutex
31 cache = map[string]string{}
32 missing []string
33)
34
35// Look returns the absolute path to name, or name itself when it is not
36// on PATH. Returning the bare name keeps the failure where it already
37// was — exec reporting it — for anything that runs before Verify, and for
38// a tool a particular binary never actually uses.
39func Look(name string) string {
40 mu.Lock()
41 defer mu.Unlock()
42 if p, ok := cache[name]; ok {
43 return p
44 }
45 p, err := exec.LookPath(name)
46 if err != nil {
47 p = name
48 missing = append(missing, name)
49 }
50 cache[name] = p
51 return p
52}
53
54// Verify reports the tools that were asked for and not found, so a daemon
55// can refuse to start rather than fail on its first request. Call it after
56// the packages that need tools are initialised.
57func Verify() error {
58 mu.Lock()
59 defer mu.Unlock()
60 if len(missing) == 0 {
61 return nil
62 }
63 return fmt.Errorf("not found on PATH: %s", strings.Join(missing, ", "))
64}
internal/toolpath/toolpath_test.go added +47
@@ -0,0 +1,47 @@
1package toolpath
2
3import (
4 "os/exec"
5 "path/filepath"
6 "strings"
7 "testing"
8)
9
10func TestLookResolvesToAnAbsolutePath(t *testing.T) {
11 got := Look("go")
12 if !filepath.IsAbs(got) {
13 t.Fatalf("Look(go) = %q, want an absolute path", got)
14 }
15 want, err := exec.LookPath("go")
16 if err != nil {
17 t.Fatal(err)
18 }
19 if got != want {
20 t.Errorf("Look(go) = %q, want %q", got, want)
21 }
22}
23
24// A second call must not search again: resolving once is the point, and a
25// daemon that re-resolved would follow a PATH that changed under it.
26func TestLookIsResolvedOnce(t *testing.T) {
27 first := Look("go")
28 if second := Look("go"); second != first {
29 t.Errorf("Look(go) returned %q then %q", first, second)
30 }
31}
32
33// An absent tool falls back to the bare name so exec reports it the way
34// it always did, and Verify names it.
35func TestMissingToolFallsBackAndIsReported(t *testing.T) {
36 const name = "gitbay-no-such-tool-exists"
37 if got := Look(name); got != name {
38 t.Errorf("Look(%q) = %q, want the bare name back", name, got)
39 }
40 err := Verify()
41 if err == nil {
42 t.Fatal("Verify found nothing missing after an unresolvable lookup")
43 }
44 if !strings.Contains(err.Error(), name) {
45 t.Errorf("Verify error %q does not name the missing tool", err)
46 }
47}