What does git merge-base --is-ancestor report, and how do scripts use its exit code?
answer
- It prints nothing at all
- The answer arrives as a status code
- Zero means yes, one means no
- Argument order reads as a sentence
- Non-zero is not always a plain no
basics
~20 sgit merge-base --is-ancestor A B prints nothing and exits 0 when commit A is reachable from commit B, and exits 1 when it is not. Scripts branch on that status to test containment without parsing any output.
solid answer
~50 s`git merge-base --is-ancestor <A> <B>` is a predicate, not a query: it produces no output and communicates the answer purely through its exit status — 0 if `A` is an ancestor of `B` (a commit is considered its own ancestor), 1 if not, and something else on a real error like an unknown revision. That makes it the right primitive for automation: `if git merge-base --is-ancestor "$commit" origin/main; then …` reads naturally in a shell and needs no output parsing, unlike comparing `git rev-list --count` results or grepping a log. Typical uses are hook and release checks — is this fix already contained in the release branch, is the branch being pushed based on current `main`, has this tag been merged. Distinguish exit 1 (a valid "no") from other non-zero statuses (an error) if the script must be trustworthy.
code
bash · 5 linesif git merge-base --is-ancestor "$fix" origin/release-2.4; then
echo "already contained"
else
echo "needs backporting"
figo deeper
Know that this form of git merge-base answers a yes/no containment question and produces no output — you read the exit status, not stdout.
Explain the exact semantics: exit 0 when the first commit is reachable from the second, 1 when it is not, a commit counts as its own ancestor, and the argument order reads as the English sentence.
Use it correctly in automation — fast-forward checks in hooks, backport gating in release tooling — and handle error statuses separately from a legitimate negative so a bad ref never masquerades as an answer.
Recognize where ancestry is the wrong question for your integration model: on a squash- or rebase-based mainline, containment by hash misreports what has shipped, and release tooling needs a patch-level check instead.
## A predicate in a family of query commands Most `git merge-base` forms print a hash: `git merge-base A B` prints the best common ancestor, `--all` prints every best common ancestor. `--is-ancestor` breaks that pattern deliberately. It prints nothing and answers through the exit status: - **0** — the first commit is an ancestor of the second. - **1** — it is not. - **other non-zero** — an error, for example a revision that does not resolve. A commit is its own ancestor, so `git merge-base --is-ancestor HEAD HEAD` exits 0. ## Why this shape is useful Shell scripts and hooks branch on exit status natively, so the check composes with no text handling at all — `if git merge-base --is-ancestor "$fix" origin/release-2.4; then … else … fi`. The alternatives are worse. `git log --oneline A..B | grep` is fragile and depends on output formatting. `git branch --contains` answers a related question but prints a branch list you must then parse. `git rev-list --count A..B` works but requires you to compare numbers and gets the direction wrong easily. `--is-ancestor` states the question exactly once, in the direction you meant. ## Getting the argument order right The order is `--is-ancestor <possible-ancestor> <descendant>`. Reversing it silently answers a different question, and since the command prints nothing there is no output to alert you. The mnemonic: read it as an English sentence — "is `A` an ancestor of `B`?" — with the arguments in the order the sentence puts them. ## What the answer means Ancestry is reachability in the commit graph: can you get from `B` to `A` by following parent links? So a 0 exit means those exact commit objects are contained. As always in Git, this is about hashes, not content. A fix that was cherry-picked or squashed into the release branch exists there as a *different* commit, so `--is-ancestor` correctly reports 1 — the original commit is genuinely not in that history, even though its change is. When you need "is this change present" rather than "is this commit present", the patch-level comparison in `git cherry` is the right tool instead. ## Where it shows up - **Release and backport tooling** — does `release-2.4` already contain this commit, or does the fix need picking? - **Pre-receive and pre-push hooks** — is the commit being pushed a descendant of the current tip, i.e. would this be a fast-forward? (`git merge-base --is-ancestor <old> <new>` is precisely that test.) - **Guard clauses in build scripts** — refuse to run a deploy from a commit that is not contained in the mainline. - **Checking whether a branch needs updating** — `--is-ancestor origin/main HEAD` being 1 means `main` has moved on since you branched. ## Distinguishing "no" from "broken" In a careful script, treat 1 and other non-zero statuses differently: capture `$?`, map 0 to yes and 1 to no, and escalate anything else as an error. Collapsing everything non-zero into "not an ancestor" means a typo in a ref name silently reports "needs backporting" forever, which is the kind of bug that hides in release tooling for months. ## Related forms worth knowing `git merge-base --independent <commits…>` filters a list down to those not reachable from any other in the list — useful when reducing a set of candidate tips. `git merge-base --octopus <commits…>` computes a common ancestor across more than two commits. Both, like the plain form, print hashes; `--is-ancestor` remains the only one whose whole answer is the exit code.
- How would you test whether a fix has already been applied to a release branch that squashes merges?`--is-ancestor` will say no, correctly: the squash produced a different commit object. Use `git cherry -v release-2.4 topic`, which compares patch identity and marks already-present changes with a minus sign, or search the branch by content. Ancestry answers "is this commit here", not "is this change here".
- What does git merge-base --is-ancestor report for a commit against itself?Exit 0. A commit is considered an ancestor of itself, which keeps the predicate consistent with reachability: you can reach a commit from itself in zero steps. Scripts relying on strict ancestry must add an equality check themselves.
- Why is collapsing every non-zero exit into "not an ancestor" a bug?Because a misspelled ref, a missing object, or a repository error also exits non-zero. Treating those as a clean "no" makes release tooling silently report that every fix needs backporting, and the failure never surfaces. Handle 0 and 1 explicitly and escalate anything else.
saying these in an interview costs you the question
- Expects the command to print true or false
- Reverses the argument order and trusts the result
- Treats any non-zero exit as a definitive no
- Thinks it detects cherry-picked or squashed equivalents
- Confuses it with git merge-base --all, which prints hashes