import * as git from '../lib/git.js' import * as branch from '../lib/branch.js' import * as config from '../lib/config.js' import * as provider from '../lib/provider.js' import { alignDevelopWithRemote } from '../lib/worktree.js' import type { MergeResult, MergeOptions } from './types.js' export async function execute( opts: MergeOptions, cwd?: string, ): Promise { try { // Get current branch const currentBranch = await git.getCurrentBranch(cwd) // Detect type const branchType = branch.detectBranchType(currentBranch) // Block if main/develop/other if (branchType === 'main' || branchType === 'develop' || branchType === 'other') { return { success: false, strategy: 'merge', target: '', branchDeleted: false, error: `Cannot merge from ${branchType} branch`, } } // Read config — resolve from the worktree cwd (= --workdir), NOT process.cwd(). const cfg = await config.readConfigForPlatform((await config.resolveConfigPath(cwd)) ?? undefined) const warnings: string[] = [] let developAligned: boolean | undefined let developSynced: boolean | undefined // Determine default strategy based on branch type let strategy: 'squash' | 'merge' = 'squash' if (branchType === 'release' || branchType === 'hotfix') { strategy = 'merge' } // Override if explicitly requested if (opts.merge) strategy = 'merge' if (opts.squash) strategy = 'squash' // Determine target branch + enforce the hard branch-target invariant. const targetBranch = branch.getTargetBranch(branchType, cfg) branch.assertBranchTarget(branchType, targetBranch, cfg) // Find PR const pr = await provider.findPR(currentBranch, cfg.git.provider, cwd) if (!pr) { return { success: false, strategy, target: targetBranch, branchDeleted: false, error: `No PR found for branch ${currentBranch}`, } } // Never complete a PR whose target contradicts the branch type (e.g. a // feature PR pointing at main) — this would have stopped the feature→main // incident at merge time. if (pr.target && pr.target !== targetBranch) { return { success: false, prNumber: pr.number, strategy, target: pr.target, branchDeleted: false, error: `PR #${pr.number} targets '${pr.target}' but a ${branchType} branch must target '${targetBranch}'. Refusing to merge — abandon or retarget the PR.`, } } // Simon's rule: never integrate a feature onto a STALE local develop. Align // the develop worktree with origin FIRST (ff if behind, refuse if diverged). if (targetBranch === cfg.git.branches.develop) { const dev = cfg.worktrees?.structure?.develop if (dev) { const align = await alignDevelopWithRemote(dev, cfg.git.branches.develop) if (!align.ok) { return { success: false, prNumber: pr.number, strategy, target: targetBranch, branchDeleted: false, error: `Pre-merge alignment failed: ${align.error}`, } } developAligned = true } else { warnings.push('No develop worktree in config — skipped local develop alignment.') } } // Merge PR via provider const mergeResult = await provider.mergePR(pr.number, cfg.git.provider, strategy, cwd) if (!mergeResult.success) { return { success: false, prNumber: pr.number, strategy, target: targetBranch, branchDeleted: false, error: mergeResult.error, } } // Fetch to verify await git.fetchAll(cwd) // Re-sync local develop with the just-merged origin/develop so it stays aligned // (the merge happened on the REMOTE — local develop would otherwise be behind). if (targetBranch === cfg.git.branches.develop && cfg.worktrees?.structure?.develop) { const after = await alignDevelopWithRemote(cfg.worktrees.structure.develop, cfg.git.branches.develop) developSynced = after.ok if (!after.ok) warnings.push(`Post-merge re-sync of local develop failed: ${after.error}`) } // Check if branch was deleted by provider const stillExists = await git.branchExists(currentBranch, true, cwd) return { success: true, prNumber: pr.number, strategy, target: targetBranch, branchDeleted: !stillExists, developAligned, developSynced, warnings: warnings.length ? warnings : undefined, } } catch (err) { const message = err instanceof Error ? err.message : String(err) return { success: false, strategy: 'merge', target: '', branchDeleted: false, error: message, } } }