package gitexplorer import ( "os" "path/filepath" "testing" "time" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" ) // createTestRepo creates a temporary git repository with some commits for testing. func createTestRepo(t *testing.T) (string, func()) { t.Helper() dir, err := os.MkdirTemp("", "gitexplorer-test-*") if err != nil { t.Fatalf("failed to create temp dir: %v", err) } cleanup := func() { _ = os.RemoveAll(dir) } repo, err := git.PlainInit(dir, false) if err != nil { cleanup() t.Fatalf("failed to init repo: %v", err) } wt, err := repo.Worktree() if err != nil { cleanup() t.Fatalf("failed to get worktree: %v", err) } // Create initial file and commit readme := filepath.Join(dir, "README.md") if err := os.WriteFile(readme, []byte("# Test Repo\n"), 0644); err != nil { cleanup() t.Fatalf("failed to write README: %v", err) } if _, err := wt.Add("README.md"); err != nil { cleanup() t.Fatalf("failed to add README: %v", err) } sig := &object.Signature{ Name: "Test User", Email: "test@example.com", When: time.Now().Add(-2 * time.Hour), } _, err = wt.Commit("Initial commit", &git.CommitOptions{Author: sig}) if err != nil { cleanup() t.Fatalf("failed to create initial commit: %v", err) } // Create a second file and commit subdir := filepath.Join(dir, "src") if err := os.MkdirAll(subdir, 0755); err != nil { cleanup() t.Fatalf("failed to create subdir: %v", err) } mainFile := filepath.Join(subdir, "main.go") if err := os.WriteFile(mainFile, []byte("package main\n\nfunc main() {}\n"), 0644); err != nil { cleanup() t.Fatalf("failed to write main.go: %v", err) } if _, err := wt.Add("src/main.go"); err != nil { cleanup() t.Fatalf("failed to add main.go: %v", err) } sig.When = time.Now().Add(-1 * time.Hour) _, err = wt.Commit("Add main.go", &git.CommitOptions{Author: sig}) if err != nil { cleanup() t.Fatalf("failed to create second commit: %v", err) } // Update README and commit if err := os.WriteFile(readme, []byte("# Test Repo\n\nThis is a test repository.\n"), 0644); err != nil { cleanup() t.Fatalf("failed to update README: %v", err) } if _, err := wt.Add("README.md"); err != nil { cleanup() t.Fatalf("failed to add updated README: %v", err) } sig.When = time.Now() _, err = wt.Commit("Update README", &git.CommitOptions{Author: sig}) if err != nil { cleanup() t.Fatalf("failed to create third commit: %v", err) } return dir, cleanup } func TestNewGitClient(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } if client == nil { t.Fatal("client is nil") } if client.defaultRemote != "origin" { t.Errorf("defaultRemote = %q, want %q", client.defaultRemote, "origin") } // Test with invalid path _, err = NewGitClient("/nonexistent/path", "") if err == nil { t.Error("expected error for nonexistent path") } } func TestResolveRef(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } // Test resolving HEAD result, err := client.ResolveRef("HEAD") if err != nil { t.Fatalf("ResolveRef(HEAD) failed: %v", err) } if result.Commit == "" { t.Error("commit hash is empty") } // Test resolving master branch result, err = client.ResolveRef("master") if err != nil { t.Fatalf("ResolveRef(master) failed: %v", err) } if result.Type != "branch" { t.Errorf("type = %q, want %q", result.Type, "branch") } // Test resolving invalid ref _, err = client.ResolveRef("nonexistent") if err == nil { t.Error("expected error for nonexistent ref") } } func TestGetLog(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } // Get full log entries, err := client.GetLog("HEAD", 10, "", "", "") if err != nil { t.Fatalf("GetLog failed: %v", err) } if len(entries) != 3 { t.Errorf("got %d entries, want 3", len(entries)) } // Check order (newest first) if entries[0].Subject != "Update README" { t.Errorf("first entry subject = %q, want %q", entries[0].Subject, "Update README") } // Test with limit entries, err = client.GetLog("HEAD", 1, "", "", "") if err != nil { t.Fatalf("GetLog with limit failed: %v", err) } if len(entries) != 1 { t.Errorf("got %d entries, want 1", len(entries)) } // Test with author filter entries, err = client.GetLog("HEAD", 10, "Test User", "", "") if err != nil { t.Fatalf("GetLog with author failed: %v", err) } if len(entries) != 3 { t.Errorf("got %d entries, want 3", len(entries)) } entries, err = client.GetLog("HEAD", 10, "nonexistent", "", "") if err != nil { t.Fatalf("GetLog with nonexistent author failed: %v", err) } if len(entries) != 0 { t.Errorf("got %d entries, want 0", len(entries)) } // Test with path filter entries, err = client.GetLog("HEAD", 10, "", "", "src") if err != nil { t.Fatalf("GetLog with path failed: %v", err) } if len(entries) != 1 { t.Errorf("got %d entries, want 1 (only src/main.go commit)", len(entries)) } } func TestGetCommitInfo(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } info, err := client.GetCommitInfo("HEAD", true) if err != nil { t.Fatalf("GetCommitInfo failed: %v", err) } if info.Author != "Test User" { t.Errorf("author = %q, want %q", info.Author, "Test User") } if info.Email != "test@example.com" { t.Errorf("email = %q, want %q", info.Email, "test@example.com") } if len(info.Parents) != 1 { t.Errorf("parents = %d, want 1", len(info.Parents)) } if info.Stats == nil { t.Error("stats is nil") } // Test without stats info, err = client.GetCommitInfo("HEAD", false) if err != nil { t.Fatalf("GetCommitInfo without stats failed: %v", err) } if info.Stats != nil { t.Error("stats should be nil") } } func TestGetDiffFiles(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } result, err := client.GetDiffFiles("HEAD~2", "HEAD") if err != nil { t.Fatalf("GetDiffFiles failed: %v", err) } if len(result.Files) < 1 { t.Error("expected at least one changed file") } // Check that we have the expected files foundReadme := false foundMain := false for _, f := range result.Files { if f.Path == "README.md" { foundReadme = true } if f.Path == "src/main.go" { foundMain = true } } if !foundReadme { t.Error("expected README.md in diff") } if !foundMain { t.Error("expected src/main.go in diff") } } func TestGetFileAtCommit(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } content, err := client.GetFileAtCommit("HEAD", "README.md") if err != nil { t.Fatalf("GetFileAtCommit failed: %v", err) } if content.Path != "README.md" { t.Errorf("path = %q, want %q", content.Path, "README.md") } if content.Content == "" { t.Error("content is empty") } // Test nested file content, err = client.GetFileAtCommit("HEAD", "src/main.go") if err != nil { t.Fatalf("GetFileAtCommit for nested file failed: %v", err) } if content.Path != "src/main.go" { t.Errorf("path = %q, want %q", content.Path, "src/main.go") } // Test nonexistent file _, err = client.GetFileAtCommit("HEAD", "nonexistent.txt") if err == nil { t.Error("expected error for nonexistent file") } // Test path traversal _, err = client.GetFileAtCommit("HEAD", "../../../etc/passwd") if err == nil { t.Error("expected error for path traversal") } } func TestIsAncestor(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } // First commit is ancestor of HEAD result, err := client.IsAncestor("HEAD~2", "HEAD") if err != nil { t.Fatalf("IsAncestor failed: %v", err) } if !result.IsAncestor { t.Error("HEAD~2 should be ancestor of HEAD") } // HEAD is not ancestor of first commit result, err = client.IsAncestor("HEAD", "HEAD~2") if err != nil { t.Fatalf("IsAncestor failed: %v", err) } if result.IsAncestor { t.Error("HEAD should not be ancestor of HEAD~2") } } func TestCommitsBetween(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } result, err := client.CommitsBetween("HEAD~2", "HEAD", 10) if err != nil { t.Fatalf("CommitsBetween failed: %v", err) } // Should have 2 commits (HEAD~1 and HEAD, exclusive of HEAD~2) if result.Count != 2 { t.Errorf("count = %d, want 2", result.Count) } } func TestListBranches(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } result, err := client.ListBranches(false) if err != nil { t.Fatalf("ListBranches failed: %v", err) } if result.Total < 1 { t.Error("expected at least one branch") } foundMaster := false for _, b := range result.Branches { if b.Name == "master" { foundMaster = true if !b.IsHead { t.Error("master should be HEAD") } } } if !foundMaster { t.Error("expected master branch") } } func TestSearchCommits(t *testing.T) { repoPath, cleanup := createTestRepo(t) defer cleanup() client, err := NewGitClient(repoPath, "") if err != nil { t.Fatalf("NewGitClient failed: %v", err) } result, err := client.SearchCommits("HEAD", "README", 10) if err != nil { t.Fatalf("SearchCommits failed: %v", err) } if result.Count < 1 { t.Error("expected at least one match for 'README'") } // Search with no matches result, err = client.SearchCommits("HEAD", "nonexistent-query-xyz", 10) if err != nil { t.Fatalf("SearchCommits for no match failed: %v", err) } if result.Count != 0 { t.Errorf("count = %d, want 0", result.Count) } }