Skip to content

Latest commit

 

History

History
464 lines (370 loc) · 10.6 KB

File metadata and controls

464 lines (370 loc) · 10.6 KB

Git Exec

Go library that wraps Git commands and returns structured output.

Features

  • Structured output via Go structs instead of string parsing
  • Standard Go error handling with context
  • Session management with persistent user configuration
  • Merge conflict detection and resolution
  • Mockery-compatible interfaces

Installation

go get github.com/instruqt/git-exec

Quick Start

package main

import (
    "fmt"
    "log"
    "os"
    
    "github.com/instruqt/git-exec/pkg/git"
)

func main() {
    // Create Git instance
    gitInstance, err := git.NewGit()
    if err != nil {
        log.Fatal(err)
    }
    
    // Initialize repository
    err = gitInstance.Init("/path/to/repo")
    if err != nil {
        log.Fatal(err)
    }
    
    gitInstance.SetWorkingDirectory("/path/to/repo")
    
    // Configure user
    err = gitInstance.SetConfig("user.name", "Your Name")
    if err != nil {
        log.Fatal(err)
    }
    
    err = gitInstance.SetConfig("user.email", "you@example.com")
    if err != nil {
        log.Fatal(err)
    }
    
    // Add and commit files
    err = gitInstance.Add([]string{"."})
    if err != nil {
        log.Fatal(err)
    }
    
    err = gitInstance.Commit("Initial commit")
    if err != nil {
        log.Fatal(err)
    }
    
    // Check status
    files, err := gitInstance.Status()
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Repository has %d files\n", len(files))
}

Usage Examples

Basic Operations

// Get commit history
logs, err := gitInstance.Log(git.LogWithMaxCount("5"))
if err != nil {
    log.Fatal(err)
}

for _, logEntry := range logs {
    fmt.Printf("%s: %s - %s\n", logEntry.Hash[:8], logEntry.Message, logEntry.Author)
}

// Check repository status
files, err := gitInstance.Status()
if err != nil {
    log.Fatal(err)
}

for _, file := range files {
    fmt.Printf("%s %s\n", file.Status, file.Name)
}

Session Management

Sessions maintain user configuration across operations:

// Create session with user configuration
session, err := git.NewSession("/path/to/project",
    git.SessionWithUser("Alice Developer", "alice@company.com"),
    git.SessionWithMetadata("user", "id", "user-123"),
    git.SessionWithMetadata("user", "role", "developer"), 
    git.SessionWithMetadata("project", "name", "web-app"),
    git.SessionWithMetadata("project", "version", "1.0.0"),
    git.SessionWithMetadata("team", "name", "frontend"),
)
if err != nil {
    log.Fatal(err)
}

// User context is automatically applied to all operations
err = session.Add([]string{"README.md"})
if err != nil {
    log.Fatal(err)
}

err = session.Commit("Initial project setup")
if err != nil {
    log.Fatal(err)
}

// Load existing session
loadedSession, err := git.LoadSession("/path/to/project")
if err != nil {
    log.Fatal(err)
}

Branch Management

// Create and switch to new branch
err = gitInstance.CreateBranch("feature/auth")
if err != nil {
    log.Fatal(err)
}

result, err := gitInstance.Checkout(git.CheckoutWithBranch("feature/auth"))
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Switched to branch: %s\n", result.Branch)

// List all branches
branches, err := gitInstance.ListBranches()
if err != nil {
    log.Fatal(err)
}

for _, branch := range branches {
    marker := " "
    if branch.Active {
        marker = "*"
    }
    fmt.Printf("  %s %s\n", marker, branch.Name)
}

Enhanced Checkout Operations

The checkout operation returns detailed information about the checkout result:

// Checkout existing branch
result, err := gitInstance.Checkout(git.CheckoutWithBranch("feature"))
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Success: %v\n", result.Success)
fmt.Printf("Branch: %s\n", result.Branch)
fmt.Printf("Previous HEAD: %s\n", result.PreviousHEAD)
fmt.Printf("New HEAD: %s\n", result.NewHEAD)

// Create and checkout new branch
result, err = gitInstance.Checkout(git.CheckoutWithCreate("new-feature"))
if err != nil {
    log.Fatal(err)
}

if result.NewBranch {
    fmt.Printf("Created new branch: %s\n", result.Branch)
}

// Checkout specific commit (detached HEAD)
result, err = gitInstance.Checkout(git.CheckoutWithCommit("abc123"))
if err != nil {
    log.Fatal(err)
}

if result.DetachedHEAD {
    fmt.Printf("Detached HEAD at commit: %s\n", result.Commit)
}

// Checkout with file modifications
result, err = gitInstance.Checkout(git.CheckoutWithBranch("main"))
if err != nil {
    log.Fatal(err)
}

if len(result.ModifiedFiles) > 0 {
    fmt.Printf("Modified files: %v\n", result.ModifiedFiles)
}

if result.Warning != "" {
    fmt.Printf("Warning: %s\n", result.Warning)
}

// Checkout specific files from HEAD or a commit
result, err = gitInstance.Checkout(git.CheckoutWithFiles([]string{"src/main.go", "README.md"}))
if err != nil {
    log.Fatal(err)
}

// Checkout files from a specific commit
result, err = gitInstance.Checkout(
    git.CheckoutWithCommit("abc123"),
    git.CheckoutWithFiles([]string{"config.json"}),
)
if err != nil {
    log.Fatal(err)
}

Merge Operations with Conflict Resolution

// Attempt merge
result, err := gitInstance.Merge(git.MergeWithBranch("feature/auth"))
if err != nil {
    log.Fatal(err)
}

if result.Success {
    fmt.Printf("Merge successful: %s\n", result.MergedBranch)
} else {
    fmt.Printf("Merge conflicts detected in %d files\n", len(result.ConflictedFiles))
    
    // Show conflict details
    for _, conflict := range result.Conflicts {
        fmt.Printf("Conflict in %s:\n", conflict.Path)
        for i, section := range conflict.Sections {
            fmt.Printf("  Section %d: Our=%q, Their=%q\n", 
                i+1, section.OurContent, section.TheirContent)
        }
    }
    
    // Option 1: Resolve conflicts programmatically
    resolutions := []types.ConflictResolution{
        {
            FilePath: "conflicted-file.txt",
            UseOurs:  true, // or provide custom Resolution content
        },
    }
    
    err = gitInstance.ResolveConflicts(resolutions)
    if err != nil {
        log.Fatal(err)
    }
    
    // Continue merge
    err = gitInstance.MergeContinue()
    if err != nil {
        log.Fatal(err)
    }
}

Manual Conflict Resolution

Alternatively, conflicts can be resolved by manually editing files:

// Attempt merge
result, err := gitInstance.Merge(git.MergeWithBranch("feature/auth"))
if err != nil {
    log.Fatal(err)
}

if !result.Success && len(result.Conflicts) > 0 {
    fmt.Printf("Merge conflicts detected in %d files:\n", len(result.ConflictedFiles))
    
    for _, conflictFile := range result.ConflictedFiles {
        fmt.Printf("  - %s\n", conflictFile)
    }
    
    fmt.Println("\nPlease resolve conflicts manually:")
    fmt.Println("1. Edit the conflicted files to resolve conflicts")
    fmt.Println("2. Remove conflict markers (<<<<<<, ======, >>>>>>)")
    fmt.Println("3. Stage resolved files and continue merge")
    
    // Wait for manual resolution...
    // User edits files externally in IDE/editor
    
    // After manual resolution, stage the resolved files
    err = gitInstance.Add(result.ConflictedFiles)
    if err != nil {
        log.Fatal(err)
    }
    
    // Continue the merge
    err = gitInstance.MergeContinue()
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println("Merge completed successfully")
}

Configuration Management

Complete git configuration management with support for getting, setting, listing, and unsetting config values across different scopes.

// Set configuration values
err = gitInstance.SetConfig("user.name", "John Doe")
if err != nil {
    log.Fatal(err)
}

err = gitInstance.SetConfig("user.email", "john@example.com")
if err != nil {
    log.Fatal(err)
}

// Get configuration values
name, err := gitInstance.GetConfig("user.name")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("User name: %s\n", name)

// List all configuration entries
configs, err := gitInstance.ListConfig()
if err != nil {
    log.Fatal(err)
}

for _, config := range configs {
    fmt.Printf("%s=%s (scope: %s, source: %s)\n", 
        config.Key, config.Value, config.Scope, config.Source)
}

// Unset a configuration value
err = gitInstance.UnsetConfig("user.nickname")
if err != nil {
    log.Fatal(err)
}

Configuration Scopes

Work with different configuration scopes (local, global, system):

// Set local repository config
err = gitInstance.SetConfig("core.autocrlf", "false", git.ConfigWithLocalScope())
if err != nil {
    log.Fatal(err)
}

// Set global user config  
err = gitInstance.SetConfig("user.name", "John Doe", git.ConfigWithGlobalScope())
if err != nil {
    log.Fatal(err)
}

// Get config from specific scope
email, err := gitInstance.GetConfig("user.email", git.ConfigWithGlobalScope())
if err != nil {
    log.Fatal(err)
}

// List configs with scope and origin information
configs, err := gitInstance.ListConfig(git.ConfigWithAllScopes(), git.ConfigWithShowOrigin())
if err != nil {
    log.Fatal(err)
}

for _, config := range configs {
    fmt.Printf("[%s] %s=%s (from %s)\n", 
        config.Scope, config.Key, config.Value, config.Source)
}

Bare Repository Support

The library provides full support for bare repositories, commonly used for server-side Git operations:

// Create a bare repository
err := gitInstance.Init("/path/to/repo.git", git.InitWithBare())
if err != nil {
    log.Fatal(err)
}

// Clone as bare repository
err = gitInstance.Clone("https://github.com/user/repo.git", "local.git", 
    git.CloneWithBare())
if err != nil {
    log.Fatal(err)
}

// Check if repository is bare
isBare, err := gitInstance.IsBareRepository()
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Repository is bare: %v\n", isBare)

// All standard operations work in bare repositories
// (branches, tags, commits, etc. - just no working directory operations)
branches, err := gitInstance.ListBranches()
tags, err := gitInstance.ListTags()
err = gitInstance.CreateBranch("feature")
err = gitInstance.Tag("v1.0.0")

Bare repositories are ideal for:

  • Git servers (GitHub, GitLab, etc.)
  • Centralized repositories
  • Deployment targets
  • Backup repositories

Testing

Run tests:

go test ./...

Generate mocks for testing:

go install github.com/vektra/mockery/v2@latest
mockery --dir=pkg/git --name=Git
mockery --dir=pkg/git --name=Session

Examples

See the /examples directory for complete working examples:

  • 01_basic_operations.go - Basic Git operations
  • 02_session_management.go - Session management with persistent context
  • 03_merge_operations.go - Merge operations and conflict resolution
  • 04_branch_management.go - Branch creation, switching, and management

Run examples:

go run examples/01_basic_operations.go