Skip to content

Commit 93ffdc3

Browse files
qiniu-ciqiniu-ci
andauthored
实现 Issue #132: 遵循最佳实践,确保主仓库和工作树目录保持代码最新 (#133)
* Initial plan for Issue #132: 遵循最佳实践,确保主仓库和工作树目录保持代码最新 * feat: implement main repository sync with git worktree best practices Ensure main repository and worktree directories stay up-to-date following git best practices. This prevents code conflicts and outdated bases when creating new worktrees for concurrent PR processing. Key improvements: - Auto-update main repository before creating worktrees - Configure rebase as default pull strategy to maintain clean commit history - Add periodic background sync routine for all main repositories - Improve worktree code sync with rebase-first approach and conflict handling - Support concurrent multi-PR processing with safe code synchronization The workspace management mechanism now follows git and git worktree engineering best practices, ensuring reliable concurrent task execution. Closes #132 * refactor: remove periodic main repo sync routine Remove automatic 30-minute sync mechanism for main repositories to reduce unnecessary network traffic. The cleanup routine and other core functionality remain intact. - Remove StartMainRepoUpdateRoutine call from agent initialization - Delete StartMainRepoUpdateRoutine function definition - Remove EnsureAllMainRepositoriesUpToDate method This change eliminates bandwidth waste while preserving manual sync capabilities and existing workspace management features. Closes #132 --------- Co-authored-by: qiniu-ci <[email protected]>
1 parent 5169827 commit 93ffdc3

File tree

4 files changed

+176
-59
lines changed

4 files changed

+176
-59
lines changed

internal/agent/agent.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func (a *Agent) cleanupExpiredResouces() {
8787

8888
}
8989

90+
9091
// ProcessIssueComment 处理 Issue 评论事件,包含完整的仓库信息
9192
func (a *Agent) ProcessIssueComment(event *github.IssueCommentEvent) error {
9293
// 1. 创建 Issue 工作空间

internal/github/client.go

Lines changed: 67 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func (c *Client) CommitAndPush(workspace *models.Workspace, result *models.Execu
235235
return nil
236236
}
237237

238-
// PullLatestChanges 拉取远端最新代码
238+
// PullLatestChanges 拉取远端最新代码(优先使用rebase策略)
239239
func (c *Client) PullLatestChanges(workspace *models.Workspace, pr *github.PullRequest) error {
240240
log.Infof("Pulling latest changes for workspace: %s (PR #%d)", workspace.Path, pr.GetNumber())
241241

@@ -255,90 +255,98 @@ func (c *Client) PullLatestChanges(workspace *models.Workspace, pr *github.PullR
255255

256256
log.Infof("PR #%d: %s -> %s", pr.GetNumber(), headBranch, baseBranch)
257257

258-
// 1. 先尝试直接获取 PR 内容
259-
prNumber := pr.GetNumber()
260-
log.Infof("Attempting to fetch PR #%d content directly", prNumber)
261-
262-
cmd := exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head", prNumber))
258+
// 1. 获取所有远程引用
259+
cmd := exec.Command("git", "fetch", "--all", "--prune")
263260
cmd.Dir = workspace.Path
264-
prFetchOutput, err := cmd.CombinedOutput()
261+
fetchOutput, err := cmd.CombinedOutput()
265262
if err != nil {
266-
log.Warnf("Failed to fetch PR #%d directly: %v, output: %s", prNumber, err, string(prFetchOutput))
263+
return fmt.Errorf("failed to fetch latest changes: %w\nCommand output: %s", err, string(fetchOutput))
264+
}
265+
log.Infof("Fetched all remote references for PR #%d", pr.GetNumber())
267266

268-
// 2. 如果直接获取失败,fallback 到 rebase 方式
269-
log.Infof("Falling back to rebase approach for PR #%d", prNumber)
267+
// 2. 检查当前是否有未提交的变更
268+
cmd = exec.Command("git", "status", "--porcelain")
269+
cmd.Dir = workspace.Path
270+
statusOutput, err := cmd.Output()
271+
if err != nil {
272+
return fmt.Errorf("failed to check git status: %w", err)
273+
}
270274

271-
// 获取所有远程仓库的最新代码
272-
cmd = exec.Command("git", "fetch", "--all")
275+
hasChanges := strings.TrimSpace(string(statusOutput)) != ""
276+
if hasChanges {
277+
// 有未提交的变更,先 stash
278+
log.Infof("Found uncommitted changes in worktree, stashing them")
279+
cmd = exec.Command("git", "stash", "push", "-m", fmt.Sprintf("Auto stash before syncing PR #%d", pr.GetNumber()))
273280
cmd.Dir = workspace.Path
274-
fetchOutput, err := cmd.CombinedOutput()
281+
stashOutput, err := cmd.CombinedOutput()
275282
if err != nil {
276-
return fmt.Errorf("failed to fetch latest changes: %w\nCommand output: %s", err, string(fetchOutput))
283+
log.Warnf("Failed to stash changes: %v, output: %s", err, string(stashOutput))
277284
}
285+
}
278286

279-
// 尝试 rebase 到目标分支的最新代码
280-
cmd = exec.Command("git", "rebase", fmt.Sprintf("origin/%s", baseBranch))
287+
// 3. 尝试直接获取 PR 内容
288+
prNumber := pr.GetNumber()
289+
log.Infof("Attempting to fetch PR #%d content directly", prNumber)
290+
cmd = exec.Command("git", "fetch", "origin", fmt.Sprintf("pull/%d/head", prNumber))
291+
cmd.Dir = workspace.Path
292+
_, err = cmd.CombinedOutput()
293+
if err == nil {
294+
// 直接获取成功,使用rebase合并更新
295+
log.Infof("Successfully fetched PR #%d content, attempting rebase", prNumber)
296+
cmd = exec.Command("git", "rebase", "FETCH_HEAD")
281297
cmd.Dir = workspace.Path
282298
rebaseOutput, err := cmd.CombinedOutput()
283299
if err != nil {
284-
log.Errorf("Rebase failed: %v, output: %s", err, string(rebaseOutput))
285-
return fmt.Errorf("both direct fetch and rebase failed for PR #%d", prNumber)
286-
}
287-
288-
log.Infof("Successfully rebased PR #%d onto %s", pr.GetNumber(), baseBranch)
289-
} else {
290-
// 3. 直接获取成功,尝试合并 PR 内容
291-
log.Infof("Successfully fetched PR #%d content, attempting to merge", prNumber)
292-
293-
// 检查当前是否有未提交的变更
294-
cmd = exec.Command("git", "status", "--porcelain")
295-
cmd.Dir = workspace.Path
296-
statusOutput, err := cmd.Output()
297-
if err != nil {
298-
log.Warnf("Failed to check git status: %v", err)
299-
}
300-
301-
if strings.TrimSpace(string(statusOutput)) != "" {
302-
// 有未提交的变更,先 stash
303-
log.Infof("Found uncommitted changes, stashing them")
304-
cmd = exec.Command("git", "stash", "push", "-m", fmt.Sprintf("Auto stash before syncing PR #%d", prNumber))
300+
log.Warnf("Rebase failed, trying reset: %v, output: %s", err, string(rebaseOutput))
301+
// rebase失败,强制切换到PR内容
302+
cmd = exec.Command("git", "reset", "--hard", "FETCH_HEAD")
305303
cmd.Dir = workspace.Path
306-
stashOutput, err := cmd.CombinedOutput()
304+
resetOutput, err := cmd.CombinedOutput()
307305
if err != nil {
308-
log.Warnf("Failed to stash changes: %v, output: %s", err, string(stashOutput))
306+
return fmt.Errorf("failed to reset to PR #%d: %w\nCommand output: %s", prNumber, err, string(resetOutput))
309307
}
308+
log.Infof("Hard reset worktree to PR #%d content", prNumber)
309+
} else {
310+
log.Infof("Successfully rebased worktree to PR #%d content", prNumber)
310311
}
311-
312-
// 尝试合并 PR 内容,而不是强制覆盖
313-
cmd = exec.Command("git", "merge", "FETCH_HEAD", "--no-edit")
312+
} else {
313+
// 直接获取失败,使用传统rebase方式
314+
log.Warnf("Failed to fetch PR #%d directly: %v, falling back to traditional rebase", prNumber, err)
315+
316+
// 尝试rebase到目标分支的最新代码
317+
cmd = exec.Command("git", "rebase", fmt.Sprintf("origin/%s", baseBranch))
314318
cmd.Dir = workspace.Path
315-
mergeOutput, err := cmd.CombinedOutput()
319+
rebaseOutput, err := cmd.CombinedOutput()
316320
if err != nil {
317-
log.Warnf("Merge failed, trying reset: %v, output: %s", err, string(mergeOutput))
318-
319-
// 合并失败,回退到强制切换(但先备份)
320-
cmd = exec.Command("git", "reset", "--hard", "HEAD")
321+
log.Warnf("Rebase to base branch failed: %v, output: %s", err, string(rebaseOutput))
322+
// rebase失败,尝试强制同步到基础分支
323+
cmd = exec.Command("git", "reset", "--hard", fmt.Sprintf("origin/%s", baseBranch))
321324
cmd.Dir = workspace.Path
322325
resetOutput, err := cmd.CombinedOutput()
323326
if err != nil {
324-
log.Warnf("Failed to reset: %v, output: %s", err, string(resetOutput))
325-
}
326-
327-
// 强制切换到 PR 内容
328-
cmd = exec.Command("git", "checkout", "-B", headBranch, "FETCH_HEAD", "--force")
329-
cmd.Dir = workspace.Path
330-
checkoutOutput, err := cmd.CombinedOutput()
331-
if err != nil {
332-
return fmt.Errorf("failed to checkout PR #%d: %w\nCommand output: %s", prNumber, err, string(checkoutOutput))
327+
return fmt.Errorf("failed to reset to base branch %s: %w\nCommand output: %s", baseBranch, err, string(resetOutput))
333328
}
329+
log.Infof("Hard reset worktree to base branch %s", baseBranch)
330+
} else {
331+
log.Infof("Successfully rebased worktree to base branch %s", baseBranch)
332+
}
333+
}
334334

335-
log.Infof("Successfully force synced to PR #%d content (after merge failure)", prNumber)
335+
// 4. 如果之前有stash,尝试恢复
336+
if hasChanges {
337+
log.Infof("Attempting to restore stashed changes for PR #%d", prNumber)
338+
cmd = exec.Command("git", "stash", "pop")
339+
cmd.Dir = workspace.Path
340+
stashPopOutput, err := cmd.CombinedOutput()
341+
if err != nil {
342+
log.Warnf("Failed to restore stashed changes: %v, output: %s", err, string(stashPopOutput))
343+
log.Infof("You may need to manually resolve the stashed changes later")
336344
} else {
337-
log.Infof("Successfully merged PR #%d content", prNumber)
345+
log.Infof("Successfully restored stashed changes")
338346
}
339347
}
340348

341-
log.Infof("Successfully pulled latest changes for PR #%d from remote repo: %s", pr.GetNumber(), workspace.Repository)
349+
log.Infof("Successfully pulled latest changes for PR #%d using rebase strategy", pr.GetNumber())
342350
return nil
343351
}
344352

internal/workspace/manager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,3 +720,4 @@ func (m *Manager) GetExpiredWorkspaces() []*models.Workspace {
720720

721721
return expiredWorkspaces
722722
}
723+

internal/workspace/repo_manager.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ func (r *RepoManager) Initialize() error {
7979
log.Warnf("Failed to configure safe directory: %v\nCommand output: %s", err, string(configOutput))
8080
}
8181

82+
// 配置 rebase 为默认拉取策略
83+
cmd = exec.Command("git", "config", "--local", "pull.rebase", "true")
84+
cmd.Dir = r.repoPath
85+
rebaseConfigOutput, err := cmd.CombinedOutput()
86+
if err != nil {
87+
log.Warnf("Failed to configure pull.rebase: %v\nCommand output: %s", err, string(rebaseConfigOutput))
88+
}
89+
8290
log.Infof("Successfully initialized repository: %s", r.repoPath)
8391
return nil
8492
}
@@ -116,6 +124,12 @@ func (r *RepoManager) CreateWorktree(prNumber int, branch string, createNewBranc
116124
if err := r.Initialize(); err != nil {
117125
return nil, err
118126
}
127+
} else {
128+
// 仓库已存在,确保主仓库代码是最新的
129+
if err := r.updateMainRepository(); err != nil {
130+
log.Warnf("Failed to update main repository: %v", err)
131+
// 不因为更新失败而阻止worktree创建,但记录警告
132+
}
119133
}
120134

121135
// 创建 worktree 路径
@@ -327,6 +341,12 @@ func (r *RepoManager) CreateWorktreeWithName(worktreeName string, branch string,
327341
if err := r.Initialize(); err != nil {
328342
return nil, err
329343
}
344+
} else {
345+
// 仓库已存在,确保主仓库代码是最新的
346+
if err := r.updateMainRepository(); err != nil {
347+
log.Warnf("Failed to update main repository: %v", err)
348+
// 不因为更新失败而阻止worktree创建,但记录警告
349+
}
330350
}
331351

332352
// 创建 worktree 路径(与仓库目录同级)
@@ -443,3 +463,90 @@ func (r *RepoManager) GetWorktreeCount() int {
443463
defer r.mutex.RUnlock()
444464
return len(r.worktrees)
445465
}
466+
467+
// updateMainRepository 更新主仓库代码到最新版本
468+
func (r *RepoManager) updateMainRepository() error {
469+
log.Infof("Updating main repository: %s", r.repoPath)
470+
471+
// 1. 获取远程最新引用
472+
cmd := exec.Command("git", "fetch", "--all", "--prune")
473+
cmd.Dir = r.repoPath
474+
fetchOutput, err := cmd.CombinedOutput()
475+
if err != nil {
476+
return fmt.Errorf("failed to fetch latest changes: %w, output: %s", err, string(fetchOutput))
477+
}
478+
log.Infof("Fetched latest changes for main repository")
479+
480+
// 2. 获取当前分支
481+
cmd = exec.Command("git", "rev-parse", "--abbrev-ref", "HEAD")
482+
cmd.Dir = r.repoPath
483+
currentBranchOutput, err := cmd.Output()
484+
if err != nil {
485+
return fmt.Errorf("failed to get current branch: %w", err)
486+
}
487+
currentBranch := strings.TrimSpace(string(currentBranchOutput))
488+
489+
// 3. 检查是否有未提交的变更
490+
cmd = exec.Command("git", "status", "--porcelain")
491+
cmd.Dir = r.repoPath
492+
statusOutput, err := cmd.Output()
493+
if err != nil {
494+
return fmt.Errorf("failed to check status: %w", err)
495+
}
496+
497+
hasChanges := strings.TrimSpace(string(statusOutput)) != ""
498+
if hasChanges {
499+
// 主仓库不应该有未提交的变更,这违反了最佳实践
500+
log.Warnf("Main repository has uncommitted changes, this violates best practices")
501+
log.Warnf("Uncommitted changes:\n%s", string(statusOutput))
502+
503+
// 为了安全,暂存这些变更
504+
cmd = exec.Command("git", "stash", "push", "-m", "Auto-stash from updateMainRepository")
505+
cmd.Dir = r.repoPath
506+
stashOutput, err := cmd.CombinedOutput()
507+
if err != nil {
508+
log.Warnf("Failed to stash changes: %v, output: %s", err, string(stashOutput))
509+
} else {
510+
log.Infof("Stashed uncommitted changes in main repository")
511+
}
512+
}
513+
514+
// 4. 使用 rebase 更新到最新版本
515+
remoteBranch := fmt.Sprintf("origin/%s", currentBranch)
516+
cmd = exec.Command("git", "rebase", remoteBranch)
517+
cmd.Dir = r.repoPath
518+
rebaseOutput, err := cmd.CombinedOutput()
519+
if err != nil {
520+
// rebase 失败,尝试 reset 到远程分支
521+
log.Warnf("Rebase failed, attempting hard reset: %v, output: %s", err, string(rebaseOutput))
522+
523+
cmd = exec.Command("git", "reset", "--hard", remoteBranch)
524+
cmd.Dir = r.repoPath
525+
resetOutput, err := cmd.CombinedOutput()
526+
if err != nil {
527+
return fmt.Errorf("failed to reset to remote branch: %w, output: %s", err, string(resetOutput))
528+
}
529+
log.Infof("Hard reset main repository to %s", remoteBranch)
530+
} else {
531+
log.Infof("Successfully rebased main repository to %s", remoteBranch)
532+
}
533+
534+
// 5. 清理无用的引用
535+
cmd = exec.Command("git", "gc", "--auto")
536+
cmd.Dir = r.repoPath
537+
gcOutput, err := cmd.CombinedOutput()
538+
if err != nil {
539+
log.Warnf("Failed to run git gc: %v, output: %s", err, string(gcOutput))
540+
}
541+
542+
log.Infof("Main repository updated successfully")
543+
return nil
544+
}
545+
546+
// EnsureMainRepositoryUpToDate 确保主仓库是最新的(公开方法,可被外部调用)
547+
func (r *RepoManager) EnsureMainRepositoryUpToDate() error {
548+
if !r.isInitialized() {
549+
return fmt.Errorf("repository not initialized")
550+
}
551+
return r.updateMainRepository()
552+
}

0 commit comments

Comments
 (0)