package gitrepo import ( "errors" "os" "os/exec" "path/filepath" "strings" "testing" ) func TestResolveAndCheckout(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") path, err := RepoPath([]string{root}, "repo") if err != nil { t.Fatalf("RepoPath() error = %v", err) } if path != bare { t.Fatalf("RepoPath() = %q, want %q", path, bare) } rev, err := Resolve(path, "refs/heads/main", "") if err != nil { t.Fatalf("Resolve() error = %v", err) } workspace := filepath.Join(root, "workspace") if err := os.MkdirAll(workspace, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(workspace, "stale"), []byte("old"), 0o644); err != nil { t.Fatal(err) } if err := Checkout(path, rev, workspace); err != nil { t.Fatalf("Checkout() error = %v", err) } data, err := os.ReadFile(filepath.Join(workspace, "README.md")) if err != nil { t.Fatalf("checked out file missing: %v", err) } if string(data) != "hello\n" { t.Fatalf("README = %q", data) } if _, err := os.Stat(filepath.Join(workspace, "stale")); !os.IsNotExist(err) { t.Fatalf("stale file survived: %v", err) } for _, check := range []struct { args []string want string }{ {[]string{"rev-parse", "HEAD"}, rev}, {[]string{"rev-list", "--count", "HEAD"}, "1"}, {[]string{"remote"}, ""}, {[]string{"ls-files"}, "README.md"}, {[]string{"status", "--porcelain"}, ""}, } { cmd := exec.Command("git", append([]string{"-C", workspace}, check.args...)...) out, err := cmd.CombinedOutput() if err != nil || strings.TrimSpace(string(out)) != check.want { t.Fatalf("git %v = %q, %v; want %q", check.args, out, err, check.want) } } } func TestCheckoutKeepsBareRepoReadOnly(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") rev, err := Resolve(bare, "refs/heads/main", "") if err != nil { t.Fatal(err) } if err := os.Chmod(bare, 0o555); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(bare, 0o755) }) if err := Checkout(bare, rev, filepath.Join(root, "workspace")); err != nil { t.Fatalf("Checkout() against read-only repo error = %v", err) } if _, err := os.Stat(filepath.Join(bare, "index")); !os.IsNotExist(err) { t.Fatalf("checkout wrote bare repo index: %v", err) } } func TestDefaultBranchAndChangedPaths(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") ref, first, err := DefaultBranch(bare) if err != nil || ref != "refs/heads/main" || first == "" { t.Fatalf("ref=%q rev=%q err=%v", ref, first, err) } all, err := ChangedPaths(bare, "0000000000000000000000000000000000000000", first) if err != nil || len(all) != 1 || all[0] != "README.md" { t.Fatalf("initial paths=%q err=%v", all, err) } work := filepath.Join(root, "work") if err := os.MkdirAll(filepath.Join(work, "src", "nested"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(work, "src", "nested", "app.go"), []byte("package app\n"), 0o644); err != nil { t.Fatal(err) } runGit(t, work, "add", ".") runGit(t, work, "commit", "-m", "add source") runGit(t, work, "remote", "add", "origin", bare) runGit(t, work, "push", "origin", "main") second, err := Resolve(bare, "refs/heads/main", "") if err != nil { t.Fatal(err) } changed, err := ChangedPaths(bare, first, second) if err != nil || len(changed) != 1 || changed[0] != "src/nested/app.go" { t.Fatalf("changed=%q err=%v", changed, err) } workspace := filepath.Join(root, "old-revision") if err := Checkout(bare, first, workspace); err != nil { t.Fatalf("Checkout() old revision error = %v", err) } if got := strings.TrimSpace(runGit(t, workspace, "rev-parse", "HEAD")); got != first { t.Fatalf("old checkout HEAD = %q, want %q", got, first) } } func TestChangedPathsRejectsOptionLikeRevisions(t *testing.T) { if _, err := ChangedPaths(t.TempDir(), "--output=/tmp/x", strings.Repeat("a", 40)); err == nil { t.Fatal("option-like old revision accepted") } if _, err := ChangedPaths(t.TempDir(), strings.Repeat("0", 40), "--help"); err == nil { t.Fatal("option-like new revision accepted") } } func TestResolvePrefersRevAndRejectsMissingRefOrBadRev(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") rev, err := Resolve(bare, "refs/heads/missing", "main") if err != nil { t.Fatalf("Resolve rev error = %v", err) } if rev == "" { t.Fatal("empty rev") } if _, err := Resolve(bare, "", ""); err == nil { t.Fatal("empty ref/rev accepted") } if _, err := Resolve(bare, "", "missing"); err == nil { t.Fatal("bad rev accepted") } } func TestCheckoutRejectsEmptyBadRevAndBadWorkspace(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") if err := Checkout(bare, "", filepath.Join(root, "workspace")); err == nil { t.Fatal("empty rev checkout accepted") } if err := Checkout(bare, "missing", filepath.Join(root, "workspace")); err == nil { t.Fatal("bad rev checkout accepted") } file := filepath.Join(root, "file") if err := os.WriteFile(file, []byte("x"), 0o644); err != nil { t.Fatal(err) } rev, err := Resolve(bare, "refs/heads/main", "") if err != nil { t.Fatal(err) } if err := Checkout(bare, rev, filepath.Join(file, "workspace")); err == nil { t.Fatal("bad workspace accepted") } } func TestCheckoutPropagatesMkdirFailure(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "repo") rev, err := Resolve(bare, "refs/heads/main", "") if err != nil { t.Fatal(err) } boom := errors.New("boom") old := mkdirAll mkdirAll = func(string, os.FileMode) error { return boom } defer func() { mkdirAll = old }() if err := Checkout(bare, rev, filepath.Join(root, "workspace")); !errors.Is(err, boom) { t.Fatalf("err = %v", err) } } func TestRepoPathFindsPlainRepoDirAndRejectsMissing(t *testing.T) { root := t.TempDir() bare := createBareRepo(t, root, "plain") plain := filepath.Join(root, "plain") if err := os.Rename(bare, plain); err != nil { t.Fatal(err) } path, err := RepoPath([]string{root}, "plain") if err != nil { t.Fatalf("RepoPath plain error = %v", err) } if path != plain { t.Fatalf("path = %q, want %q", path, plain) } if _, err := RepoPath([]string{root}, "missing"); err == nil { t.Fatal("missing repo accepted") } } func TestRepoPathRejectsTraversal(t *testing.T) { if _, err := RepoPath([]string{t.TempDir()}, "../repo"); err == nil { t.Fatal("traversal repo accepted") } } func createBareRepo(t *testing.T, root string, name string) string { t.Helper() work := filepath.Join(root, "work") runGit(t, root, "init", work) runGit(t, work, "config", "user.email", "test@example.invalid") runGit(t, work, "config", "user.name", "Test") if err := os.WriteFile(filepath.Join(work, "README.md"), []byte("hello\n"), 0o644); err != nil { t.Fatal(err) } runGit(t, work, "add", "README.md") runGit(t, work, "commit", "-m", "initial") runGit(t, work, "branch", "-M", "main") bare := filepath.Join(root, name+".git") runGit(t, root, "clone", "--bare", work, bare) return bare } func runGit(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v failed: %v: %s", args, err, out) } return string(out) }