Commit cabf60cf5b
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
cmd/gitbay-runner/main.go +5 −4
| @@ -24,6 +24,7 @@ import ( | ||
| 24 | 24 | "time" |
| 25 | 25 | |
| 26 | 26 | "gitbay.org/gitbay/internal/buildinfo" |
| 27 | "gitbay.org/gitbay/internal/toolpath" | |
| 27 | 28 | ) |
| 28 | 29 | |
| 29 | 30 | type job struct { |
| @@ -203,7 +204,7 @@ func (r *runner) run(j job) bool { | ||
| 203 | 204 | defer os.RemoveAll(dir) |
| 204 | 205 | |
| 205 | 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 | 208 | pipe, err := logCmd.StdinPipe() |
| 208 | 209 | if err != nil { |
| 209 | 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 | 296 | steps = append(steps, []string{"-C", dir, "checkout", "-q", j.SHA}) |
| 296 | 297 | for _, args := range steps { |
| 297 | cmd := exec.Command("git", args...) | |
| 298 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 298 | 299 | cmd.Env = append(os.Environ(), "GIT_SSH_COMMAND="+gitSSH, "GIT_TERMINAL_PROMPT=0") |
| 299 | 300 | cmd.Stdout, cmd.Stderr = sink, sink |
| 300 | 301 | if ok, why := runStep(cmd, deadline); !ok { |
| @@ -305,7 +306,7 @@ func (r *runner) run(j job) bool { | ||
| 305 | 306 | |
| 306 | 307 | for _, step := range j.Steps { |
| 307 | 308 | fmt.Fprintf(sink, "$ %s\n", step) |
| 308 | cmd := exec.Command("sh", "-c", step) | |
| 309 | cmd := exec.Command(toolpath.Look("sh"), "-c", step) | |
| 309 | 310 | cmd.Dir = dir |
| 310 | 311 | cmd.Env = append(os.Environ(), |
| 311 | 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 | 325 | // ssh runs one control command against the server and returns stdout. |
| 325 | 326 | func (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 | 328 | if stdin != nil { |
| 328 | 329 | cmd.Stdin = stdin |
| 329 | 330 | } |
cmd/gitbay/local.go +5 −4
| @@ -11,6 +11,7 @@ import ( | ||
| 11 | 11 | |
| 12 | 12 | "gitbay.org/gitbay/internal/cliconfig" |
| 13 | 13 | "gitbay.org/gitbay/internal/protocol" |
| 14 | "gitbay.org/gitbay/internal/toolpath" | |
| 14 | 15 | ) |
| 15 | 16 | |
| 16 | 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 | 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 | 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 | 55 | ed.Stdin, ed.Stdout, ed.Stderr = os.Stdin, os.Stdout, os.Stderr |
| 55 | 56 | if err := ed.Run(); err != nil { |
| 56 | 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 | |
| 76 | 77 | func runGitLocal(args ...string) int { |
| 77 | cmd := exec.Command("git", args...) | |
| 78 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 78 | 79 | cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr |
| 79 | 80 | if err := cmd.Run(); err != nil { |
| 80 | 81 | if ee, ok := err.(*exec.ExitError); ok { |
| @@ -190,7 +191,7 @@ func captureSSH(t target, serverArgv []string) (string, int) { | ||
| 190 | 191 | args := sshArgs(t.inst) |
| 191 | 192 | quoted := quoteAll(serverArgv) |
| 192 | 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 | 195 | cmd.Stderr = os.Stderr |
| 195 | 196 | out, err := cmd.Output() |
| 196 | 197 | if err != nil { |
| @@ -301,7 +302,7 @@ func cmdRemoteList() int { | ||
| 301 | 302 | // currentBranch is the checked-out branch of the working directory's |
| 302 | 303 | // clone, or "" outside a clone or on a detached HEAD. |
| 303 | 304 | func 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 | 306 | if err != nil { |
| 306 | 307 | return "" |
| 307 | 308 | } |
cmd/gitbay/migrate.go +4 −3
| @@ -11,6 +11,7 @@ import ( | ||
| 11 | 11 | "github.com/spf13/cobra" |
| 12 | 12 | |
| 13 | 13 | "gitbay.org/gitbay/internal/protocol" |
| 14 | "gitbay.org/gitbay/internal/toolpath" | |
| 14 | 15 | ) |
| 15 | 16 | |
| 16 | 17 | func migrateCmd() *cobra.Command { |
| @@ -43,7 +44,7 @@ func sourceSSH(host string, port int, extra ...string) *exec.Cmd { | ||
| 43 | 44 | } |
| 44 | 45 | args = append(args, "git@"+host, "--") |
| 45 | 46 | args = append(args, extra...) |
| 46 | return exec.Command("ssh", args...) | |
| 47 | return exec.Command(toolpath.Look("ssh"), args...) | |
| 47 | 48 | } |
| 48 | 49 | |
| 49 | 50 | func runMigrate(from string, fromPort int) int { |
| @@ -99,14 +100,14 @@ func runMigrate(from string, fromPort int) int { | ||
| 99 | 100 | if fromPort != 0 && fromPort != 22 { |
| 100 | 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 | 104 | clone.Stderr = os.Stderr |
| 104 | 105 | if err := clone.Run(); err != nil { |
| 105 | 106 | fmt.Fprintf(os.Stderr, "gitbay: cloning %s failed; fix and re-run migrate (it resumes)\n", repoPath) |
| 106 | 107 | os.RemoveAll(tmp) |
| 107 | 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 | 111 | "+refs/heads/*:refs/heads/*", "+refs/tags/*:refs/tags/*") |
| 111 | 112 | if len(t.inst.SSHOptions) > 0 { |
| 112 | 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 | 14 | "gitbay.org/gitbay/internal/cliconfig" |
| 15 | 15 | "gitbay.org/gitbay/internal/protocol" |
| 16 | "gitbay.org/gitbay/internal/toolpath" | |
| 16 | 17 | ) |
| 17 | 18 | |
| 18 | 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 | |
| 61 | 62 | func 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 | 64 | if err != nil { |
| 64 | 65 | return "" |
| 65 | 66 | } |
| @@ -115,7 +116,7 @@ func runSSH(t target, serverArgv []string, stdin io.Reader) int { | ||
| 115 | 116 | } |
| 116 | 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 | 120 | cmd.Stdin = stdin |
| 120 | 121 | cmd.Stdout = os.Stdout |
| 121 | 122 | cmd.Stderr = os.Stderr |
| @@ -145,7 +146,7 @@ func sshCapture(t target, serverArgv []string) (string, int) { | ||
| 145 | 146 | quoted[i] = shellQuote(a) |
| 146 | 147 | } |
| 147 | 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 | 150 | if err != nil { |
| 150 | 151 | code := protocol.ExitProtocol |
| 151 | 152 | if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() != 255 { |
cmd/gitbayd/hook.go +3 −2
| @@ -15,6 +15,7 @@ import ( | ||
| 15 | 15 | "gitbay.org/gitbay/internal/gitutil" |
| 16 | 16 | "gitbay.org/gitbay/internal/hookd" |
| 17 | 17 | "gitbay.org/gitbay/internal/policy" |
| 18 | "gitbay.org/gitbay/internal/toolpath" | |
| 18 | 19 | ) |
| 19 | 20 | |
| 20 | 21 | // incomingSHAs lists the commits this push introduces, in order, without |
| @@ -28,7 +29,7 @@ func incomingSHAs(updates []policy.RefUpdate) ([]string, error) { | ||
| 28 | 29 | continue |
| 29 | 30 | } |
| 30 | 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 | 33 | if err != nil { |
| 33 | 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 | 63 | // part-way through a large push leaves git blocked writing into a |
| 63 | 64 | // pipe nobody is reading and Wait blocked on git. |
| 64 | 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 | 67 | stdin, err := cmd.StdinPipe() |
| 67 | 68 | if err != nil { |
| 68 | 69 | cancel() |
cmd/gitbayd/main.go +7
| @@ -32,6 +32,7 @@ import ( | ||
| 32 | 32 | "gitbay.org/gitbay/internal/notify" |
| 33 | 33 | "gitbay.org/gitbay/internal/sshd" |
| 34 | 34 | "gitbay.org/gitbay/internal/store" |
| 35 | "gitbay.org/gitbay/internal/toolpath" | |
| 35 | 36 | "gitbay.org/gitbay/internal/webhook" |
| 36 | 37 | ) |
| 37 | 38 | |
| @@ -122,6 +123,12 @@ func serveCmd() *cobra.Command { | ||
| 122 | 123 | // First line of every run: the journal then says which commit is |
| 123 | 124 | // serving, without rebuilding the binary to find out. |
| 124 | 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 | 132 | cfg, err := config.Load(configPath) |
| 126 | 133 | if err != nil { |
| 127 | 134 | return err |
cmd/gitbayd/maint.go +2 −1
| @@ -13,6 +13,7 @@ import ( | ||
| 13 | 13 | "gitbay.org/gitbay/internal/gitutil" |
| 14 | 14 | "gitbay.org/gitbay/internal/lfs" |
| 15 | 15 | "gitbay.org/gitbay/internal/store" |
| 16 | "gitbay.org/gitbay/internal/toolpath" | |
| 16 | 17 | ) |
| 17 | 18 | |
| 18 | 19 | func gcCmd() *cobra.Command { |
| @@ -51,7 +52,7 @@ func gcCmd() *cobra.Command { | ||
| 51 | 52 | if aggressive { |
| 52 | 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 | 56 | fmt.Fprintf(os.Stderr, "%s: gc failed: %v\n%s", r.Path(), err, out) |
| 56 | 57 | continue |
| 57 | 58 | } |
internal/gitd/gitd.go +2 −1
| @@ -16,6 +16,7 @@ import ( | ||
| 16 | 16 | "gitbay.org/gitbay/internal/config" |
| 17 | 17 | "gitbay.org/gitbay/internal/control" |
| 18 | 18 | "gitbay.org/gitbay/internal/store" |
| 19 | "gitbay.org/gitbay/internal/toolpath" | |
| 19 | 20 | ) |
| 20 | 21 | |
| 21 | 22 | type Server struct { |
| @@ -68,7 +69,7 @@ func (s *Server) handle(conn net.Conn) { | ||
| 68 | 69 | } |
| 69 | 70 | |
| 70 | 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 | 73 | cmd.Env = append(os.Environ(), protoEnv...) |
| 73 | 74 | cmd.Stdin = conn |
| 74 | 75 | cmd.Stdout = conn |
internal/gitutil/blame.go +3 −1
| @@ -7,6 +7,8 @@ import ( | ||
| 7 | 7 | "os/exec" |
| 8 | 8 | "strconv" |
| 9 | 9 | "strings" |
| 10 | ||
| 11 | "gitbay.org/gitbay/internal/toolpath" | |
| 10 | 12 | ) |
| 11 | 13 | |
| 12 | 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 | 30 | if err != nil { |
| 29 | 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 | 34 | fmt.Sprintf("-L%d,%d", start, end), sha, "--", path) |
| 33 | 35 | out, err := cmd.Output() |
| 34 | 36 | if err != nil { |
internal/gitutil/facts.go +5 −3
| @@ -5,12 +5,14 @@ import ( | ||
| 5 | 5 | "sort" |
| 6 | 6 | "strconv" |
| 7 | 7 | "strings" |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/toolpath" | |
| 8 | 10 | ) |
| 9 | 11 | |
| 10 | 12 | // CountCommits returns the number of commits reachable from ref, or 0 when |
| 11 | 13 | // the ref does not resolve (an empty repository). |
| 12 | 14 | func 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 | 16 | if err != nil { |
| 15 | 17 | return 0 |
| 16 | 18 | } |
| @@ -30,7 +32,7 @@ type Contributor struct { | ||
| 30 | 32 | // tie back to an account, after the repository's own .mailmap has had its |
| 31 | 33 | // say — a bare repo resolves that from HEAD:.mailmap with no config. |
| 32 | 34 | func 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 | 36 | "--use-mailmap", "--format=%aN%x01%aE", "--end-of-options", ref).Output() |
| 35 | 37 | if err != nil { |
| 36 | 38 | return nil |
| @@ -62,7 +64,7 @@ func Contributors(dir, ref string) []Contributor { | ||
| 62 | 64 | // largest first, keyed by the extension map the caller supplies. Only |
| 63 | 65 | // blobs count; git's own metadata does not. |
| 64 | 66 | func 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 | 68 | if err != nil { |
| 67 | 69 | return nil |
| 68 | 70 | } |
internal/gitutil/gitutil.go +14 −12
| @@ -11,6 +11,8 @@ import ( | ||
| 11 | 11 | "os/exec" |
| 12 | 12 | "path/filepath" |
| 13 | 13 | "strings" |
| 14 | ||
| 15 | "gitbay.org/gitbay/internal/toolpath" | |
| 14 | 16 | ) |
| 15 | 17 | |
| 16 | 18 | // InitBare creates a bare repository with the shared hooks directory wired |
| @@ -19,14 +21,14 @@ func InitBare(path, defaultBranch, hooksPath string) error { | ||
| 19 | 21 | if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { |
| 20 | 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 | 25 | if out, err := cmd.CombinedOutput(); err != nil { |
| 24 | 26 | return fmt.Errorf("git init: %v\n%s", err, out) |
| 25 | 27 | } |
| 26 | 28 | // An empty hooksPath leaves the bare repo with no hooks — used for |
| 27 | 29 | // companion repos (wikis) that carry no ref policy. |
| 28 | 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 | 32 | if out, err := cmd.CombinedOutput(); err != nil { |
| 31 | 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 | 51 | default: |
| 50 | 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 | 55 | cmd.Env = append(os.Environ(), extraEnv...) |
| 54 | 56 | cmd.Stdin = stdin |
| 55 | 57 | cmd.Stdout = stdout |
| @@ -61,7 +63,7 @@ func Transport(service, repoPath string, stdin io.Reader, stdout, errW io.Writer | ||
| 61 | 63 | // dir. It must run with the caller's environment intact so that quarantined |
| 62 | 64 | // objects during pre-receive remain visible. |
| 63 | 65 | func 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 | 67 | err := cmd.Run() |
| 66 | 68 | if err == nil { |
| 67 | 69 | return true, nil |
| @@ -87,7 +89,7 @@ func ZeroSHA(s string) bool { | ||
| 87 | 89 | |
| 88 | 90 | // RevList returns up to limit commit SHAs reachable from ref, newest first. |
| 89 | 91 | func 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 | 93 | out, err := cmd.Output() |
| 92 | 94 | if err != nil { |
| 93 | 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 | 107 | // touch filePath, newest first. The "--" keeps the path from ever being |
| 106 | 108 | // read as an option or ref. |
| 107 | 109 | func 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 | 111 | fmt.Sprintf("--max-count=%d", limit), "--end-of-options", ref, "--", filePath) |
| 110 | 112 | out, err := cmd.Output() |
| 111 | 113 | if err != nil { |
| @@ -123,7 +125,7 @@ func RevListPath(dir, ref, filePath string, limit int) ([]string, error) { | ||
| 123 | 125 | // PeelToCommit resolves a ref or object to its commit — annotated tags |
| 124 | 126 | // peel to the commit they point at. |
| 125 | 127 | func 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 | 129 | if err != nil { |
| 128 | 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 | 135 | // ReadCommit returns the raw commit object bytes. |
| 134 | 136 | func 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 | 138 | out, err := cmd.Output() |
| 137 | 139 | if err != nil { |
| 138 | 140 | return nil, fmt.Errorf("cat-file commit %s: %w", sha, err) |
| @@ -145,7 +147,7 @@ func ReadCommit(dir, sha string) ([]byte, error) { | ||
| 145 | 147 | // an interactive caller can watch. extraEnv carries credentials via |
| 146 | 148 | // GIT_ASKPASS; the URL itself must never contain them. |
| 147 | 149 | func 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 | 151 | "+refs/heads/*:refs/heads/*", |
| 150 | 152 | "+refs/tags/*:refs/tags/*", |
| 151 | 153 | "+refs/notes/*:refs/notes/*") |
| @@ -168,7 +170,7 @@ func FetchMirror(ctx context.Context, dir, url string, errW io.Writer, extraEnv | ||
| 168 | 170 | // handshakes. extraEnv carries credentials via GIT_ASKPASS; the URL must |
| 169 | 171 | // never contain them. |
| 170 | 172 | func 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 | 174 | url, "+refs/pull/*/head:refs/gh-pull/*") |
| 173 | 175 | cmd.Env = append(os.Environ(), extraEnv...) |
| 174 | 176 | cmd.Stderr = errW |
| @@ -180,7 +182,7 @@ func FetchPullHeads(ctx context.Context, dir, url string, errW io.Writer, extraE | ||
| 180 | 182 | |
| 181 | 183 | // RemoteDefaultBranch asks the remote which branch HEAD points at. |
| 182 | 184 | func 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 | 186 | cmd.Env = append(os.Environ(), extraEnv...) |
| 185 | 187 | out, err := cmd.Output() |
| 186 | 188 | if err != nil { |
| @@ -199,7 +201,7 @@ func RemoteDefaultBranch(ctx context.Context, url string, extraEnv []string) (st | ||
| 199 | 201 | |
| 200 | 202 | // SetHead points the bare repo's HEAD at a branch. |
| 201 | 203 | func 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 | 205 | if out, err := cmd.CombinedOutput(); err != nil { |
| 204 | 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 | 7 | "strconv" |
| 8 | 8 | "strings" |
| 9 | 9 | "time" |
| 10 | ||
| 11 | "gitbay.org/gitbay/internal/toolpath" | |
| 10 | 12 | ) |
| 11 | 13 | |
| 12 | 14 | type GrepMatch struct { |
| @@ -23,7 +25,7 @@ func Grep(dir, ref, query string, max int) ([]GrepMatch, error) { | ||
| 23 | 25 | defer cancel() |
| 24 | 26 | // -z: NUL after the path and the line number, so paths containing |
| 25 | 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 | 29 | out, err := cmd.Output() |
| 28 | 30 | if err != nil { |
| 29 | 31 | if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 { |
internal/gitutil/lastcommit.go +5 −3
| @@ -6,6 +6,8 @@ import ( | ||
| 6 | 6 | "strconv" |
| 7 | 7 | "strings" |
| 8 | 8 | "time" |
| 9 | ||
| 10 | "gitbay.org/gitbay/internal/toolpath" | |
| 9 | 11 | ) |
| 10 | 12 | |
| 11 | 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 | 51 | if prefix != "" { |
| 50 | 52 | args = append(args, "--", strings.TrimSuffix(prefix, "/")) |
| 51 | 53 | } |
| 52 | cmd := exec.Command("git", args...) | |
| 54 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 53 | 55 | stdout, err := cmd.StdoutPipe() |
| 54 | 56 | if err != nil { |
| 55 | 57 | return nil |
| @@ -107,7 +109,7 @@ func parseCommitHeader(s string) EntryCommit { | ||
| 107 | 109 | // TipCommit is the commit at ref, for the bar above a tree listing that |
| 108 | 110 | // answers "who touched this repository last". |
| 109 | 111 | func 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 | 113 | "--format=%H%x1f%ct%x1f%an%x1f%ae%x1f%s", "--end-of-options", ref).Output() |
| 112 | 114 | if err != nil { |
| 113 | 115 | return EntryCommit{} |
| @@ -137,7 +139,7 @@ func entryName(changed, prefix string) (string, bool) { | ||
| 137 | 139 | // page can report the facts the file listing no longer carries: its size, |
| 138 | 140 | // and whether it is executable or a symlink. |
| 139 | 141 | func 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 | 143 | if err != nil { |
| 142 | 144 | return TreeEntry{}, false |
| 143 | 145 | } |
internal/gitutil/lfs.go +4 −2
| @@ -7,6 +7,8 @@ import ( | ||
| 7 | 7 | "regexp" |
| 8 | 8 | "strconv" |
| 9 | 9 | "strings" |
| 10 | ||
| 11 | "gitbay.org/gitbay/internal/toolpath" | |
| 10 | 12 | ) |
| 11 | 13 | |
| 12 | 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 | 20 | // anywhere in the repository: every object, not just the reachable ones, |
| 19 | 21 | // since an unreachable blob is still an object gc has not removed. |
| 20 | 22 | func 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 | 24 | "--batch-check=%(objecttype) %(objectsize) %(objectname)") |
| 23 | 25 | out, err := list.Output() |
| 24 | 26 | if err != nil { |
| @@ -38,7 +40,7 @@ func LFSPointerOIDs(dir string) ([]string, error) { | ||
| 38 | 40 | if len(small) == 0 { |
| 39 | 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 | 44 | cat.Stdin = strings.NewReader(strings.Join(small, "\n") + "\n") |
| 43 | 45 | out, err = cat.Output() |
| 44 | 46 | if err != nil { |
internal/gitutil/merge.go +22 −20
| @@ -6,13 +6,15 @@ import ( | ||
| 6 | 6 | "os" |
| 7 | 7 | "os/exec" |
| 8 | 8 | "strings" |
| 9 | ||
| 10 | "gitbay.org/gitbay/internal/toolpath" | |
| 9 | 11 | ) |
| 10 | 12 | |
| 11 | 13 | // FetchInto copies srcRef from srcDir into dstDir as dstRef, forcing the |
| 12 | 14 | // update. Objects are copied, not shared — the destination owns everything |
| 13 | 15 | // afterward, which is what keeps MRs alive when their fork is deleted. |
| 14 | 16 | func 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 | 18 | srcDir, "+"+srcRef+":"+dstRef) |
| 17 | 19 | if out, err := cmd.CombinedOutput(); err != nil { |
| 18 | 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 | 30 | if oldSHA != "" { |
| 29 | 31 | args = append(args, oldSHA) |
| 30 | 32 | } |
| 31 | cmd := exec.Command("git", args...) | |
| 33 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 32 | 34 | if out, err := cmd.CombinedOutput(); err != nil { |
| 33 | 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 | |
| 38 | 40 | func 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 | 42 | if out, err := cmd.CombinedOutput(); err != nil { |
| 41 | 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 | 48 | // RevListRange returns commits in old..new, newest first. |
| 47 | 49 | func 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 | 51 | out, err := cmd.Output() |
| 50 | 52 | if err != nil { |
| 51 | 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 | 64 | // MergeTree performs a real merge of ours and theirs, returning the merged |
| 63 | 65 | // tree id. conflict=true means the merge cannot be done automatically. |
| 64 | 66 | func 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 | 68 | out, runErr := cmd.Output() |
| 67 | 69 | tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) |
| 68 | 70 | if runErr != nil { |
| @@ -81,7 +83,7 @@ func CommitTree(dir, tree string, parents []string, name, email, message string) | ||
| 81 | 83 | for _, p := range parents { |
| 82 | 84 | args = append(args, "-p", p) |
| 83 | 85 | } |
| 84 | cmd := exec.Command("git", args...) | |
| 86 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 85 | 87 | cmd.Env = append(os.Environ(), |
| 86 | 88 | "GIT_AUTHOR_NAME="+name, "GIT_AUTHOR_EMAIL="+email, |
| 87 | 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 | 100 | // truncated says whether it was cut, so the caller can say so instead of |
| 99 | 101 | // rendering a hunk that ends mid-line (#117). |
| 100 | 102 | func 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 | 104 | out, err := cmd.Output() |
| 103 | 105 | if err != nil { |
| 104 | 106 | return "", false, fmt.Errorf("diff: %w", err) |
| @@ -122,7 +124,7 @@ func cutAtLine(out []byte, limit int64) ([]byte, bool) { | ||
| 122 | 124 | |
| 123 | 125 | // MergeBase returns the best common ancestor, or an error if none exists. |
| 124 | 126 | func 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 | 128 | out, err := cmd.Output() |
| 127 | 129 | if err != nil { |
| 128 | 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 | 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 | 147 | hb.Stdin = strings.NewReader(string(content)) |
| 146 | 148 | out, err := hb.Output() |
| 147 | 149 | if err != nil { |
| @@ -159,17 +161,17 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | ||
| 159 | 161 | defer os.Remove(idx.Name()) |
| 160 | 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 | 165 | rt.Env = env |
| 164 | 166 | if out, err := rt.CombinedOutput(); err != nil { |
| 165 | 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 | 170 | ui.Env = env |
| 169 | 171 | if out, err := ui.CombinedOutput(); err != nil { |
| 170 | 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 | 175 | wt.Env = env |
| 174 | 176 | out, err = wt.Output() |
| 175 | 177 | if err != nil { |
| @@ -189,7 +191,7 @@ func CommitFileChange(dir, branch, path string, content []byte, name, email, mes | ||
| 189 | 191 | |
| 190 | 192 | // CommitParents returns the parent SHAs of a commit. |
| 191 | 193 | func 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 | 195 | if err != nil { |
| 194 | 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 | 205 | // AuthorIdent returns a commit's author name, email, and ISO date. |
| 204 | 206 | func 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 | 208 | if err != nil { |
| 207 | 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 | 218 | // CommitMessage returns a commit's full message. |
| 217 | 219 | func 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 | 221 | if err != nil { |
| 220 | 222 | return "", fmt.Errorf("log %s: %w", sha, err) |
| 221 | 223 | } |
| @@ -225,7 +227,7 @@ func CommitMessage(dir, sha string) (string, error) { | ||
| 225 | 227 | // MergeTreeOnto replays commit's changes (relative to base) onto onto, |
| 226 | 228 | // returning the resulting tree. conflict=true when it cannot apply cleanly. |
| 227 | 229 | func 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 | 231 | out, runErr := cmd.Output() |
| 230 | 232 | tree = strings.TrimSpace(strings.SplitN(string(out), "\n", 2)[0]) |
| 231 | 233 | if runErr != nil { |
| @@ -245,7 +247,7 @@ func CommitTreeIdent(dir, tree string, parents []string, | ||
| 245 | 247 | for _, p := range parents { |
| 246 | 248 | args = append(args, "-p", p) |
| 247 | 249 | } |
| 248 | cmd := exec.Command("git", args...) | |
| 250 | cmd := exec.Command(toolpath.Look("git"), args...) | |
| 249 | 251 | env := append(os.Environ(), |
| 250 | 252 | "GIT_AUTHOR_NAME="+authorName, "GIT_AUTHOR_EMAIL="+authorEmail, |
| 251 | 253 | "GIT_COMMITTER_NAME="+committerName, "GIT_COMMITTER_EMAIL="+committerEmail, |
| @@ -263,7 +265,7 @@ func CommitTreeIdent(dir, tree string, parents []string, | ||
| 263 | 265 | |
| 264 | 266 | // ResolveTree returns the tree id of a commit. |
| 265 | 267 | func 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 | 269 | if err != nil { |
| 268 | 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 | 275 | // DiffFiles lists the paths changed between old and new. |
| 274 | 276 | func 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 | 278 | if err != nil { |
| 277 | 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 | 306 | // tells a reviewer nothing. It pairs at 80, and two genuinely unrelated |
| 305 | 307 | // commits are still left unpaired there; both measured. |
| 306 | 308 | func 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 | 310 | oldBase+".."+oldHead, newBase+".."+newHead) |
| 309 | 311 | out, err := cmd.Output() |
| 310 | 312 | if err != nil { |
internal/gitutil/messages.go +7 −5
| @@ -4,6 +4,8 @@ import ( | ||
| 4 | 4 | "fmt" |
| 5 | 5 | "os/exec" |
| 6 | 6 | "strings" |
| 7 | ||
| 8 | "gitbay.org/gitbay/internal/toolpath" | |
| 7 | 9 | ) |
| 8 | 10 | |
| 9 | 11 | const zeroSHA = "0000000000000000000000000000000000000000" |
| @@ -22,7 +24,7 @@ func RevListAuthors(dir, old, new string, max int) ([]CommitAuthor, error) { | ||
| 22 | 24 | if old != "" && old != zeroSHA { |
| 23 | 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 | 28 | if err != nil { |
| 27 | 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 | 42 | // Parents returns a commit's parent shas. |
| 41 | 43 | func 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 | 45 | if err != nil { |
| 44 | 46 | return nil |
| 45 | 47 | } |
| @@ -49,7 +51,7 @@ func Parents(dir, sha string) []string { | ||
| 49 | 51 | // LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip, |
| 50 | 52 | // or "" for empty repos. |
| 51 | 53 | func 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 | 55 | if err != nil { |
| 54 | 56 | return "" |
| 55 | 57 | } |
| @@ -58,7 +60,7 @@ func LastCommitDate(dir, ref string) string { | ||
| 58 | 60 | |
| 59 | 61 | // HasCommit reports whether sha names a commit object present in dir. |
| 60 | 62 | func 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 | |
| 64 | 66 | type CommitMsg struct { |
| @@ -76,7 +78,7 @@ func RevListMessages(dir, old, new string, max int) ([]CommitMsg, error) { | ||
| 76 | 78 | if old != "" && old != zeroSHA { |
| 77 | 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 | 82 | if err != nil { |
| 81 | 83 | return nil, fmt.Errorf("rev-list messages: %w", err) |
| 82 | 84 | } |
internal/gitutil/read.go +8 −6
| @@ -10,6 +10,8 @@ import ( | ||
| 10 | 10 | "strconv" |
| 11 | 11 | "strings" |
| 12 | 12 | "time" |
| 13 | ||
| 14 | "gitbay.org/gitbay/internal/toolpath" | |
| 13 | 15 | ) |
| 14 | 16 | |
| 15 | 17 | type TreeEntry struct { |
| @@ -26,7 +28,7 @@ func ListTree(dir, ref, path string) ([]TreeEntry, error) { | ||
| 26 | 28 | if path != "" { |
| 27 | 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 | 32 | out, err := cmd.Output() |
| 31 | 33 | if err != nil { |
| 32 | 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 | 59 | // ReadBlob returns the contents of ref:path, capped at limit bytes. |
| 58 | 60 | func 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 | 62 | stdout, err := cmd.StdoutPipe() |
| 61 | 63 | if err != nil { |
| 62 | 64 | return nil, err |
| @@ -74,7 +76,7 @@ func ReadBlob(dir, ref, path string, limit int64) ([]byte, error) { | ||
| 74 | 76 | |
| 75 | 77 | // ResolveRef resolves a ref or sha to a full commit sha; errors if absent. |
| 76 | 78 | func 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 | 80 | out, err := cmd.Output() |
| 79 | 81 | if err != nil { |
| 80 | 82 | return "", fmt.Errorf("unknown ref %q", ref) |
| @@ -89,7 +91,7 @@ type Ref struct { | ||
| 89 | 91 | |
| 90 | 92 | // Refs lists branches or tags; kind is "heads" or "tags". |
| 91 | 93 | func 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 | 95 | "--format=%(refname:short) %(objectname)", "refs/"+kind) |
| 94 | 96 | out, err := cmd.Output() |
| 95 | 97 | if err != nil { |
| @@ -118,7 +120,7 @@ var ErrArchiveTooLarge = errors.New("archive exceeds the size limit") | ||
| 118 | 120 | func Archive(dir, ref, prefix string, w io.Writer) error { |
| 119 | 121 | ctx, cancel := context.WithTimeout(context.Background(), archiveTimeout) |
| 120 | 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 | 124 | lw := &cappedWriter{w: w, left: MaxArchiveBytes, stop: cancel} |
| 123 | 125 | cmd.Stdout = lw |
| 124 | 126 | err := cmd.Run() |
| @@ -152,7 +154,7 @@ func (c *cappedWriter) Write(p []byte) (int, error) { | ||
| 152 | 154 | |
| 153 | 155 | // ShowPatch returns the stat+patch text for one commit. |
| 154 | 156 | func 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 | 158 | stdout, err := cmd.StdoutPipe() |
| 157 | 159 | if err != nil { |
| 158 | 160 | return "", false, err |
internal/mirror/mirror.go +2 −1
| @@ -17,6 +17,7 @@ import ( | ||
| 17 | 17 | "gitbay.org/gitbay/internal/config" |
| 18 | 18 | "gitbay.org/gitbay/internal/control" |
| 19 | 19 | "gitbay.org/gitbay/internal/store" |
| 20 | "gitbay.org/gitbay/internal/toolpath" | |
| 20 | 21 | ) |
| 21 | 22 | |
| 22 | 23 | const askpassScript = `#!/bin/sh |
| @@ -106,7 +107,7 @@ func (w *Worker) sync(m store.Mirror) error { | ||
| 106 | 107 | args = []string{"-C", dir, "fetch", "--prune", m.URL, |
| 107 | 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 | 111 | cmd.Env = env |
| 111 | 112 | if out, err := cmd.CombinedOutput(); err != nil { |
| 112 | 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. | |
| 20 | package toolpath | |
| 21 | ||
| 22 | import ( | |
| 23 | "fmt" | |
| 24 | "os/exec" | |
| 25 | "strings" | |
| 26 | "sync" | |
| 27 | ) | |
| 28 | ||
| 29 | var ( | |
| 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. | |
| 39 | func 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. | |
| 57 | func 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 @@ | ||
| 1 | package toolpath | |
| 2 | ||
| 3 | import ( | |
| 4 | "os/exec" | |
| 5 | "path/filepath" | |
| 6 | "strings" | |
| 7 | "testing" | |
| 8 | ) | |
| 9 | ||
| 10 | func 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. | |
| 26 | func 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. | |
| 35 | func 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 | } | |