mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
Print executed Git commands in build logs to help diagnose source resolution, fetch, and checkout behavior, including bundle staging. Enable tracing with BUILDKIT_GIT_DEBUG_COMMANDS=1 or llb.GitDebugCommands(). Redact URL credentials and HTTP extra headers without changing command execution or snapshot cache keys. Tracing is disabled by default. Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
270 lines
8.5 KiB
Go
270 lines
8.5 KiB
Go
package gitutil
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetGitSSHCommandUsesConfigPath(t *testing.T) {
|
|
cmd := getGitSSHCommand("")
|
|
require.Equal(t, "ssh -F "+os.DevNull+" -o StrictHostKeyChecking=no", cmd)
|
|
|
|
cmd = getGitSSHCommand("/known-hosts")
|
|
require.Equal(t, "ssh -F "+os.DevNull+" -o UserKnownHostsFile=/known-hosts", cmd)
|
|
}
|
|
|
|
func TestGitCLIConfigEnv(t *testing.T) {
|
|
t.Setenv("HOME", "/tmp/home")
|
|
t.Setenv("XDG_CONFIG_HOME", "/tmp/xdg")
|
|
t.Setenv("USERPROFILE", `C:\Users\tester`)
|
|
t.Setenv("HOMEDRIVE", "C:")
|
|
t.Setenv("HOMEPATH", `\Users\tester`)
|
|
t.Setenv("GIT_CONFIG_GLOBAL", "/tmp/global-gitconfig")
|
|
t.Setenv("GIT_CONFIG_SYSTEM", "/tmp/system-gitconfig")
|
|
t.Setenv("SUDO_UID", "1000")
|
|
|
|
t.Run("isolated by default", func(t *testing.T) {
|
|
var got []string
|
|
cli := NewGitCLI(WithExec(func(ctx context.Context, cmd *exec.Cmd) error {
|
|
got = append([]string(nil), cmd.Env...)
|
|
return nil
|
|
}))
|
|
_, err := cli.Run(t.Context(), "status")
|
|
require.NoError(t, err)
|
|
require.Contains(t, got, "GIT_CONFIG_NOSYSTEM=1")
|
|
require.Contains(t, got, "HOME="+os.DevNull)
|
|
require.Contains(t, got, "GIT_CONFIG_GLOBAL="+os.DevNull)
|
|
require.NotContains(t, got, "HOME=/tmp/home")
|
|
require.NotContains(t, got, "XDG_CONFIG_HOME=/tmp/xdg")
|
|
require.NotContains(t, got, "GIT_CONFIG_GLOBAL=/tmp/global-gitconfig")
|
|
require.NotContains(t, got, "GIT_CONFIG_SYSTEM=/tmp/system-gitconfig")
|
|
require.NotContains(t, got, "SUDO_UID=1000")
|
|
})
|
|
|
|
t.Run("host git config opt-in", func(t *testing.T) {
|
|
var got []string
|
|
cli := NewGitCLI(
|
|
WithHostGitConfig(),
|
|
WithExec(func(ctx context.Context, cmd *exec.Cmd) error {
|
|
got = append([]string(nil), cmd.Env...)
|
|
return nil
|
|
}),
|
|
)
|
|
_, err := cli.Run(t.Context(), "status")
|
|
require.NoError(t, err)
|
|
require.NotContains(t, got, "GIT_CONFIG_NOSYSTEM=1")
|
|
require.NotContains(t, got, "HOME="+os.DevNull)
|
|
require.NotContains(t, got, "GIT_CONFIG_GLOBAL="+os.DevNull)
|
|
require.Contains(t, got, "HOME=/tmp/home")
|
|
require.Contains(t, got, "XDG_CONFIG_HOME=/tmp/xdg")
|
|
require.Contains(t, got, `USERPROFILE=C:\Users\tester`)
|
|
require.Contains(t, got, "HOMEDRIVE=C:")
|
|
require.Contains(t, got, `HOMEPATH=\Users\tester`)
|
|
require.Contains(t, got, "GIT_CONFIG_GLOBAL=/tmp/global-gitconfig")
|
|
require.Contains(t, got, "GIT_CONFIG_SYSTEM=/tmp/system-gitconfig")
|
|
require.Contains(t, got, "SUDO_UID=1000")
|
|
})
|
|
}
|
|
|
|
func TestGitCLIAdviceOption(t *testing.T) {
|
|
run := func(t *testing.T, opts ...Option) (env, args []string) {
|
|
t.Helper()
|
|
opts = append(opts, WithExec(func(ctx context.Context, cmd *exec.Cmd) error {
|
|
env = append([]string(nil), cmd.Env...)
|
|
args = append([]string(nil), cmd.Args...)
|
|
return nil
|
|
}))
|
|
cli := NewGitCLI(opts...)
|
|
_, err := cli.Run(t.Context(), "status")
|
|
require.NoError(t, err)
|
|
return env, args
|
|
}
|
|
|
|
t.Run("unset by default", func(t *testing.T) {
|
|
env, args := run(t)
|
|
require.NotContains(t, env, "GIT_ADVICE=0")
|
|
require.NotContains(t, env, "GIT_ADVICE=1")
|
|
require.NotContains(t, args, "advice.detachedHead=false")
|
|
})
|
|
|
|
t.Run("disabled", func(t *testing.T) {
|
|
env, args := run(t, WithGitAdvice(false))
|
|
require.Contains(t, env, "GIT_ADVICE=0")
|
|
require.Contains(t, args, "advice.detachedHead=false")
|
|
})
|
|
|
|
t.Run("enabled", func(t *testing.T) {
|
|
env, args := run(t, WithGitAdvice(true))
|
|
require.Contains(t, env, "GIT_ADVICE=1")
|
|
require.NotContains(t, args, "advice.detachedHead=false")
|
|
})
|
|
}
|
|
|
|
func TestGitCLIDebugCommands(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
opts []Option
|
|
want string
|
|
}{
|
|
{
|
|
name: "default",
|
|
},
|
|
{
|
|
name: "disabled",
|
|
opts: []Option{WithGitDebugCommands(false)},
|
|
},
|
|
{
|
|
name: "enabled",
|
|
opts: []Option{WithGitDebugCommands(true)},
|
|
want: "+ git -c protocol.file.allow=user -c http.extraheader=xxxxx ls-remote https://xxxxx:xxxxx@example.com/repo.git\n",
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
stderr, err := os.CreateTemp(t.TempDir(), "stderr")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { stderr.Close() })
|
|
const remote = "https://user:pass@example.com/repo.git"
|
|
const header = "http.extraheader=Authorization: Bearer secret"
|
|
var gotArgs []string
|
|
opts := append(tc.opts,
|
|
WithArgs("-c", header),
|
|
WithStreams(func(context.Context) (io.WriteCloser, io.WriteCloser, func()) {
|
|
return nil, stderr, nil
|
|
}),
|
|
WithExec(func(_ context.Context, cmd *exec.Cmd) error {
|
|
gotArgs = cmd.Args
|
|
_, err := io.WriteString(cmd.Stdout, "result")
|
|
return err
|
|
}),
|
|
)
|
|
out, err := NewGitCLI(opts...).Run(t.Context(), "ls-remote", remote)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "result", string(out))
|
|
require.Equal(t, []string{"git", "-c", "protocol.file.allow=user", "-c", header, "ls-remote", remote}, gotArgs)
|
|
trace, err := os.ReadFile(stderr.Name())
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.want, string(trace))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGitCLIStreamsAllowNil(t *testing.T) {
|
|
cli := NewGitCLI(
|
|
WithStreams(func(context.Context) (io.WriteCloser, io.WriteCloser, func()) {
|
|
return nil, nil, nil
|
|
}),
|
|
WithExec(func(context.Context, *exec.Cmd) error {
|
|
return nil
|
|
}),
|
|
)
|
|
_, err := cli.Run(t.Context(), "status")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFormatDebugCommand(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "quotes and redacts urls and config",
|
|
args: []string{
|
|
"git",
|
|
"-c", "protocol.file.allow=user",
|
|
"--work-tree", "/tmp/work tree",
|
|
"--git-dir", "/tmp/work tree/.git",
|
|
"-c", "http.https://github.com/.extraheader=Authorization: Basic c2VjcmV0",
|
|
"-c", "core.abbrev=12",
|
|
"fetch",
|
|
"https://user:pass@example.com/repo.git",
|
|
"refs/heads/main",
|
|
},
|
|
expected: `git -c protocol.file.allow=user --work-tree "/tmp/work tree" --git-dir "/tmp/work tree/.git" -c http.https://github.com/.extraheader=xxxxx -c core.abbrev=12 fetch https://xxxxx:xxxxx@example.com/repo.git refs/heads/main`,
|
|
},
|
|
{
|
|
name: "empty argument",
|
|
args: []string{"git", "config", "key", ""},
|
|
expected: `git config key ""`,
|
|
},
|
|
{
|
|
name: "git ref expressions",
|
|
args: []string{"git", "rev-parse", "HEAD^{commit}", "main^{}"},
|
|
expected: `git rev-parse HEAD^{commit} main^{}`,
|
|
},
|
|
{
|
|
name: "wildcard refspec",
|
|
args: []string{"git", "fetch", "origin", "+refs/*:refs/*"},
|
|
expected: `git fetch origin +refs/*:refs/*`,
|
|
},
|
|
{
|
|
name: "control characters",
|
|
args: []string{"git", "config", "key", "value\x1b[0m", "line\nfeed", "tab\t", "\x7f"},
|
|
expected: `git config key "value\x1b[0m" "line\nfeed" "tab\t" "\x7f"`,
|
|
},
|
|
{
|
|
name: "unicode separators",
|
|
args: []string{"git", "config", "key", "a\u00a0b", "a\u2028b"},
|
|
expected: `git config key "a\u00a0b" "a\u2028b"`,
|
|
},
|
|
{
|
|
name: "invalid UTF-8",
|
|
args: []string{"git", "config", "key", "a\xffb"},
|
|
expected: `git config key "a\xffb"`,
|
|
},
|
|
{
|
|
name: "printable replacement character",
|
|
args: []string{"git", "config", "key", "a\ufffdb"},
|
|
expected: "git config key a\ufffdb",
|
|
},
|
|
{
|
|
name: "quotes and backslashes",
|
|
args: []string{"git", "config", "key", `a"b`, "a'b", `C:\repo`},
|
|
expected: `git config key "a\"b" "a'b" "C:\\repo"`,
|
|
},
|
|
{
|
|
name: "redacts header with equals in URL scope and value",
|
|
args: []string{
|
|
"git", "-c", "http.https://example.com/repo=a=b.extraheader=Authorization: Basic dXNlcjp0b2tlbg==", "fetch",
|
|
},
|
|
expected: `git -c http.https://example.com/repo=a=b.extraheader=xxxxx fetch`,
|
|
},
|
|
{
|
|
name: "redacts mixed-case header and equals in URL credentials",
|
|
args: []string{
|
|
"git", "-c", "http.https://user:pass=word@example.com/.ExtraHeader=Authorization: token=secret", "fetch",
|
|
},
|
|
expected: `git -c http.https://xxxxx:xxxxx@example.com/.ExtraHeader=xxxxx fetch`,
|
|
},
|
|
{
|
|
name: "redacts extraheader config only",
|
|
args: []string{
|
|
"git",
|
|
"-c", "http.https://github.com/.extraheader=Authorization: Basic dXNlcjp0b2tlbg==",
|
|
"-c", "core.abbrev=12",
|
|
"fetch",
|
|
},
|
|
expected: `git -c http.https://github.com/.extraheader=xxxxx -c core.abbrev=12 fetch`,
|
|
},
|
|
{
|
|
name: "redacts credentials in extraheader config key",
|
|
args: []string{
|
|
"git",
|
|
"-c", "http.https://user:pass@example.com/.extraheader=Authorization: Basic dXNlcjp0b2tlbg==",
|
|
"fetch",
|
|
},
|
|
expected: `git -c http.https://xxxxx:xxxxx@example.com/.extraheader=xxxxx fetch`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.expected, formatDebugCommand(tt.args))
|
|
})
|
|
}
|
|
}
|