feat: initial commit of Git Automation CLI
- 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
This commit is contained in:
532
internal/git/git_test.go
Normal file
532
internal/git/git_test.go
Normal file
@@ -0,0 +1,532 @@
|
||||
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())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user