
- Add comprehensive Git workflow automation tools - Include branch management utilities - Add commit helpers with conventional commit support - Implement GitHub integration for PR management - Add configuration management system - Include comprehensive test coverage - Add professional documentation and examples
532 lines
14 KiB
Go
532 lines
14 KiB
Go
package git
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func setupTestRepo(t *testing.T) (string, func()) {
|
|
t.Helper()
|
|
|
|
// Create a temporary directory for the test repository
|
|
tempDir, err := os.MkdirTemp("", "git-test-")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp directory: %v", err)
|
|
}
|
|
|
|
// Change to the temp directory
|
|
oldDir, err := os.Getwd()
|
|
if err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to get current directory: %v", err)
|
|
}
|
|
|
|
if err := os.Chdir(tempDir); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to change to temp directory: %v", err)
|
|
}
|
|
|
|
// Initialize a git repository
|
|
ctx := context.Background()
|
|
if _, err := Run(ctx, "init"); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to initialize git repo: %v", err)
|
|
}
|
|
|
|
// Configure git user
|
|
if _, err := Run(ctx, "config", "user.name", "Test User"); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to configure git user: %v", err)
|
|
}
|
|
|
|
if _, err := Run(ctx, "config", "user.email", "test@example.com"); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to configure git email: %v", err)
|
|
}
|
|
|
|
// Create an initial commit
|
|
createTestFile(t, "README.md", "# Test Repository")
|
|
if _, err := Run(ctx, "add", "."); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to add files: %v", err)
|
|
}
|
|
|
|
if _, err := Run(ctx, "commit", "-m", "Initial commit"); err != nil {
|
|
os.RemoveAll(tempDir)
|
|
t.Fatalf("Failed to make initial commit: %v", err)
|
|
}
|
|
|
|
// Create a cleanup function
|
|
cleanup := func() {
|
|
os.Chdir(oldDir)
|
|
os.RemoveAll(tempDir)
|
|
}
|
|
|
|
return tempDir, cleanup
|
|
}
|
|
|
|
func createTestFile(t *testing.T, name, content string) {
|
|
t.Helper()
|
|
|
|
if err := os.WriteFile(name, []byte(content), 0644); err != nil {
|
|
t.Fatalf("Failed to create test file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentBranch(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initially should be on main branch
|
|
branch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
// The default branch name could be "main" or "master" depending on git version
|
|
if branch != "main" && branch != "master" {
|
|
t.Errorf("Expected branch to be 'main' or 'master', got '%s'", branch)
|
|
}
|
|
|
|
// Create a new branch
|
|
if _, err := Run(ctx, "checkout", "-b", "feature"); err != nil {
|
|
t.Fatalf("Failed to create feature branch: %v", err)
|
|
}
|
|
|
|
// Check current branch again
|
|
branch, err = CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
if branch != "feature" {
|
|
t.Errorf("Expected branch to be 'feature', got '%s'", branch)
|
|
}
|
|
}
|
|
|
|
func TestCreateBranch(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a new branch
|
|
err := CreateBranch(ctx, "test-branch")
|
|
if err != nil {
|
|
t.Fatalf("CreateBranch failed: %v", err)
|
|
}
|
|
|
|
// Check current branch
|
|
branch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
if branch != "test-branch" {
|
|
t.Errorf("Expected branch to be 'test-branch', got '%s'", branch)
|
|
}
|
|
}
|
|
|
|
func TestSwitchBranch(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a new branch
|
|
if _, err := Run(ctx, "checkout", "-b", "feature"); err != nil {
|
|
t.Fatalf("Failed to create feature branch: %v", err)
|
|
}
|
|
|
|
// Get the current branch to determine if it's main or master
|
|
currentBranch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
// Switch back to main/master
|
|
var targetBranch string
|
|
if currentBranch == "feature" {
|
|
// We need to determine what the original branch was
|
|
// Let's check if main exists
|
|
if _, err := Run(ctx, "rev-parse", "--verify", "main"); err == nil {
|
|
targetBranch = "main"
|
|
} else if _, err := Run(ctx, "rev-parse", "--verify", "master"); err == nil {
|
|
targetBranch = "master"
|
|
} else {
|
|
t.Fatalf("Neither main nor master branch exists")
|
|
}
|
|
}
|
|
|
|
err = SwitchBranch(ctx, targetBranch)
|
|
if err != nil {
|
|
t.Fatalf("SwitchBranch failed: %v", err)
|
|
}
|
|
|
|
// Check current branch
|
|
branch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
if branch != targetBranch {
|
|
t.Errorf("Expected branch to be '%s', got '%s'", targetBranch, branch)
|
|
}
|
|
}
|
|
|
|
func TestLocalBranches(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initially should have just one branch
|
|
branches, err := LocalBranches(ctx)
|
|
if err != nil {
|
|
t.Fatalf("LocalBranches failed: %v", err)
|
|
}
|
|
|
|
if len(branches) != 1 {
|
|
t.Errorf("Expected 1 branch, got %d", len(branches))
|
|
}
|
|
|
|
// Create a new branch
|
|
if _, err := Run(ctx, "checkout", "-b", "feature"); err != nil {
|
|
t.Fatalf("Failed to create feature branch: %v", err)
|
|
}
|
|
|
|
// Should now have two branches
|
|
branches, err = LocalBranches(ctx)
|
|
if err != nil {
|
|
t.Fatalf("LocalBranches failed: %v", err)
|
|
}
|
|
|
|
if len(branches) != 2 {
|
|
t.Errorf("Expected 2 branches, got %d", len(branches))
|
|
}
|
|
}
|
|
|
|
func TestIsCleanWorkingDir(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Initially should be clean
|
|
clean, err := IsCleanWorkingDir(ctx)
|
|
if err != nil {
|
|
t.Fatalf("IsCleanWorkingDir failed: %v", err)
|
|
}
|
|
|
|
if !clean {
|
|
t.Error("Expected working directory to be clean")
|
|
}
|
|
|
|
// Create a file
|
|
createTestFile(t, "test.txt", "test content")
|
|
|
|
// Should now be dirty
|
|
clean, err = IsCleanWorkingDir(ctx)
|
|
if err != nil {
|
|
t.Fatalf("IsCleanWorkingDir failed: %v", err)
|
|
}
|
|
|
|
if clean {
|
|
t.Error("Expected working directory to be dirty")
|
|
}
|
|
}
|
|
|
|
func TestAddAll(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a file
|
|
createTestFile(t, "test.txt", "test content")
|
|
|
|
// Add the file
|
|
err := AddAll(ctx)
|
|
if err != nil {
|
|
t.Fatalf("AddAll failed: %v", err)
|
|
}
|
|
|
|
// Note: After adding files, the working directory is not necessarily clean
|
|
// because the files are staged but not committed. This is normal git behavior.
|
|
// So we'll just check that there are no unstaged changes.
|
|
|
|
// Check if there are any untracked files
|
|
output, err := Run(ctx, "status", "--porcelain")
|
|
if err != nil {
|
|
t.Fatalf("Failed to get git status: %v", err)
|
|
}
|
|
|
|
// If there are any untracked files, they would show up with ?? at the beginning
|
|
for i := range output {
|
|
line := string(output[i])
|
|
if len(line) > 2 && strings.HasPrefix(line, "??") {
|
|
t.Errorf("Found untracked file: %s", line[3:])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCommit(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create and add a file
|
|
createTestFile(t, "test.txt", "test content")
|
|
if err := AddAll(ctx); err != nil {
|
|
t.Fatalf("AddAll failed: %v", err)
|
|
}
|
|
|
|
// Commit the file
|
|
err := Commit(ctx, "test commit")
|
|
if err != nil {
|
|
t.Fatalf("Commit failed: %v", err)
|
|
}
|
|
|
|
// Should now be clean
|
|
clean, err := IsCleanWorkingDir(ctx)
|
|
if err != nil {
|
|
t.Fatalf("IsCleanWorkingDir failed: %v", err)
|
|
}
|
|
|
|
if !clean {
|
|
t.Error("Expected working directory to be clean after commit")
|
|
}
|
|
}
|
|
|
|
func TestDeleteBranch(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a new branch
|
|
if _, err := Run(ctx, "checkout", "-b", "feature"); err != nil {
|
|
t.Fatalf("Failed to create feature branch: %v", err)
|
|
}
|
|
|
|
// Switch back to main/master
|
|
var targetBranch string
|
|
if _, err := Run(ctx, "rev-parse", "--verify", "main"); err == nil {
|
|
targetBranch = "main"
|
|
} else if _, err := Run(ctx, "rev-parse", "--verify", "master"); err == nil {
|
|
targetBranch = "master"
|
|
} else {
|
|
t.Fatalf("Neither main nor master branch exists")
|
|
}
|
|
|
|
if _, err := Run(ctx, "checkout", targetBranch); err != nil {
|
|
t.Fatalf("Failed to switch back to %s: %v", targetBranch, err)
|
|
}
|
|
|
|
// Delete the branch
|
|
err := DeleteBranch(ctx, "feature")
|
|
if err != nil {
|
|
t.Fatalf("DeleteBranch failed: %v", err)
|
|
}
|
|
|
|
// Should now have just one branch
|
|
branches, err := LocalBranches(ctx)
|
|
if err != nil {
|
|
t.Fatalf("LocalBranches failed: %v", err)
|
|
}
|
|
|
|
if len(branches) != 1 {
|
|
t.Errorf("Expected 1 branch after delete, got %d", len(branches))
|
|
}
|
|
}
|
|
|
|
func TestGetRemoteURL(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Add a remote
|
|
if _, err := Run(ctx, "remote", "add", "origin", "https://github.com/user/repo.git"); err != nil {
|
|
t.Fatalf("Failed to add remote: %v", err)
|
|
}
|
|
|
|
// Get the remote URL
|
|
url, err := GetRemoteURL(ctx, "origin")
|
|
if err != nil {
|
|
t.Fatalf("GetRemoteURL failed: %v", err)
|
|
}
|
|
|
|
expectedURL := "https://github.com/user/repo.git"
|
|
if url != expectedURL {
|
|
t.Errorf("Expected URL to be '%s', got '%s'", expectedURL, url)
|
|
}
|
|
}
|
|
|
|
func TestFetch(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Add a remote
|
|
if _, err := Run(ctx, "remote", "add", "origin", "https://github.com/user/repo.git"); err != nil {
|
|
t.Fatalf("Failed to add remote: %v", err)
|
|
}
|
|
|
|
// Fetch from remote
|
|
err := Fetch(ctx)
|
|
if err != nil {
|
|
// We expect this to fail since the remote URL is fake
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("Fetch failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRebaseOntoTracking(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Add a remote
|
|
if _, err := Run(ctx, "remote", "add", "origin", "https://github.com/user/repo.git"); err != nil {
|
|
t.Fatalf("Failed to add remote: %v", err)
|
|
}
|
|
|
|
// Set up tracking branch
|
|
branch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
if _, err := Run(ctx, "push", "-u", "origin", branch); err != nil {
|
|
// We expect this to fail since the remote URL is fake
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("Push failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test rebase
|
|
err = RebaseOntoTracking(ctx)
|
|
if err != nil {
|
|
// We expect this to fail since the remote URL is fake or there's no upstream
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") &&
|
|
!strings.Contains(err.Error(), "no upstream configured") {
|
|
t.Fatalf("RebaseOntoTracking failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPushCurrent(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Add a remote
|
|
if _, err := Run(ctx, "remote", "add", "origin", "https://github.com/user/repo.git"); err != nil {
|
|
t.Fatalf("Failed to add remote: %v", err)
|
|
}
|
|
|
|
// Test push without force
|
|
err := PushCurrent(ctx, false)
|
|
if err != nil {
|
|
// We expect this to fail since the remote URL is fake
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("PushCurrent failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test push with force
|
|
err = PushCurrent(ctx, true)
|
|
if err != nil {
|
|
// We expect this to fail since the remote URL is fake
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("PushCurrent failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpstreamFor(t *testing.T) {
|
|
_, cleanup := setupTestRepo(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Add a remote
|
|
if _, err := Run(ctx, "remote", "add", "origin", "https://github.com/user/repo.git"); err != nil {
|
|
t.Fatalf("Failed to add remote: %v", err)
|
|
}
|
|
|
|
// Get current branch
|
|
branch, err := CurrentBranch(ctx)
|
|
if err != nil {
|
|
t.Fatalf("CurrentBranch failed: %v", err)
|
|
}
|
|
|
|
// Set up tracking branch
|
|
if _, err := Run(ctx, "push", "-u", "origin", branch); err != nil {
|
|
// We expect this to fail since the remote URL is fake
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("Push failed with unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Test UpstreamFor
|
|
upstream, err := UpstreamFor(ctx, branch)
|
|
if err != nil {
|
|
// We expect this to fail since the remote URL is fake or there's no upstream
|
|
// But we want to test that the function is called correctly
|
|
if !strings.Contains(err.Error(), "no upstream configured") &&
|
|
!strings.Contains(err.Error(), "could not read Username") &&
|
|
!strings.Contains(err.Error(), "Could not resolve host") {
|
|
t.Fatalf("UpstreamFor failed with unexpected error: %v", err)
|
|
}
|
|
} else {
|
|
// If it succeeds, check the format
|
|
expected := "origin/" + branch
|
|
if upstream != expected {
|
|
t.Errorf("Expected upstream to be '%s', got '%s'", expected, upstream)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGitError(t *testing.T) {
|
|
err := fmtError("test error")
|
|
|
|
if err == nil {
|
|
t.Fatal("Expected fmtError to return an error, got nil")
|
|
}
|
|
|
|
gitErr, ok := err.(*gitError)
|
|
if !ok {
|
|
t.Fatalf("Expected error to be of type *gitError, got %T", err)
|
|
}
|
|
|
|
if gitErr.msg != "test error" {
|
|
t.Errorf("Expected error message to be 'test error', got '%s'", gitErr.msg)
|
|
}
|
|
|
|
if gitErr.Error() != "test error" {
|
|
t.Errorf("Expected Error() to return 'test error', got '%s'", gitErr.Error())
|
|
}
|
|
} |