package gitx import ( "context" "errors" "strings" "testing" "sourcecraft.dev/bigbes/sr-ht-compare/core" ) func ctx() context.Context { return context.Background() } func TestOpen(t *testing.T) { reposRoot := newFixtureRepo(t) t.Run("valid", func(t *testing.T) { repo, err := Open(reposRoot, fxOwner, fxName) if err != nil { t.Fatalf("Open: %v", err) } if !strings.HasSuffix(repo.Dir(), "~alice/demo") { t.Errorf("dir = %q", repo.Dir()) } }) // Every hostile input must fail with ErrNotFound and never escape reposRoot. traversal := []struct { name string owner, repo string }{ {"dotdot owner", "..", "demo"}, {"tilde owner", "~evil", "demo"}, {"slash in repo", "alice", "../../etc"}, {"dotdot repo", "alice", ".."}, {"absolute-ish repo", "alice", "/etc/passwd"}, {"leading dash owner", "-x", "demo"}, {"missing repo", "alice", "nonexistent"}, {"missing owner", "bob", "demo"}, } for _, tt := range traversal { t.Run(tt.name, func(t *testing.T) { _, err := Open(reposRoot, tt.owner, tt.repo) if err == nil { t.Fatalf("Open(%q,%q) succeeded, want error", tt.owner, tt.repo) } if !errors.Is(err, core.ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } }) } } func TestRefs(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) branches, tags, err := repo.Refs(ctx()) if err != nil { t.Fatalf("Refs: %v", err) } if len(branches) != 2 { t.Fatalf("branches = %v, want 2", branches) } if branches[0].Name != "main" { t.Errorf("branches[0] = %q, want HEAD branch main first", branches[0].Name) } if branches[1].Name != "feature/with-slash" { t.Errorf("branches[1] = %q, want feature/with-slash", branches[1].Name) } if branches[0].SHA != fxRev(t, reposRoot, "main") { t.Errorf("main SHA = %q, want %q", branches[0].SHA, fxRev(t, reposRoot, "main")) } if len(tags) != 2 { t.Fatalf("tags = %v, want 2", tags) } // Newest creator-date first: v1.0.0 was tagged after v0.9.0. if tags[0].Name != "v1.0.0" || tags[1].Name != "v0.9.0" { t.Errorf("tags order = [%q %q], want [v1.0.0 v0.9.0]", tags[0].Name, tags[1].Name) } def, err := repo.DefaultBranch(ctx()) if err != nil { t.Fatalf("DefaultBranch: %v", err) } if def != "main" { t.Errorf("DefaultBranch = %q, want main", def) } } func TestResolveCommit(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) mainSHA := fxRev(t, reposRoot, "main") rootSHA := fxRev(t, reposRoot, "main^1~3") t.Run("branch", func(t *testing.T) { ci, err := repo.ResolveCommit(ctx(), "main") if err != nil { t.Fatalf("ResolveCommit(main): %v", err) } if ci.SHA != mainSHA { t.Errorf("SHA = %q, want %q", ci.SHA, mainSHA) } if len(ci.ParentSHAs) != 2 { t.Errorf("main is a merge, parents = %v", ci.ParentSHAs) } if ci.AuthorName != "Alice Example" { t.Errorf("author = %q", ci.AuthorName) } }) t.Run("annotated tag peels to commit", func(t *testing.T) { ci, err := repo.ResolveCommit(ctx(), "v1.0.0") if err != nil { t.Fatalf("ResolveCommit(v1.0.0): %v", err) } if ci.SHA != mainSHA { t.Errorf("SHA = %q, want %q", ci.SHA, mainSHA) } }) t.Run("full SHA", func(t *testing.T) { ci, err := repo.ResolveCommit(ctx(), rootSHA) if err != nil { t.Fatalf("ResolveCommit(root SHA): %v", err) } if ci.SHA != rootSHA { t.Errorf("SHA = %q, want %q", ci.SHA, rootSHA) } if len(ci.ParentSHAs) != 0 { t.Errorf("root parents = %v, want none", ci.ParentSHAs) } }) t.Run("branch with slash", func(t *testing.T) { ci, err := repo.ResolveCommit(ctx(), "feature/with-slash") if err != nil { t.Fatalf("ResolveCommit(feature/with-slash): %v", err) } if ci.SHA != fxRev(t, reposRoot, "feature/with-slash") { t.Errorf("SHA = %q", ci.SHA) } }) t.Run("nonexistent ref is ErrBadRef", func(t *testing.T) { _, err := repo.ResolveCommit(ctx(), "does-not-exist") if !errors.Is(err, core.ErrBadRef) { t.Errorf("err = %v, want ErrBadRef", err) } }) t.Run("malformed ref is ErrBadRef", func(t *testing.T) { _, err := repo.ResolveCommit(ctx(), "@") if !errors.Is(err, core.ErrBadRef) { t.Errorf("err = %v, want ErrBadRef", err) } }) } func TestLog(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) base := fxRev(t, reposRoot, "main^1~2") // c2 head := fxRev(t, reposRoot, "main^1") // binary commit commits, err := repo.Log(ctx(), base, head, 10) if err != nil { t.Fatalf("Log: %v", err) } if len(commits) != 2 { t.Fatalf("commits = %d, want 2 (rename, binary)", len(commits)) } // Newest first. if !strings.HasPrefix(commits[0].Subject, "binary") { t.Errorf("commits[0] = %q, want binary", commits[0].Subject) } if !strings.HasPrefix(commits[1].Subject, "rename") { t.Errorf("commits[1] = %q, want rename", commits[1].Subject) } t.Run("limit", func(t *testing.T) { commits, err := repo.Log(ctx(), base, head, 1) if err != nil { t.Fatalf("Log: %v", err) } if len(commits) != 1 { t.Errorf("commits = %d, want 1", len(commits)) } }) } func TestDiffThreeDotVsTwoDot(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) // base = binary commit (has rename + binary on its side), head = feature. // Their merge base is c2. Three-dot must show only head-side changes // (feature.txt); two-dot compares the endpoints directly and surfaces the // base-only files. base := fxRev(t, reposRoot, "main^1") head := "feature/with-slash" // slash branch name passes through verbatim three, err := repo.Diff(ctx(), core.CompareSpec{Base: base, Head: head, ThreeDot: true}) if err != nil { t.Fatalf("Diff three-dot: %v", err) } if !strings.Contains(three.Text, "feature.txt") { t.Errorf("three-dot diff missing feature.txt") } if strings.Contains(three.Text, "bin.dat") { t.Errorf("three-dot diff should NOT contain base-only bin.dat") } two, err := repo.Diff(ctx(), core.CompareSpec{Base: base, Head: head, ThreeDot: false}) if err != nil { t.Fatalf("Diff two-dot: %v", err) } if !strings.Contains(two.Text, "bin.dat") { t.Errorf("two-dot diff should surface base-side bin.dat") } } func TestDiffStat(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) // c2 -> binary: a.txt renamed to a-renamed.txt, bin.dat added (binary). spec := core.CompareSpec{ Base: fxRev(t, reposRoot, "main^1~2"), Head: fxRev(t, reposRoot, "main^1"), } files, err := repo.DiffStat(ctx(), spec) if err != nil { t.Fatalf("DiffStat: %v", err) } byPath := map[string]FileChange{} for _, f := range files { byPath[f.Path] = f } ren, ok := byPath["a-renamed.txt"] if !ok { t.Fatalf("missing rename entry; files = %+v", files) } if ren.Status != "R" || ren.OldPath != "a.txt" { t.Errorf("rename = %+v, want R from a.txt", ren) } bin, ok := byPath["bin.dat"] if !ok { t.Fatalf("missing bin.dat; files = %+v", files) } if bin.Status != "A" || !bin.Binary { t.Errorf("bin.dat = %+v, want A binary", bin) } if bin.Additions != 0 || bin.Deletions != 0 { t.Errorf("binary counts = %d/%d, want 0/0", bin.Additions, bin.Deletions) } } func TestTruncation(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) spec := core.CompareSpec{Base: fxRev(t, reposRoot, "main^1~3"), Head: "main", ThreeDot: true} full, err := repo.Diff(ctx(), spec) if err != nil { t.Fatalf("Diff (uncapped): %v", err) } if full.Truncated { t.Fatalf("baseline diff unexpectedly truncated") } if len(full.Text) <= 32 { t.Fatalf("fixture diff too small (%d bytes) to test truncation", len(full.Text)) } repo.limitOverride = 32 capped, err := repo.Diff(ctx(), spec) if err != nil { t.Fatalf("Diff (capped): %v", err) } if !capped.Truncated { t.Errorf("capped diff not marked truncated") } if len(capped.Text) > 32 { t.Errorf("capped diff = %d bytes, want <= 32", len(capped.Text)) } } func TestMergeBase(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) got, err := repo.MergeBase(ctx(), fxRev(t, reposRoot, "main^1"), "feature/with-slash") if err != nil { t.Fatalf("MergeBase: %v", err) } want := fxRev(t, reposRoot, "main^1~2") // c2 if got != want { t.Errorf("MergeBase = %q, want %q (c2)", got, want) } } func TestParents(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) t.Run("merge", func(t *testing.T) { parents, err := repo.Parents(ctx(), "main") if err != nil { t.Fatalf("Parents: %v", err) } if len(parents) != 2 { t.Errorf("parents = %v, want 2", parents) } }) t.Run("root", func(t *testing.T) { parents, err := repo.Parents(ctx(), fxRev(t, reposRoot, "main^1~3")) if err != nil { t.Fatalf("Parents: %v", err) } if len(parents) != 0 { t.Errorf("root parents = %v, want none", parents) } }) } func TestCommitPatch(t *testing.T) { reposRoot := newFixtureRepo(t) repo := openFixture(t, reposRoot) t.Run("root commit", func(t *testing.T) { root := fxRev(t, reposRoot, "main^1~3") patch, files, ci, err := repo.CommitPatch(ctx(), root) if err != nil { t.Fatalf("CommitPatch(root): %v", err) } if ci.SHA != root || len(ci.ParentSHAs) != 0 { t.Errorf("ci = %+v, want root with no parents", ci) } if !strings.Contains(patch.Text, "a.txt") { t.Errorf("root patch missing a.txt") } if len(files) != 1 || files[0].Path != "a.txt" || files[0].Status != "A" { t.Errorf("root files = %+v, want single A a.txt", files) } }) t.Run("binary commit", func(t *testing.T) { bin := fxRev(t, reposRoot, "main^1") patch, files, _, err := repo.CommitPatch(ctx(), bin) if err != nil { t.Fatalf("CommitPatch(binary): %v", err) } if len(files) != 1 || files[0].Path != "bin.dat" || !files[0].Binary { t.Errorf("binary commit files = %+v, want single binary bin.dat", files) } if !strings.Contains(patch.Text, "bin.dat") { t.Errorf("binary patch missing bin.dat header") } }) t.Run("merge commit diffs against first parent", func(t *testing.T) { patch, files, ci, err := repo.CommitPatch(ctx(), "main") if err != nil { t.Fatalf("CommitPatch(merge): %v", err) } if len(ci.ParentSHAs) != 2 { t.Fatalf("expected merge commit, parents = %v", ci.ParentSHAs) } // vs first parent (binary commit), the merge brings in feature.txt. if len(files) != 1 || files[0].Path != "feature.txt" { t.Errorf("merge files = %+v, want feature.txt", files) } if !strings.Contains(patch.Text, "feature.txt") { t.Errorf("merge patch missing feature.txt (diff-tree -p on a merge is empty; must diff vs parent)") } }) }