task: task-1753636924-a1d4c708 - created

Change-Id: Ic78528c47ae38114b9b7504f1c4a76f95e93eb13
diff --git a/server/git/clone_manager.go b/server/git/clone_manager.go
new file mode 100644
index 0000000..afedd65
--- /dev/null
+++ b/server/git/clone_manager.go
@@ -0,0 +1,160 @@
+package git
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sync"
+)
+
+// CloneManager manages separate Git repository clones for each agent
+// This eliminates Git concurrency issues by giving each agent its own working directory
+type CloneManager struct {
+	baseRepoURL    string
+	workspacePath  string
+	agentClones    map[string]string // agent name -> clone path
+	mu             sync.RWMutex
+}
+
+// NewCloneManager creates a new CloneManager
+func NewCloneManager(baseRepoURL, workspacePath string) *CloneManager {
+	return &CloneManager{
+		baseRepoURL:   baseRepoURL,
+		workspacePath: workspacePath,
+		agentClones:   make(map[string]string),
+	}
+}
+
+// GetAgentClonePath returns the Git clone path for a specific agent
+// Creates the clone if it doesn't exist
+func (cm *CloneManager) GetAgentClonePath(agentName string) (string, error) {
+	cm.mu.Lock()
+	defer cm.mu.Unlock()
+
+	// Check if clone already exists
+	if clonePath, exists := cm.agentClones[agentName]; exists {
+		// Verify the clone still exists on disk
+		if _, err := os.Stat(clonePath); err == nil {
+			return clonePath, nil
+		}
+		// Remove stale entry if directory doesn't exist
+		delete(cm.agentClones, agentName)
+	}
+
+	// Create new clone for the agent
+	clonePath := filepath.Join(cm.workspacePath, fmt.Sprintf("agent-%s", agentName))
+	
+	// Ensure workspace directory exists
+	if err := os.MkdirAll(cm.workspacePath, 0755); err != nil {
+		return "", fmt.Errorf("failed to create workspace directory: %w", err)
+	}
+
+	// Remove existing clone directory if it exists
+	if err := os.RemoveAll(clonePath); err != nil {
+		return "", fmt.Errorf("failed to remove existing clone: %w", err)
+	}
+
+	// Clone the repository
+	if err := cm.cloneRepository(clonePath); err != nil {
+		return "", fmt.Errorf("failed to clone repository for agent %s: %w", agentName, err)
+	}
+
+	// Store the clone path
+	cm.agentClones[agentName] = clonePath
+	
+	return clonePath, nil
+}
+
+// cloneRepository performs the actual Git clone operation
+func (cm *CloneManager) cloneRepository(clonePath string) error {
+	ctx := context.Background()
+	
+	// Clone the repository
+	cmd := exec.CommandContext(ctx, "git", "clone", cm.baseRepoURL, clonePath)
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("git clone failed: %w", err)
+	}
+
+	return nil
+}
+
+// RefreshAgentClone pulls the latest changes for an agent's clone
+func (cm *CloneManager) RefreshAgentClone(agentName string) error {
+	cm.mu.RLock()
+	clonePath, exists := cm.agentClones[agentName]
+	cm.mu.RUnlock()
+
+	if !exists {
+		return fmt.Errorf("no clone exists for agent %s", agentName)
+	}
+
+	ctx := context.Background()
+	
+	// Change to clone directory and pull latest changes
+	cmd := exec.CommandContext(ctx, "git", "-C", clonePath, "pull", "origin")
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("failed to pull latest changes for agent %s: %w", agentName, err)
+	}
+
+	return nil
+}
+
+// CleanupAgentClone removes the clone directory for an agent
+func (cm *CloneManager) CleanupAgentClone(agentName string) error {
+	cm.mu.Lock()
+	defer cm.mu.Unlock()
+
+	clonePath, exists := cm.agentClones[agentName]
+	if !exists {
+		return nil // Already cleaned up
+	}
+
+	// Remove the clone directory
+	if err := os.RemoveAll(clonePath); err != nil {
+		return fmt.Errorf("failed to remove clone for agent %s: %w", agentName, err)
+	}
+
+	// Remove from tracking
+	delete(cm.agentClones, agentName)
+	
+	return nil
+}
+
+// CleanupAllClones removes all agent clone directories
+func (cm *CloneManager) CleanupAllClones() error {
+	cm.mu.Lock()
+	defer cm.mu.Unlock()
+
+	var errors []error
+	
+	for agentName, clonePath := range cm.agentClones {
+		if err := os.RemoveAll(clonePath); err != nil {
+			errors = append(errors, fmt.Errorf("failed to remove clone for agent %s: %w", agentName, err))
+		}
+	}
+
+	// Clear all tracked clones
+	cm.agentClones = make(map[string]string)
+
+	if len(errors) > 0 {
+		return fmt.Errorf("cleanup errors: %v", errors)
+	}
+
+	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
+}
\ No newline at end of file