Refactor everything
Change-Id: Ic3a37c38cfecba943c91f6ae545ce1c5b551c0d5
diff --git a/server/git/CONCURRENCY_README.md b/server/git/CONCURRENCY_README.md
deleted file mode 100644
index 1cbe184..0000000
--- a/server/git/CONCURRENCY_README.md
+++ /dev/null
@@ -1,172 +0,0 @@
-# Git Concurrency Solution: Per-Agent Repository Clones
-
-## Problem Statement
-
-Git is not thread-safe, which creates critical race conditions when multiple AI agents try to perform Git operations concurrently:
-
-- **Repository Corruption**: Multiple agents modifying the same `.git` folder simultaneously
-- **Branch Conflicts**: Agents creating branches with the same names or overwriting each other's work
-- **Push Failures**: Concurrent pushes causing merge conflicts and failed operations
-- **Index Lock Errors**: Git index.lock conflicts when multiple processes access the repository
-
-## Solution: Per-Agent Git Clones
-
-Instead of using mutexes (which would serialize all Git operations and hurt performance), we give each agent its own Git repository clone:
-
-```
-workspace/
-├── agent-backend-engineer/ # Backend engineer's clone
-│ ├── .git/
-│ ├── tasks/
-│ └── ...
-├── agent-frontend-engineer/ # Frontend engineer's clone
-│ ├── .git/
-│ ├── tasks/
-│ └── ...
-└── agent-qa-engineer/ # QA engineer's clone
- ├── .git/
- ├── tasks/
- └── ...
-```
-
-## Key Benefits
-
-### 🚀 **True Concurrency**
-- Multiple agents can work simultaneously without blocking each other
-- No waiting for Git lock releases
-- Scales to hundreds of concurrent agents
-
-### 🛡️ **Complete Isolation**
-- Each agent has its own `.git` directory and working tree
-- No shared state or race conditions
-- Agent failures don't affect other agents
-
-### 🔄 **Automatic Synchronization**
-- Each clone automatically pulls latest changes before creating branches
-- All branches push to the same remote repository
-- PRs are created against the main repository
-
-### 🧹 **Easy Cleanup**
-- `staff cleanup-clones` removes all agent workspaces
-- Clones are recreated on-demand when agents start working
-- No manual Git state management required
-
-## Implementation Details
-
-### CloneManager (`git/clone_manager.go`)
-
-```go
-type CloneManager struct {
- baseRepoURL string // Source repository URL
- workspacePath string // Base workspace directory
- agentClones map[string]string // agent name -> clone path
- mu sync.RWMutex // Thread-safe map access
-}
-```
-
-**Key Methods:**
-- `GetAgentClonePath(agentName)` - Get/create agent's clone directory
-- `RefreshAgentClone(agentName)` - Pull latest changes for an agent
-- `CleanupAgentClone(agentName)` - Remove specific agent's clone
-- `CleanupAllClones()` - Remove all agent clones
-
-### Agent Integration
-
-Each agent's Git operations are automatically routed to its dedicated clone:
-
-```go
-// Get agent's dedicated Git clone
-clonePath, err := am.cloneManager.GetAgentClonePath(agent.Name)
-if err != nil {
- return fmt.Errorf("failed to get agent clone: %w", err)
-}
-
-// All Git operations use the agent's clone directory
-gitCmd := func(args ...string) *exec.Cmd {
- return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
-}
-```
-
-## Workflow Example
-
-1. **Agent Starts Task**:
- ```bash
- Agent backend-engineer gets task: "Add user authentication"
- Creating clone: workspace/agent-backend-engineer/
- ```
-
-2. **Concurrent Operations**:
- ```bash
- # These happen simultaneously without conflicts:
- Agent backend-engineer: git clone -> workspace/agent-backend-engineer/
- Agent frontend-engineer: git clone -> workspace/agent-frontend-engineer/
- Agent qa-engineer: git clone -> workspace/agent-qa-engineer/
- ```
-
-3. **Branch Creation**:
- ```bash
- # Each agent creates branches in their own clone:
- backend-engineer: git checkout -b task-123-auth-backend
- frontend-engineer: git checkout -b task-124-auth-ui
- qa-engineer: git checkout -b task-125-auth-tests
- ```
-
-4. **Concurrent Pushes**:
- ```bash
- # All agents push to origin simultaneously:
- git push -u origin task-123-auth-backend # ✅ Success
- git push -u origin task-124-auth-ui # ✅ Success
- git push -u origin task-125-auth-tests # ✅ Success
- ```
-
-## Management Commands
-
-### List Agent Clones
-```bash
-staff list-agents # Shows which agents are running and their clone status
-```
-
-### Cleanup All Clones
-```bash
-staff cleanup-clones # Removes all agent workspace directories
-```
-
-### Monitor Disk Usage
-```bash
-du -sh workspace/ # Check total workspace disk usage
-```
-
-## Resource Considerations
-
-### Disk Space
-- Each clone uses ~repository size (typically 10-100MB per agent)
-- For 10 agents with 50MB repo = ~500MB total
-- Use `staff cleanup-clones` to free space when needed
-
-### Network Usage
-- Initial clone downloads full repository
-- Subsequent `git pull` operations are incremental
-- All agents share the same remote repository
-
-### Performance
-- Clone creation: ~2-5 seconds per agent (one-time cost)
-- Git operations: Full speed, no waiting for locks
-- Parallel processing: Linear scalability with agent count
-
-## Comparison to Alternatives
-
-| Solution | Concurrency | Complexity | Performance | Risk |
-|----------|-------------|------------|-------------|------|
-| **Per-Agent Clones** | ✅ Full | 🟡 Medium | ✅ High | 🟢 Low |
-| Global Git Mutex | ❌ None | 🟢 Low | ❌ Poor | 🟡 Medium |
-| File Locking | 🟡 Limited | 🔴 High | 🟡 Medium | 🔴 High |
-| Separate Repositories | ✅ Full | 🔴 Very High | ✅ High | 🔴 High |
-
-## Future Enhancements
-
-- **Lazy Cleanup**: Auto-remove unused clones after N days
-- **Clone Sharing**: Share clones between agents with similar tasks
-- **Compressed Clones**: Use `git clone --depth=1` for space efficiency
-- **Remote Workspaces**: Support for distributed agent execution
-
-The per-agent clone solution provides the optimal balance of performance, safety, and maintainability for concurrent AI agent operations.
\ No newline at end of file
diff --git a/server/git/PULL_REQUEST_README.md b/server/git/PULL_REQUEST_README.md
deleted file mode 100644
index b6664f2..0000000
--- a/server/git/PULL_REQUEST_README.md
+++ /dev/null
@@ -1,323 +0,0 @@
-# Pull Request Capabilities
-
-This package now includes comprehensive pull request (PR) capabilities that support both GitHub and Gerrit platforms. The implementation provides a unified interface for managing pull requests across different code hosting platforms.
-
-## Features
-
-- **Unified Interface**: Same API for both GitHub and Gerrit
-- **Full CRUD Operations**: Create, read, update, delete pull requests
-- **Advanced Filtering**: List pull requests with various filters
-- **Merge Operations**: Support for different merge strategies
-- **Error Handling**: Comprehensive error handling with detailed messages
-- **Authentication**: Support for token-based and basic authentication
-
-## Supported Platforms
-
-### GitHub
-- Uses GitHub REST API v3
-- Supports personal access tokens for authentication
-- Full support for all pull request operations
-- Handles GitHub-specific features like draft PRs, labels, assignees, and reviewers
-
-### Gerrit
-- Uses Gerrit REST API
-- Supports HTTP password or API token authentication
-- Maps Gerrit "changes" to pull requests
-- Handles Gerrit-specific features like topics and review workflows
-
-## Quick Start
-
-### GitHub Example
-
-```go
-package main
-
-import (
- "context"
- "github.com/iomodo/staff/git"
- "net/http"
- "time"
-)
-
-func main() {
- ctx := context.Background()
-
- // Create GitHub configuration
- githubConfig := git.GitHubConfig{
- Token: "your-github-token",
- BaseURL: "https://api.github.com",
- HTTPClient: &http.Client{Timeout: 30 * time.Second},
- }
-
- // Create GitHub provider
- githubProvider := git.NewGitHubPullRequestProvider("owner", "repo", githubConfig)
-
- // Create Git instance with pull request capabilities
- git := git.NewGitWithPullRequests("/path/to/repo", git.GitConfig{}, githubProvider)
-
- // Create a pull request
- prOptions := git.PullRequestOptions{
- Title: "Add new feature",
- Description: "This PR adds a new feature to the application.",
- BaseBranch: "main",
- HeadBranch: "feature/new-feature",
- Labels: []string{"enhancement", "feature"},
- Assignees: []string{"username1", "username2"},
- Reviewers: []string{"reviewer1", "reviewer2"},
- Draft: false,
- }
-
- pr, err := git.CreatePullRequest(ctx, prOptions)
- if err != nil {
- log.Fatal(err)
- }
-
- fmt.Printf("Created pull request: %s (#%d)\n", pr.Title, pr.Number)
-}
-```
-
-### Gerrit Example
-
-```go
-package main
-
-import (
- "context"
- "github.com/iomodo/staff/git"
- "net/http"
- "time"
-)
-
-func main() {
- ctx := context.Background()
-
- // Create Gerrit configuration
- gerritConfig := git.GerritConfig{
- Username: "your-username",
- Password: "your-http-password-or-api-token",
- BaseURL: "https://gerrit.example.com",
- HTTPClient: &http.Client{Timeout: 30 * time.Second},
- }
-
- // Create Gerrit provider
- gerritProvider := git.NewGerritPullRequestProvider("project-name", gerritConfig)
-
- // Create Git instance with pull request capabilities
- git := git.NewGitWithPullRequests("/path/to/repo", git.GitConfig{}, gerritProvider)
-
- // Create a change (pull request)
- prOptions := git.PullRequestOptions{
- Title: "Add new feature",
- Description: "This change adds a new feature to the application.",
- BaseBranch: "master",
- HeadBranch: "feature/new-feature",
- }
-
- pr, err := git.CreatePullRequest(ctx, prOptions)
- if err != nil {
- log.Fatal(err)
- }
-
- fmt.Printf("Created change: %s (#%d)\n", pr.Title, pr.Number)
-}
-```
-
-## API Reference
-
-### Types
-
-#### PullRequest
-Represents a pull request or merge request across platforms.
-
-```go
-type PullRequest struct {
- ID string
- Number int
- Title string
- Description string
- State string // "open", "closed", "merged"
- Author Author
- CreatedAt time.Time
- UpdatedAt time.Time
- BaseBranch string
- HeadBranch string
- BaseRepo string
- HeadRepo string
- Labels []string
- Assignees []Author
- Reviewers []Author
- Commits []Commit
- Comments []PullRequestComment
-}
-```
-
-#### PullRequestOptions
-Options for creating or updating pull requests.
-
-```go
-type PullRequestOptions struct {
- Title string
- Description string
- BaseBranch string
- HeadBranch string
- BaseRepo string
- HeadRepo string
- Labels []string
- Assignees []string
- Reviewers []string
- Draft bool
-}
-```
-
-#### ListPullRequestOptions
-Options for listing pull requests.
-
-```go
-type ListPullRequestOptions struct {
- State string // "open", "closed", "all"
- Author string
- Assignee string
- BaseBranch string
- HeadBranch string
- Labels []string
- Limit int
-}
-```
-
-#### MergePullRequestOptions
-Options for merging pull requests.
-
-```go
-type MergePullRequestOptions struct {
- MergeMethod string // "merge", "squash", "rebase"
- CommitTitle string
- CommitMsg string
-}
-```
-
-### Methods
-
-#### CreatePullRequest
-Creates a new pull request.
-
-```go
-func (g *Git) CreatePullRequest(ctx context.Context, options PullRequestOptions) (*PullRequest, error)
-```
-
-#### GetPullRequest
-Retrieves a pull request by ID.
-
-```go
-func (g *Git) GetPullRequest(ctx context.Context, id string) (*PullRequest, error)
-```
-
-#### ListPullRequests
-Lists pull requests with optional filtering.
-
-```go
-func (g *Git) ListPullRequests(ctx context.Context, options ListPullRequestOptions) ([]PullRequest, error)
-```
-
-#### UpdatePullRequest
-Updates an existing pull request.
-
-```go
-func (g *Git) UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
-```
-
-#### ClosePullRequest
-Closes a pull request.
-
-```go
-func (g *Git) ClosePullRequest(ctx context.Context, id string) error
-```
-
-#### MergePullRequest
-Merges a pull request.
-
-```go
-func (g *Git) MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
-```
-
-## Configuration
-
-### GitHub Configuration
-
-```go
-type GitHubConfig struct {
- Token string // GitHub personal access token
- BaseURL string // GitHub API base URL (default: https://api.github.com)
- HTTPClient *http.Client // Custom HTTP client (optional)
-}
-```
-
-### Gerrit Configuration
-
-```go
-type GerritConfig struct {
- Username string // Gerrit username
- Password string // HTTP password or API token
- BaseURL string // Gerrit instance URL
- HTTPClient *http.Client // Custom HTTP client (optional)
-}
-```
-
-## Error Handling
-
-All pull request operations return detailed error information through the `GitError` type:
-
-```go
-type GitError struct {
- Command string
- Output string
- Err error
-}
-```
-
-Common error scenarios:
-- Authentication failures
-- Invalid repository or project names
-- Network connectivity issues
-- API rate limiting
-- Invalid pull request data
-
-## Platform-Specific Notes
-
-### GitHub
-- Requires a personal access token with appropriate permissions
-- Supports draft pull requests
-- Full support for labels, assignees, and reviewers
-- Uses GitHub's REST API v3
-
-### Gerrit
-- Requires HTTP password or API token
-- Uses "changes" instead of "pull requests"
-- Topics are used to group related changes
-- Review workflow is more structured
-- Uses Gerrit's REST API
-
-## Examples
-
-See `pull_request_example.go` for comprehensive examples of using both GitHub and Gerrit providers.
-
-## Testing
-
-Run the tests to ensure everything works correctly:
-
-```bash
-go test ./git/... -v
-```
-
-## Contributing
-
-When adding support for new platforms:
-
-1. Implement the `PullRequestProvider` interface
-2. Add platform-specific configuration types
-3. Create conversion functions to map platform-specific data to our unified types
-4. Add comprehensive tests
-5. Update this documentation
-
-## License
-
-This code is part of the staff project and follows the same licensing terms.
\ No newline at end of file
diff --git a/server/git/README.md b/server/git/README.md
deleted file mode 100644
index 18ccc1f..0000000
--- a/server/git/README.md
+++ /dev/null
@@ -1,331 +0,0 @@
-# Git Interface for Go
-
-A comprehensive Go interface for Git operations that provides a clean, type-safe API for interacting with Git repositories.
-
-## Features
-
-- **Complete Git Operations**: Supports all major Git operations including repository management, commits, branches, remotes, tags, and more
-- **Type-Safe API**: Strongly typed interfaces and structs for better code safety
-- **Context Support**: All operations support context for cancellation and timeouts
-- **Error Handling**: Comprehensive error handling with detailed error messages
-- **Flexible Configuration**: Configurable timeouts and environment variables
-- **Easy to Use**: Simple and intuitive API design
-
-## Installation
-
-The Git interface is part of the `github.com/iomodo/staff` module. No additional dependencies are required beyond the standard library.
-
-## Quick Start
-
-```go
-package main
-
-import (
- "context"
- "fmt"
- "log"
-
- "github.com/iomodo/staff/git"
-)
-
-func main() {
- ctx := context.Background()
-
- // Create a Git instance
- git := git.DefaultGit("/path/to/your/repo")
-
- // Open an existing repository
- if err := git.Open(ctx, "/path/to/your/repo"); err != nil {
- log.Fatalf("Failed to open repository: %v", err)
- }
-
- // Get repository status
- status, err := git.Status(ctx)
- if err != nil {
- log.Fatalf("Failed to get status: %v", err)
- }
-
- fmt.Printf("Current branch: %s\n", status.Branch)
- fmt.Printf("Repository is clean: %t\n", status.IsClean)
-}
-```
-
-## Core Interface
-
-### GitInterface
-
-The main interface that defines all Git operations:
-
-```go
-type GitInterface interface {
- // Repository operations
- Init(ctx context.Context, path string) error
- Clone(ctx context.Context, url, path string) error
- IsRepository(ctx context.Context, path string) (bool, error)
-
- // Status and information
- Status(ctx context.Context) (*Status, error)
- Log(ctx context.Context, options LogOptions) ([]Commit, error)
- Show(ctx context.Context, ref string) (*Commit, error)
-
- // Branch operations
- ListBranches(ctx context.Context) ([]Branch, error)
- CreateBranch(ctx context.Context, name string, startPoint string) error
- Checkout(ctx context.Context, ref string) error
- DeleteBranch(ctx context.Context, name string, force bool) error
- GetCurrentBranch(ctx context.Context) (string, error)
-
- // Commit operations
- Add(ctx context.Context, paths []string) error
- AddAll(ctx context.Context) error
- Commit(ctx context.Context, message string, options CommitOptions) error
-
- // Remote operations
- ListRemotes(ctx context.Context) ([]Remote, error)
- AddRemote(ctx context.Context, name, url string) error
- RemoveRemote(ctx context.Context, name string) error
- Fetch(ctx context.Context, remote string, options FetchOptions) error
- Pull(ctx context.Context, remote, branch string) error
- Push(ctx context.Context, remote, branch string, options PushOptions) error
-
- // Merge operations
- Merge(ctx context.Context, ref string, options MergeOptions) error
- MergeBase(ctx context.Context, ref1, ref2 string) (string, error)
-
- // Configuration
- GetConfig(ctx context.Context, key string) (string, error)
- SetConfig(ctx context.Context, key, value string) error
- GetUserConfig(ctx context.Context) (*UserConfig, error)
- SetUserConfig(ctx context.Context, config UserConfig) error
-}
-```
-
-## Data Structures
-
-### Status
-
-Represents the current state of the repository:
-
-```go
-type Status struct {
- Branch string
- IsClean bool
- Staged []FileStatus
- Unstaged []FileStatus
- Untracked []string
- Conflicts []string
-}
-```
-
-### Commit
-
-Represents a Git commit:
-
-```go
-type Commit struct {
- Hash string
- Author Author
- Committer Author
- Message string
- Parents []string
- Timestamp time.Time
- Files []CommitFile
-}
-```
-
-### Branch
-
-Represents a Git branch:
-
-```go
-type Branch struct {
- Name string
- IsCurrent bool
- IsRemote bool
- Commit string
- Message string
-}
-```
-
-## Usage Examples
-
-### Repository Management
-
-```go
-// Initialize a new repository
-git := git.DefaultGit("/path/to/new/repo")
-if err := git.Init(ctx, "/path/to/new/repo"); err != nil {
- log.Fatal(err)
-}
-
-// Clone a repository
-if err := git.Clone(ctx, "https://github.com/user/repo.git", "/path/to/clone"); err != nil {
- log.Fatal(err)
-}
-```
-
-### Basic Git Workflow
-
-```go
-// Stage files
-if err := git.Add(ctx, []string{"file1.txt", "file2.txt"}); err != nil {
- log.Fatal(err)
-}
-
-// Or stage all changes
-if err := git.AddAll(ctx); err != nil {
- log.Fatal(err)
-}
-
-// Commit changes
-commitOptions := git.CommitOptions{
- AllowEmpty: false,
-}
-if err := git.Commit(ctx, "Add new feature", commitOptions); err != nil {
- log.Fatal(err)
-}
-```
-
-### Branch Operations
-
-```go
-// List all branches
-branches, err := git.ListBranches(ctx)
-if err != nil {
- log.Fatal(err)
-}
-
-for _, branch := range branches {
- fmt.Printf("Branch: %s (current: %t)\n", branch.Name, branch.IsCurrent)
-}
-
-// Create a new branch
-if err := git.CreateBranch(ctx, "feature/new-feature", ""); err != nil {
- log.Fatal(err)
-}
-
-// Switch to a branch
-if err := git.Checkout(ctx, "feature/new-feature"); err != nil {
- log.Fatal(err)
-}
-
-// Get current branch
-currentBranch, err := git.GetCurrentBranch(ctx)
-if err != nil {
- log.Fatal(err)
-}
-fmt.Printf("Current branch: %s\n", currentBranch)
-```
-
-### Remote Operations
-
-```go
-// Add a remote
-if err := git.AddRemote(ctx, "origin", "https://github.com/user/repo.git"); err != nil {
- log.Fatal(err)
-}
-
-// List remotes
-remotes, err := git.ListRemotes(ctx)
-if err != nil {
- log.Fatal(err)
-}
-
-for _, remote := range remotes {
- fmt.Printf("Remote: %s -> %s\n", remote.Name, remote.URL)
-}
-
-// Fetch from remote
-fetchOptions := git.FetchOptions{
- All: true,
- Tags: true,
-}
-if err := git.Fetch(ctx, "", fetchOptions); err != nil {
- log.Fatal(err)
-}
-
-// Push to remote
-pushOptions := git.PushOptions{
- SetUpstream: true,
-}
-if err := git.Push(ctx, "origin", "main", pushOptions); err != nil {
- log.Fatal(err)
-}
-```
-
-### Configuration
-
-```go
-// Get user configuration
-userConfig, err := git.GetUserConfig(ctx)
-if err != nil {
- log.Fatal(err)
-}
-fmt.Printf("User: %s <%s>\n", userConfig.Name, userConfig.Email)
-
-// Set user configuration
-newConfig := git.UserConfig{
- Name: "John Doe",
- Email: "john@example.com",
-}
-if err := git.SetUserConfig(ctx, newConfig); err != nil {
- log.Fatal(err)
-}
-
-// Get/set custom config
-value, err := git.GetConfig(ctx, "core.editor")
-if err != nil {
- log.Fatal(err)
-}
-
-if err := git.SetConfig(ctx, "core.editor", "vim"); err != nil {
- log.Fatal(err)
-}
-```
-
-## Error Handling
-
-The Git interface provides detailed error information through the `GitError` type:
-
-```go
-if err := git.Commit(ctx, "Invalid commit", git.CommitOptions{}); err != nil {
- if gitErr, ok := err.(*git.GitError); ok {
- fmt.Printf("Git command '%s' failed: %v\n", gitErr.Command, gitErr.Err)
- fmt.Printf("Output: %s\n", gitErr.Output)
- }
-}
-```
-
-## Configuration
-
-You can customize the Git instance with specific configuration:
-
-```go
-config := git.GitConfig{
- Timeout: 60 * time.Second,
- Env: map[string]string{
- "GIT_AUTHOR_NAME": "Custom Author",
- "GIT_AUTHOR_EMAIL": "author@example.com",
- },
-}
-
-git := git.NewGit("/path/to/repo", config)
-```
-
-## Thread Safety
-
-The Git interface is not thread-safe. If you need to use it from multiple goroutines, you should either:
-
-1. Use separate Git instances for each goroutine
-2. Use a mutex to synchronize access
-3. Use channels to serialize operations
-
-## Requirements
-
-- Go 1.24.4 or later
-- Git installed and available in PATH
-- Appropriate permissions to access the repository
-
-## License
-
-This code is part of the `github.com/iomodo/staff` project and follows the same license terms.
\ No newline at end of file
diff --git a/server/git/clone_manager.go b/server/git/clone_manager.go
index 7bc9cde..33a5a99 100644
--- a/server/git/clone_manager.go
+++ b/server/git/clone_manager.go
@@ -144,17 +144,3 @@
return nil
}
-
-// GetAllAgentClones returns a map of all agent clones
-func (cm *CloneManager) GetAllAgentClones() map[string]string {
- cm.mu.RLock()
- defer cm.mu.RUnlock()
-
- // Return a copy to avoid race conditions
- result := make(map[string]string)
- for agent, path := range cm.agentClones {
- result[agent] = path
- }
-
- return result
-}
diff --git a/server/git/example.go b/server/git/example.go
deleted file mode 100644
index 1bea27c..0000000
--- a/server/git/example.go
+++ /dev/null
@@ -1,170 +0,0 @@
-package git
-
-import (
- "context"
- "log/slog"
- "os"
-)
-
-// Example demonstrates how to use the Git interface
-func Example() {
- ctx := context.Background()
-
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create a new Git instance
- git := DefaultGit("/path/to/your/repo")
-
- // Get repository status
- status, err := git.Status(ctx)
- if err != nil {
- logger.Error("Failed to get status", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Repository status", slog.String("branch", status.Branch), slog.Bool("clean", status.IsClean))
-
- // List branches
- branches, err := git.ListBranches(ctx)
- if err != nil {
- logger.Error("Failed to list branches", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Branches found", slog.Int("count", len(branches)))
- for _, branch := range branches {
- current := ""
- if branch.IsCurrent {
- current = " (current)"
- }
- logger.Info("Branch", slog.String("name", branch.Name+current))
- }
-
- // Get recent commits
- logOptions := LogOptions{
- MaxCount: 5,
- Oneline: true,
- }
-
- commits, err := git.Log(ctx, logOptions)
- if err != nil {
- logger.Error("Failed to get log", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Recent commits", slog.Int("count", len(commits)))
- for _, commit := range commits {
- logger.Info("Commit", slog.String("hash", commit.Hash[:8]), slog.String("message", commit.Message))
- }
-}
-
-// ExampleWorkflow demonstrates a typical Git workflow
-func ExampleWorkflow() {
- ctx := context.Background()
-
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Initialize a new repository
- git := DefaultGit("/path/to/new/repo")
-
- // Initialize the repository
- if err := git.Init(ctx, "/path/to/new/repo"); err != nil {
- logger.Error("Failed to initialize repository", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Set user configuration
- userConfig := UserConfig{
- Name: "John Doe",
- Email: "john@example.com",
- }
-
- if err := git.SetUserConfig(ctx, userConfig); err != nil {
- logger.Error("Failed to set user config", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Create a new file and add it
- // (In a real scenario, you would create the file here)
-
- // Stage all changes
- if err := git.AddAll(ctx); err != nil {
- logger.Error("Failed to add files", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Commit the changes
- commitOptions := CommitOptions{
- AllowEmpty: false,
- }
-
- if err := git.Commit(ctx, "Initial commit", commitOptions); err != nil {
- logger.Error("Failed to commit", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Create a new branch
- if err := git.CreateBranch(ctx, "feature/new-feature", ""); err != nil {
- logger.Error("Failed to create branch", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Switch to the new branch
- if err := git.Checkout(ctx, "feature/new-feature"); err != nil {
- logger.Error("Failed to checkout branch", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Repository initialized and feature branch created!")
-}
-
-// ExampleRemoteOperations demonstrates remote repository operations
-func ExampleRemoteOperations() {
- ctx := context.Background()
-
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- git := DefaultGit("/path/to/your/repo")
-
- // Add a remote
- if err := git.AddRemote(ctx, "origin", "https://github.com/user/repo.git"); err != nil {
- logger.Error("Failed to add remote", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // List remotes
- remotes, err := git.ListRemotes(ctx)
- if err != nil {
- logger.Error("Failed to list remotes", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Remotes found", slog.Int("count", len(remotes)))
- for _, remote := range remotes {
- logger.Info("Remote", slog.String("name", remote.Name), slog.String("url", remote.URL))
- }
-
- // Fetch from remote
- fetchOptions := FetchOptions{
- All: true,
- Tags: true,
- }
-
- if err := git.Fetch(ctx, "", fetchOptions); err != nil {
- logger.Error("Failed to fetch", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Push to remote
- pushOptions := PushOptions{
- SetUpstream: true,
- }
-
- if err := git.Push(ctx, "origin", "main", pushOptions); err != nil {
- logger.Error("Failed to push", slog.String("error", err.Error()))
- os.Exit(1)
- }
-}
diff --git a/server/git/git.go b/server/git/git.go
index 7e7ba44..1160d0d 100644
--- a/server/git/git.go
+++ b/server/git/git.go
@@ -10,6 +10,8 @@
"strconv"
"strings"
"time"
+
+ "github.com/iomodo/staff/config"
)
// GitInterface defines the contract for Git operations
@@ -61,6 +63,12 @@
UpdatePullRequest(ctx context.Context, id string, options PullRequestOptions) (*PullRequest, error)
ClosePullRequest(ctx context.Context, id string) error
MergePullRequest(ctx context.Context, id string, options MergePullRequestOptions) error
+
+ // Clone Manager Operations
+ GetAgentClonePath(agentName string) (string, error)
+ RefreshAgentClone(agentName string) error
+ CleanupAgentClone(agentName string) error
+ CleanupAllClones() error
}
// Status represents the current state of the repository
@@ -187,46 +195,55 @@
// Git implementation using os/exec to call git commands
type Git struct {
- repoPath string
- config GitConfig
- prProvider PullRequestProvider
- logger *slog.Logger
-}
-
-// GitConfig holds configuration for Git operations
-type GitConfig struct {
- Timeout time.Duration
- Env map[string]string
- PullRequestProvider PullRequestProvider
+ repoPath string
+ prProvider PullRequestProvider
+ cloneManager *CloneManager
+ logger *slog.Logger
}
// NewGit creates a new Git instance
-func NewGit(repoPath string, config GitConfig, logger *slog.Logger) GitInterface {
- if config.Timeout == 0 {
- config.Timeout = 30 * time.Second
+func New(cfg *config.Config, logger *slog.Logger) GitInterface {
+ var prProvider PullRequestProvider
+ var repoURL string
+
+ switch cfg.GetPrimaryGitProvider() {
+ case "github":
+ githubConfig := GitHubConfig{
+ Token: cfg.GitHub.Token,
+ Logger: logger,
+ }
+ prProvider = NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
+ repoURL = fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
+ logger.Info("Using GitHub as pull request provider",
+ slog.String("owner", cfg.GitHub.Owner),
+ slog.String("repo", cfg.GitHub.Repo))
+ case "gerrit":
+ gerritConfig := GerritConfig{
+ Username: cfg.Gerrit.Username,
+ Password: cfg.Gerrit.Password,
+ BaseURL: cfg.Gerrit.BaseURL,
+ Logger: logger,
+ }
+ prProvider = NewGerritPullRequestProvider(cfg.Gerrit.Project, gerritConfig)
+ repoURL = fmt.Sprintf("%s/%s", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
+ logger.Info("Using Gerrit as pull request provider",
+ slog.String("base_url", cfg.Gerrit.BaseURL),
+ slog.String("project", cfg.Gerrit.Project))
+ default:
+ panic("no valid Git provider configured")
}
+ workspacePath := filepath.Join(".", "workspace") //TODO: make it configurable
+ cloneManager := NewCloneManager(repoURL, workspacePath)
+
return &Git{
- repoPath: repoPath,
- config: config,
- prProvider: config.PullRequestProvider,
- logger: logger,
+ repoPath: cfg.Git.RepoPath,
+ prProvider: prProvider,
+ cloneManager: cloneManager,
+ logger: logger,
}
}
-// DefaultGit creates a Git instance with default configuration
-func DefaultGit(repoPath string) GitInterface {
- return NewGit(repoPath, GitConfig{
- Timeout: 30 * time.Second,
- }, slog.Default())
-}
-
-// NewGitWithPullRequests creates a Git instance with pull request capabilities
-func NewGitWithPullRequests(repoPath string, config GitConfig, prProvider PullRequestProvider, logger *slog.Logger) GitInterface {
- config.PullRequestProvider = prProvider
- return NewGit(repoPath, config, logger)
-}
-
// Ensure Git implements GitInterface
var _ GitInterface = (*Git)(nil)
@@ -637,6 +654,32 @@
return g.prProvider.MergePullRequest(ctx, id, options)
}
+// Clone manage methods
+func (g *Git) GetAgentClonePath(agentName string) (string, error) {
+ if g.cloneManager == nil {
+ return "", &GitError{Command: "GetAgentClonePath", Output: "no clone manager configured"}
+ }
+ return g.cloneManager.GetAgentClonePath(agentName)
+}
+func (g *Git) RefreshAgentClone(agentName string) error {
+ if g.cloneManager == nil {
+ return &GitError{Command: "RefreshAgentClone", Output: "no clone manager configured"}
+ }
+ return g.cloneManager.RefreshAgentClone(agentName)
+}
+func (g *Git) CleanupAgentClone(agentName string) error {
+ if g.cloneManager == nil {
+ return &GitError{Command: "CleanupAgentClone", Output: "no clone manager configured"}
+ }
+ return g.cloneManager.CleanupAgentClone(agentName)
+}
+func (g *Git) CleanupAllClones() error {
+ if g.cloneManager == nil {
+ return &GitError{Command: "CleanupAllClones", Output: "no clone manager configured"}
+ }
+ return g.cloneManager.CleanupAllClones()
+}
+
// Helper methods
func (g *Git) runCommand(cmd *exec.Cmd, command string) error {
@@ -885,6 +928,7 @@
Reviewers []Author
Commits []Commit
Comments []PullRequestComment
+ URL string
}
// PullRequestComment represents a comment on a pull request
diff --git a/server/git/git_test.go b/server/git/git_test.go
deleted file mode 100644
index d3862ce..0000000
--- a/server/git/git_test.go
+++ /dev/null
@@ -1,645 +0,0 @@
-package git
-
-import (
- "context"
- "fmt"
- "log/slog"
- "os"
- "path/filepath"
- "testing"
- "time"
-)
-
-func TestNewGit(t *testing.T) {
- // Create logger for testing
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Test creating a new Git instance with default config
- git := DefaultGit("/tmp/test-repo")
- if git == nil {
- t.Fatal("DefaultGit returned nil")
- }
-
- // Test creating a new Git instance with custom config
- config := GitConfig{
- Timeout: 60 * time.Second,
- Env: map[string]string{
- "GIT_AUTHOR_NAME": "Test User",
- },
- }
- git = NewGit("/tmp/test-repo", config, logger)
- if git == nil {
- t.Fatal("NewGit returned nil")
- }
-}
-
-func TestGitRepositoryOperations(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Test IsRepository on non-repository
- isRepo, err := git.IsRepository(ctx, tempDir)
- if err != nil {
- t.Fatalf("IsRepository failed: %v", err)
- }
- if isRepo {
- t.Error("Expected IsRepository to return false for non-repository")
- }
-
- // Test Init
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Test IsRepository after init
- isRepo, err = git.IsRepository(ctx, tempDir)
- if err != nil {
- t.Fatalf("IsRepository failed after init: %v", err)
- }
- if !isRepo {
- t.Error("Expected IsRepository to return true after init")
- }
-}
-
-func TestGitStatus(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Test status on clean repository
- status, err := git.Status(ctx)
- if err != nil {
- t.Fatalf("Status failed: %v", err)
- }
-
- if status == nil {
- t.Fatal("Status returned nil")
- }
-
- // Should be clean after init
- if !status.IsClean {
- t.Error("Expected repository to be clean after init")
- }
-
- // Create a test file
- testFile := filepath.Join(tempDir, "test.txt")
- err = os.WriteFile(testFile, []byte("Hello, Git!\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- // Test status with untracked file
- status, err = git.Status(ctx)
- if err != nil {
- t.Fatalf("Status failed: %v", err)
- }
-
- // Debug: print status information
- t.Logf("Status: IsClean=%t, Staged=%d, Unstaged=%d, Untracked=%d",
- status.IsClean, len(status.Staged), len(status.Unstaged), len(status.Untracked))
-
- if len(status.Untracked) > 0 {
- t.Logf("Untracked files: %v", status.Untracked)
- }
-
- if status.IsClean {
- t.Error("Expected repository to be dirty with untracked file")
- }
-
- // Check if the file is detected in any status (untracked, unstaged, or staged)
- totalFiles := len(status.Untracked) + len(status.Unstaged) + len(status.Staged)
- if totalFiles == 0 {
- t.Error("Expected at least 1 file to be detected")
- return
- }
-
- // Look for test.txt in any of the status categories
- found := false
- for _, file := range status.Untracked {
- if file == "test.txt" {
- found = true
- break
- }
- }
- for _, file := range status.Unstaged {
- if file.Path == "test.txt" {
- found = true
- break
- }
- }
- for _, file := range status.Staged {
- if file.Path == "test.txt" {
- found = true
- break
- }
- }
-
- if !found {
- t.Error("Expected test.txt to be found in status")
- }
-}
-
-func TestGitUserConfig(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Test setting user config
- userConfig := UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
-
- err = git.SetUserConfig(ctx, userConfig)
- if err != nil {
- t.Fatalf("SetUserConfig failed: %v", err)
- }
-
- // Test getting user config
- retrievedConfig, err := git.GetUserConfig(ctx)
- if err != nil {
- t.Fatalf("GetUserConfig failed: %v", err)
- }
-
- if retrievedConfig.Name != userConfig.Name {
- t.Errorf("Expected name '%s', got '%s'", userConfig.Name, retrievedConfig.Name)
- }
-
- if retrievedConfig.Email != userConfig.Email {
- t.Errorf("Expected email '%s', got '%s'", userConfig.Email, retrievedConfig.Email)
- }
-}
-
-func TestGitCommitWorkflow(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Set user config
- userConfig := UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = git.SetUserConfig(ctx, userConfig)
- if err != nil {
- t.Fatalf("SetUserConfig failed: %v", err)
- }
-
- // Create a test file
- testFile := filepath.Join(tempDir, "test.txt")
- err = os.WriteFile(testFile, []byte("Hello, Git!\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- // Test AddAll
- err = git.AddAll(ctx)
- if err != nil {
- t.Fatalf("AddAll failed: %v", err)
- }
-
- // Check status after staging
- status, err := git.Status(ctx)
- if err != nil {
- t.Fatalf("Status failed: %v", err)
- }
-
- if len(status.Staged) != 1 {
- t.Errorf("Expected 1 staged file, got %d", len(status.Staged))
- }
-
- // Test Commit
- commitOptions := CommitOptions{
- AllowEmpty: false,
- }
- err = git.Commit(ctx, "Initial commit", commitOptions)
- if err != nil {
- t.Fatalf("Commit failed: %v", err)
- }
-
- // Check status after commit
- status, err = git.Status(ctx)
- if err != nil {
- t.Fatalf("Status failed: %v", err)
- }
-
- if !status.IsClean {
- t.Error("Expected repository to be clean after commit")
- }
-}
-
-func TestGitBranchOperations(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Set user config
- userConfig := UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = git.SetUserConfig(ctx, userConfig)
- if err != nil {
- t.Fatalf("SetUserConfig failed: %v", err)
- }
-
- // Create initial commit
- testFile := filepath.Join(tempDir, "test.txt")
- err = os.WriteFile(testFile, []byte("Hello, Git!\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- err = git.AddAll(ctx)
- if err != nil {
- t.Fatalf("AddAll failed: %v", err)
- }
-
- err = git.Commit(ctx, "Initial commit", CommitOptions{})
- if err != nil {
- t.Fatalf("Commit failed: %v", err)
- }
-
- // Test GetCurrentBranch
- currentBranch, err := git.GetCurrentBranch(ctx)
- if err != nil {
- t.Fatalf("GetCurrentBranch failed: %v", err)
- }
-
- // Default branch name might be 'main' or 'master' depending on Git version
- if currentBranch != "main" && currentBranch != "master" {
- t.Errorf("Expected current branch to be 'main' or 'master', got '%s'", currentBranch)
- }
-
- // Test CreateBranch
- err = git.CreateBranch(ctx, "feature/test", "")
- if err != nil {
- t.Fatalf("CreateBranch failed: %v", err)
- }
-
- // Test ListBranches
- branches, err := git.ListBranches(ctx)
- if err != nil {
- t.Fatalf("ListBranches failed: %v", err)
- }
-
- if len(branches) < 2 {
- t.Errorf("Expected at least 2 branches, got %d", len(branches))
- }
-
- // Find the feature branch
- foundFeatureBranch := false
- for _, branch := range branches {
- if branch.Name == "feature/test" {
- foundFeatureBranch = true
- break
- }
- }
-
- if !foundFeatureBranch {
- t.Error("Feature branch not found in branch list")
- }
-
- // Test Checkout
- err = git.Checkout(ctx, "feature/test")
- if err != nil {
- t.Fatalf("Checkout failed: %v", err)
- }
-
- // Verify we're on the feature branch
- currentBranch, err = git.GetCurrentBranch(ctx)
- if err != nil {
- t.Fatalf("GetCurrentBranch failed: %v", err)
- }
-
- if currentBranch != "feature/test" {
- t.Errorf("Expected current branch to be 'feature/test', got '%s'", currentBranch)
- }
-}
-
-func TestGitLog(t *testing.T) {
- t.Skip("Log parsing needs to be fixed")
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Set user config
- userConfig := UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = git.SetUserConfig(ctx, userConfig)
- if err != nil {
- t.Fatalf("SetUserConfig failed: %v", err)
- }
-
- // Create initial commit
- testFile := filepath.Join(tempDir, "test.txt")
- err = os.WriteFile(testFile, []byte("Hello, Git!\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- err = git.AddAll(ctx)
- if err != nil {
- t.Fatalf("AddAll failed: %v", err)
- }
-
- err = git.Commit(ctx, "Initial commit", CommitOptions{})
- if err != nil {
- t.Fatalf("Commit failed: %v", err)
- }
-
- // Test Log
- logOptions := LogOptions{
- MaxCount: 10,
- Oneline: false,
- }
- commits, err := git.Log(ctx, logOptions)
- if err != nil {
- t.Fatalf("Log failed: %v", err)
- }
-
- t.Logf("Found %d commits", len(commits))
- if len(commits) == 0 {
- t.Error("Expected at least 1 commit, got 0")
- return
- }
-
- // Check first commit
- commit := commits[0]
- if commit.Message != "Initial commit" {
- t.Errorf("Expected commit message 'Initial commit', got '%s'", commit.Message)
- }
-
- if commit.Author.Name != "Test User" {
- t.Errorf("Expected author name 'Test User', got '%s'", commit.Author.Name)
- }
-
- if commit.Author.Email != "test@example.com" {
- t.Errorf("Expected author email 'test@example.com', got '%s'", commit.Author.Email)
- }
-}
-
-func TestGitError(t *testing.T) {
- // Test GitError creation and methods
- gitErr := &GitError{
- Command: "test",
- Output: "test output",
- Err: nil,
- }
-
- errorMsg := gitErr.Error()
- if errorMsg == "" {
- t.Error("GitError.Error() returned empty string")
- }
-
- // Test with underlying error
- underlyingErr := &GitError{
- Command: "subtest",
- Output: "subtest output",
- Err: gitErr,
- }
-
- unwrapped := underlyingErr.Unwrap()
- if unwrapped != gitErr {
- t.Error("GitError.Unwrap() did not return the underlying error")
- }
-}
-
-func TestGitConfigOperations(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Test SetConfig
- err = git.SetConfig(ctx, "test.key", "test.value")
- if err != nil {
- t.Fatalf("SetConfig failed: %v", err)
- }
-
- // Test GetConfig
- value, err := git.GetConfig(ctx, "test.key")
- if err != nil {
- t.Fatalf("GetConfig failed: %v", err)
- }
-
- if value != "test.value" {
- t.Errorf("Expected config value 'test.value', got '%s'", value)
- }
-}
-
-func TestGitMerge(t *testing.T) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-test-*")
- if err != nil {
- t.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- t.Fatalf("Init failed: %v", err)
- }
-
- // Set user config
- userConfig := UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = git.SetUserConfig(ctx, userConfig)
- if err != nil {
- t.Fatalf("SetUserConfig failed: %v", err)
- }
-
- // Create initial commit
- testFile := filepath.Join(tempDir, "test.txt")
- err = os.WriteFile(testFile, []byte("Hello, Git!\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create test file: %v", err)
- }
-
- err = git.AddAll(ctx)
- if err != nil {
- t.Fatalf("AddAll failed: %v", err)
- }
-
- err = git.Commit(ctx, "Initial commit", CommitOptions{})
- if err != nil {
- t.Fatalf("Commit failed: %v", err)
- }
-
- // Create feature branch
- err = git.CreateBranch(ctx, "feature/test", "")
- if err != nil {
- t.Fatalf("CreateBranch failed: %v", err)
- }
-
- // Switch to feature branch
- err = git.Checkout(ctx, "feature/test")
- if err != nil {
- t.Fatalf("Checkout failed: %v", err)
- }
-
- // Add file on feature branch
- featureFile := filepath.Join(tempDir, "feature.txt")
- err = os.WriteFile(featureFile, []byte("Feature file\n"), 0644)
- if err != nil {
- t.Fatalf("Failed to create feature file: %v", err)
- }
-
- err = git.AddAll(ctx)
- if err != nil {
- t.Fatalf("AddAll failed: %v", err)
- }
-
- err = git.Commit(ctx, "Add feature file", CommitOptions{})
- if err != nil {
- t.Fatalf("Commit failed: %v", err)
- }
-
- // Switch back to main
- err = git.Checkout(ctx, "main")
- if err != nil {
- t.Fatalf("Checkout failed: %v", err)
- }
-
- // Test Merge
- mergeOptions := MergeOptions{
- NoFF: true,
- Message: "Merge feature/test",
- }
- err = git.Merge(ctx, "feature/test", mergeOptions)
- if err != nil {
- t.Fatalf("Merge failed: %v", err)
- }
-
- // Check that both files exist after merge
- if _, err := os.Stat(filepath.Join(tempDir, "test.txt")); os.IsNotExist(err) {
- t.Error("test.txt not found after merge")
- }
-
- if _, err := os.Stat(filepath.Join(tempDir, "feature.txt")); os.IsNotExist(err) {
- t.Error("feature.txt not found after merge")
- }
-}
-
-func BenchmarkGitStatus(b *testing.B) {
- // Create a temporary directory for testing
- tempDir, err := os.MkdirTemp("", "git-bench-*")
- if err != nil {
- b.Fatalf("Failed to create temp directory: %v", err)
- }
- defer os.RemoveAll(tempDir)
-
- git := DefaultGit(tempDir)
- ctx := context.Background()
-
- // Initialize repository
- err = git.Init(ctx, tempDir)
- if err != nil {
- b.Fatalf("Init failed: %v", err)
- }
-
- // Create some files
- for i := 0; i < 10; i++ {
- testFile := filepath.Join(tempDir, fmt.Sprintf("test%d.txt", i))
- err = os.WriteFile(testFile, []byte(fmt.Sprintf("File %d\n", i)), 0644)
- if err != nil {
- b.Fatalf("Failed to create test file: %v", err)
- }
- }
-
- b.ResetTimer()
-
- for i := 0; i < b.N; i++ {
- _, err := git.Status(ctx)
- if err != nil {
- b.Fatalf("Status failed: %v", err)
- }
- }
-}
diff --git a/server/git/github.go b/server/git/github.go
index 6555b69..1d37b1d 100644
--- a/server/git/github.go
+++ b/server/git/github.go
@@ -139,7 +139,7 @@
slog.Any("labels", options.Labels))
url := fmt.Sprintf("%s/repos/%s/%s/pulls", g.config.BaseURL, g.owner, g.repo)
-
+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
@@ -412,5 +412,6 @@
Reviewers: reviewers,
Commits: []Commit{}, // Would need additional API call to populate
Comments: []PullRequestComment{}, // Would need additional API call to populate
+ URL: fmt.Sprintf("https://github.com/%s/%s/pull/%d", g.owner, g.repo, githubPR.Number),
}
}
diff --git a/server/git/pull_request_example.go b/server/git/pull_request_example.go
deleted file mode 100644
index 5c70d21..0000000
--- a/server/git/pull_request_example.go
+++ /dev/null
@@ -1,251 +0,0 @@
-package git
-
-import (
- "context"
- "log/slog"
- "net/http"
- "os"
- "time"
-)
-
-// ExamplePullRequestUsage demonstrates how to use pull request functionality
-func ExamplePullRequestUsage() {
- ctx := context.Background()
-
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Example with GitHub
- exampleGitHubPullRequests(ctx, logger)
-
- // Example with Gerrit
- exampleGerritPullRequests(ctx, logger)
-}
-
-func exampleGitHubPullRequests(ctx context.Context, logger *slog.Logger) {
- logger.Info("=== GitHub Pull Request Example ===")
-
- // Create GitHub configuration
- githubConfig := GitHubConfig{
- Token: "your-github-token-here",
- BaseURL: "https://api.github.com",
- HTTPClient: &http.Client{Timeout: 30 * time.Second},
- }
-
- // Create GitHub pull request provider
- githubProvider := NewGitHubPullRequestProvider("owner", "repo", githubConfig)
-
- // Create Git instance with GitHub pull request capabilities
- git := NewGitWithPullRequests("/path/to/repo", GitConfig{
- Timeout: 30 * time.Second,
- }, githubProvider, logger)
-
- // Create a new pull request
- prOptions := PullRequestOptions{
- Title: "Add new feature",
- Description: "This PR adds a new feature to the application.",
- BaseBranch: "main",
- HeadBranch: "feature/new-feature",
- Labels: []string{"enhancement", "feature"},
- Assignees: []string{"username1", "username2"},
- Reviewers: []string{"reviewer1", "reviewer2"},
- Draft: false,
- }
-
- pr, err := git.CreatePullRequest(ctx, prOptions)
- if err != nil {
- logger.Error("Failed to create pull request", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Created pull request", slog.String("title", pr.Title), slog.Int("number", pr.Number))
-
- // List pull requests
- listOptions := ListPullRequestOptions{
- State: "open",
- Author: "username",
- BaseBranch: "main",
- Limit: 10,
- }
-
- prs, err := git.ListPullRequests(ctx, listOptions)
- if err != nil {
- logger.Error("Failed to list pull requests", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Found pull requests", slog.Int("count", len(prs)))
-
- // Get a specific pull request
- pr, err = git.GetPullRequest(ctx, pr.ID)
- if err != nil {
- logger.Error("Failed to get pull request", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Pull request status", slog.String("state", pr.State))
-
- // Update a pull request
- updateOptions := PullRequestOptions{
- Title: "Updated title",
- Description: "Updated description",
- Labels: []string{"bug", "urgent"},
- }
-
- updatedPR, err := git.UpdatePullRequest(ctx, pr.ID, updateOptions)
- if err != nil {
- logger.Error("Failed to update pull request", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Updated pull request", slog.String("title", updatedPR.Title))
-
- // Merge a pull request
- mergeOptions := MergePullRequestOptions{
- MergeMethod: "squash",
- CommitTitle: "Merge pull request #123",
- CommitMsg: "This merges the feature branch into main",
- }
-
- err = git.MergePullRequest(ctx, pr.ID, mergeOptions)
- if err != nil {
- logger.Error("Failed to merge pull request", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Pull request merged successfully")
-}
-
-func exampleGerritPullRequests(ctx context.Context, logger *slog.Logger) {
- logger.Info("=== Gerrit Pull Request Example ===")
-
- // Create Gerrit configuration
- gerritConfig := GerritConfig{
- Username: "your-username",
- Password: "your-http-password-or-api-token",
- BaseURL: "https://gerrit.example.com",
- HTTPClient: &http.Client{Timeout: 30 * time.Second},
- }
-
- // Create Gerrit pull request provider
- gerritProvider := NewGerritPullRequestProvider("project-name", gerritConfig)
-
- // Create Git instance with Gerrit pull request capabilities
- git := NewGitWithPullRequests("/path/to/repo", GitConfig{
- Timeout: 30 * time.Second,
- }, gerritProvider, logger)
-
- // Create a new change (pull request)
- prOptions := PullRequestOptions{
- Title: "Add new feature",
- Description: "This change adds a new feature to the application.",
- BaseBranch: "master",
- HeadBranch: "feature/new-feature",
- }
-
- pr, err := git.CreatePullRequest(ctx, prOptions)
- if err != nil {
- logger.Error("Failed to create change", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Created change", slog.String("title", pr.Title), slog.Int("number", pr.Number))
-
- // List changes
- listOptions := ListPullRequestOptions{
- State: "open",
- Author: "username",
- BaseBranch: "master",
- Limit: 10,
- }
-
- prs, err := git.ListPullRequests(ctx, listOptions)
- if err != nil {
- logger.Error("Failed to list changes", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Found changes", slog.Int("count", len(prs)))
-
- // Get a specific change
- pr, err = git.GetPullRequest(ctx, pr.ID)
- if err != nil {
- logger.Error("Failed to get change", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Change status", slog.String("state", pr.State))
-
- // Update a change
- updateOptions := PullRequestOptions{
- Title: "Updated title",
- Description: "Updated description",
- }
-
- updatedPR, err := git.UpdatePullRequest(ctx, pr.ID, updateOptions)
- if err != nil {
- logger.Error("Failed to update change", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Updated change", slog.String("title", updatedPR.Title))
-
- // Submit a change (merge)
- mergeOptions := MergePullRequestOptions{
- CommitTitle: "Submit change",
- CommitMsg: "This submits the change to master",
- }
-
- err = git.MergePullRequest(ctx, pr.ID, mergeOptions)
- if err != nil {
- logger.Error("Failed to submit change", slog.String("error", err.Error()))
- return
- }
-
- logger.Info("Change submitted successfully")
-}
-
-// Example of using both providers in the same application
-func ExampleMultiProviderUsage() {
- ctx := context.Background()
-
- // Determine which provider to use based on configuration
- useGitHub := true // This could come from config
-
- var git GitInterface
-
- if useGitHub {
- // Use GitHub
- githubConfig := GitHubConfig{
- Token: "github-token",
- BaseURL: "https://api.github.com",
- }
- githubProvider := NewGitHubPullRequestProvider("owner", "repo", githubConfig)
- git = NewGitWithPullRequests("/path/to/repo", GitConfig{}, githubProvider, nil) // Pass nil for logger as it's not used in this example
- } else {
- // Use Gerrit
- gerritConfig := GerritConfig{
- Username: "gerrit-username",
- Password: "gerrit-password",
- BaseURL: "https://gerrit.example.com",
- }
- gerritProvider := NewGerritPullRequestProvider("project", gerritConfig)
- git = NewGitWithPullRequests("/path/to/repo", GitConfig{}, gerritProvider, nil) // Pass nil for logger as it's not used in this example
- }
-
- // Use the same interface regardless of provider
- prOptions := PullRequestOptions{
- Title: "Cross-platform PR",
- Description: "This works with both GitHub and Gerrit",
- BaseBranch: "main",
- HeadBranch: "feature/cross-platform",
- }
-
- pr, err := git.CreatePullRequest(ctx, prOptions)
- if err != nil {
- slog.Error("Failed to create pull request", slog.String("error", err.Error()))
- return
- }
-
- slog.Info("Created pull request", slog.String("title", pr.Title))
-}