blob: 665be50c74453ddcff4f928b4a6dd86461ddb0ae [file] [log] [blame]
Philip Zeyligerd3ac1122025-05-14 02:54:18 +00001package git_tools
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11func setupTestRepo(t *testing.T) string {
12 // Create a temporary directory for the test repository
13 tempDir, err := os.MkdirTemp("", "git-tools-test")
14 if err != nil {
15 t.Fatalf("Failed to create temp directory: %v", err)
16 }
17
18 // Initialize a git repository
19 cmd := exec.Command("git", "-C", tempDir, "init")
20 if out, err := cmd.CombinedOutput(); err != nil {
21 t.Fatalf("Failed to initialize git repo: %v - %s", err, out)
22 }
23
24 // Configure git user
25 cmd = exec.Command("git", "-C", tempDir, "config", "user.email", "test@example.com")
26 if out, err := cmd.CombinedOutput(); err != nil {
27 t.Fatalf("Failed to configure git user email: %v - %s", err, out)
28 }
29
30 cmd = exec.Command("git", "-C", tempDir, "config", "user.name", "Test User")
31 if out, err := cmd.CombinedOutput(); err != nil {
32 t.Fatalf("Failed to configure git user name: %v - %s", err, out)
33 }
34
35 return tempDir
36}
37
38func createAndCommitFile(t *testing.T, repoDir, filename, content string, stage bool) string {
39 filePath := filepath.Join(repoDir, filename)
Autoformatter8c463622025-05-16 21:54:17 +000040 if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
Philip Zeyligerd3ac1122025-05-14 02:54:18 +000041 t.Fatalf("Failed to write file: %v", err)
42 }
43
44 if stage {
45 cmd := exec.Command("git", "-C", repoDir, "add", filename)
46 if out, err := cmd.CombinedOutput(); err != nil {
47 t.Fatalf("Failed to add file: %v - %s", err, out)
48 }
49
50 cmd = exec.Command("git", "-C", repoDir, "commit", "-m", "Add "+filename)
51 if out, err := cmd.CombinedOutput(); err != nil {
52 t.Fatalf("Failed to commit file: %v - %s", err, out)
53 }
54
55 // Get the commit hash
56 cmd = exec.Command("git", "-C", repoDir, "rev-parse", "HEAD")
57 out, err := cmd.Output()
58 if err != nil {
59 t.Fatalf("Failed to get commit hash: %v", err)
60 }
61 return string(out[:len(out)-1]) // Trim newline
62 }
63
64 return ""
65}
66
67func TestGitRawDiff(t *testing.T) {
68 repoDir := setupTestRepo(t)
69 defer os.RemoveAll(repoDir)
70
71 // Create initial file
72 initHash := createAndCommitFile(t, repoDir, "test.txt", "initial content\n", true)
73
74 // Modify the file
75 modHash := createAndCommitFile(t, repoDir, "test.txt", "initial content\nmodified content\n", true)
76
77 // Test the diff between the two commits
78 diff, err := GitRawDiff(repoDir, initHash, modHash)
79 if err != nil {
80 t.Fatalf("GitRawDiff failed: %v", err)
81 }
82
83 if len(diff) != 1 {
84 t.Fatalf("Expected 1 file in diff, got %d", len(diff))
85 }
86
87 if diff[0].Path != "test.txt" {
88 t.Errorf("Expected path to be test.txt, got %s", diff[0].Path)
89 }
90
91 if diff[0].Status != "M" {
92 t.Errorf("Expected status to be M (modified), got %s", diff[0].Status)
93 }
94
95 if diff[0].OldMode == "" || diff[0].NewMode == "" {
96 t.Error("Expected file modes to be present")
97 }
98
99 if diff[0].OldHash == "" || diff[0].NewHash == "" {
100 t.Error("Expected file hashes to be present")
101 }
102
103 // Test with invalid commit hash
104 _, err = GitRawDiff(repoDir, "invalid", modHash)
105 if err == nil {
106 t.Error("Expected error for invalid commit hash, got none")
107 }
108}
109
110func TestGitShow(t *testing.T) {
111 repoDir := setupTestRepo(t)
112 defer os.RemoveAll(repoDir)
113
114 // Create file and commit
115 commitHash := createAndCommitFile(t, repoDir, "test.txt", "test content\n", true)
116
117 // Test GitShow
118 show, err := GitShow(repoDir, commitHash)
119 if err != nil {
120 t.Fatalf("GitShow failed: %v", err)
121 }
122
123 if show == "" {
124 t.Error("Expected non-empty output from GitShow")
125 }
126
127 // Test with invalid commit hash
128 _, err = GitShow(repoDir, "invalid")
129 if err == nil {
130 t.Error("Expected error for invalid commit hash, got none")
131 }
132}
133
134func TestParseGitLog(t *testing.T) {
135 // Test with the format from --pretty="%H%x00%s%x00%d"
136 logOutput := "abc123\x00Initial commit\x00 (HEAD -> main, origin/main)\n" +
137 "def456\x00Add feature X\x00 (tag: v1.0.0)\n" +
138 "ghi789\x00Fix bug Y\x00"
139
140 entries, err := parseGitLog(logOutput)
141 if err != nil {
142 t.Fatalf("parseGitLog returned error: %v", err)
143 }
144
145 if len(entries) != 3 {
146 t.Fatalf("Expected 3 log entries, got %d", len(entries))
147 }
148
149 // Check first entry
150 if entries[0].Hash != "abc123" {
151 t.Errorf("Expected hash abc123, got %s", entries[0].Hash)
152 }
153 if len(entries[0].Refs) != 2 {
154 t.Errorf("Expected 2 refs, got %d", len(entries[0].Refs))
155 }
156 if entries[0].Refs[0] != "main" || entries[0].Refs[1] != "origin/main" {
157 t.Errorf("Incorrect refs parsed: %v", entries[0].Refs)
158 }
159 if entries[0].Subject != "Initial commit" {
160 t.Errorf("Expected subject 'Initial commit', got '%s'", entries[0].Subject)
161 }
162
163 // Check second entry
164 if entries[1].Hash != "def456" {
165 t.Errorf("Expected hash def456, got %s", entries[1].Hash)
166 }
167 if len(entries[1].Refs) != 1 {
168 t.Errorf("Expected 1 ref, got %d", len(entries[1].Refs))
169 }
170 if entries[1].Refs[0] != "v1.0.0" {
171 t.Errorf("Incorrect tag parsed: %v", entries[1].Refs)
172 }
173 if entries[1].Subject != "Add feature X" {
174 t.Errorf("Expected subject 'Add feature X', got '%s'", entries[1].Subject)
175 }
176
177 // Check third entry
178 if entries[2].Hash != "ghi789" {
179 t.Errorf("Expected hash ghi789, got %s", entries[2].Hash)
180 }
181 if len(entries[2].Refs) != 0 {
182 t.Errorf("Expected 0 refs, got %d", len(entries[2].Refs))
183 }
184 if entries[2].Subject != "Fix bug Y" {
185 t.Errorf("Expected subject 'Fix bug Y', got '%s'", entries[2].Subject)
186 }
187}
188
189func TestParseRefs(t *testing.T) {
190 testCases := []struct {
191 decoration string
192 expected []string
193 }{
194 {"(HEAD -> main, origin/main)", []string{"main", "origin/main"}},
195 {"(tag: v1.0.0)", []string{"v1.0.0"}},
196 {"(HEAD -> feature/branch, origin/feature/branch, tag: v0.9.0)", []string{"feature/branch", "origin/feature/branch", "v0.9.0"}},
197 {" (tag: v2.0.0) ", []string{"v2.0.0"}},
198 {"", nil},
199 {" ", nil},
200 {"()", nil},
201 }
202
203 for i, tc := range testCases {
204 refs := parseRefs(tc.decoration)
205
206 if len(refs) != len(tc.expected) {
207 t.Errorf("Case %d: Expected %d refs, got %d", i, len(tc.expected), len(refs))
208 continue
209 }
210
211 for j, ref := range refs {
212 if j >= len(tc.expected) || ref != tc.expected[j] {
213 t.Errorf("Case %d: Expected ref '%s', got '%s'", i, tc.expected[j], ref)
214 }
215 }
216 }
217}
218
219func TestGitRecentLog(t *testing.T) {
220 // Create a temporary directory for the test repository
221 tmpDir, err := os.MkdirTemp("", "git-test-*")
222 if err != nil {
223 t.Fatalf("Failed to create temp dir: %v", err)
224 }
225 defer os.RemoveAll(tmpDir)
226
227 // Initialize a git repository
228 initCmd := exec.Command("git", "-C", tmpDir, "init")
229 if out, err := initCmd.CombinedOutput(); err != nil {
230 t.Fatalf("Failed to initialize git repository: %v\n%s", err, out)
231 }
232
233 // Configure git user for the test repository
234 exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run()
235 exec.Command("git", "-C", tmpDir, "config", "user.email", "test@example.com").Run()
236
237 // Create initial commit
238 initialFile := filepath.Join(tmpDir, "initial.txt")
Autoformatter8c463622025-05-16 21:54:17 +0000239 os.WriteFile(initialFile, []byte("initial content"), 0o644)
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000240 exec.Command("git", "-C", tmpDir, "add", "initial.txt").Run()
241 initialCommitCmd := exec.Command("git", "-C", tmpDir, "commit", "-m", "Initial commit")
242 out, err := initialCommitCmd.CombinedOutput()
243 if err != nil {
244 t.Fatalf("Failed to create initial commit: %v\n%s", err, out)
245 }
246
247 // Get the initial commit hash
248 initialCommitCmd = exec.Command("git", "-C", tmpDir, "rev-parse", "HEAD")
249 initialCommitBytes, err := initialCommitCmd.Output()
250 if err != nil {
251 t.Fatalf("Failed to get initial commit hash: %v", err)
252 }
253 initialCommitHash := strings.TrimSpace(string(initialCommitBytes))
254
255 // Add a second commit
256 secondFile := filepath.Join(tmpDir, "second.txt")
Autoformatter8c463622025-05-16 21:54:17 +0000257 os.WriteFile(secondFile, []byte("second content"), 0o644)
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000258 exec.Command("git", "-C", tmpDir, "add", "second.txt").Run()
259 secondCommitCmd := exec.Command("git", "-C", tmpDir, "commit", "-m", "Second commit")
260 out, err = secondCommitCmd.CombinedOutput()
261 if err != nil {
262 t.Fatalf("Failed to create second commit: %v\n%s", err, out)
263 }
264
265 // Create a branch and tag
266 exec.Command("git", "-C", tmpDir, "branch", "test-branch").Run()
267 exec.Command("git", "-C", tmpDir, "tag", "-a", "v1.0.0", "-m", "Version 1.0.0").Run()
268
269 // Add a third commit
270 thirdFile := filepath.Join(tmpDir, "third.txt")
Autoformatter8c463622025-05-16 21:54:17 +0000271 os.WriteFile(thirdFile, []byte("third content"), 0o644)
Philip Zeyligerd3ac1122025-05-14 02:54:18 +0000272 exec.Command("git", "-C", tmpDir, "add", "third.txt").Run()
273 thirdCommitCmd := exec.Command("git", "-C", tmpDir, "commit", "-m", "Third commit")
274 out, err = thirdCommitCmd.CombinedOutput()
275 if err != nil {
276 t.Fatalf("Failed to create third commit: %v\n%s", err, out)
277 }
278
279 // Test GitRecentLog
280 log, err := GitRecentLog(tmpDir, initialCommitHash)
281 if err != nil {
282 t.Fatalf("GitRecentLog failed: %v", err)
283 }
284
285 // No need to check specific entries in order
286 // Just validate we can find the second and third commits we created
287
288 // Verify that we have the correct behavior with the fromCommit parameter:
289 // 1. We should find the second and third commits
290 // 2. We should NOT find the initial commit (it should be excluded)
291 foundThird := false
292 foundSecond := false
293 foundInitial := false
294 for _, entry := range log {
295 t.Logf("Found entry: %s - %s", entry.Hash, entry.Subject)
296 if entry.Subject == "Third commit" {
297 foundThird = true
298 } else if entry.Subject == "Second commit" {
299 foundSecond = true
300 } else if entry.Subject == "Initial commit" {
301 foundInitial = true
302 }
303 }
304
305 if !foundThird {
306 t.Errorf("Expected to find 'Third commit' in log entries")
307 }
308 if !foundSecond {
309 t.Errorf("Expected to find 'Second commit' in log entries")
310 }
311 if foundInitial {
312 t.Errorf("Should NOT have found 'Initial commit' in log entries (fromCommit parameter should exclude it)")
313 }
314}
315
316func TestParseRefsEdgeCases(t *testing.T) {
317 testCases := []struct {
318 name string
319 decoration string
320 expected []string
321 }{
322 {
323 name: "Multiple tags and branches",
324 decoration: "(HEAD -> main, origin/main, tag: v1.0.0, tag: beta)",
325 expected: []string{"main", "origin/main", "v1.0.0", "beta"},
326 },
327 {
328 name: "Leading/trailing whitespace",
329 decoration: " (HEAD -> main) ",
330 expected: []string{"main"},
331 },
332 {
333 name: "No parentheses",
334 decoration: "HEAD -> main, tag: v1.0.0",
335 expected: []string{"main", "v1.0.0"},
336 },
337 {
338 name: "Feature branch with slash",
339 decoration: "(HEAD -> feature/new-ui)",
340 expected: []string{"feature/new-ui"},
341 },
342 {
343 name: "Only HEAD with no branch",
344 decoration: "(HEAD)",
345 expected: []string{"HEAD"},
346 },
347 }
348
349 for _, tc := range testCases {
350 t.Run(tc.name, func(t *testing.T) {
351 refs := parseRefs(tc.decoration)
352
353 if len(refs) != len(tc.expected) {
354 t.Errorf("%s: Expected %d refs, got %d", tc.name, len(tc.expected), len(refs))
355 return
356 }
357
358 for i, ref := range refs {
359 if ref != tc.expected[i] {
360 t.Errorf("%s: Expected ref[%d] = '%s', got '%s'", tc.name, i, tc.expected[i], ref)
361 }
362 }
363 })
364 }
365}
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700366
367func TestGitSaveFile(t *testing.T) {
368 // Create a temporary directory for the test repository
369 tmpDir, err := os.MkdirTemp("", "gitsave-test-")
370 if err != nil {
371 t.Fatalf("Failed to create temp dir: %v", err)
372 }
373 defer os.RemoveAll(tmpDir)
374
375 // Initialize a git repository
376 cmd := exec.Command("git", "init")
377 cmd.Dir = tmpDir
378 output, err := cmd.CombinedOutput()
379 if err != nil {
380 t.Fatalf("Failed to initialize git repo: %v, output: %s", err, output)
381 }
382
383 // Create and add a test file to the repo
384 testFilePath := "test-file.txt"
385 testFileContent := "initial content"
386 testFileFull := filepath.Join(tmpDir, testFilePath)
387
Autoformatter8c463622025-05-16 21:54:17 +0000388 err = os.WriteFile(testFileFull, []byte(testFileContent), 0o644)
Philip Zeyliger272a90e2025-05-16 14:49:51 -0700389 if err != nil {
390 t.Fatalf("Failed to write test file: %v", err)
391 }
392
393 // Add the file to git
394 cmd = exec.Command("git", "add", testFilePath)
395 cmd.Dir = tmpDir
396 output, err = cmd.CombinedOutput()
397 if err != nil {
398 t.Fatalf("Failed to add test file to git: %v, output: %s", err, output)
399 }
400
401 // Commit the file
402 cmd = exec.Command("git", "commit", "-m", "Initial commit")
403 cmd.Dir = tmpDir
404 cmd.Env = append(os.Environ(),
405 "GIT_AUTHOR_NAME=Test",
406 "GIT_AUTHOR_EMAIL=test@example.com",
407 "GIT_COMMITTER_NAME=Test",
408 "GIT_COMMITTER_EMAIL=test@example.com")
409 output, err = cmd.CombinedOutput()
410 if err != nil {
411 t.Fatalf("Failed to commit test file: %v, output: %s", err, output)
412 }
413
414 // Test successful save
415 newContent := "updated content"
416 err = GitSaveFile(tmpDir, testFilePath, newContent)
417 if err != nil {
418 t.Errorf("GitSaveFile failed: %v", err)
419 }
420
421 // Verify the file was updated
422 content, err := os.ReadFile(testFileFull)
423 if err != nil {
424 t.Fatalf("Failed to read updated file: %v", err)
425 }
426 if string(content) != newContent {
427 t.Errorf("File content not updated correctly; got %q, want %q", string(content), newContent)
428 }
429
430 // Test saving a file outside the repo
431 err = GitSaveFile(tmpDir, "../outside.txt", "malicious content")
432 if err == nil {
433 t.Error("GitSaveFile should have rejected a path outside the repository")
434 }
435
436 // Test saving a file not tracked by git
437 err = GitSaveFile(tmpDir, "untracked.txt", "untracked content")
438 if err == nil {
439 t.Error("GitSaveFile should have rejected an untracked file")
440 }
441}