Scenario: bisect-into-a-pull
git bisect in the monorepo ends at a pull X. A pull splices upstream
changes in as one commit, so the bug came in with one of the upstream
commits squashed into it. Which one?
- Upstream:
seed, then three commits:add config.txt,log at debug levelandfix a typo. - Monorepo:
vendor/a/spliced in atseed, thencheck.sh, a test outside the splice that fails when the library logs at debug level. Then a pull of the three upstream commits, and one more commit.
The upstream's own history has no test for it: the bug only shows in the monorepo. So the upstream commits are tested inside the monorepo, as it was right before the pull.
The drawing names the monorepo's commits a (add vendor/a), b
(add check.sh), X (the pull) and d (add notes), and the upstream's k
(seed), l, m and n, the three pulled commits. X merged from U(b) = k
and records U(X) = n, so the pulled commits are U(b)..U(X): l, m and n.
Output
setup.bash builds this state:
$ build_scenario scenario_bisect_into_a_pull
The drawing shows what Git and git-splice say about it, with each
commit named by its place in the history
(check_drawing):
$ check_drawing "$TESTDIR/bisect-into-a-pull.drawio.svg" a=HEAD~3 b=HEAD~2 X=HEAD~1 d=HEAD k=refs/splices/upstream/main~3 l=refs/splices/upstream/main~2 m=refs/splices/upstream/main~1 n=refs/splices/upstream/main
ok bisect-into-a-pull.drawio.svg: 8 commits, 11 arrows, 5 boxes, 2 frames
Bisect the monorepo
$ git log --format=%s
add notes
splice: pull vendor/a from main at 803779d
add check.sh
add vendor/a
initial commit
The commit that added check.sh is the known good one. Before it,
./check.sh doesn't exist, and git bisect run would count that as bad:
$ git bisect start HEAD "$(git log -1 --format=%H --diff-filter=A -- check.sh)" >/dev/null && git bisect run ./check.sh >/dev/null && git show --stat --format='first bad commit: %s' refs/bisect/bad
first bad commit: splice: pull vendor/a from main at 803779d
vendor/a/.splice | 2 +-
vendor/a/config.txt | 1 +
vendor/a/file.txt | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
The first bad commit is the pull X. It has the changes of three upstream commits in one.
$ X="$(git rev-parse refs/bisect/bad)" && git bisect reset >/dev/null
The upstream commits in the pull
.splice records the synced commit: U(X^) before the pull, in X's
first parent, and U(X) after it, in X, here saved as U1 and U2. The
commits between them came in with X.
The fetched upstream refs have them:
$ U1="$(git config --blob "$X^:vendor/a/.splice" splice.commit)" && U2="$(git config --blob "$X:vendor/a/.splice" splice.commit)" && git log --oneline "$U1..$U2"
803779d fix a typo
83019ad log at debug level
4192552 add config.txt
#109 proposes
git splice show <commit> to print them.
That range is only right if nothing was pushed since the pull before X.
A push doesn't write to .splice, so U(X^) then is older than where X
merged from, and the range has commits the monorepo already had
(after-a-push).
Bisect the upstream commits inside the monorepo
Two worktrees: ../before, the monorepo right before the pull, and
../lib, the upstream at U(X). Both have the upstream's layout, so
they need a worktree of their own.
$ git worktree add -q --detach ../before "$X^" && git worktree add -q --detach ../lib "$U2" && cd ../lib
Bisect runs in ../lib. For each candidate, it copies the candidate's
files into ../before/vendor/a and runs the monorepo's test there:
$ git bisect start "$U2" "$U1" && git bisect run sh -c 'git --work-tree=../before/vendor/a checkout -f HEAD -- . && cd ../before && ./check.sh'
Bisecting: 0 revisions left to test after this (roughly 1 step)
[83019adee0a2cf37e98fc79274ba471a0ba8f99a] log at debug level
running 'sh' '-c' 'git --work-tree=../before/vendor/a checkout -f HEAD -- . && cd ../before && ./check.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[419255245bf628a762cb138a18347abd227d07e7] add config.txt
running 'sh' '-c' 'git --work-tree=../before/vendor/a checkout -f HEAD -- . && cd ../before && ./check.sh'
83019adee0a2cf37e98fc79274ba471a0ba8f99a is the first bad commit
commit 83019adee0a2cf37e98fc79274ba471a0ba8f99a
Author: Upstream <upstream@example.com>
Date: Thu Jan 1 00:00:00 2026 +0000
log at debug level
config.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
bisect found first bad commit
That's the exact upstream commit. If the bug shows in the upstream
alone, git bisect run in ../lib without ../before is enough.
The copy leaves files in the folder that a candidate doesn't have, e.g.
ones an upstream commit deleted. It also only writes files: the index
of ../before doesn't change, so a test that asks Git, e.g. with
git ls-files or git grep, doesn't see files a candidate added. And if X merged local changes into the
folder, the candidates are tested without them.
#110 proposes
git splice expand, which would turn X into one monorepo commit per
upstream commit, so that git bisect continues in the monorepo itself.