gitstate_test.go

  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}