git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[RFC v3] git-bisect-rebase

From
ACAlejandro Colomar <alx@kernel.org>
Date
Oct 8, 2026, 22:18 UTC
Message-ID
<asgU5dr0Z3x5jN_f@debian>
In-Reply-To
<asFRVdMTpshsazgM@debian>
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>


#!/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;


#!/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;


#!/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;
Previous: Nico Williams
Message 14 of 14 in “[RFC] git-brebase”
  1. Alejandro ColomarOct 3, 2026
  2. Alejandro ColomarOct 3, 2026
  3. Nico WilliamsOct 3, 2026
  4. Nico WilliamsOct 3, 2026
  5. Alejandro ColomarOct 3, 2026
  6. Nico WilliamsOct 3, 2026
  7. Alejandro ColomarOct 3, 2026
  8. Nico WilliamsOct 3, 2026
  9. Alejandro ColomarOct 3, 2026
  10. Alejandro ColomarOct 3, 2026
  11. Nico WilliamsOct 3, 2026
  12. Alejandro ColomarOct 3, 2026
  13. Nico WilliamsOct 3, 2026
  14. [RFC v3] git-bisect-rebaseAlejandro Colomar, Oct 8, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.