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 ") { t.Errorf("GIT_COMMITTER_IDENT = %q, want it to contain TestUser ", 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) } }