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