What you will build
A release check may need to know whether a required commit is included in a candidate tip. Timestamps and branch names cannot reliably answer that graph question. Ask Git directly with merge-base --is-ancestor, preserving its distinction between a negative answer and an error.
Create two empty commits in a disposable repository, then verify the forward relation, reverse relation, equality and an invalid revision. The script does not inspect, fetch from or change your project repository. Commit identity is supplied only to fixture commands and is never written to account configuration.
Prerequisites
Bash, Git and standard Linux utilities including mktemp and rm. Tested with Git 2.43.0. Run in a writable scratch directory. Save as ancestry.sh; it creates a temporary local repository and removes it on exit.
#!/usr/bin/env bash
set -euo pipefail
repo=$(mktemp -d "$PWD/ancestry-fixture.XXXXXX")
trap 'rm -rf -- "$repo"' EXIT
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
git init -q "$repo"
cd "$repo"
git -c user.name=Fixture -c [email protected] commit -q --allow-empty -m base
base=$(git rev-parse HEAD)
git -c user.name=Fixture -c [email protected] commit -q --allow-empty -m next
tip=$(git rev-parse HEAD)
classify() {
local status
if git merge-base --is-ancestor "$1" "$2" 2>/dev/null; then
echo ancestor
else
status=$?
if (( status == 1 )); then echo not-ancestor
else echo error
fi
fi
}
[[ $(classify "$base" "$tip") == ancestor ]]
[[ $(classify "$tip" "$base") == not-ancestor ]]
[[ $(classify "$tip" "$tip") == ancestor ]]
[[ $(classify does-not-exist "$tip") == error ]]
printf '%s\n' 'forward=ancestor reverse=not-ancestor same=ancestor invalid=error'
Run and expected output
bash ancestry.shforward=ancestor reverse=not-ancestor same=ancestor invalid=errorHow it works
The official command reference defines exit zero as ancestor, one as not ancestor and another nonzero status as an error. The first commit argument is the proposed ancestor; the second is the candidate descendant. Swapping arguments changes the question.
The helper runs Git in an if condition, so expected nonzero outcomes do not trigger errexit. It captures status immediately in the else branch before another command can overwrite it. This preserves information that would be lost by reducing every failure to a generic no.
Base is an ancestor of the new tip, while the new tip is not an ancestor of base. A commit is also considered an ancestor of itself. The invalid name must produce the error classification. Shell comparisons turn each response into an executable assertion.
The temporary path is absolute because the script changes into the repository. Its EXIT trap can therefore remove the correct directory from any current location. Global and system Git configuration are disabled for this process, reducing dependence on machine-wide signing or hook settings.
Edge cases and scope
The helper prints classifications for demonstration. In a real gate, make the error branch fail the gate and preserve stderr for diagnosis. Suppression here keeps the invalid-revision fixture output stable; it should not become the default operational logging policy.
An ancestry answer depends on locally available history. A shallow clone or missing object can prevent a meaningful check. Fetching history is a separate operation; this example has no network access. Use immutable commit IDs for reproducible reports because branch references can move between runs.
Troubleshooting
If every check reports not-ancestor, verify argument order and stop collapsing nonzero exits. If a valid name reports error, remove stderr suppression and inspect object availability. If Bash exits early, keep expected-failure commands inside if conditions. The final line appears only after all four assertions pass.
Primary references
Continue with Git tutorials and all Java Samples categories.