* Squash-merge a worktree branch into the target branch. * * Performs the following steps: * 1. Verify the worktree is clean * 2. Check branch divergence — fail if base branch has advanced * 3. Checkout target branch in the main repo * 4. Execute `git merge --squash --no-commit <ta
(
worktreePath: string,
targetBranch: string,
options?: { commitMessage?: string }
)
| 394 | * 6. Remove the worktree and delete the task branch |
| 395 | */ |
| 396 | async merge( |
| 397 | worktreePath: string, |
| 398 | targetBranch: string, |
| 399 | options?: { commitMessage?: string } |
| 400 | ): Promise<{ sha: string; taskBranch: string }> { |
| 401 | const mergeStart = performance.now(); |
| 402 | const step = (label: string, start: number) => |
| 403 | console.log(`[WorktreeManager.merge] ${label}: ${(performance.now() - start).toFixed(0)}ms`); |
| 404 | |
| 405 | // Determine the current branch of the worktree |
| 406 | let t = performance.now(); |
| 407 | const currentBranchRaw = await execGit(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']); |
| 408 | const taskBranch = currentBranchRaw.trim(); |
| 409 | step('rev-parse branch', t); |
| 410 | |
| 411 | // 1. Check worktree is clean |
| 412 | t = performance.now(); |
| 413 | const clean = await this.isWorktreeClean(worktreePath); |
| 414 | step('isWorktreeClean', t); |
| 415 | if (!clean) { |
| 416 | throw new WorktreeDirtyError(worktreePath); |
| 417 | } |
| 418 | |
| 419 | // 2. Check branch divergence |
| 420 | t = performance.now(); |
| 421 | const status = await this.getBranchStatus(taskBranch, targetBranch); |
| 422 | step('getBranchStatus', t); |
| 423 | if (status.behind > 0) { |
| 424 | throw new BranchesDivergedError(taskBranch, targetBranch, status.ahead, status.behind); |
| 425 | } |
| 426 | |
| 427 | // 3. Checkout target branch in main repo |
| 428 | t = performance.now(); |
| 429 | await execGit(this.repoPath, ['checkout', targetBranch]); |
| 430 | step('checkout target', t); |
| 431 | |
| 432 | // 4. Squash merge (no commit yet) |
| 433 | t = performance.now(); |
| 434 | try { |
| 435 | await execGit(this.repoPath, [ |
| 436 | 'merge', |
| 437 | '--squash', |
| 438 | '--no-commit', |
| 439 | taskBranch, |
| 440 | ]); |
| 441 | } catch (err) { |
| 442 | if (err instanceof GitError) { |
| 443 | // Check for merge conflicts |
| 444 | const conflictedFiles = await this.getConflictedFiles(); |
| 445 | if (conflictedFiles.length > 0) { |
| 446 | // Abort the merge to leave the repo clean |
| 447 | const mergeAborted = await execGit(this.repoPath, ['merge', '--abort']).then(() => true).catch(() => { |
| 448 | // merge --abort may fail if no merge in progress, ignore |
| 449 | return false; |
| 450 | }); |
| 451 | throw new MergeConflictError(conflictedFiles, ConflictOp.MERGE, { |
| 452 | mergeAborted, |
| 453 | mergeStrategy: 'squash', |
no test coverage detected