Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/gitx/git_test.go

83aa601b98007ead72afeadb131a93d5694e04ca · 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 }