Files
simplegit/gitcmd/command_test.go
T
2026-07-15 11:06:48 -04:00

364 lines
9.0 KiB
Go

package gitcmd
import (
"bytes"
"context"
"crypto/sha1"
"fmt"
"io"
"os/exec"
"strings"
"testing"
"time"
)
func TestVersion(t *testing.T) {
ver, err := Version(context.Background())
if err != nil {
t.Fatal(err)
}
if ver == "" {
t.Error("expected version string, got empty")
}
t.Logf("git version: %s", ver)
}
func TestIsSafeValue(t *testing.T) {
cases := []struct {
arg string
want bool
}{
{"", true},
{"HEAD", true},
{"main", true},
{"file.txt", true},
{"-rf", false},
{"--evil", false},
{"--exec=foo", false},
}
for _, tc := range cases {
if got := isSafeValue(tc.arg); got != tc.want {
t.Errorf("isSafeValue(%q) = %v, want %v", tc.arg, got, tc.want)
}
}
}
func TestIsTrustedArg(t *testing.T) {
cases := []struct {
arg string
want bool
}{
{"log", true},
{"", true},
{"--pretty", true},
{"--format=%H%x1f%h", true},
{"--format=%(refname:short)", true},
{"foo(bar)", true},
{"HEAD", true},
{"foo;bar", false},
{"foo`bar", false},
{"$(evil)", false},
{"foo|bar", false},
{"foo&bar", false},
{"foo\nbar", false},
{"foo\rbar", false},
}
for _, tc := range cases {
if got := isTrustedArg(tc.arg); got != tc.want {
t.Errorf("isTrustedArg(%q) = %v, want %v", tc.arg, got, tc.want)
}
}
}
func TestCommandRejectsDynamicOption(t *testing.T) {
_, _, err := NewCommand("show").
AddDynamicArguments("--evil-option").
RunStdString(context.Background())
if err == nil {
t.Fatal("expected error for dynamic argument starting with '-', got nil")
}
if !strings.Contains(err.Error(), "cannot start with '-'") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestCommandDashesAndList(t *testing.T) {
cmd := NewCommand("ls-tree", "HEAD").AddDashesAndList("foo", "bar")
if len(cmd.args) != 5 {
t.Fatalf("expected 5 args, got %d: %v", len(cmd.args), cmd.args)
}
if cmd.args[2] != "--" {
t.Errorf("expected '--' at index 2, got %q", cmd.args[2])
}
}
func TestCommandAddDashesAndListKeepsLeadingDash(t *testing.T) {
cmd := NewCommand("ls-tree", "HEAD").AddDashesAndList("-rf", "bar")
want := []string{"ls-tree", "HEAD", "--", "-rf", "bar"}
if len(cmd.args) != len(want) {
t.Fatalf("args = %v, want %v", cmd.args, want)
}
for i := range want {
if cmd.args[i] != want[i] {
t.Errorf("args[%d] = %q, want %q", i, cmd.args[i], want[i])
}
}
}
func TestCommandAdd(t *testing.T) {
c := NewCommand("log").Add("--format=%H", "HEAD")
want := []string{"log", "--format=%H", "HEAD"}
if len(c.args) != len(want) {
t.Fatalf("args = %v, want %v", c.args, want)
}
for i := range want {
if c.args[i] != want[i] {
t.Errorf("args[%d] = %q, want %q", i, c.args[i], want[i])
}
}
if len(c.preErrs) != 0 {
t.Errorf("unexpected preErrs: %v", c.preErrs)
}
}
func TestCommandAddShortCircuitsOnPreErr(t *testing.T) {
c := NewCommand("log")
c.AddDynamicArguments("--bad")
c.Add("--format=%H")
if len(c.args) != 1 || c.args[0] != "log" {
t.Errorf("args = %v, want [log]", c.args)
}
if len(c.preErrs) == 0 {
t.Error("expected a preErr to be recorded")
}
}
func TestCommandAddOptionValues(t *testing.T) {
c := NewCommand("show").AddOptionValues("--git-dir", "/some/path")
want := []string{"show", "--git-dir", "/some/path"}
if len(c.args) != len(want) {
t.Fatalf("args = %v, want %v", c.args, want)
}
for i := range want {
if c.args[i] != want[i] {
t.Errorf("args[%d] = %q, want %q", i, c.args[i], want[i])
}
}
c2 := NewCommand("show").AddOptionValues("git-dir", "v")
if len(c2.args) != 1 || len(c2.preErrs) == 0 {
t.Errorf("invalid option: args=%v preErrs=%v", c2.args, c2.preErrs)
}
c3 := NewCommand("show").AddOptionValues("--git-dir", "-x")
if len(c3.args) != 2 || len(c3.preErrs) == 0 {
t.Errorf("leading-dash value: args=%v preErrs=%v", c3.args, c3.preErrs)
}
if c3.args[1] != "--git-dir" {
t.Errorf("expected --git-dir appended, got args=%v", c3.args)
}
}
func TestCommandPreErrsAccumulate(t *testing.T) {
c := NewCommand("show", "foo|bar")
c.AddDynamicArguments("--evil")
_, _, err := c.RunStdBytes(context.Background())
if err == nil {
t.Fatal("expected joined preErr, got nil")
}
if !strings.Contains(err.Error(), "foo|bar") {
t.Errorf("err should mention foo|bar: %v", err)
}
if !strings.Contains(err.Error(), "--evil") {
t.Errorf("err should mention --evil: %v", err)
}
}
func TestCommandWithDir(t *testing.T) {
f := newFixture(t)
out, _, err := NewCommand("rev-parse", "--is-bare-repository").
WithDir(f.repo.Path).
RunStdString(context.Background())
if err != nil {
t.Fatal(err)
}
if got := strings.TrimSpace(out); got != "true" {
t.Errorf("is-bare-repository = %q, want true", got)
}
}
func TestCommandWithEnv(t *testing.T) {
out, _, err := NewCommand("var", "GIT_COMMITTER_IDENT").
WithEnv("GIT_COMMITTER_NAME=TestUser", "GIT_COMMITTER_EMAIL=test@example.com").
RunStdString(context.Background())
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "TestUser <test@example.com>") {
t.Errorf("GIT_COMMITTER_IDENT = %q, want it to contain TestUser <test@example.com>", out)
}
}
func TestCommandWithStdin(t *testing.T) {
content := "hello world\n"
out, _, err := NewCommand("hash-object", "--stdin").
WithStdin(strings.NewReader(content)).
RunStdString(context.Background())
if err != nil {
t.Fatal(err)
}
got := strings.TrimSpace(out)
h := sha1.New()
fmt.Fprintf(h, "blob %d\x00", len(content))
h.Write([]byte(content))
want := fmt.Sprintf("%x", h.Sum(nil))
if len(got) != 40 {
t.Errorf("hash-object output len = %d, want 40: %q", len(got), got)
}
if got != want {
t.Errorf("hash-object = %q, want %q", got, want)
}
}
func TestCommandWithTimeout(t *testing.T) {
out, _, err := NewCommand("version").WithTimeout(30 * time.Second).RunStdString(context.Background())
if err != nil {
t.Fatalf("generous timeout: %v", err)
}
if !strings.Contains(out, "git version") {
t.Errorf("version output = %q", out)
}
_, _, err = NewCommand("version").WithTimeout(1 * time.Nanosecond).RunStdBytes(context.Background())
if err == nil {
t.Error("expected error from expired timeout, got nil")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err = NewCommand("version").RunStdBytes(ctx)
if err == nil {
t.Error("expected error from cancelled context, got nil")
}
}
func TestCommandCmdString(t *testing.T) {
s := NewCommand("log", "--format=%H").CmdString()
if !strings.Contains(s, "log") || !strings.Contains(s, "--format=%H") {
t.Errorf("CmdString = %q", s)
}
s2 := NewCommand("ls-tree").AddDashesAndList("my file.txt").CmdString()
if !strings.Contains(s2, `"my file.txt"`) {
t.Errorf("expected quoted 'my file.txt' in %q", s2)
}
}
func TestCommandRunStdStringAndFailing(t *testing.T) {
out, stderr, err := NewCommand("version").RunStdString(context.Background())
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "git version") {
t.Errorf("stdout = %q", out)
}
if stderr != "" {
t.Errorf("stderr = %q, want empty", stderr)
}
_, stderr2, err := NewCommand("--bogus-flag").RunStdString(context.Background())
if err == nil {
t.Fatal("expected error from bogus flag")
}
if stderr2 == "" {
t.Error("expected non-empty stderr from bogus flag")
}
}
func TestCommandRun(t *testing.T) {
if err := NewCommand("version").Run(context.Background()); err != nil {
t.Fatalf("Run: %v", err)
}
}
func TestCommandRunStdBytes(t *testing.T) {
out, _, err := NewCommand("version").RunStdBytes(context.Background())
if err != nil {
t.Fatal(err)
}
if len(out) == 0 {
t.Error("expected non-empty stdout")
}
}
func TestCommandRunPiped(t *testing.T) {
var buf bytes.Buffer
if err := NewCommand("version").RunPiped(context.Background(), &buf, io.Discard); err != nil {
t.Fatalf("RunPiped: %v", err)
}
if !strings.Contains(buf.String(), "git version") {
t.Errorf("piped stdout = %q", buf.String())
}
}
func TestCommandAddRecordsPreErr(t *testing.T) {
c := NewCommand("log").Add("foo;bar")
if len(c.args) != 1 || c.args[0] != "log" {
t.Errorf("args = %v, want [log]", c.args)
}
if len(c.preErrs) != 1 {
t.Errorf("preErrs = %v, want exactly 1", c.preErrs)
}
}
func TestCommandRunPipedPreErr(t *testing.T) {
var buf bytes.Buffer
err := NewCommand("show").AddDynamicArguments("--bad").
RunPiped(context.Background(), &buf, io.Discard)
if err == nil {
t.Fatal("expected preErr from RunPiped, got nil")
}
if buf.Len() != 0 {
t.Errorf("piped buffer = %q, want empty (command must not run)", buf.String())
}
}
func TestExecutablePresent(t *testing.T) {
exe, err := Executable()
if err != nil {
t.Fatalf("Executable: %v", err)
}
if exe == "" {
t.Error("Executable returned empty path")
}
}
func TestSetExecutableHonored(t *testing.T) {
real, err := exec.LookPath("git")
if err != nil {
t.Skip("git not found")
}
SetExecutable(real)
exe, err := Executable()
if err != nil {
t.Fatalf("Executable: %v", err)
}
if exe != real {
t.Errorf("Executable = %q, want %q", exe, real)
}
if _, _, err := NewCommand("version").RunStdString(context.Background()); err != nil {
t.Errorf("version after SetExecutable: %v", err)
}
}