blob: 33a5a9977d8bd0b83c1a7490d34cfa31ea965011 [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package git
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "sync"
10)
11
12// CloneManager manages separate Git repository clones for each agent
13// This eliminates Git concurrency issues by giving each agent its own working directory
14type CloneManager struct {
iomodo50598c62025-07-27 22:06:32 +040015 baseRepoURL string
16 workspacePath string
17 agentClones map[string]string // agent name -> clone path
18 mu sync.RWMutex
user5a7d60d2025-07-27 21:22:04 +040019}
20
21// NewCloneManager creates a new CloneManager
22func NewCloneManager(baseRepoURL, workspacePath string) *CloneManager {
23 return &CloneManager{
24 baseRepoURL: baseRepoURL,
25 workspacePath: workspacePath,
26 agentClones: make(map[string]string),
27 }
28}
29
30// GetAgentClonePath returns the Git clone path for a specific agent
31// Creates the clone if it doesn't exist
32func (cm *CloneManager) GetAgentClonePath(agentName string) (string, error) {
33 cm.mu.Lock()
34 defer cm.mu.Unlock()
35
36 // Check if clone already exists
37 if clonePath, exists := cm.agentClones[agentName]; exists {
38 // Verify the clone still exists on disk
39 if _, err := os.Stat(clonePath); err == nil {
40 return clonePath, nil
41 }
42 // Remove stale entry if directory doesn't exist
43 delete(cm.agentClones, agentName)
44 }
45
46 // Create new clone for the agent
47 clonePath := filepath.Join(cm.workspacePath, fmt.Sprintf("agent-%s", agentName))
iomodo50598c62025-07-27 22:06:32 +040048
user5a7d60d2025-07-27 21:22:04 +040049 // Ensure workspace directory exists
50 if err := os.MkdirAll(cm.workspacePath, 0755); err != nil {
51 return "", fmt.Errorf("failed to create workspace directory: %w", err)
52 }
53
54 // Remove existing clone directory if it exists
55 if err := os.RemoveAll(clonePath); err != nil {
56 return "", fmt.Errorf("failed to remove existing clone: %w", err)
57 }
58
59 // Clone the repository
60 if err := cm.cloneRepository(clonePath); err != nil {
61 return "", fmt.Errorf("failed to clone repository for agent %s: %w", agentName, err)
62 }
63
64 // Store the clone path
65 cm.agentClones[agentName] = clonePath
iomodo50598c62025-07-27 22:06:32 +040066
user5a7d60d2025-07-27 21:22:04 +040067 return clonePath, nil
68}
69
70// cloneRepository performs the actual Git clone operation
71func (cm *CloneManager) cloneRepository(clonePath string) error {
72 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +040073
user5a7d60d2025-07-27 21:22:04 +040074 // Clone the repository
75 cmd := exec.CommandContext(ctx, "git", "clone", cm.baseRepoURL, clonePath)
76 if err := cmd.Run(); err != nil {
77 return fmt.Errorf("git clone failed: %w", err)
78 }
79
80 return nil
81}
82
83// RefreshAgentClone pulls the latest changes for an agent's clone
84func (cm *CloneManager) RefreshAgentClone(agentName string) error {
85 cm.mu.RLock()
86 clonePath, exists := cm.agentClones[agentName]
87 cm.mu.RUnlock()
88
89 if !exists {
90 return fmt.Errorf("no clone exists for agent %s", agentName)
91 }
92
93 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +040094
user5a7d60d2025-07-27 21:22:04 +040095 // Change to clone directory and pull latest changes
96 cmd := exec.CommandContext(ctx, "git", "-C", clonePath, "pull", "origin")
97 if err := cmd.Run(); err != nil {
98 return fmt.Errorf("failed to pull latest changes for agent %s: %w", agentName, err)
99 }
100
101 return nil
102}
103
104// CleanupAgentClone removes the clone directory for an agent
105func (cm *CloneManager) CleanupAgentClone(agentName string) error {
106 cm.mu.Lock()
107 defer cm.mu.Unlock()
108
109 clonePath, exists := cm.agentClones[agentName]
110 if !exists {
111 return nil // Already cleaned up
112 }
113
114 // Remove the clone directory
115 if err := os.RemoveAll(clonePath); err != nil {
116 return fmt.Errorf("failed to remove clone for agent %s: %w", agentName, err)
117 }
118
119 // Remove from tracking
120 delete(cm.agentClones, agentName)
iomodo50598c62025-07-27 22:06:32 +0400121
user5a7d60d2025-07-27 21:22:04 +0400122 return nil
123}
124
125// CleanupAllClones removes all agent clone directories
126func (cm *CloneManager) CleanupAllClones() error {
127 cm.mu.Lock()
128 defer cm.mu.Unlock()
129
130 var errors []error
iomodo50598c62025-07-27 22:06:32 +0400131
user5a7d60d2025-07-27 21:22:04 +0400132 for agentName, clonePath := range cm.agentClones {
133 if err := os.RemoveAll(clonePath); err != nil {
134 errors = append(errors, fmt.Errorf("failed to remove clone for agent %s: %w", agentName, err))
135 }
136 }
137
138 // Clear all tracked clones
139 cm.agentClones = make(map[string]string)
140
141 if len(errors) > 0 {
142 return fmt.Errorf("cleanup errors: %v", errors)
143 }
144
145 return nil
146}