Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/gitx/git_test.go
cb5a7c7c656de5a381e0234208c552c0856e43e3
· 5.5 KB · 157 lines
raw
| 1 | package gitx |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "os/exec" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // runGit drives git directly (not through Repo.run, which doesn't exist |
| 10 | // yet at setup time) with the same kind of isolation Repo.run uses -- |
| 11 | // GIT_CONFIG_NOSYSTEM and a throwaway HOME -- so the test behaves the same |
| 12 | // on a machine with commit.gpgsign=true globally as it does in CI. |
| 13 | func runGit(t *testing.T, dir string, args ...string) { |
| 14 | t.Helper() |
| 15 | cmd := exec.Command("git", append([]string{"-C", dir, "-c", "commit.gpgsign=false"}, args...)...) |
| 16 | cmd.Env = []string{ |
| 17 | "PATH=" + os.Getenv("PATH"), |
| 18 | "HOME=" + dir, |
| 19 | "GIT_CONFIG_NOSYSTEM=1", |
| 20 | "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.com", |
| 21 | "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.com", |
| 22 | } |
| 23 | if out, err := cmd.CombinedOutput(); err != nil { |
| 24 | t.Fatalf("git %v: %v\n%s", args, err, out) |
| 25 | } |
| 26 | } |
| 27 | |
| 28 | // newTestRepo builds a real repository with one empty commit, a branch |
| 29 | // "feature/x" (a branch name containing a slash), a second branch |
| 30 | // "release", and a tag "release/1.0". The last two coexist by construction |
| 31 | // deliberately: refs/heads and refs/tags are separate namespaces, so |
| 32 | // "release" (a branch) and "release/1.0" (a tag) can both exist even |
| 33 | // though a branch "release" and a branch "release/1.0" could not -- git |
| 34 | // refuses that, since a ref name is a path and "release" can't be both a |
| 35 | // file and a directory. Opened through gitx.Open, because SplitRefPath |
| 36 | // resolves against Refs(), which shells out to `git for-each-ref` -- there |
| 37 | // is no faking that, it has to be a real repo with real refs. |
| 38 | func newTestRepo(t *testing.T) *Repo { |
| 39 | t.Helper() |
| 40 | dir := t.TempDir() |
| 41 | runGit(t, dir, "init", "-q", "-b", "main") |
| 42 | runGit(t, dir, "commit", "-q", "--allow-empty", "-m", "init") |
| 43 | runGit(t, dir, "branch", "feature/x") |
| 44 | runGit(t, dir, "branch", "release") |
| 45 | runGit(t, dir, "tag", "release/1.0") |
| 46 | |
| 47 | r, err := Open(dir) |
| 48 | if err != nil { |
| 49 | t.Fatalf("Open: %v", err) |
| 50 | } |
| 51 | return r |
| 52 | } |
| 53 | |
| 54 | // TestSplitRefPath guards the longest-prefix-wins rule that lets ref names |
| 55 | // contain slashes. A naive split at the first "/" gets both of the two |
| 56 | // interesting cases here wrong: it would send "feature/x/src/main.go" to |
| 57 | // ref "feature" (not a ref at all) instead of branch "feature/x", and it |
| 58 | // would send "release/1.0/docs/readme.md" to branch "release" instead of |
| 59 | // the more specific tag "release/1.0" -- the real ambiguity SplitRefPath |
| 60 | // exists to resolve, since both "release" and "release/1.0" are valid refs |
| 61 | // here and only the longest match is the intended one. |
| 62 | func TestSplitRefPath(t *testing.T) { |
| 63 | r := newTestRepo(t) |
| 64 | |
| 65 | cases := []struct { |
| 66 | name string |
| 67 | rest string |
| 68 | wantRef string |
| 69 | wantPath string |
| 70 | }{ |
| 71 | {"single-segment branch with a path", "main/src/main.go", "main", "src/main.go"}, |
| 72 | {"slashed branch name resolves in full, not just its first segment", |
| 73 | "feature/x/src/main.go", "feature/x", "src/main.go"}, |
| 74 | {"slashed tag wins over a shorter branch that's also a valid prefix", |
| 75 | "release/1.0/docs/readme.md", "release/1.0", "docs/readme.md"}, |
| 76 | {"shorter branch still resolves when no longer candidate matches", |
| 77 | "release/README.md", "release", "README.md"}, |
| 78 | {"slashed branch with no path", "feature/x", "feature/x", ""}, |
| 79 | {"empty rest falls back to HEAD", "", "main", ""}, |
| 80 | {"no ref matches: whole thing is a path at HEAD", |
| 81 | "no/such/branch/file.go", "main", "no/such/branch/file.go"}, |
| 82 | } |
| 83 | |
| 84 | for _, tc := range cases { |
| 85 | t.Run(tc.name, func(t *testing.T) { |
| 86 | ref, path := r.SplitRefPath(tc.rest) |
| 87 | if ref != tc.wantRef || path != tc.wantPath { |
| 88 | t.Errorf("SplitRefPath(%q) = (%q, %q), want (%q, %q)", |
| 89 | tc.rest, ref, path, tc.wantRef, tc.wantPath) |
| 90 | } |
| 91 | }) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // TestIsAncestor builds its own repo (rather than reusing newTestRepo, |
| 96 | // whose branches all point at the same single commit) because it needs a |
| 97 | // real linear ancestor/descendant pair plus a genuinely diverged pair: |
| 98 | // "main" advances past the point where "other" branched off, then "other" |
| 99 | // advances on its own, so main's tip and other's tip share c1 as a common |
| 100 | // ancestor but are not ancestors of each other. |
| 101 | func TestIsAncestor(t *testing.T) { |
| 102 | dir := t.TempDir() |
| 103 | runGit(t, dir, "init", "-q", "-b", "main") |
| 104 | runGit(t, dir, "commit", "-q", "--allow-empty", "-m", "c1") |
| 105 | |
| 106 | r, err := Open(dir) |
| 107 | if err != nil { |
| 108 | t.Fatalf("Open: %v", err) |
| 109 | } |
| 110 | c1, err := r.Resolve("main") |
| 111 | if err != nil { |
| 112 | t.Fatalf("Resolve(main): %v", err) |
| 113 | } |
| 114 | |
| 115 | runGit(t, dir, "branch", "other") |
| 116 | runGit(t, dir, "commit", "-q", "--allow-empty", "-m", "c2") |
| 117 | c2, err := r.Resolve("main") |
| 118 | if err != nil { |
| 119 | t.Fatalf("Resolve(main): %v", err) |
| 120 | } |
| 121 | |
| 122 | runGit(t, dir, "checkout", "-q", "other") |
| 123 | runGit(t, dir, "commit", "-q", "--allow-empty", "-m", "c3-diverged") |
| 124 | c3, err := r.Resolve("other") |
| 125 | if err != nil { |
| 126 | t.Fatalf("Resolve(other): %v", err) |
| 127 | } |
| 128 | |
| 129 | cases := []struct { |
| 130 | name string |
| 131 | a, b string |
| 132 | want bool |
| 133 | }{ |
| 134 | {"strict ancestor", c1, c2, true}, |
| 135 | {"reversed order is not an ancestor relationship", c2, c1, false}, |
| 136 | {"a commit is its own ancestor", c1, c1, true}, |
| 137 | {"diverged commits are not ancestors either direction", c2, c3, false}, |
| 138 | {"diverged commits, reverse direction", c3, c2, false}, |
| 139 | } |
| 140 | for _, tc := range cases { |
| 141 | t.Run(tc.name, func(t *testing.T) { |
| 142 | got, err := r.IsAncestor(tc.a, tc.b) |
| 143 | if err != nil { |
| 144 | t.Fatalf("IsAncestor(%q, %q): %v", tc.a, tc.b, err) |
| 145 | } |
| 146 | if got != tc.want { |
| 147 | t.Errorf("IsAncestor(%q, %q) = %v, want %v", tc.a, tc.b, got, tc.want) |
| 148 | } |
| 149 | }) |
| 150 | } |
| 151 | |
| 152 | t.Run("bogus revision is an error, not false", func(t *testing.T) { |
| 153 | if _, err := r.IsAncestor("deadbeef", c1); err == nil { |
| 154 | t.Error("IsAncestor with a bogus revision: got nil error, want non-nil") |
| 155 | } |
| 156 | }) |
| 157 | } |