Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions platform/git/exec/gitexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,33 @@ func Env(opts EnvOptions) []string {

// Command builds a git invocation in dir with the ambient environment removed.
// An empty dir runs in the current working directory. PATH is passed so git can
// find its helpers; nothing else the host sets reaches the command.
// find its helpers, with git's own directory first; nothing else the host sets
// reaches the command.
func Command(ctx context.Context, git, dir string, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, git, args...)
cmd.Dir = dir
cmd.Env = Env(EnvOptions{Literal: []string{"PATH=" + os.Getenv("PATH")}})
cmd.Env = Env(EnvOptions{Literal: []string{"PATH=" + PathWithGitFirst(git)}})
return cmd
}

// PathWithGitFirst returns PATH with the directory of git in front, for a
// command that builds its own environment instead of using Command. Git runs
// some of its own commands by name, such as upload-pack and maintenance, and a
// git built without a usable exec path finds them on PATH. Without this, a
// newer pinned git can end up running an older one found elsewhere on PATH,
// which fails on options it does not know. A git given without a directory is
// found through PATH already, so it is left alone.
func PathWithGitFirst(git string) string {
path := os.Getenv("PATH")
if !filepath.IsAbs(git) {
return path
}
if path == "" {
return filepath.Dir(git)
}
return filepath.Dir(git) + string(os.PathListSeparator) + path
}

// Run executes a git command and discards its output, reporting stderr in the
// error so a failure says what git said rather than only that it exited.
func Run(ctx context.Context, git, dir string, args ...string) error {
Expand Down
33 changes: 33 additions & 0 deletions platform/git/exec/gitexec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -228,3 +229,35 @@ func TestCommandFailure(t *testing.T) {
assert.Empty(t, commandErr.Operation())
})
}

// A git that runs another git by name has to get itself, not whichever git is
// first on the caller's PATH.
func TestCommand_RunsHelpersFromGitsOwnDirectory(t *testing.T) {
dir := t.TempDir()
fakeGit := filepath.Join(dir, "git")
require.NoError(t, os.WriteFile(fakeGit, []byte("#!/bin/sh\ncommand -v git\n"), 0o755))

t.Setenv("PATH", t.TempDir()+string(os.PathListSeparator)+os.Getenv("PATH"))

got, err := Output(context.Background(), fakeGit, "")
require.NoError(t, err)
assert.Equal(t, fakeGit, got)
}

func TestPathWithGitFirst(t *testing.T) {
t.Setenv("PATH", "/usr/bin:/bin")
tests := []struct {
name string
git string
want string
}{
{name: "an absolute git goes first", git: "/opt/pinned/git", want: "/opt/pinned:/usr/bin:/bin"},
{name: "a bare name is found through PATH already", git: "git", want: "/usr/bin:/bin"},
{name: "a relative path is left alone", git: "../pinned/git", want: "/usr/bin:/bin"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, PathWithGitFirst(tt.git))
})
}
}
6 changes: 5 additions & 1 deletion platform/git/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,11 @@ func (r *Repo) MergeBase(ctx context.Context, a, b string) (string, error) {
func (r *Repo) command(ctx context.Context, args ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, r.cfg.Git, args...)
cmd.Dir = r.cfg.Path
cmd.Env = gitexec.Env(gitexec.EnvOptions{Transport: true, Passthrough: []string{"HOME"}})
cmd.Env = gitexec.Env(gitexec.EnvOptions{
Transport: true,
Passthrough: []string{"HOME"},
Literal: []string{"PATH=" + gitexec.PathWithGitFirst(r.cfg.Git)},
})
return cmd
}

Expand Down
Loading