mirror of
https://github.com/imjasonh/infinite-git
synced 2026-07-10 01:48:35 +00:00
Major rewrite to eliminate git CLI dependency: - Created object package for Git object format handling (blob, tree, commit) - Implemented packfile generation from scratch - Rewrote repo initialization to create Git repos without CLI - Implemented git-upload-pack protocol in pure Go - Updated all server handlers to use new implementation - Added Git packet tracing to test.sh for debugging - Fixed tests to work with new implementation The server now works entirely with Go code, no git commands needed. Clone operations work correctly, but pull operations still have protocol negotiation issues that need further debugging. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
276 lines
6.5 KiB
Go
276 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
"github.com/imjasonh/infinite-git/internal/repo"
|
|
"github.com/imjasonh/infinite-git/internal/server"
|
|
)
|
|
|
|
func TestCloneAndPull(t *testing.T) {
|
|
// Create temporary directories
|
|
serverRepoDir := t.TempDir()
|
|
clientRepoDir := t.TempDir()
|
|
|
|
// Initialize server repository
|
|
serverRepo, err := repo.New(serverRepoDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server repo: %v", err)
|
|
}
|
|
|
|
srv := server.New(serverRepo)
|
|
|
|
// Start test server
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
// Clone the repository
|
|
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
|
URL: ts.URL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to clone: %v", err)
|
|
}
|
|
|
|
// Get initial commit
|
|
ref, err := gitRepo.Head()
|
|
if err != nil {
|
|
t.Fatalf("failed to get HEAD: %v", err)
|
|
}
|
|
initialCommit := ref.Hash()
|
|
t.Logf("Initial commit: %s", initialCommit)
|
|
|
|
// Count initial commits
|
|
initialCount := countCommits(t, gitRepo)
|
|
t.Logf("Initial commit count: %d", initialCount)
|
|
|
|
// Perform multiple pulls
|
|
commits := []plumbing.Hash{initialCommit}
|
|
for i := 0; i < 3; i++ {
|
|
// Pull from origin
|
|
w, err := gitRepo.Worktree()
|
|
if err != nil {
|
|
t.Fatalf("failed to get worktree: %v", err)
|
|
}
|
|
|
|
err = w.Pull(&git.PullOptions{
|
|
RemoteName: "origin",
|
|
})
|
|
if err != nil && err != git.NoErrAlreadyUpToDate {
|
|
t.Fatalf("pull %d failed: %v", i+1, err)
|
|
}
|
|
|
|
// Get new HEAD
|
|
ref, err := gitRepo.Head()
|
|
if err != nil {
|
|
t.Fatalf("failed to get HEAD after pull %d: %v", i+1, err)
|
|
}
|
|
newCommit := ref.Hash()
|
|
|
|
// Verify we got a new commit
|
|
if newCommit == commits[len(commits)-1] {
|
|
t.Errorf("pull %d did not generate a new commit", i+1)
|
|
}
|
|
commits = append(commits, newCommit)
|
|
t.Logf("Pull %d got commit: %s", i+1, newCommit)
|
|
|
|
// Verify commit count increased
|
|
newCount := countCommits(t, gitRepo)
|
|
if newCount != initialCount+i+1 {
|
|
t.Errorf("expected %d commits after pull %d, got %d", initialCount+i+1, i+1, newCount)
|
|
}
|
|
|
|
// Verify new files exist
|
|
expectedFile := filepath.Join(clientRepoDir, fmt.Sprintf("pull_%d.txt", i+1))
|
|
if _, err := os.Stat(expectedFile); os.IsNotExist(err) {
|
|
t.Errorf("expected file %s does not exist", expectedFile)
|
|
}
|
|
}
|
|
|
|
// Verify all commits are unique
|
|
uniqueCommits := make(map[plumbing.Hash]bool)
|
|
for _, commit := range commits {
|
|
if uniqueCommits[commit] {
|
|
t.Errorf("found duplicate commit: %s", commit)
|
|
}
|
|
uniqueCommits[commit] = true
|
|
}
|
|
}
|
|
|
|
func TestConcurrentPulls(t *testing.T) {
|
|
// Create temporary directories
|
|
serverRepoDir := t.TempDir()
|
|
|
|
// Initialize server repository
|
|
serverRepo, err := repo.New(serverRepoDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server repo: %v", err)
|
|
}
|
|
|
|
srv := server.New(serverRepo)
|
|
|
|
// Start test server
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
// Clone initial repository
|
|
baseClientDir := t.TempDir()
|
|
baseRepo, err := git.PlainClone(baseClientDir, false, &git.CloneOptions{
|
|
URL: ts.URL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to clone: %v", err)
|
|
}
|
|
|
|
initialCount := countCommits(t, baseRepo)
|
|
t.Logf("Initial commit count: %d", initialCount)
|
|
|
|
// Perform concurrent pulls
|
|
const numPulls = 5
|
|
var wg sync.WaitGroup
|
|
commits := make([]plumbing.Hash, numPulls)
|
|
errors := make([]error, numPulls)
|
|
|
|
for i := 0; i < numPulls; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
|
|
// Clone a fresh repo for this goroutine
|
|
clientDir := filepath.Join(t.TempDir(), fmt.Sprintf("client_%d", idx))
|
|
gitRepo, err := git.PlainClone(clientDir, false, &git.CloneOptions{
|
|
URL: ts.URL,
|
|
})
|
|
if err != nil {
|
|
errors[idx] = fmt.Errorf("clone failed: %w", err)
|
|
return
|
|
}
|
|
|
|
// Get the commit we received
|
|
ref, err := gitRepo.Head()
|
|
if err != nil {
|
|
errors[idx] = fmt.Errorf("get HEAD failed: %w", err)
|
|
return
|
|
}
|
|
commits[idx] = ref.Hash()
|
|
t.Logf("Concurrent pull %d got commit: %s", idx, commits[idx])
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Check for errors
|
|
for i, err := range errors {
|
|
if err != nil {
|
|
t.Errorf("goroutine %d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Verify all commits are unique
|
|
uniqueCommits := make(map[plumbing.Hash]bool)
|
|
for i, commit := range commits {
|
|
if uniqueCommits[commit] {
|
|
t.Errorf("concurrent pull %d got duplicate commit: %s", i, commit)
|
|
}
|
|
uniqueCommits[commit] = true
|
|
}
|
|
|
|
// All pulls should have generated unique commits
|
|
if len(uniqueCommits) != numPulls {
|
|
t.Errorf("expected %d unique commits, got %d", numPulls, len(uniqueCommits))
|
|
}
|
|
}
|
|
|
|
func TestPushRejection(t *testing.T) {
|
|
// Create temporary directories
|
|
serverRepoDir := t.TempDir()
|
|
clientRepoDir := t.TempDir()
|
|
|
|
// Initialize server repository
|
|
serverRepo, err := repo.New(serverRepoDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to create server repo: %v", err)
|
|
}
|
|
|
|
srv := server.New(serverRepo)
|
|
|
|
// Start test server
|
|
ts := httptest.NewServer(srv.Handler())
|
|
defer ts.Close()
|
|
|
|
// Clone the repository
|
|
gitRepo, err := git.PlainClone(clientRepoDir, false, &git.CloneOptions{
|
|
URL: ts.URL,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to clone: %v", err)
|
|
}
|
|
|
|
// Create a new file and commit
|
|
testFile := filepath.Join(clientRepoDir, "test-push.txt")
|
|
if err := os.WriteFile(testFile, []byte("test push content"), 0644); err != nil {
|
|
t.Fatalf("failed to write test file: %v", err)
|
|
}
|
|
|
|
w, err := gitRepo.Worktree()
|
|
if err != nil {
|
|
t.Fatalf("failed to get worktree: %v", err)
|
|
}
|
|
|
|
if _, err := w.Add("test-push.txt"); err != nil {
|
|
t.Fatalf("failed to add file: %v", err)
|
|
}
|
|
|
|
if _, err := w.Commit("Test push commit", &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: "Test User",
|
|
Email: "test@example.com",
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("failed to commit: %v", err)
|
|
}
|
|
|
|
// Try to push - should fail
|
|
err = gitRepo.Push(&git.PushOptions{
|
|
RemoteName: "origin",
|
|
Auth: &http.BasicAuth{
|
|
Username: "test",
|
|
Password: "test",
|
|
},
|
|
})
|
|
|
|
if err == nil {
|
|
t.Fatal("push should have been rejected but succeeded")
|
|
}
|
|
|
|
// Verify the error indicates push was forbidden
|
|
t.Logf("Push rejected with error: %v", err)
|
|
}
|
|
|
|
// Helper function to count commits
|
|
func countCommits(t *testing.T, repo *git.Repository) int {
|
|
iter, err := repo.Log(&git.LogOptions{})
|
|
if err != nil {
|
|
t.Fatalf("failed to get log: %v", err)
|
|
}
|
|
defer iter.Close()
|
|
|
|
count := 0
|
|
err = iter.ForEach(func(c *object.Commit) error {
|
|
count++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to iterate commits: %v", err)
|
|
}
|
|
return count
|
|
}
|