This repository has been archived on 2026-03-10. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
labmcp/internal/gitexplorer/client_test.go
Torjus Håkestad 75673974a2 feat: add git-explorer MCP server for read-only repository access
Implements a new MCP server that provides read-only access to git
repositories using go-git. Designed for deployment verification by
comparing deployed flake revisions against source repositories.

9 tools: resolve_ref, get_log, get_commit_info, get_diff_files,
get_file_at_commit, is_ancestor, commits_between, list_branches,
search_commits.

Includes CLI commands, NixOS module, and comprehensive tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 04:26:38 +01:00

447 lines
10 KiB
Go

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)
}
}