1package gitstate
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func TestGetGitState_NotARepo(t *testing.T) {
12 tmpDir := t.TempDir()
13
14 state := GetGitState(tmpDir)
15
16 if state.IsRepo {
17 t.Error("expected IsRepo to be false for non-repo directory")
18 }
19 if state.Worktree != "" {
20 t.Errorf("expected empty Worktree, got %q", state.Worktree)
21 }
22 if state.Branch != "" {
23 t.Errorf("expected empty Branch, got %q", state.Branch)
24 }
25 if state.Commit != "" {
26 t.Errorf("expected empty Commit, got %q", state.Commit)
27 }
28}
29
30func TestGetGitState_RegularRepo(t *testing.T) {
31 tmpDir := t.TempDir()
32
33 // Initialize a git repo
34 runGit(t, tmpDir, "init")
35 runGit(t, tmpDir, "config", "user.email", "test@test.com")
36 runGit(t, tmpDir, "config", "user.name", "Test")
37
38 // Create a commit
39 testFile := filepath.Join(tmpDir, "test.txt")
40 if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil {
41 t.Fatal(err)
42 }
43 runGit(t, tmpDir, "add", ".")
44 runGit(t, tmpDir, "commit", "-m", "initial")
45
46 state := GetGitState(tmpDir)
47
48 if !state.IsRepo {
49 t.Error("expected IsRepo to be true")
50 }
51 if state.Worktree != tmpDir {
52 t.Errorf("expected Worktree %q, got %q", tmpDir, state.Worktree)
53 }
54 // Default branch might be master or main depending on git config
55 if state.Branch != "master" && state.Branch != "main" {
56 t.Errorf("expected Branch 'master' or 'main', got %q", state.Branch)
57 }
58 if state.Commit == "" {
59 t.Error("expected non-empty Commit")
60 }
61 if len(state.Commit) < 7 {
62 t.Errorf("expected short commit hash, got %q", state.Commit)
63 }
64}
65
66func TestGetGitState_Worktree(t *testing.T) {
67 tmpDir := t.TempDir()
68 mainRepo := filepath.Join(tmpDir, "main")
69 worktreeDir := filepath.Join(tmpDir, "worktree")
70
71 // Create main repo
72 if err := os.MkdirAll(mainRepo, 0o755); err != nil {
73 t.Fatal(err)
74 }
75 runGit(t, mainRepo, "init")
76 runGit(t, mainRepo, "config", "user.email", "test@test.com")
77 runGit(t, mainRepo, "config", "user.name", "Test")
78
79 // Create initial commit
80 testFile := filepath.Join(mainRepo, "test.txt")
81 if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil {
82 t.Fatal(err)
83 }
84 runGit(t, mainRepo, "add", ".")
85 runGit(t, mainRepo, "commit", "-m", "initial")
86
87 // Create a worktree on a new branch
88 runGit(t, mainRepo, "worktree", "add", "-b", "feature", worktreeDir)
89
90 // Check state in main repo
91 mainState := GetGitState(mainRepo)
92 if !mainState.IsRepo {
93 t.Error("expected main repo IsRepo to be true")
94 }
95 if mainState.Worktree != mainRepo {
96 t.Errorf("expected main Worktree %q, got %q", mainRepo, mainState.Worktree)
97 }
98
99 // Check state in worktree
100 worktreeState := GetGitState(worktreeDir)
101 if !worktreeState.IsRepo {
102 t.Error("expected worktree IsRepo to be true")
103 }
104 if worktreeState.Worktree != worktreeDir {
105 t.Errorf("expected worktree Worktree %q, got %q", worktreeDir, worktreeState.Worktree)
106 }
107 if worktreeState.Branch != "feature" {
108 t.Errorf("expected worktree Branch 'feature', got %q", worktreeState.Branch)
109 }
110
111 // Both should have the same commit (initially)
112 if mainState.Commit != worktreeState.Commit {
113 t.Errorf("expected same commit, got main=%q worktree=%q", mainState.Commit, worktreeState.Commit)
114 }
115}
116
117func TestGetGitState_DetachedHead(t *testing.T) {
118 tmpDir := t.TempDir()
119
120 // Initialize and create commits
121 runGit(t, tmpDir, "init")
122 runGit(t, tmpDir, "config", "user.email", "test@test.com")
123 runGit(t, tmpDir, "config", "user.name", "Test")
124
125 testFile := filepath.Join(tmpDir, "test.txt")
126 if err := os.WriteFile(testFile, []byte("hello"), 0o644); err != nil {
127 t.Fatal(err)
128 }
129 runGit(t, tmpDir, "add", ".")
130 runGit(t, tmpDir, "commit", "-m", "initial")
131
132 // Get the commit hash
133 commit := strings.TrimSpace(runGitOutput(t, tmpDir, "rev-parse", "HEAD"))
134
135 // Checkout to detached HEAD
136 runGit(t, tmpDir, "checkout", commit)
137
138 state := GetGitState(tmpDir)
139
140 if !state.IsRepo {
141 t.Error("expected IsRepo to be true")
142 }
143 if state.Branch != "" {
144 t.Errorf("expected empty Branch for detached HEAD, got %q", state.Branch)
145 }
146 if state.Commit == "" {
147 t.Error("expected non-empty Commit")
148 }
149}
150
151func TestGitState_Equal(t *testing.T) {
152 tests := []struct {
153 name string
154 a *GitState
155 b *GitState
156 expected bool
157 }{
158 {"both nil", nil, nil, true},
159 {"one nil", &GitState{}, nil, false},
160 {"other nil", nil, &GitState{}, false},
161 {"both empty", &GitState{}, &GitState{}, true},
162 {"same values", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, true},
163 {"different worktree", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, &GitState{Worktree: "/bar", Branch: "main", Commit: "abc123", IsRepo: true}, false},
164 {"different branch", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, &GitState{Worktree: "/foo", Branch: "dev", Commit: "abc123", IsRepo: true}, false},
165 {"different commit", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, &GitState{Worktree: "/foo", Branch: "main", Commit: "def456", IsRepo: true}, false},
166 {"different IsRepo", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: true}, &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", IsRepo: false}, false},
167 {"different subject", &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", Subject: "fix bug", IsRepo: true}, &GitState{Worktree: "/foo", Branch: "main", Commit: "abc123", Subject: "add feature", IsRepo: true}, false},
168 }
169
170 for _, tt := range tests {
171 t.Run(tt.name, func(t *testing.T) {
172 if got := tt.a.Equal(tt.b); got != tt.expected {
173 t.Errorf("Equal() = %v, want %v", got, tt.expected)
174 }
175 })
176 }
177}
178
179func TestGitState_String(t *testing.T) {
180 tests := []struct {
181 name string
182 state *GitState
183 expected string
184 }{
185 {"nil state", nil, ""},
186 {"not a repo", &GitState{IsRepo: false}, ""},
187 {"with branch", &GitState{Worktree: "/srv/myrepo", Branch: "main", Commit: "abc1234", Subject: "fix bug", IsRepo: true}, `/srv/myrepo (main) now at abc1234 "fix bug"`},
188 {"detached head", &GitState{Worktree: "/srv/myrepo", Branch: "", Commit: "abc1234", Subject: "add feature", IsRepo: true}, `/srv/myrepo (detached) now at abc1234 "add feature"`},
189 {"long subject truncated", &GitState{Worktree: "/srv/myrepo", Branch: "main", Commit: "abc1234", Subject: "this is a very long commit message that should be truncated", IsRepo: true}, `/srv/myrepo (main) now at abc1234 "this is a very long commit message that should ..."`},
190 }
191
192 for _, tt := range tests {
193 t.Run(tt.name, func(t *testing.T) {
194 if got := tt.state.String(); got != tt.expected {
195 t.Errorf("String() = %q, want %q", got, tt.expected)
196 }
197 })
198 }
199}
200
201func TestTildeReplace(t *testing.T) {
202 home, err := os.UserHomeDir()
203 if err != nil {
204 t.Skip("no home directory")
205 }
206
207 tests := []struct {
208 name string
209 path string
210 expected string
211 }{
212 {"home dir", home, "~"},
213 {"subdir of home", home + "/projects/foo", "~/projects/foo"},
214 {"not in home", "/var/log", "/var/log"},
215 {"root", "/", "/"},
216 }
217
218 for _, tt := range tests {
219 t.Run(tt.name, func(t *testing.T) {
220 if got := tildeReplace(tt.path); got != tt.expected {
221 t.Errorf("tildeReplace(%q) = %q, want %q", tt.path, got, tt.expected)
222 }
223 })
224 }
225}
226
227func runGit(t *testing.T, dir string, args ...string) {
228 t.Helper()
229 // For commits, use --no-verify to skip hooks
230 if len(args) > 0 && args[0] == "commit" {
231 newArgs := []string{"commit", "--no-verify"}
232 newArgs = append(newArgs, args[1:]...)
233 args = newArgs
234 }
235 cmd := exec.Command("git", args...)
236 cmd.Dir = dir
237 output, err := cmd.CombinedOutput()
238 if err != nil {
239 t.Fatalf("git %v failed: %v\n%s", args, err, output)
240 }
241}
242
243func runGitOutput(t *testing.T, dir string, args ...string) string {
244 t.Helper()
245 cmd := exec.Command("git", args...)
246 cmd.Dir = dir
247 output, err := cmd.Output()
248 if err != nil {
249 t.Fatalf("git %v failed: %v", args, err)
250 }
251 return string(output)
252}