Skip to content

Commit 9f50f1e

Browse files
committed
Add isAncestor helper
Wraps `git merge-base --is-ancestor` for the upcoming base-SHA walk. A candidate SHA on a side branch isn't reachable from the main-train HEAD, so the walk needs this primitive to skip it.
1 parent 96ab707 commit 9f50f1e

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

src/git.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getCommitContextsBetweenShas,
1414
getCommitParents,
1515
getRepoInfo,
16+
isAncestor,
1617
normalizePathspec,
1718
parseRepoUrl,
1819
resolveFirstSyncBoundary,
@@ -911,6 +912,29 @@ describe("merge commit handling", () => {
911912
});
912913
});
913914

915+
describe("isAncestor", () => {
916+
it("returns true when sha is an ancestor of headSha", () => {
917+
expect(isAncestor(mergeRepo.commits.base, mergeRepo.commits.mergeCommit, mergeRepo.cwd)).toBe(true);
918+
});
919+
920+
it("returns true for a sha equal to headSha", () => {
921+
expect(isAncestor(mergeRepo.commits.mergeCommit, mergeRepo.commits.mergeCommit, mergeRepo.cwd)).toBe(true);
922+
});
923+
924+
it("returns false when sha is not on headSha's history", () => {
925+
// featureBranch is reachable from mergeCommit (parent #2), but mergeCommit
926+
// is not reachable from featureBranch — that's the asymmetric case the
927+
// walk relies on to skip side-branch candidates.
928+
expect(isAncestor(mergeRepo.commits.mergeCommit, mergeRepo.commits.featureBranch, mergeRepo.cwd)).toBe(false);
929+
});
930+
931+
it("returns false for an unknown sha", () => {
932+
expect(isAncestor("0000000000000000000000000000000000000000", mergeRepo.commits.mergeCommit, mergeRepo.cwd)).toBe(
933+
false,
934+
);
935+
});
936+
});
937+
914938
describe("getCommitContextsBetweenShas with merge commits", () => {
915939
it("should include merge commit when path filtering would exclude it", () => {
916940
// The merge node itself adds no file changes, so default simplification

src/git.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,25 @@ export function commitExists(sha: string, cwd: string = process.cwd()): boolean
165165
}
166166
}
167167

168+
/**
169+
* True iff `sha` is reachable by walking parents from `headSha`.
170+
*
171+
* Used to verify that a candidate base SHA is actually on HEAD's history before
172+
* we hand it to `git log <base>..<HEAD>` — a candidate from a side branch (e.g.
173+
* a hotfix release) will scan a wrong range otherwise.
174+
*/
175+
export function isAncestor(sha: string, headSha: string, cwd: string = process.cwd()): boolean {
176+
try {
177+
execSync(`git merge-base --is-ancestor ${sha} ${headSha}`, {
178+
cwd,
179+
stdio: ["ignore", "ignore", "ignore"],
180+
});
181+
return true;
182+
} catch {
183+
return false;
184+
}
185+
}
186+
168187
const SHA_PATTERN = /^[0-9a-f]{7,40}$/i;
169188

170189
/**

0 commit comments

Comments
 (0)