From: Alejandro Colomar <alx@kernel.org>
To: Junio C Hamano <gitster@pobox.com>, git@vger.kernel.org
Cc: Ben Boeckel <mathstuf@gmail.com>,
Nico Williams <nico@cryptonector.com>,
Viktor Dukhovni <viktor@openssl.org>
Subject: [RFC v3] git-bisect-rebase
Date: Fri, 9 Oct 2026 00:18:49 +0200 [thread overview]
Message-ID: <asgU5dr0Z3x5jN_f@debian> (raw)
In-Reply-To: <asFRVdMTpshsazgM@debian>
[-- Attachment #1.1: Type: text/plain, Size: 2579 bytes --]
Hi!
I've improved the usability of the script, thinking about two different
use cases:
- Moving a single branch.
- Moving a tree of branches.
For that, I've implemented --abort/--continue/--quit options, which are
similar to the options in git-rebase(1), but different in that they act
on the bisect-rebase operation, and not on a single rebase.
The --continue flag is useful for the case of rebasing a single tree,
not having to specify the target and flags each time.
The --quit flag is useful to maintain the ability to terminate the
bisect-rebase at the current point, having advanced the branch by some
distance. This allows changing to other branches (descendants) that
will be moved together with the parent, to minimize conflict resolution.
I wrote in a different subthread the algorithms that would be used for
each of these two cases, but I'll paste them again here:
So, the usual algorithm for moving a tree would be (assuming A is
a branch with two descendants, B and C):
$ git bisect-rebase master A;
$ git rebase --abort;
$ git bisect-rebase --quit;
$ git bisect-rebase A B;
## ... until success
$ git bisect-rebase A C;
## ... until success
$ git tag tmpA;
$ git bisect-rebase master A;
## resolve conflicts here
$ git rebase --continue;
$ git bisect-rebase --quit;
$ git rebase --onto A tmpA B;
$ git rebase --onto A tmpA C;
$ git tag -d tmpA;
## ... rinse and repeat (goto first command, until no conflicts)
And the algorithm for moving a single branch would be simpler:
$ git bisect-rebase master X;
## resolve conflicts here
$ git rebase --continue;
$ git bisect-rebase --continue;
## ... rinse and repeat (goto first command, until no conflicts)
The script has been split into 3 smaller scripts. They are all
attached, but can be also found in the Linux man-pages repository
(alongside their history, which might clarify some design decisions):
<https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/src/bin/git-bisect-rebase>
<https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/src/bin/git-bisect-rebase--callback>
<https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/tree/src/bin/git-bisect-rebase--continue>
Of course, the entry point is git-bisect-rebase, with the other two
being internal helpers.
Have a lovely night!
Alex
--
<https://www.alejandro-colomar.es>
[-- Attachment #1.2: git-bisect-rebase --]
[-- Type: text/plain, Size: 3468 bytes --]
#!/bin/bash
# Copyright 2026, Alejandro Colomar <alx@kernel.org>
# SPDX-License-Identifier: GPL-3.0-or-later
set -Eeufo pipefail;
shopt -s lastpipe;
err()
{
>&2 printf '%s\n' "$(basename "$0"): error: $*";
exit 1;
}
git rev-parse --git-dir | read -r git_dir;
git rev-parse --git-path bisect-rebase | read -r dir;
while test $# -ge 1; do
case "$1" in
--abort)
test -d "$dir/" || err 'Not in a bisect-rebase operation?';
set +e;
git rebase --abort >/dev/null 2>/dev/null;
git bisect reset >/dev/null 2>/dev/null;
xargs git checkout <"$dir/head-name";
xargs git reset --hard <"$dir/orig-head";
rm -rf "$dir";
exit 0;
;;
--continue)
test -d "$dir/" || err 'Not in a bisect-rebase operation?';
if test -d "$git_dir/rebase-merge"; then
err 'You must finish the current rebase operation.';
fi;
git-bisect-rebase--continue;
exit 0;
;;
--quit)
test -d "$dir/" || err 'Not in a bisect-rebase operation?';
if test -d "$git_dir/rebase-merge"; then
err 'You must finish the current rebase operation.';
fi;
rm -rf "$dir";
exit 0;
;;
*)
break;
;;
esac;
done;
mkdir "$dir";
# shellcheck disable=SC2064 # we want expansion now
trap "rm -rf '$dir'" EXIT;
# shellcheck disable=SC2312 # False positive: <https://github.com/koalaman/shellcheck/issues/3554>
cat <<-'__EOF__' |
git bisect-rebase [<option> ...] [<upstream> [<branch>]]
Rebase <branch> on top of the first conflicting commit from <upstream>.
--
Mode options
abort! Abort the bisect-rebase operation and reset HEAD to the original <branch>.
continue! Restart the bisect-rebase process after having resolved a particular rebase.
quit! Abort the bisect-rebase operation but HEAD is not reset.
Options
pre-exec!=cmd Run <cmd> before every git-rebase(1) operation.
post-exec!=cmd Run <cmd> after every git-rebase(1) operation.
git-rebase(1) options
x,exec=cmd See git-rebase(1).
r,rebase-merges? ^
apply!* ^
empty!*= ^
keep-empty* ^
reapply-cherry-picks* ^
allow-empty-message* ^
m,merge!* ^
s,strategy!*= ^
X,strategy-option!*= ^
rerere-autoupdate* ^
S,gpg-sign*? ^
q,quiet!* ^
v,verbose!* ^
stat!* ^
n,no-stat!* ^
verify* ^
C!*= ^
no-ff!* ^
f,force-rebase!* ^
ignore-whitespace* ^
update-refs* ^
git-bisect(1) options
first-parent! See git-bisect(1).
__EOF__
eval "$(git rev-parse --parseopt --stuck-long --keep-dashdash -- "$@" || echo exit $?)";
touch "$dir/git-bisect-options";
touch "$dir/git-rebase-options";
while test $# -ge 1; do
case "$1" in
--abort | --continue | --quit)
err "$1 is incompatible with other arguments.";
;;
--)
shift;
break;
;;
--pre-exec=*)
echo "$1" | sed 's/--pre-exec=//' >"$dir/pre-exec";
;;
--post-exec=*)
echo "$1" | sed 's/--post-exec=//' >"$dir/post-exec";
;;
--first-parent)
echo "$1" >>"$dir/git-bisect-options";
;;
-*)
echo "$1" >>"$dir/git-rebase-options";
;;
*)
exit 1; # Unreachable
;;
esac;
shift;
done;
if test $# -lt 1; then
git rev-parse '@{u}';
else
git rev-parse --verify "$1";
fi >"$dir/onto";
test $# -lt 1 || shift;
if test $# -lt 1; then
git rev-parse --abbrev-ref HEAD;
git rev-parse HEAD;
else
git rev-parse --verify --symbolic "$1";
git rev-parse --verify "$1";
fi \
| sed '/^HEAD$/d' \
| sed '1!d' \
>"$dir/head-name";
test $# -lt 1 || shift;
if test $# -gt 0; then
err 'Too many arguments.';
fi;
git rev-parse HEAD >"$dir/orig-head";
trap - EXIT;
git-bisect-rebase--continue;
exit 0;
[-- Attachment #1.3: git-bisect-rebase--callback --]
[-- Type: text/plain, Size: 1270 bytes --]
#!/bin/bash
# Copyright 2026, Alejandro Colomar <alx@kernel.org>
# SPDX-License-Identifier: GPL-3.0-or-later
set -Eeufo pipefail;
shopt -s lastpipe;
git rev-parse --git-dir | sed 's,$,/bisect-rebase,' | read -r dir;
# shellcheck disable=SC2064 # we want expansion now
trap "xargs git checkout --detach <'$dir/bisect-head' 2>/dev/null" EXIT;
git rev-parse HEAD >"$dir/bisect-head";
if test -e "$dir/pre-exec"; then
printf '%s' 'Pre-rebase exec: ';
if
bash "$dir/pre-exec";
x="$?";
true;
then
echo "exit status: $x";
if test "$x" -ne 0; then
exit "$x";
fi;
fi;
fi;
xargs git checkout <"$dir/head-name" >/dev/null 2>/dev/null;
git rev-parse HEAD >"$dir/good-head";
printf '%s' 'Rebase: ';
cat "$dir/git-rebase-options" "$dir/bisect-head" \
| if xargs git rebase >/dev/null 2>/dev/null; then
echo 'success';
else
echo 'conflict';
git rebase --abort >/dev/null;
exit 1;
fi;
if test -e "$dir/post-exec"; then
printf '%s' 'Post-rebase exec: ';
if
bash "$dir/post-exec";
x="$?";
true;
then
echo "exit status: $x";
if test "$x" -ne 0; then
xargs git reset --hard <"$dir/good-head";
exit "$x";
fi;
fi;
fi;
{
git rev-parse --abbrev-ref HEAD;
git rev-parse HEAD;
} \
| sed '/^HEAD$/d' \
| sed '1!d' \
>"$dir/head-name";
exit 0;
[-- Attachment #1.4: git-bisect-rebase--continue --]
[-- Type: text/plain, Size: 1072 bytes --]
#!/bin/bash
# Copyright 2026, Alejandro Colomar <alx@kernel.org>
# SPDX-License-Identifier: GPL-3.0-or-later
set -Eeufo pipefail;
shopt -s lastpipe;
err()
{
>&2 printf '%s\n' "$(basename "$0"): error: $*";
exit 1;
}
git rev-parse --git-path bisect-rebase | read -r dir;
# Try the target first.
xargs git checkout --detach <"$dir/onto" 2>/dev/null;
if git-bisect-rebase--callback; then
xargs git checkout <"$dir/head-name" >/dev/null 2>/dev/null;
rm -rf "$dir";
exit 0;
fi;
# Bisect.
xargs git bisect start <"$dir/git-bisect-options" >/dev/null;
xargs git bisect bad <"$dir/onto" >/dev/null;
cat "$dir/head-name" "$dir/onto" | xargs git merge-base | xargs git bisect good;
git bisect run git-bisect-rebase--callback;
git rev-parse bisect/bad >"$dir/bisect-bad";
git bisect reset >/dev/null 2>/dev/null;
# Perform the conflicting rebase
xargs git checkout <"$dir/head-name" >/dev/null 2>/dev/null;
cat "$dir/git-rebase-options" "$dir/bisect-bad" | xargs git rebase;
if test -e "$dir/post-exec"; then
echo 'Running post-rebase exec.';
bash "$dir/post-exec";
fi;
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 833 bytes --]
prev parent reply other threads:[~2026-10-08 22:18 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-03 20:11 [RFC] git-brebase Alejandro Colomar
2026-10-03 20:13 ` Alejandro Colomar
2026-10-03 20:39 ` Nico Williams
2026-10-03 20:48 ` Nico Williams
2026-10-03 21:07 ` Alejandro Colomar
2026-10-03 21:17 ` Nico Williams
2026-10-03 21:29 ` Alejandro Colomar
2026-10-03 21:50 ` Nico Williams
2026-10-03 20:56 ` Alejandro Colomar
2026-10-03 21:13 ` Nico Williams
2026-10-03 21:38 ` Alejandro Colomar
2026-10-03 22:19 ` Nico Williams
2026-10-03 21:17 ` Alejandro Colomar
2026-10-08 22:18 ` Alejandro Colomar [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=asgU5dr0Z3x5jN_f@debian \
--to=alx@kernel.org \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=mathstuf@gmail.com \
--cc=nico@cryptonector.com \
--cc=viktor@openssl.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox