134 lines
3.8 KiB
TypeScript
134 lines
3.8 KiB
TypeScript
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;
|
|
}
|