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 (
2424 "time"
2525
2626 "gitbay.org/gitbay/internal/buildinfo"
27 "gitbay.org/gitbay/internal/toolpath"
2728)
2829
2930type job struct {
@@ -203,7 +204,7 @@ func (r *runner) run(j job) bool {
203204 defer os.RemoveAll(dir)
204205
205206 // 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))...)
207208 pipe, err := logCmd.StdinPipe()
208209 if err != nil {
209210 log.Printf("build %d: log pipe: %v", j.ID, err)
@@ -294,7 +295,7 @@ func (r *runner) run(j job) bool {
294295 }
295296 steps = append(steps, []string{"-C", dir, "checkout", "-q", j.SHA})
296297 for _, args := range steps {
297 cmd := exec.Command("git", args...)
298 cmd := exec.Command(toolpath.Look("git"), args...)
298299 cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0")
299300 cmd.Stdout, cmd.Stderr = sink, sink
300301 if ok, why := runStep(cmd, deadline); !ok {
@@ -305,7 +306,7 @@ func (r *runner) run(j job) bool {
305306
306307 for _, step := range j.Steps {
307308 fmt.Fprintf(sink, "$ %s\n", step)
308 cmd := exec.Command("sh", "-c", step)
309 cmd := exec.Command(toolpath.Look("sh"), "-c", step)
309310 cmd.Dir = dir
310311 cmd.Env = append(os.Environ(),
311312 "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 {
323324
324325// ssh runs one control command against the server and returns stdout.
325326func (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...)...)
327328 if stdin != nil {
328329 cmd.Stdin = stdin
329330 }
cmd/gitbay/local.go +5 −4
@@ -11,6 +11,7 @@ import (
1111
1212 "gitbay.org/gitbay/internal/cliconfig"
1313 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/toolpath"
1415)
1516
1617// hasBodyFlag reports whether args already carry body/message input.
@@ -50,7 +51,7 @@ func maybeEditor(args []string, kind string, prefill func() string) ([]string, *
5051 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)
5152 f.Close()
5253
53 ed := exec.Command("sh", "-c", editor+" "+shellQuote(f.Name()))
54 ed := exec.Command(toolpath.Look("sh"), "-c", editor+" "+shellQuote(f.Name()))
5455 ed.Stdin, ed.Stdout, ed.Stderr = os.Stdin, os.Stdout, os.Stderr
5556 if err := ed.Run(); err != nil {
5657 return nil, nil, false, fmt.Errorf("editor: %w", err)
@@ -74,7 +75,7 @@ func maybeEditor(args []string, kind string, prefill func() string) ([]string, *
7475}
7576
7677func runGitLocal(args ...string) int {
77 cmd := exec.Command("git", args...)
78 cmd := exec.Command(toolpath.Look("git"), args...)
7879 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
7980 if err := cmd.Run(); err != nil {
8081 if ee, ok := err.(*exec.ExitError); ok {
@@ -190,7 +191,7 @@ func captureSSH(t target, serverArgv []string) (string, int) {
190191 args := sshArgs(t.inst)
191192 quoted := quoteAll(serverArgv)
192193 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...)
194195 cmd.Stderr = os.Stderr
195196 out, err := cmd.Output()
196197 if err != nil {
@@ -301,7 +302,7 @@ func cmdRemoteList() int {
301302// currentBranch is the checked-out branch of the working directory's
302303// clone, or "" outside a clone or on a detached HEAD.
303304func 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()
305306 if err != nil {
306307 return ""
307308 }
cmd/gitbay/migrate.go +4 −3
@@ -11,6 +11,7 @@ import (
1111 "github.com/spf13/cobra"
1212
1313 "gitbay.org/gitbay/internal/protocol"
14 "gitbay.org/gitbay/internal/toolpath"
1415)
1516
1617func migrateCmd() *cobra.Command {
@@ -43,7 +44,7 @@ func sourceSSH(host string, port int, extra ...string) *exec.Cmd {
4344 }
4445 args = append(args, "git@"+host, "--")
4546 args = append(args, extra...)
46 return exec.Command("ssh", args...)
47 return exec.Command(toolpath.Look("ssh"), args...)
4748}
4849
4950func runMigrate(from string, fromPort int) int {
@@ -99,14 +100,14 @@ func runMigrate(from string, fromPort int) int {
99100 if fromPort != 0 && fromPort != 22 {
100101 srcURL = fmt.Sprintf("ssh://git@%s:%d/%s.git", from, fromPort, repoPath)
101102 }
102 clone := exec.Command("git", "clone", "--quiet", "--mirror", srcURL, tmp+"/r")
103 clone := exec.Command(toolpath.Look("git"), "clone", "--quiet", "--mirror", srcURL, tmp+"/r")
103104 clone.Stderr = os.Stderr
104105 if err := clone.Run(); err != nil {
105106 fmt.Fprintf(os.Stderr, "gitbay: cloning %s failed; fix and re-run migrate (it resumes)\n", repoPath)
106107 os.RemoveAll(tmp)
107108 return protocol.ExitFailure
108109 }
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),
110111 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*")
111112 if len(t.inst.SSHOptions) > 0 {
112113 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 (
1313
1414 "gitbay.org/gitbay/internal/cliconfig"
1515 "gitbay.org/gitbay/internal/protocol"
16 "gitbay.org/gitbay/internal/toolpath"
1617)
1718
1819// context is the resolved target for a command: which instance to talk to
@@ -59,7 +60,7 @@ func resolveTarget() (target, error) {
5960}
6061
6162func 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()
6364 if err != nil {
6465 return ""
6566 }
@@ -115,7 +116,7 @@ func runSSH(t target, serverArgv []string, stdin io.Reader) int {
115116 }
116117 args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " "))
117118
118 cmd := exec.Command("ssh", args...)
119 cmd := exec.Command(toolpath.Look("ssh"), args...)
119120 cmd.Stdin = stdin
120121 cmd.Stdout = os.Stdout
121122 cmd.Stderr = os.Stderr
@@ -145,7 +146,7 @@ func sshCapture(t target, serverArgv []string) (string, int) {
145146 quoted[i] = shellQuote(a)
146147 }
147148 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()
149150 if err != nil {
150151 code := protocol.ExitProtocol
151152 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() != 255 {
cmd/gitbayd/hook.go +3 −2
@@ -15,6 +15,7 @@ import (
1515 "gitbay.org/gitbay/internal/gitutil"
1616 "gitbay.org/gitbay/internal/hookd"
1717 "gitbay.org/gitbay/internal/policy"
18 "gitbay.org/gitbay/internal/toolpath"
1819)
1920
2021// incomingSHAs lists the commits this push introduces, in order, without
@@ -28,7 +29,7 @@ func incomingSHAs(updates []policy.RefUpdate) ([]string, error) {
2829 continue
2930 }
3031 // 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()
3233 if err != nil {
3334 return nil, fmt.Errorf("rev-list %s: %w", u.New, err)
3435 }
@@ -62,7 +63,7 @@ func streamIncomingCommits(updates []policy.RefUpdate, emit func(hookd.RawCommit
6263 // part-way through a large push leaves git blocked writing into a
6364 // pipe nobody is reading and Wait blocked on git.
6465 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")
6667 stdin, err := cmd.StdinPipe()
6768 if err != nil {
6869 cancel()
cmd/gitbayd/main.go +7
@@ -32,6 +32,7 @@ import (
3232 "gitbay.org/gitbay/internal/notify"
3333 "gitbay.org/gitbay/internal/sshd"
3434 "gitbay.org/gitbay/internal/store"
35 "gitbay.org/gitbay/internal/toolpath"
3536 "gitbay.org/gitbay/internal/webhook"
3637)
3738
@@ -122,6 +123,12 @@ func serveCmd() *cobra.Command {
122123 // First line of every run: the journal then says which commit is
123124 // serving, without rebuilding the binary to find out.
124125 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 }
125132 cfg, err := config.Load(configPath)
126133 if err != nil {
127134 return err
cmd/gitbayd/maint.go +2 −1
@@ -13,6 +13,7 @@ import (
1313 "gitbay.org/gitbay/internal/gitutil"
1414 "gitbay.org/gitbay/internal/lfs"
1515 "gitbay.org/gitbay/internal/store"
16 "gitbay.org/gitbay/internal/toolpath"
1617)
1718
1819func gcCmd() *cobra.Command {
@@ -51,7 +52,7 @@ func gcCmd() *cobra.Command {
5152 if aggressive {
5253 gcArgs = append(gcArgs, "--aggressive")
5354 }
54 if out, err := exec.Command("git", gcArgs...).CombinedOutput(); err != nil {
55 if out, err := exec.Command(toolpath.Look("git"), gcArgs...).CombinedOutput(); err != nil {
5556 fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out)
5657 continue
5758 }
internal/gitd/gitd.go +2 −1
@@ -16,6 +16,7 @@ import (
1616 "gitbay.org/gitbay/internal/config"
1717 "gitbay.org/gitbay/internal/control"
1818 "gitbay.org/gitbay/internal/store"
19 "gitbay.org/gitbay/internal/toolpath"
1920)
2021
2122type Server struct {
@@ -68,7 +69,7 @@ func (s *Server) handle(conn net.Conn) {
6869 }
6970
7071 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)
7273 cmd.Env = append(os.Environ(), protoEnv...)
7374 cmd.Stdin = conn
7475 cmd.Stdout = conn
internal/gitutil/blame.go +3 −1
@@ -7,6 +7,8 @@ import (
77 "os/exec"
88 "strconv"
99 "strings"
10
11 "gitbay.org/gitbay/internal/toolpath"
1012)
1113
1214// 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) {
2830 if err != nil {
2931 return nil, err
3032 }
31 cmd := exec.Command("git", "-C", dir, "blame", "--porcelain",
33 cmd := exec.Command(toolpath.Look("git"), "-C", dir, "blame", "--porcelain",
3234 fmt.Sprintf("-L%d,%d", start, end), sha, "--", path)
3335 out, err := cmd.Output()
3436 if err != nil {
internal/gitutil/facts.go +5 −3
@@ -5,12 +5,14 @@ import (
55 "sort"
66 "strconv"
77 "strings"
8
9 "gitbay.org/gitbay/internal/toolpath"
810)
911
1012// CountCommits returns the number of commits reachable from ref, or 0 when
1113// the ref does not resolve (an empty repository).
1214func 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()
1416 if err != nil {
1517 return 0
1618 }
@@ -30,7 +32,7 @@ type Contributor struct {
3032// tie back to an account, after the repository's own .mailmap has had its
3133// say — a bare repo resolves that from HEAD:.mailmap with no config.
3234func 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",
3436 "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output()
3537 if err != nil {
3638 return nil
@@ -62,7 +64,7 @@ func Contributors(dir, ref string) []Contributor {
6264// largest first, keyed by the extension map the caller supplies. Only
6365// blobs count; git's own metadata does not.
6466func 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()
6668 if err != nil {
6769 return nil
6870 }
internal/gitutil/gitutil.go +14 −12
@@ -11,6 +11,8 @@ import (
1111 "os/exec"
1212 "path/filepath"
1313 "strings"
14
15 "gitbay.org/gitbay/internal/toolpath"
1416)
1517
1618// InitBare creates a bare repository with the shared hooks directory wired
@@ -19,14 +21,14 @@ func InitBare(path, defaultBranch, hooksPath string) error {
1921 if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
2022 return err
2123 }
22 cmd := exec.Command("git", "init", "--bare", "--initial-branch="+defaultBranch, path)
24 cmd := exec.Command(toolpath.Look("git"), "init", "--bare", "--initial-branch="+defaultBranch, path)
2325 if out, err := cmd.CombinedOutput(); err != nil {
2426 return fmt.Errorf("git init: %v\n%s", err, out)
2527 }
2628 // An empty hooksPath leaves the bare repo with no hooks — used for
2729 // companion repos (wikis) that carry no ref policy.
2830 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)
3032 if out, err := cmd.CombinedOutput(); err != nil {
3133 return fmt.Errorf("git config core.hooksPath: %v\n%s", err, out)
3234 }
@@ -49,7 +51,7 @@ func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer
4951 default:
5052 return fmt.Errorf("unknown service %q", service)
5153 }
52 cmd := exec.Command("git", args...)
54 cmd := exec.Command(toolpath.Look("git"), args...)
5355 cmd.Env = append(os.Environ(), extraEnv...)
5456 cmd.Stdin = stdin
5557 cmd.Stdout = stdout
@@ -61,7 +63,7 @@ func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer
6163// dir. It must run with the caller's environment intact so that quarantined
6264// objects during pre-receive remain visible.
6365func 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)
6567 err := cmd.Run()
6668 if err == nil {
6769 return true, nil
@@ -87,7 +89,7 @@ func ZeroSHA(s string) bool {
8789
8890// RevList returns up to limit commit SHAs reachable from ref, newest first.
8991func 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)
9193 out, err := cmd.Output()
9294 if err != nil {
9395 return nil, fmt.Errorf("rev-list %s: %w", ref, err)
@@ -105,7 +107,7 @@ func RevList(dir, ref string, limit int) ([]string, error) {
105107// touch filePath, newest first. The "--" keeps the path from ever being
106108// read as an option or ref.
107109func 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",
109111 fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref, "--", filePath)
110112 out, err := cmd.Output()
111113 if err != nil {
@@ -123,7 +125,7 @@ func RevListPath(dir, ref, filePath string, limit int) ([]string, error) {
123125// PeelToCommit resolves a ref or object to its commit — annotated tags
124126// peel to the commit they point at.
125127func 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()
127129 if err != nil {
128130 return "", fmt.Errorf("rev-parse %s^{commit}: %w", ref, err)
129131 }
@@ -132,7 +134,7 @@ func PeelToCommit(dir, ref string) (string, error) {
132134
133135// ReadCommit returns the raw commit object bytes.
134136func 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)
136138 out, err := cmd.Output()
137139 if err != nil {
138140 return nil, fmt.Errorf("cat-file commit %s: %w", sha, err)
@@ -145,7 +147,7 @@ func ReadCommit(dir, sha string) ([]byte, error) {
145147// an interactive caller can watch. extraEnv carries credentials via
146148// GIT_ASKPASS; the URL itself must never contain them.
147149func 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,
149151 "+refs/heads/*:refs/heads/*",
150152 "+refs/tags/*:refs/tags/*",
151153 "+refs/notes/*:refs/notes/*")
@@ -168,7 +170,7 @@ func FetchMirror(ctx context.Context, dir, url string, errW io.Writer, extraEnv
168170// handshakes. extraEnv carries credentials via GIT_ASKPASS; the URL must
169171// never contain them.
170172func 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",
172174 url, "+refs/pull/*/head:refs/gh-pull/*")
173175 cmd.Env = append(os.Environ(), extraEnv...)
174176 cmd.Stderr = errW
@@ -180,7 +182,7 @@ func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraE
180182
181183// RemoteDefaultBranch asks the remote which branch HEAD points at.
182184func 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")
184186 cmd.Env = append(os.Environ(), extraEnv...)
185187 out, err := cmd.Output()
186188 if err != nil {
@@ -199,7 +201,7 @@ func RemoteDefaultBranch(ctx context.Context, url string, extraEnv []string) (st
199201
200202// SetHead points the bare repo's HEAD at a branch.
201203func 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)
203205 if out, err := cmd.CombinedOutput(); err != nil {
204206 return fmt.Errorf("symbolic-ref: %v\n%s", err, out)
205207 }
internal/gitutil/grep.go +3 −1
@@ -7,6 +7,8 @@ import (
77 "strconv"
88 "strings"
99 "time"
10
11 "gitbay.org/gitbay/internal/toolpath"
1012)
1113
1214type GrepMatch struct {
@@ -23,7 +25,7 @@ func Grep(dir, ref, query string, max int) ([]GrepMatch, error) {
2325 defer cancel()
2426 // -z: NUL after the path and the line number, so paths containing
2527 // ':' 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)
2729 out, err := cmd.Output()
2830 if err != nil {
2931 if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
internal/gitutil/lastcommit.go +5 −3
@@ -6,6 +6,8 @@ import (
66 "strconv"
77 "strings"
88 "time"
9
10 "gitbay.org/gitbay/internal/toolpath"
911)
1012
1113// 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 {
4951 if prefix != "" {
5052 args = append(args, "--", strings.TrimSuffix(prefix, "/"))
5153 }
52 cmd := exec.Command("git", args...)
54 cmd := exec.Command(toolpath.Look("git"), args...)
5355 stdout, err := cmd.StdoutPipe()
5456 if err != nil {
5557 return nil
@@ -107,7 +109,7 @@ func parseCommitHeader(s string) EntryCommit {
107109// TipCommit is the commit at ref, for the bar above a tree listing that
108110// answers "who touched this repository last".
109111func 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",
111113 "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "--end-of-options", ref).Output()
112114 if err != nil {
113115 return EntryCommit{}
@@ -137,7 +139,7 @@ func entryName(changed, prefix string) (string, bool) {
137139// page can report the facts the file listing no longer carries: its size,
138140// and whether it is executable or a symlink.
139141func 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()
141143 if err != nil {
142144 return TreeEntry{}, false
143145 }
internal/gitutil/lfs.go +4 −2
@@ -7,6 +7,8 @@ import (
77 "regexp"
88 "strconv"
99 "strings"
10
11 "gitbay.org/gitbay/internal/toolpath"
1012)
1113
1214// 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})$`)
1820// anywhere in the repository: every object, not just the reachable ones,
1921// since an unreachable blob is still an object gc has not removed.
2022func 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",
2224 "--batch-check=%(objecttype) %(objectsize) %(objectname)")
2325 out, err := list.Output()
2426 if err != nil {
@@ -38,7 +40,7 @@ func LFSPointerOIDs(dir string) ([]string, error) {
3840 if len(small) == 0 {
3941 return nil, nil
4042 }
41 cat := exec.Command("git", "-C", dir, "cat-file", "--batch")
43 cat := exec.Command(toolpath.Look("git"), "-C", dir, "cat-file", "--batch")
4244 cat.Stdin = strings.NewReader(strings.Join(small, "\n") + "\n")
4345 out, err = cat.Output()
4446 if err != nil {
internal/gitutil/merge.go +22 −20
@@ -6,13 +6,15 @@ import (
66 "os"
77 "os/exec"
88 "strings"
9
10 "gitbay.org/gitbay/internal/toolpath"
911)
1012
1113// FetchInto copies srcRef from srcDir into dstDir as dstRef, forcing the
1214// update. Objects are copied, not shared — the destination owns everything
1315// afterward, which is what keeps MRs alive when their fork is deleted.
1416func 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",
1618 srcDir, "+"+srcRef+":"+dstRef)
1719 if out, err := cmd.CombinedOutput(); err != nil {
1820 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 {
2830 if oldSHA != "" {
2931 args = append(args, oldSHA)
3032 }
31 cmd := exec.Command("git", args...)
33 cmd := exec.Command(toolpath.Look("git"), args...)
3234 if out, err := cmd.CombinedOutput(); err != nil {
3335 return fmt.Errorf("update-ref %s: %v\n%s", ref, err, out)
3436 }
@@ -36,7 +38,7 @@ func UpdateRefCAS(dir, ref, newSHA, oldSHA string) error {
3638}
3739
3840func 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)
4042 if out, err := cmd.CombinedOutput(); err != nil {
4143 return fmt.Errorf("delete-ref %s: %v\n%s", ref, err, out)
4244 }
@@ -45,7 +47,7 @@ func DeleteRef(dir, ref string) error {
4547
4648// RevListRange returns commits in old..new, newest first.
4749func 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)
4951 out, err := cmd.Output()
5052 if err != nil {
5153 return nil, fmt.Errorf("rev-list %s..%s: %w", old, new, err)
@@ -62,7 +64,7 @@ func RevListRange(dir, old, new string) ([]string, error) {
6264// MergeTree performs a real merge of ours and theirs, returning the merged
6365// tree id. conflict=true means the merge cannot be done automatically.
6466func 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)
6668 out, runErr := cmd.Output()
6769 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
6870 if runErr != nil {
@@ -81,7 +83,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
8183 for _, p := range parents {
8284 args = append(args, "-p", p)
8385 }
84 cmd := exec.Command("git", args...)
86 cmd := exec.Command(toolpath.Look("git"), args...)
8587 cmd.Env = append(os.Environ(),
8688 "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email,
8789 "GIT_COMMITTER_NAME="+name, "GIT_COMMITTER_EMAIL="+email,
@@ -98,7 +100,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string)
98100// truncated says whether it was cut, so the caller can say so instead of
99101// rendering a hunk that ends mid-line (#117).
100102func 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)
102104 out, err := cmd.Output()
103105 if err != nil {
104106 return "", false, fmt.Errorf("diff: %w", err)
@@ -122,7 +124,7 @@ func cutAtLine(out []byte, limit int64) ([]byte, bool) {
122124
123125// MergeBase returns the best common ancestor, or an error if none exists.
124126func 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)
126128 out, err := cmd.Output()
127129 if err != nil {
128130 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
141143 }
142144
143145 // 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")
145147 hb.Stdin = strings.NewReader(string(content))
146148 out, err := hb.Output()
147149 if err != nil {
@@ -159,17 +161,17 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
159161 defer os.Remove(idx.Name())
160162 env := append(os.Environ(), "GIT_INDEX_FILE="+idx.Name())
161163
162 rt := exec.Command("git", "-C", dir, "read-tree", parent+"^{tree}")
164 rt := exec.Command(toolpath.Look("git"), "-C", dir, "read-tree", parent+"^{tree}")
163165 rt.Env = env
164166 if out, err := rt.CombinedOutput(); err != nil {
165167 return "", fmt.Errorf("read-tree: %v\n%s", err, out)
166168 }
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)
168170 ui.Env = env
169171 if out, err := ui.CombinedOutput(); err != nil {
170172 return "", fmt.Errorf("update-index: %v\n%s", err, out)
171173 }
172 wt := exec.Command("git", "-C", dir, "write-tree")
174 wt := exec.Command(toolpath.Look("git"), "-C", dir, "write-tree")
173175 wt.Env = env
174176 out, err = wt.Output()
175177 if err != nil {
@@ -189,7 +191,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes
189191
190192// CommitParents returns the parent SHAs of a commit.
191193func 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()
193195 if err != nil {
194196 return nil, fmt.Errorf("rev-list --parents %s: %w", sha, err)
195197 }
@@ -202,7 +204,7 @@ func CommitParents(dir, sha string) ([]string, error) {
202204
203205// AuthorIdent returns a commit's author name, email, and ISO date.
204206func 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()
206208 if err != nil {
207209 return "", "", "", fmt.Errorf("log %s: %w", sha, err)
208210 }
@@ -215,7 +217,7 @@ func AuthorIdent(dir, sha string) (name, email, date string, err error) {
215217
216218// CommitMessage returns a commit's full message.
217219func 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()
219221 if err != nil {
220222 return "", fmt.Errorf("log %s: %w", sha, err)
221223 }
@@ -225,7 +227,7 @@ func CommitMessage(dir, sha string) (string, error) {
225227// MergeTreeOnto replays commit's changes (relative to base) onto onto,
226228// returning the resulting tree. conflict=true when it cannot apply cleanly.
227229func 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)
229231 out, runErr := cmd.Output()
230232 tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0])
231233 if runErr != nil {
@@ -245,7 +247,7 @@ func CommitTreeIdent(dir, tree string, parents []string,
245247 for _, p := range parents {
246248 args = append(args, "-p", p)
247249 }
248 cmd := exec.Command("git", args...)
250 cmd := exec.Command(toolpath.Look("git"), args...)
249251 env := append(os.Environ(),
250252 "GIT_AUTHOR_NAME="+authorName, "GIT_AUTHOR_EMAIL="+authorEmail,
251253 "GIT_COMMITTER_NAME="+committerName, "GIT_COMMITTER_EMAIL="+committerEmail,
@@ -263,7 +265,7 @@ func CommitTreeIdent(dir, tree string, parents []string,
263265
264266// ResolveTree returns the tree id of a commit.
265267func 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()
267269 if err != nil {
268270 return "", fmt.Errorf("rev-parse %s^{tree}: %w", sha, err)
269271 }
@@ -272,7 +274,7 @@ func ResolveTree(dir, sha string) (string, error) {
272274
273275// DiffFiles lists the paths changed between old and new.
274276func 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()
276278 if err != nil {
277279 return nil, fmt.Errorf("diff --name-only: %w", err)
278280 }
@@ -304,7 +306,7 @@ func DiffFiles(dir, old, new string) ([]string, error) {
304306// tells a reviewer nothing. It pairs at 80, and two genuinely unrelated
305307// commits are still left unpaired there; both measured.
306308func 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",
308310 oldBase+".."+oldHead, newBase+".."+newHead)
309311 out, err := cmd.Output()
310312 if err != nil {
internal/gitutil/messages.go +7 −5
@@ -4,6 +4,8 @@ import (
44 "fmt"
55 "os/exec"
66 "strings"
7
8 "gitbay.org/gitbay/internal/toolpath"
79)
810
911const zeroSHA = "0000000000000000000000000000000000000000"
@@ -22,7 +24,7 @@ func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) {
2224 if old != "" && old != zeroSHA {
2325 args = append(args, "^"+old)
2426 }
25 out, err := exec.Command("git", args...).Output()
27 out, err := exec.Command(toolpath.Look("git"), args...).Output()
2628 if err != nil {
2729 return nil, fmt.Errorf("rev-list authors: %w", err)
2830 }
@@ -39,7 +41,7 @@ func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) {
3941
4042// Parents returns a commit's parent shas.
4143func 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()
4345 if err != nil {
4446 return nil
4547 }
@@ -49,7 +51,7 @@ func Parents(dir, sha string) []string {
4951// LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip,
5052// or "" for empty repos.
5153func 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()
5355 if err != nil {
5456 return ""
5557 }
@@ -58,7 +60,7 @@ func LastCommitDate(dir, ref string) string {
5860
5961// HasCommit reports whether sha names a commit object present in dir.
6062func 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
6264}
6365
6466type CommitMsg struct {
@@ -76,7 +78,7 @@ func RevListMessages(dir, old, new string, max int) ([]CommitMsg, error) {
7678 if old != "" && old != zeroSHA {
7779 args = append(args, "^"+old)
7880 }
79 out, err := exec.Command("git", args...).Output()
81 out, err := exec.Command(toolpath.Look("git"), args...).Output()
8082 if err != nil {
8183 return nil, fmt.Errorf("rev-list messages: %w", err)
8284 }
internal/gitutil/read.go +8 −6
@@ -10,6 +10,8 @@ import (
1010 "strconv"
1111 "strings"
1212 "time"
13
14 "gitbay.org/gitbay/internal/toolpath"
1315)
1416
1517type TreeEntry struct {
@@ -26,7 +28,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
2628 if path != "" {
2729 spec = ref + ":" + path
2830 }
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)
3032 out, err := cmd.Output()
3133 if err != nil {
3234 return nil, fmt.Errorf("ls-tree %s: %w", spec, err)
@@ -56,7 +58,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) {
5658
5759// ReadBlob returns the contents of ref:path, capped at limit bytes.
5860func 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)
6062 stdout, err := cmd.StdoutPipe()
6163 if err != nil {
6264 return nil, err
@@ -74,7 +76,7 @@ func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) {
7476
7577// ResolveRef resolves a ref or sha to a full commit sha; errors if absent.
7678func 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}")
7880 out, err := cmd.Output()
7981 if err != nil {
8082 return "", fmt.Errorf("unknown ref %q", ref)
@@ -89,7 +91,7 @@ type Ref struct {
8991
9092// Refs lists branches or tags; kind is "heads" or "tags".
9193func 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",
9395 "--format=%(refname:short) %(objectname)", "refs/"+kind)
9496 out, err := cmd.Output()
9597 if err != nil {
@@ -118,7 +120,7 @@ var ErrArchiveTooLarge = errors.New("archive exceeds the size limit")
118120func Archive(dir, ref, prefix string, w io.Writer) error {
119121 ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout)
120122 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)
122124 lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel}
123125 cmd.Stdout = lw
124126 err := cmd.Run()
@@ -152,7 +154,7 @@ func (c *cappedWriter) Write(p []byte) (int, error) {
152154
153155// ShowPatch returns the stat+patch text for one commit.
154156func 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)
156158 stdout, err := cmd.StdoutPipe()
157159 if err != nil {
158160 return "", false, err
internal/mirror/mirror.go +2 −1
@@ -17,6 +17,7 @@ import (
1717 "gitbay.org/gitbay/internal/config"
1818 "gitbay.org/gitbay/internal/control"
1919 "gitbay.org/gitbay/internal/store"
20 "gitbay.org/gitbay/internal/toolpath"
2021)
2122
2223const askpassScript = `#!/bin/sh
@@ -106,7 +107,7 @@ func (w *Worker) sync(m store.Mirror) error {
106107 args = []string{"-C", dir, "fetch", "--prune", m.URL,
107108 "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*"}
108109 }
109 cmd := exec.CommandContext(ctx, "git", args...)
110 cmd := exec.CommandContext(ctx, toolpath.Look("git"), args...)
110111 cmd.Env = env
111112 if out, err := cmd.CombinedOutput(); err != nil {
112113 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}