import { createLogger } from '../../shared/utils/index.js'; import { type BranchBaseResolutionCache, listCandidateRefs } from './branchBaseRefCache.js'; import { runGit } from './branchGitCommands.js'; type BaseRefCandidate = { baseRef: string; baseCommit: string; firstSubject: string; distance: number; }; const TAKT_COMMIT_PREFIX = 'takt:'; const log = createLogger('branchGitResolver'); export function resolveMergeBase(gitCwd: string, baseRef: string, branch: string): string { return runGit(gitCwd, ['merge-base', baseRef, branch]); } function buildPriorityRefs(defaultBranch: string, branch: string): string[] { const refs = [defaultBranch, `origin/${defaultBranch}`]; const distinctRefs: string[] = []; for (const ref of refs) { if (!ref || ref === branch || ref.endsWith(`/${branch}`)) { continue; } if (!distinctRefs.includes(ref)) { distinctRefs.push(ref); } } return distinctRefs; } function getFirstParentDistance(gitCwd: string, baseCommit: string, branch: string): number { const output = runGit(gitCwd, ['rev-list', '--count', '--first-parent', `${baseCommit}..${branch}`]); return Number.parseInt(output, 10); } function getFirstParentFirstSubject(gitCwd: string, baseCommit: string, branch: string): string { const output = runGit(gitCwd, ['log', '--format=%s', '--reverse', '--first-parent', `${baseCommit}..${branch}`]); const firstLine = output.split('\n')[0]; if (!firstLine) { return ''; } return firstLine.trim(); } function resolveBaseCandidate(gitCwd: string, baseRef: string, branch: string): BaseRefCandidate | null { try { const baseCommit = resolveMergeBase(gitCwd, baseRef, branch); if (!baseCommit) { return null; } const distance = getFirstParentDistance(gitCwd, baseCommit, branch); if (!Number.isFinite(distance) || distance <= 0) { return null; } const firstSubject = getFirstParentFirstSubject(gitCwd, baseCommit, branch); return { baseRef, baseCommit, firstSubject, distance }; } catch (error) { log.debug('Failed to resolve base candidate', { error: String(error), gitCwd, baseRef, branch }); return null; } } function chooseBestBaseCandidate(candidates: BaseRefCandidate[]): BaseRefCandidate | null { if (candidates.length === 0) { return null; } const sorted = [...candidates].sort((a, b) => { const aTakt = a.firstSubject.startsWith(TAKT_COMMIT_PREFIX); const bTakt = b.firstSubject.startsWith(TAKT_COMMIT_PREFIX); if (aTakt !== bTakt) { return aTakt ? -1 : 1; } if (a.distance !== b.distance) { return a.distance - b.distance; } const aRemote = a.baseRef.includes('/'); const bRemote = b.baseRef.includes('/'); if (aRemote !== bRemote) { return aRemote ? 1 : -1; } return a.baseRef.localeCompare(b.baseRef); }); const best = sorted[0]; return best ? best : null; } export function resolveBranchBaseCommitFromRefs( gitCwd: string, defaultBranch: string, branch: string, cache?: BranchBaseResolutionCache, ): string | null { const priorityRefs = buildPriorityRefs(defaultBranch, branch); const priorityCandidates: BaseRefCandidate[] = []; for (const ref of priorityRefs) { const candidate = resolveBaseCandidate(gitCwd, ref, branch); if (candidate) { priorityCandidates.push(candidate); } } const priorityBest = chooseBestBaseCandidate(priorityCandidates); if (priorityBest) { return priorityBest.baseCommit; } const refs = listCandidateRefs(gitCwd, branch, cache).filter(ref => !priorityRefs.includes(ref)); const candidates: BaseRefCandidate[] = []; for (const ref of refs) { const candidate = resolveBaseCandidate(gitCwd, ref, branch); if (candidate) { candidates.push(candidate); } } const best = chooseBestBaseCandidate(candidates); if (!best) { return null; } return best.baseCommit; }