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:
Dev
2025-09-11 17:02:12 +03:00
commit 15bbfdcda2
27 changed files with 5727 additions and 0 deletions

532
internal/git/git_test.go Normal file
View 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())
}
}