Skip to content

Commit 2f27083

Browse files
authored
feat: add periodic cleanup for expired workspaces (#345)
* feat: add periodic cleanup for expired workspaces - Add automatic cleanup of expired workspaces every 24 hours - Implement startPeriodicCleanup and cleanupExpiredWorkspaces methods - Remove unused repo_manager.go file - Improve cleanup logging and error handling * feat: improve MCP config management and workspace cleanup - Add MCPConfigPath field to Workspace model for tracking MCP config files - Move MCP config files to workspace-related directories instead of global temp - Implement comprehensive MCP config cleanup in workspace manager - Add recovery support for existing MCP config files during workspace restoration - Enhance cleanup logic to ensure all workspace-related files are properly removed * feat: add force sync for PR content in fork workspaces - Add FetchAndCheckoutPR method to GitService for handling PR content updates - Implement force sync mechanism to ensure PR workspaces have latest content - Handle force pushes and updates in fork PRs using GitHub PR refs - Update workspace validation to use stored branch information * optimize PR branch sync with in-place updates for existing branches * improve git command error logging in workspace git service
1 parent 91e8fc0 commit 2f27083

File tree

7 files changed

+305
-22
lines changed

7 files changed

+305
-22
lines changed

internal/code/claude_docker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func NewClaudeDocker(workspace *models.Workspace, cfg *config.Config) (Code, err
3535
if err != nil {
3636
return nil, fmt.Errorf("failed to create MCP config: %w", err)
3737
}
38+
// Set MCP config path in workspace for tracking and cleanup
39+
workspace.MCPConfigPath = mcpConfigPath
3840
log.Infof("MCP config file created at: %s", mcpConfigPath)
3941

4042
// Check if corresponding container is already running

internal/code/mcp_config.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7+
"path/filepath"
78
"strconv"
89

910
"github.com/qiniu/codeagent/internal/config"
@@ -129,8 +130,24 @@ func (g *MCPConfigGenerator) CreateTempConfig() (string, error) {
129130
return "", err
130131
}
131132

132-
// 创建临时文件在/tmp目录中
133-
tempFile, err := os.CreateTemp(g.config.Workspace.BaseDir, "codeagent-mcp-*.json")
133+
// 创建MCP配置文件在workspace目录中,与代码仓和session目录保持一致
134+
var mcpConfigDir string
135+
if g.workspace.SessionPath != "" {
136+
// 如果有session目录,将MCP配置放在session目录的同级
137+
mcpConfigDir = filepath.Dir(g.workspace.SessionPath)
138+
} else {
139+
// 如果没有session目录,将MCP配置放在代码仓的同级
140+
mcpConfigDir = filepath.Dir(g.workspace.Path)
141+
}
142+
143+
// 确保目录存在
144+
if err := os.MkdirAll(mcpConfigDir, 0755); err != nil {
145+
return "", fmt.Errorf("failed to create MCP config directory: %w", err)
146+
}
147+
148+
// 创建临时文件在workspace相关目录中
149+
// TODO(CarlJi): mcp config file name should be align with the workspace name
150+
tempFile, err := os.CreateTemp(mcpConfigDir, "codeagent-mcp-*.json")
134151
if err != nil {
135152
return "", fmt.Errorf("failed to create temp file: %w", err)
136153
}
@@ -146,5 +163,6 @@ func (g *MCPConfigGenerator) CreateTempConfig() (string, error) {
146163
return "", err
147164
}
148165

166+
log.Infof("Created MCP config file: %s", tempFile.Name())
149167
return tempFile.Name(), nil
150168
}

internal/workspace/git_service.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type GitService interface {
2323
CreateAndCheckoutBranch(repoPath, branchName string) error
2424
CheckoutBranch(repoPath, branchName string) error
2525
CreateTrackingBranch(repoPath, branchName string) error
26+
FetchAndCheckoutPR(repoPath string, prNumber int) error
2627
}
2728

2829
type gitService struct{}
@@ -46,18 +47,25 @@ func (g *gitService) CloneRepository(repoURL, clonePath, branch string, createNe
4647
if createNewBranch {
4748
// Clone the default branch first, then create new branch
4849
cmd = exec.Command("git", "clone", "--depth", "50", repoURL, clonePath)
50+
log.Infof("Executing Git command: %s", cmd.String())
4951
} else {
5052
// Try to clone specific branch directly
5153
cmd = exec.Command("git", "clone", "--depth", "50", "--branch", branch, repoURL, clonePath)
54+
log.Infof("Executing Git command: %s", cmd.String())
5255
}
5356

5457
output, err := cmd.CombinedOutput()
5558
if err != nil {
59+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
5660
if !createNewBranch {
5761
// If direct branch clone failed, try cloning default branch first
5862
log.Warnf("Failed to clone specific branch %s directly, cloning default branch: %v", branch, err)
5963
cmd = exec.Command("git", "clone", "--depth", "50", repoURL, clonePath)
64+
log.Infof("Executing fallback Git command: %s", cmd.String())
6065
output, err = cmd.CombinedOutput()
66+
if err != nil {
67+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
68+
}
6169
}
6270
if err != nil {
6371
return GitError("clone", clonePath, fmt.Errorf("%s: %w", string(output), err))
@@ -103,8 +111,10 @@ func (g *gitService) CloneRepository(repoURL, clonePath, branch string, createNe
103111
func (g *gitService) GetRemoteURL(repoPath string) (string, error) {
104112
cmd := exec.Command("git", "remote", "get-url", "origin")
105113
cmd.Dir = repoPath
114+
log.Infof("Executing Git command: %s", cmd.String())
106115
output, err := cmd.Output()
107116
if err != nil {
117+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
108118
return "", GitError("get_remote_url", repoPath, err)
109119
}
110120
return strings.TrimSpace(string(output)), nil
@@ -114,8 +124,10 @@ func (g *gitService) GetRemoteURL(repoPath string) (string, error) {
114124
func (g *gitService) GetCurrentBranch(repoPath string) (string, error) {
115125
cmd := exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
116126
cmd.Dir = repoPath
127+
log.Infof("Executing Git command: %s", cmd.String())
117128
output, err := cmd.Output()
118129
if err != nil {
130+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
119131
return "", GitError("get_current_branch", repoPath, err)
120132
}
121133
return strings.TrimSpace(string(output)), nil
@@ -125,8 +137,10 @@ func (g *gitService) GetCurrentBranch(repoPath string) (string, error) {
125137
func (g *gitService) GetCurrentCommit(repoPath string) (string, error) {
126138
cmd := exec.Command("git", "rev-parse", "HEAD")
127139
cmd.Dir = repoPath
140+
log.Infof("Executing Git command: %s", cmd.String())
128141
output, err := cmd.Output()
129142
if err != nil {
143+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
130144
return "", fmt.Errorf("failed to get current commit: %w", err)
131145
}
132146
return strings.TrimSpace(string(output)), nil
@@ -136,8 +150,10 @@ func (g *gitService) GetCurrentCommit(repoPath string) (string, error) {
136150
func (g *gitService) GetBranchCommit(repoPath, branch string) (string, error) {
137151
cmd := exec.Command("git", "rev-parse", fmt.Sprintf("origin/%s", branch))
138152
cmd.Dir = repoPath
153+
log.Infof("Executing Git command: %s", cmd.String())
139154
output, err := cmd.Output()
140155
if err != nil {
156+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
141157
return "", fmt.Errorf("failed to get branch commit for %s: %w", branch, err)
142158
}
143159
return strings.TrimSpace(string(output)), nil
@@ -183,7 +199,9 @@ func (g *gitService) ValidateBranch(repoPath, expectedBranch string) bool {
183199
func (g *gitService) ConfigureSafeDirectory(repoPath string) error {
184200
cmd := exec.Command("git", "config", "--local", "--add", "safe.directory", repoPath)
185201
cmd.Dir = repoPath
202+
log.Infof("Executing Git command: %s", cmd.String())
186203
if output, err := cmd.CombinedOutput(); err != nil {
204+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
187205
return GitError("config_safe_directory", repoPath, fmt.Errorf("%s: %w", string(output), err))
188206
}
189207
return nil
@@ -193,7 +211,9 @@ func (g *gitService) ConfigureSafeDirectory(repoPath string) error {
193211
func (g *gitService) ConfigurePullStrategy(repoPath string) error {
194212
cmd := exec.Command("git", "config", "--local", "pull.rebase", "true")
195213
cmd.Dir = repoPath
214+
log.Infof("Executing Git command: %s", cmd.String())
196215
if output, err := cmd.CombinedOutput(); err != nil {
216+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
197217
return fmt.Errorf("failed to configure pull strategy: %w, output: %s", err, string(output))
198218
}
199219
return nil
@@ -203,7 +223,9 @@ func (g *gitService) ConfigurePullStrategy(repoPath string) error {
203223
func (g *gitService) CreateAndCheckoutBranch(repoPath, branchName string) error {
204224
cmd := exec.Command("git", "checkout", "-b", branchName)
205225
cmd.Dir = repoPath
226+
log.Infof("Executing Git command: %s", cmd.String())
206227
if output, err := cmd.CombinedOutput(); err != nil {
228+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
207229
return fmt.Errorf("failed to create new branch %s: %w, output: %s", branchName, err, string(output))
208230
}
209231
return nil
@@ -213,7 +235,9 @@ func (g *gitService) CreateAndCheckoutBranch(repoPath, branchName string) error
213235
func (g *gitService) CheckoutBranch(repoPath, branchName string) error {
214236
cmd := exec.Command("git", "checkout", branchName)
215237
cmd.Dir = repoPath
238+
log.Infof("Executing Git command: %s", cmd.String())
216239
if output, err := cmd.CombinedOutput(); err != nil {
240+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
217241
return fmt.Errorf("failed to checkout branch %s: %w, output: %s", branchName, err, string(output))
218242
}
219243
return nil
@@ -223,8 +247,64 @@ func (g *gitService) CheckoutBranch(repoPath, branchName string) error {
223247
func (g *gitService) CreateTrackingBranch(repoPath, branchName string) error {
224248
cmd := exec.Command("git", "checkout", "-b", branchName, fmt.Sprintf("origin/%s", branchName))
225249
cmd.Dir = repoPath
250+
log.Infof("Executing Git command: %s", cmd.String())
226251
if output, err := cmd.CombinedOutput(); err != nil {
252+
log.Errorf("Git command failed: %s, output: %s, error: %v", cmd.String(), string(output), err)
227253
return fmt.Errorf("failed to create tracking branch %s: %w, output: %s", branchName, err, string(output))
228254
}
229255
return nil
230256
}
257+
258+
// FetchAndCheckoutPR fetches and checks out PR content using GitHub's PR refs
259+
// Always uses force mode to handle updates, force pushes, and ensure latest content
260+
func (g *gitService) FetchAndCheckoutPR(repoPath string, prNumber int) error {
261+
log.Infof("Fetching PR #%d content using GitHub PR refs (force mode)", prNumber)
262+
263+
prBranchName := fmt.Sprintf("pr-%d", prNumber)
264+
currentBranch, err := g.GetCurrentBranch(repoPath)
265+
266+
// If we're already on the PR branch, use a lightweight in-place update
267+
if err == nil && currentBranch == prBranchName {
268+
log.Infof("Already on PR branch %s, performing in-place sync", prBranchName)
269+
270+
// Step 1: First fetch the PR content to FETCH_HEAD without creating/updating local branch
271+
fetchCmd := exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head", prNumber))
272+
fetchCmd.Dir = repoPath
273+
log.Infof("Executing Git command: %s", fetchCmd.String())
274+
if output, err := fetchCmd.CombinedOutput(); err != nil {
275+
log.Errorf("Git command failed: %s, output: %s, error: %v", fetchCmd.String(), string(output), err)
276+
return fmt.Errorf("failed to fetch PR #%d content: %w, output: %s", prNumber, err, string(output))
277+
}
278+
279+
// Step 2: Reset current branch to the fetched content
280+
resetCmd := exec.Command("git", "reset", "--hard", "FETCH_HEAD")
281+
resetCmd.Dir = repoPath
282+
log.Infof("Executing Git command: %s", resetCmd.String())
283+
if output, err := resetCmd.CombinedOutput(); err != nil {
284+
log.Errorf("Git command failed: %s, output: %s, error: %v", resetCmd.String(), string(output), err)
285+
return fmt.Errorf("failed to reset PR branch to latest content: %w, output: %s", err, string(output))
286+
}
287+
288+
log.Infof("Successfully updated PR #%d branch in-place", prNumber)
289+
return nil
290+
}
291+
292+
fetchCmd := exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head:%s", prNumber, prBranchName), "--force")
293+
fetchCmd.Dir = repoPath
294+
log.Infof("Executing Git command: %s", fetchCmd.String())
295+
if output, err := fetchCmd.CombinedOutput(); err != nil {
296+
log.Errorf("Git command failed: %s, output: %s, error: %v", fetchCmd.String(), string(output), err)
297+
return fmt.Errorf("failed to fetch PR #%d: %w, output: %s", prNumber, err, string(output))
298+
}
299+
300+
checkoutCmd := exec.Command("git", "checkout", prBranchName)
301+
checkoutCmd.Dir = repoPath
302+
log.Infof("Executing Git command: %s", checkoutCmd.String())
303+
if output, err := checkoutCmd.CombinedOutput(); err != nil {
304+
log.Errorf("Git command failed: %s, output: %s, error: %v", checkoutCmd.String(), string(output), err)
305+
return fmt.Errorf("failed to checkout PR branch %s: %w, output: %s", prBranchName, err, string(output))
306+
}
307+
308+
log.Infof("Successfully fetched and checked out PR #%d content to branch: %s", prNumber, prBranchName)
309+
return nil
310+
}

0 commit comments

Comments
 (0)