Skip to contentSkip to conversation

git-brebase

RFC Alejandro Colomar 2 participants 14 messages Oct 3 – Oct 8

lore
Share & tools
lore
#0 lore raw

Hi!

I've significantly improved the idea from the original thread, thanks to suggestions from several people (the most fundamental, by Nico Williams and Ben Boeckel), and inspired to write it after knowing about the tool written by Nico Williams and Viktor Dukhovni (but I implemented it from scratch, without reading their implementation, other than looking at the file size --which, being under 100 LoC, gave me the confidence that it could be implemented easily--).

I believe now, after several improvements, it is not only simpler than git-imerge, but also more powerful.

I've not used git-imerge, but I've watched the youtube video of the talk in which the author explains how it works (and it's quite nice, to be fair). First some similarities between both:

- Both git-imerge and my tool support --first-parent (per the README of git-imerge). My tool has support for this by using git-bisect(1) interally for the bisection.

This feature was suggested to me by Ben Boeckel.

- Both git-imerge and my tool reach the same tip after a successful session. They differ in the intermediate history.

Below goes an overview of key limitations of the git-imerge approach (IMO), and which are not present in mine. If git-imerge supports any of this, I'm sorry; I didn't find them in their README.

- git-imerge creates a 2D matrix of conflict resolutions. This is nice for the case of two flat branches. However, the branch to be rebased might also contain merge commits. This is less common than having merge commits in the target branch, but it happens, and in those cases, it's frequent to want to keep the structure of the branch, with --rebase-merges. My tool has support for this by using git-rebase(1) internally for the rebases.

- One may want to skip arbitrary commits (because they're known to be broken, and possibly immediately reverted). For that, I've provided a specific flag, --pre-exec, which is similar to git-rebase(1)'s --exec, but which is executed before each rebase operation, at the BISECT_HEAD commit. An exit code of 125 skips that revision immediately, without trying to do any rebases.

This feature was suggested to me by Ben Boeckel.

- One may also want to find and resolve semantic conflicts that do not appear as physical text conflicts. For that, I've provided a specific flag, --post-exec, which is similar to --pre-exec, but runs after each successful git-rebase(1) operation. This would usually build and test the software itself.

This feature was suggested to me by Ben Boeckel.

- git-imerge is limited when rebasing trees of branches. Let's consider something more complex:

	*---*---*---*---*---M
	 \
	  *---*---A---*---B
	   \ /     \
	    *       *---C---D

Let's say M is master, to which we want to rebase the tree composed of branches A, B, and C, so that it results in this:

	*---*---*---*---*---M
	                     \
	                      *---*---A'--*---B'
	                       \ /     \
	                        *       *---C'--D'

With my tool, this becomes trivial (I've been doing this all day earlier today, resolving in a few hours what would have taken me days). The approach in this case would be to rebase from root to leaves, but to solve conlicts from leaves to root:

1) Use git-brebase to rebase A until the first conflict with M.

2) Abort the conflicting rebase. We don't want to resolve the conflict yet, or we'd have to repeat the same resolution later.

3) Rebase all its direct descendants into the new A, by performing operations 1 and 2 but with the descendant branches. Do this recursively with children of children.

4) Once all descendants have been moved below the new A, it's time to solve the conflicts in A. This will result in advancing A by just one commit of M, since we had aborted exactly at the conflicting rebase.

5) Rebase the direct descendants on top of the new A, this time with git-rebase(1) --not brebase!-- with --interactive, dropping all commits that exist in A. This avoids resolving the same conflicts again. Do this recursively with children of children.

6) Rinse and repeat since step 1, until everything is successful. At that point, we have reached the end of the session.

This approach is different from git-imerge, in that git-imerge manages in a single session the entire rebase operation until success, while my tool performs each conflict resolution in a single step, and they are entirely independent, and can be interrupted to do other work. My tool requires repeated invocations until reaching the end point, denoted by a successful exit status.

Something that git-imerge has that my tool hasn't is the ability to do merge commits. My tool exclusively does rebases. However, once the end commit is reached, creating that could be used to produce a merge commit. It could be done by first reaching the rebase tip in a disposable branch, then perform a regular merge commit with git-merge(1), and resolve conflicts by doing something like

	$ git checkout disposable -- .
and then finish the merge.  It's not a critical limitation of my tool,
IMO.  (Although, admittedly, it's a trick that not everyone would know
to do.)

Another difference is that, by doing rebases, my tool doesn't remember the entire history matrix that git-imerge holds while doing the work. I see this as an advantage, as once we've finished one conflict step, and we've verified with git-range-diff(1) and with proper testing that it's correct, the extra history would clutter the
'git log --graph --oneline HEAD target current' (something essential when doing these operations). Having a lean history in the process helps get it right, being able to check important commits in the log.

Now about details of the implementaion:

- The tool supports --first-parent, and passes it transparently to git-bisect(1).

- The tool supports the flags --pre-exec and --post-exec, which are interpreted especially by the tool.

- The tool accepts other flags, and passes them transparently to git-rebase(1). If some flag isn't supported by git-rebase(1), it will be that program which will complain. Also, I haven't made an attempt to validate that the flags passed make sense with this tool (for example, passing --abort would be accepted by git-rebase(1), but it wouldn't make sense, and would probably fail at some point).

It would be good to curate a list of flags that make sense.

- My tool, being a simple shell script with rudimentary option parsing, only accepts flags that take a single shell argument. That is, --foo=bar is ok, but --foo bar is not okay (and will probably result in parsing errors).

- The tool is meant to be used almost as a drop-in of git-rebase(1). It does the same thing, except that instead of rebasing on the target, it rebases on the first commit of the target branch which has conflicts.

(It has grown a bit fatter than it was, but it's still way below
git-imerge.)

	$ wc -l <src/bin/git-brebase 
	164

We'll discuss the exact way it should be integrated within git(1), but first it'd be interesting to get feedback about the tool itself, regardless of the actual form. Actually, because of the specialized flags --pre-exec and --post-exec, and the --first-parent flag from git-bisect(1) --and the fact that it runs git-bisect(1) machinery--, I'm not entirely sure that it should be just a new flag to git-rebase(1). It might be confusing to have these three flags being dependent on another flag, and not being able to use this within a git-bisect(1) session, unlike other git-rebase(1) operations. That might call for a new git command.

Please let me know any feedback! :)

Junio, since you seemed to love git-imerge, I wonder what you'll think of this tool. :-)

Having presented the tool, below goes the implementation.

Have a lovely day!
Alex

---
#!/bin/bash
# Copyright 2026, Alejandro Colomar <[email protected]> # SPDX-License-Identifier: GPL-3.0-or-later

set -Eeufo pipefail;
shopt -s lastpipe;

err()

{
	>&2 printf '%s\n' "$(basename "$0"): error: $*";
	exit 1;
}

fp='';
other='';
pre='';
post='';
while test $# -ge 1; do

	case "$1" in
	--first-parent)
		fp='--first-parent';
		;;
	--pre-exec=*)
		echo "$1" \
		| sed 's/--pre-exec=//' \
		| read -r pre;
		;;
	--post-exec=*)
		echo "$1" \
		| sed 's/--post-exec=//' \
		| read -r post;
		;;
	-*)
		other="$other $1";
		;;
	*)
		break;
		;;
	esac;
	shift;

done;
gbopts="$fp";
gropts="$other";

if test $# -lt 1; then

	err 'Missing target commit.';
fi;
if test $# -gt 1; then
	err 'Too many arguments.';

fi;
git rev-list -1 "$1" \
| read -r tgt;
git rev-parse --abbrev-ref HEAD \
| read -r branch;

# Set up the callback script for 'git rebase run'. mktemp \
| read -r callback;
cat >"$callback" <<__EOF__
#!/bin/bash

	set -Eeufo pipefail;
	shopt -s lastpipe;

	git rev-list -1 HEAD \
	| read -r bisect_head;

	if test -n '$pre'; then
		printf '%s' 'Pre-rebase exec: ';
		pre='$pre';
		if
			\$pre;
			x="\$?";
			true;
		then
			case "\$x" in
			0)
				echo 'success';
				;;
			125)
				echo 'skip';
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit 125;
				;;
			*)
				echo "failure (\$x)";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit "\$x";
				;;
			esac;
		fi;
	fi;

	git switch '$branch' >/dev/null 2>/dev/null;
	git rev-list -1 HEAD \
	| read -r old_head;
	printf '%s' 'Rebase: ';
	if git rebase $gropts "\$bisect_head" >/dev/null 2>/dev/null; then
		echo 'success';
	else
		echo 'conflict';
		git rebase --abort >/dev/null;
		git checkout --detach "\$bisect_head" 2>/dev/null;
		exit 1;
	fi;

	if test -n '$post'; then
		printf '%s' 'Post-rebase exec: ';
		post='$post';
		if
			\$post;
			x="\$?";
			true;
		then
			case "\$x" in
			0)
				echo 'success';
				;;
			125)
				echo 'skip';
				git reset --hard "\$old_head";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit 125;
				;;
			*)
				echo "failure (\$x)";
				git reset --hard "\$old_head";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit "\$x";
				;;
			esac;
		fi;
	fi;
	git checkout --detach "\$bisect_head" 2>/dev/null;
	exit 0;

__EOF__
chmod +x "$callback";

# Try the target first.
git checkout --detach "$tgt" 2>/dev/null;
if "$callback"; then

	exit 0;

fi;
git status;

# Bisect.

# shellcheck disable=SC2248  # gbopts may hold multiple options
git bisect start $gbopts >/dev/null;
git bisect bad "$tgt" >/dev/null;
git merge-base "$branch" "$tgt" \
| xargs -I{} git bisect good {};
git bisect run "$callback";
git rev-list -1 bisect/bad \
| read -r bad;
git bisect reset >/dev/null 2>/dev/null;

# Perform the conflicting rebase
git switch "$branch";

# shellcheck disable=SC2086  # gropts may hold multiple options
git rebase $gropts "$bad";
if test -v post; then
	echo 'Running post-rebase exec.';
	$post;
fi;
Signature · 1 line
<https://www.alejandro-colomar.es>
#1 lore raw

(I meant this to be a reply to the previous thread, but I somehow
forgot while writing it. Here's a link, for context.)

<https://lore.kernel.org/git/ar5KL4_IKXYbx3Sb@debian/T/#u>

Cheers,
Alex

Quoted 357 lines

Date: 2026-10-03 22:11:54+0200
From: Alejandro Colomar <[email protected]>

Hi!

I've significantly improved the idea from the original thread, thanks to suggestions from several people (the most fundamental, by Nico Williams and Ben Boeckel), and inspired to write it after knowing about the tool written by Nico Williams and Viktor Dukhovni (but I implemented it from scratch, without reading their implementation, other than looking at the file size --which, being under 100 LoC, gave me the confidence that it could be implemented easily--).

I believe now, after several improvements, it is not only simpler than git-imerge, but also more powerful.

I've not used git-imerge, but I've watched the youtube video of the talk in which the author explains how it works (and it's quite nice, to be fair). First some similarities between both:

(quoted diff) +0 -2
-  Both git-imerge and my tool support --first-parent (per the README of
   git-imerge).  My tool has support for this by using git-bisect(1)
   interally for the bisection.
 
   This feature was suggested to me by Ben Boeckel.
 
-  Both git-imerge and my tool reach the same tip after a successful
   session.  They differ in the intermediate history.

Below goes an overview of key limitations of the git-imerge approach (IMO), and which are not present in mine. If git-imerge supports any of this, I'm sorry; I didn't find them in their README.

(quoted diff) +0 -4
-  git-imerge creates a 2D matrix of conflict resolutions.  This is nice
   for the case of two flat branches.  However, the branch to be rebased
   might also contain merge commits.  This is less common than having
   merge commits in the target branch, but it happens, and in those
   cases, it's frequent to want to keep the structure of the branch,
   with --rebase-merges.  My tool has support for this by using
   git-rebase(1) internally for the rebases.
 
-  One may want to skip arbitrary commits (because they're known to be
   broken, and possibly immediately reverted).  For that, I've provided
   a specific flag, --pre-exec, which is similar to git-rebase(1)'s
   --exec, but which is executed before each rebase operation, at the
   BISECT_HEAD commit.  An exit code of 125 skips that revision
   immediately, without trying to do any rebases.
 
   This feature was suggested to me by Ben Boeckel.
 
-  One may also want to find and resolve semantic conflicts that do not
   appear as physical text conflicts.  For that, I've provided a
   specific flag, --post-exec, which is similar to --pre-exec, but runs
   after each successful git-rebase(1) operation.  This would usually
   build and test the software itself.
 
   This feature was suggested to me by Ben Boeckel.
 
-  git-imerge is limited when rebasing trees of branches.  Let's
   consider something more complex:
	*---*---*---*---*---M
	 \
	  *---*---A---*---B
	   \ /     \
	    *       *---C---D

Let's say M is master, to which we want to rebase the tree composed of branches A, B, and C, so that it results in this:

	*---*---*---*---*---M
	                     \
	                      *---*---A'--*---B'
	                       \ /     \
	                        *       *---C'--D'

With my tool, this becomes trivial (I've been doing this all day earlier today, resolving in a few hours what would have taken me days). The approach in this case would be to rebase from root to leaves, but to solve conlicts from leaves to root:

1) Use git-brebase to rebase A until the first conflict with M.

2) Abort the conflicting rebase. We don't want to resolve the conflict yet, or we'd have to repeat the same resolution later.

3) Rebase all its direct descendants into the new A, by performing operations 1 and 2 but with the descendant branches. Do this recursively with children of children.

4) Once all descendants have been moved below the new A, it's time to solve the conflicts in A. This will result in advancing A by just one commit of M, since we had aborted exactly at the conflicting rebase.

5) Rebase the direct descendants on top of the new A, this time with git-rebase(1) --not brebase!-- with --interactive, dropping all commits that exist in A. This avoids resolving the same conflicts again. Do this recursively with children of children.

6) Rinse and repeat since step 1, until everything is successful. At that point, we have reached the end of the session.

This approach is different from git-imerge, in that git-imerge manages in a single session the entire rebase operation until success, while my tool performs each conflict resolution in a single step, and they are entirely independent, and can be interrupted to do other work. My tool requires repeated invocations until reaching the end point, denoted by a successful exit status.

Something that git-imerge has that my tool hasn't is the ability to do merge commits. My tool exclusively does rebases. However, once the end commit is reached, creating that could be used to produce a merge commit. It could be done by first reaching the rebase tip in a disposable branch, then perform a regular merge commit with git-merge(1), and resolve conflicts by doing something like

	$ git checkout disposable -- .
and then finish the merge.  It's not a critical limitation of my tool,
IMO.  (Although, admittedly, it's a trick that not everyone would know
to do.)

Another difference is that, by doing rebases, my tool doesn't remember the entire history matrix that git-imerge holds while doing the work. I see this as an advantage, as once we've finished one conflict step, and we've verified with git-range-diff(1) and with proper testing that it's correct, the extra history would clutter the
'git log --graph --oneline HEAD target current' (something essential when doing these operations). Having a lean history in the process helps get it right, being able to check important commits in the log.

Now about details of the implementaion:

(quoted diff) +0 -5
-  The tool supports --first-parent, and passes it transparently to
   git-bisect(1).
 
-  The tool supports the flags --pre-exec and --post-exec, which are
   interpreted especially by the tool.
 
-  The tool accepts other flags, and passes them transparently to
   git-rebase(1).  If some flag isn't supported by git-rebase(1), it
   will be that program which will complain.  Also, I haven't made an
   attempt to validate that the flags passed make sense with this tool
   (for example, passing --abort would be accepted by git-rebase(1), but
    it wouldn't make sense, and would probably fail at some point).
 
   It would be good to curate a list of flags that make sense.
 
-  My tool, being a simple shell script with rudimentary option parsing,
   only accepts flags that take a single shell argument.  That is,
   --foo=bar is ok, but --foo bar is not okay (and will probably result
   in parsing errors).
 
-  The tool is meant to be used almost as a drop-in of git-rebase(1).
   It does the same thing, except that instead of rebasing on the
   target, it rebases on the first commit of the target branch which
   has conflicts.

(It has grown a bit fatter than it was, but it's still way below
git-imerge.)

	$ wc -l <src/bin/git-brebase 
	164

We'll discuss the exact way it should be integrated within git(1), but first it'd be interesting to get feedback about the tool itself, regardless of the actual form. Actually, because of the specialized flags --pre-exec and --post-exec, and the --first-parent flag from git-bisect(1) --and the fact that it runs git-bisect(1) machinery--, I'm not entirely sure that it should be just a new flag to git-rebase(1). It might be confusing to have these three flags being dependent on another flag, and not being able to use this within a git-bisect(1) session, unlike other git-rebase(1) operations. That might call for a new git command.

Please let me know any feedback! :)

Junio, since you seemed to love git-imerge, I wonder what you'll think of this tool. :-)

Having presented the tool, below goes the implementation.

Have a lovely day!
Alex

---
#!/bin/bash
# Copyright 2026, Alejandro Colomar <[email protected]> # SPDX-License-Identifier: GPL-3.0-or-later

set -Eeufo pipefail;
shopt -s lastpipe;

err()

{
	>&2 printf '%s\n' "$(basename "$0"): error: $*";
	exit 1;
}

fp='';
other='';
pre='';
post='';
while test $# -ge 1; do

	case "$1" in
	--first-parent)
		fp='--first-parent';
		;;
	--pre-exec=*)
		echo "$1" \
		| sed 's/--pre-exec=//' \
		| read -r pre;
		;;
	--post-exec=*)
		echo "$1" \
		| sed 's/--post-exec=//' \
		| read -r post;
		;;
	-*)
		other="$other $1";
		;;
	*)
		break;
		;;
	esac;
	shift;

done;
gbopts="$fp";
gropts="$other";

if test $# -lt 1; then

	err 'Missing target commit.';
fi;
if test $# -gt 1; then
	err 'Too many arguments.';

fi;
git rev-list -1 "$1" \
| read -r tgt;
git rev-parse --abbrev-ref HEAD \
| read -r branch;

# Set up the callback script for 'git rebase run'. mktemp \
| read -r callback;
cat >"$callback" <<__EOF__
#!/bin/bash

	set -Eeufo pipefail;
	shopt -s lastpipe;

	git rev-list -1 HEAD \
	| read -r bisect_head;

	if test -n '$pre'; then
		printf '%s' 'Pre-rebase exec: ';
		pre='$pre';
		if
			\$pre;
			x="\$?";
			true;
		then
			case "\$x" in
			0)
				echo 'success';
				;;
			125)
				echo 'skip';
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit 125;
				;;
			*)
				echo "failure (\$x)";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit "\$x";
				;;
			esac;
		fi;
	fi;

	git switch '$branch' >/dev/null 2>/dev/null;
	git rev-list -1 HEAD \
	| read -r old_head;
	printf '%s' 'Rebase: ';
	if git rebase $gropts "\$bisect_head" >/dev/null 2>/dev/null; then
		echo 'success';
	else
		echo 'conflict';
		git rebase --abort >/dev/null;
		git checkout --detach "\$bisect_head" 2>/dev/null;
		exit 1;
	fi;

	if test -n '$post'; then
		printf '%s' 'Post-rebase exec: ';
		post='$post';
		if
			\$post;
			x="\$?";
			true;
		then
			case "\$x" in
			0)
				echo 'success';
				;;
			125)
				echo 'skip';
				git reset --hard "\$old_head";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit 125;
				;;
			*)
				echo "failure (\$x)";
				git reset --hard "\$old_head";
				git checkout --detach "\$bisect_head" 2>/dev/null;
				exit "\$x";
				;;
			esac;
		fi;
	fi;
	git checkout --detach "\$bisect_head" 2>/dev/null;
	exit 0;

__EOF__
chmod +x "$callback";

# Try the target first.
git checkout --detach "$tgt" 2>/dev/null;
if "$callback"; then

	exit 0;

fi;
git status;

# Bisect.

# shellcheck disable=SC2248  # gbopts may hold multiple options
git bisect start $gbopts >/dev/null;
git bisect bad "$tgt" >/dev/null;
git merge-base "$branch" "$tgt" \
| xargs -I{} git bisect good {};
git bisect run "$callback";
git rev-list -1 bisect/bad \
| read -r bad;
git bisect reset >/dev/null 2>/dev/null;

# Perform the conflicting rebase
git switch "$branch";

# shellcheck disable=SC2086  # gropts may hold multiple options
git rebase $gropts "$bad";
if test -v post; then
	echo 'Running post-rebase exec.';
	$post;
fi;
Signature · 1 line
<https://www.alejandro-colomar.es>
Signature · 1 line
<https://www.alejandro-colomar.es>
#2 lore raw
In reply to Alejandro Colomar
Quoted 8 lines · Alejandro Colomar

We'll discuss the exact way it should be integrated within git(1), but first it'd be interesting to get feedback about the tool itself, regardless of the actual form. Actually, because of the specialized flags --pre-exec and --post-exec, and the --first-parent flag from git-bisect(1) --and the fact that it runs git-bisect(1) machinery--, I'm not entirely sure that it should be just a new flag to git-rebase(1). It might be confusing to have these three flags being dependent on another flag, and not being able to use this within a git-bisect(1)

IMO that's not a problem at all. There are a lot of Unix/Linux commands that have flags that only make sense when used with other specific flags. So I still like a --first-conflict or --onto-first-conflict option.

(I really like --pre-exec and --post-exec, BTW.)

Quoted 2 lines

session, unlike other git-rebase(1) operations. That might call for a new git command.

That might still be the case in that this will be such a useful tool that it deserves a name. But also, git-rebase(1) should always have been this useful, so that argues for this to be either... a new option like --onto-first-conflict, or even a new default behavior.

Does jj have a feature like this? What do they call it?

Quoted 14 lines

---
#!/bin/bash
# Copyright 2026, Alejandro Colomar <[email protected]> # SPDX-License-Identifier: GPL-3.0-or-later

set -Eeufo pipefail;
shopt -s lastpipe;

err()

{
	>&2 printf '%s\n' "$(basename "$0"): error: $*";
	exit 1;
}
Quoted 5 lines

fp='';
other='';
pre='';
post='';
while test $# -ge 1; do

I normally use

while getopts +:<short-options-here> opt; do ...

I also have a getopts_long-like function (see my gists) for bash if you like.

Quoted 5 lines

[...]

# Set up the callback script for 'git rebase run'. mktemp \
| read -r callback;

I like to set a trap to remove temp files.

Quoted 5 lines

cat >"$callback" <<__EOF__
#!/bin/bash
...
__EOF__
chmod +x "$callback";

Here what might be better is to have a command-line option to execute this callback without having to write it to a file, and use environment variables to pass arguments to it.

Quoted 2 lines

# Perform the conflicting rebase
git switch "$branch";

Ah, that came from:

Quoted 2 lines

git rev-parse --abbrev-ref HEAD \
| read -r branch;

which means I can't use this in detached HEAD mode :(

I work in detached HEAD mode almost exclusively. I know, that's.. weird. But it works for me. Can we avoid forcing the user to be on a branch?

Nico

#3 lore raw
Quoted 3 lines · Nico Williams

that it deserves a name. But also, git-rebase(1) should always have been this useful, so that argues for this to be either... a new option like --onto-first-conflict, or even a new default behavior.

I.e., maybe if I run git rebase $upstream/$branch and there's conflicts then it should do this bisection to drop me at the first upstream commit to introduce conflicts, stop, inform me of this, inform me that after I finish resolving conflicts I need to run git rebase --continue until that's done, that I should then git rebase $upstream/$branch again.

Additionally when git rebase --continue finishes successfully, if we're not at $upstream/$branch then --continue should run git rebase $upstream/$branch directly. This means recording the target committish along with other git rebase state during the rebase. This would feel very natural to me.

If we take this approach then we'd need an option not to enable this behavior but to disable it, something like --direct.

The more I think about it, the more I want this approach to be the default for git rebase. But maybe there's a good reason not to? Maybe first ship the option, then later make it the default?

Nico

#4 lore raw

Hi Nico,

Quoted 15 lines

Date: 2026-10-03 15:48:46-0500
From: Nico Williams <[email protected]>

Quoted 3 lines · Nico Williams

that it deserves a name. But also, git-rebase(1) should always have been this useful, so that argues for this to be either... a new option like --onto-first-conflict, or even a new default behavior.

I.e., maybe if I run git rebase $upstream/$branch and there's conflicts then it should do this bisection to drop me at the first upstream commit to introduce conflicts, stop, inform me of this, inform me that after I finish resolving conflicts I need to run git rebase --continue until that's done, that I should then git rebase $upstream/$branch again.

Quoted 5 lines

Additionally when git rebase --continue finishes successfully, if we're not at $upstream/$branch then --continue should run git rebase $upstream/$branch directly. This means recording the target committish along with other git rebase state during the rebase. This would feel very natural to me.

What would --abort do? Go back to the begining of this iteration, or to the very first initial state of the branch?

The useful thing would be to just go back to the begining of this iteration --that is essential for rebasing entire trees of branches, as explained in the previous message--.

Since --abort doesn't go all the way back, I think --continue shouldn't continue all the way forward, for consistency.

If that's desired behavior, it should go in this tool, and not as part of git-rebase(1). git-rebase(1) is a much simpler and much more fundamental tool, which is used to build this more complex tool. That's one reason I'm rather opposed to having this as part of git-rebase(1); it would confuse about the responsibility of git-rebase(1). IMO, git-rebase(1) is a plumbing command, and git-brebase would be a porcelain thingy.

Quoted 2 lines

If we take this approach then we'd need an option not to enable this behavior but to disable it, something like --direct.

That hints it might be just be a different command.

Quoted 6 lines

The more I think about it, the more I want this approach to be the default for git rebase. But maybe there's a good reason not to? Maybe first ship the option, then later make it the default?

Nico

Have a lovely night!
Alex

Signature · 1 line
<https://www.alejandro-colomar.es>
#5 lore raw
Quoted 2 lines · Alejandro Colomar

Since --abort doesn't go all the way back, I think --continue shouldn't continue all the way forward, for consistency.

Fair.

Quoted 8 lines

If that's desired behavior, it should go in this tool, and not as part of git-rebase(1). git-rebase(1) is a much simpler and much more fundamental tool, which is used to build this more complex tool. That's one reason I'm rather opposed to having this as part of git-rebase(1); it would confuse about the responsibility of git-rebase(1). IMO, git-rebase(1) is a plumbing command, and git-brebase would be a porcelain thingy.

Quoted 4 lines
Quoted 2 lines

If we take this approach then we'd need an option not to enable this behavior but to disable it, something like --direct.

That hints it might be just be a different command.

If this was 2007, and you were writing the first version of rebase, and you had already worked out that you wanted this feature, what would you do then? Would you make it the default? I _think_ I would.

Basically, this makes rebasing much nicer, so why not make it the default?

Nico

#6 lore raw

Hi Nico,

Quoted 20 lines

Date: 2026-10-03 16:17:22-0500
From: Nico Williams <[email protected]>

Quoted 2 lines · Alejandro Colomar

Since --abort doesn't go all the way back, I think --continue shouldn't continue all the way forward, for consistency.

Fair.

Quoted 11 lines

If that's desired behavior, it should go in this tool, and not as part of git-rebase(1). git-rebase(1) is a much simpler and much more fundamental tool, which is used to build this more complex tool. That's one reason I'm rather opposed to having this as part of git-rebase(1); it would confuse about the responsibility of git-rebase(1). IMO, git-rebase(1) is a plumbing command, and git-brebase would be a porcelain thingy.

Quoted 2 lines

If we take this approach then we'd need an option not to enable this behavior but to disable it, something like --direct.

Quoted 8 lines
Quoted 1 line

That hints it might be just be a different command.

If this was 2007, and you were writing the first version of rebase, and you had already worked out that you wanted this feature, what would you do then? Would you make it the default? I _think_ I would.

Basically, this makes rebasing much nicer, so why not make it the default?

I'm currently defaulting to brebase for every rebase I want to do.

However, I still use the regular git-rebase(1) for more precise operations. To be specific, I use it for changing history (without moving the base), and I also use it for resolving conflicts as part of a git-brebase operation. They seem to me to be different tools, even though they're clearly related.

I think we use git-rebase(1) for what we should be using git-brebase only because we didn't have the latter. That might be the reason we confuse them and think they're the same tool. They're used for very different operations.

Cheers,
Alex

Quoted 3 lines

Nico

Signature · 1 line
<https://www.alejandro-colomar.es>
#7 lore raw
Quoted 7 lines · Alejandro Colomar

I'm currently defaulting to brebase for every rebase I want to do.

However, I still use the regular git-rebase(1) for more precise operations. To be specific, I use it for changing history (without moving the base), and I also use it for resolving conflicts as part of a git-brebase operation. They seem to me to be different tools, even though they're clearly related.

I see now. Though, of course, if I use git-rebase but don't change the base, then obviously I want a plain rebase. But, ok.

#8 lore raw
In reply to Nico Williams

Hi Nico,

Quoted 3 lines

Date: 2026-10-03 15:39:41-0500
From: Nico Williams <[email protected]>

[...]

Quoted 7 lines
Quoted 2 lines

It might be confusing to have these three flags being dependent on another flag, and not being able to use this within a git-bisect(1)

IMO that's not a problem at all. There are a lot of Unix/Linux commands that have flags that only make sense when used with other specific flags. So I still like a --first-conflict or --onto-first-conflict option.

Yeah, it could make sense. I'm not sure, but it could be.

Quoted 1 line

(I really like --pre-exec and --post-exec, BTW.)

:)

Quoted 9 lines
Quoted 2 lines

session, unlike other git-rebase(1) operations. That might call for a new git command.

That might still be the case in that this will be such a useful tool that it deserves a name. But also, git-rebase(1) should always have been this useful, so that argues for this to be either... a new option like --onto-first-conflict, or even a new default behavior.

Does jj have a feature like this? What do they call it?

No idea.

[...]

Quoted 8 lines
Quoted 1 line

while test $# -ge 1; do

I normally use

while getopts +:<short-options-here> opt; do ...

I also have a getopts_long-like function (see my gists) for bash if you like.

I think getopts(1) is not usable for git(1)-related scripts, because getopts(1) interprets '--' as the end of the options, but git(1) uses it for distinguishing commits from paths. If anyone shows me how it can be used, I'd be interested, because I've hit this issue in the past with other script.

Quoted 7 lines
Quoted 5 lines

[...]

# Set up the callback script for 'git rebase run'. mktemp \
| read -r callback;

I like to set a trap to remove temp files.

Hmmm, makes sense. If so, I'll also try to filter out the line that prints the name of the command, since 'git bisect run' prints it, and we don't want users to try to open a file that doens't exist.

Quoted 8 lines
Quoted 5 lines

cat >"$callback" <<__EOF__
#!/bin/bash
...
__EOF__
chmod +x "$callback";

Here what might be better is to have a command-line option to execute this callback without having to write it to a file,

How would you do it?

Quoted 2 lines

and use environment
variables to pass arguments to it.

The callback doesn't really need any arguments, since 'git bisect run' won't pass any arguments to it.

Quoted 9 lines
Quoted 2 lines

# Perform the conflicting rebase
git switch "$branch";

Ah, that came from:

Quoted 2 lines

git rev-parse --abbrev-ref HEAD \
| read -r branch;

which means I can't use this in detached HEAD mode :(

Oh! I wasn't aware that git-rebase(1) supported detached HEAD mode.

Quoted 2 lines

I work in detached HEAD mode almost exclusively. I know, that's.. weird. But it works for me.

Ouch! Indeed. :)
Out of curiosity, are there any interesting reasons for such self-implied pain?

Quoted 2 lines

Can we avoid forcing the user to be on a
branch?

I guess I could keep a variable that remembers the state of the HEAD across all the rebases. It should be doable. I'll have a look (maybe tomorrow).

Have a lovely night!
Alex

Quoted 2 lines

Nico

Signature · 1 line
<https://www.alejandro-colomar.es>
#9 lore raw
Quoted 72 lines

Date: 2026-10-03 22:56:32+0200
From: Alejandro Colomar <[email protected]>

Hi Nico,

Quoted 3 lines

Date: 2026-10-03 15:39:41-0500
From: Nico Williams <[email protected]>

[...]

Quoted 7 lines
Quoted 2 lines

It might be confusing to have these three flags being dependent on another flag, and not being able to use this within a git-bisect(1)

IMO that's not a problem at all. There are a lot of Unix/Linux commands that have flags that only make sense when used with other specific flags. So I still like a --first-conflict or --onto-first-conflict option.

Yeah, it could make sense. I'm not sure, but it could be.

Quoted 1 line

(I really like --pre-exec and --post-exec, BTW.)

:)

Quoted 9 lines
Quoted 2 lines

session, unlike other git-rebase(1) operations. That might call for a new git command.

That might still be the case in that this will be such a useful tool that it deserves a name. But also, git-rebase(1) should always have been this useful, so that argues for this to be either... a new option like --onto-first-conflict, or even a new default behavior.

Does jj have a feature like this? What do they call it?

No idea.

[...]

Quoted 8 lines
Quoted 1 line

while test $# -ge 1; do

I normally use

while getopts +:<short-options-here> opt; do ...

I also have a getopts_long-like function (see my gists) for bash if you like.

I think getopts(1) is not usable for git(1)-related scripts, because getopts(1) interprets '--' as the end of the options, but git(1) uses it for distinguishing commits from paths. If anyone shows me how it can be used, I'd be interested, because I've hit this issue in the past with other script.

Quoted 7 lines
Quoted 5 lines

[...]

# Set up the callback script for 'git rebase run'. mktemp \
| read -r callback;

I like to set a trap to remove temp files.

Hmmm, makes sense. If so, I'll also try to filter out the line that prints the name of the command, since 'git bisect run' prints it, and we don't want users to try to open a file that doens't exist.

Quoted 8 lines
Quoted 5 lines

cat >"$callback" <<__EOF__
#!/bin/bash
...
__EOF__
chmod +x "$callback";

Here what might be better is to have a command-line option to execute this callback without having to write it to a file,

Quoted 7 lines

How would you do it?

Quoted 2 lines

and use environment
variables to pass arguments to it.

The callback doesn't really need any arguments, since 'git bisect run' won't pass any arguments to it.

Self-correction: 'git rebase run' does actually pass arguments to the command. However, I still don't see the need.

Cheers,
Alex

Quoted 36 lines
Quoted 10 lines
Quoted 3 lines

# Perform the conflicting rebase
git switch "$branch";

Ah, that came from:

Quoted 2 lines

git rev-parse --abbrev-ref HEAD \
| read -r branch;

which means I can't use this in detached HEAD mode :(

Oh! I wasn't aware that git-rebase(1) supported detached HEAD mode.

Quoted 2 lines

I work in detached HEAD mode almost exclusively. I know, that's.. weird. But it works for me.

Ouch! Indeed. :)
Out of curiosity, are there any interesting reasons for such self-implied pain?

Quoted 2 lines

Can we avoid forcing the user to be on a
branch?

I guess I could keep a variable that remembers the state of the HEAD across all the rebases. It should be doable. I'll have a look (maybe tomorrow).

Have a lovely night!
Alex

Quoted 2 lines

Nico

Signature · 1 line
<https://www.alejandro-colomar.es>
Signature · 1 line
<https://www.alejandro-colomar.es>
#10 lore raw
In reply to Alejandro Colomar
Quoted 8 lines · Alejandro Colomar
Quoted 2 lines

I also have a getopts_long-like function (see my gists) for bash if you like.

I think getopts(1) is not usable for git(1)-related scripts, because getopts(1) interprets '--' as the end of the options, but git(1) uses it for distinguishing commits from paths. If anyone shows me how it can be used, I'd be interested, because I've hit this issue in the past with other script.

https://gist.github.com/nicowilliams/f3fe2b10b380aecdef403acb246dced2

Though there's many ways to do this.

Quoted 10 lines
Quoted 8 lines
Quoted 5 lines

cat >"$callback" <<__EOF__
#!/bin/bash
...
__EOF__
chmod +x "$callback";

Here what might be better is to have a command-line option to execute this callback without having to write it to a file,

How would you do it?

I'd have an option or sub-command of the main script that says "do the callback thing", then when you run git bisect run ... put in the name of this script as the command and the "do the callback thing" option next.

Quoted 5 lines
Quoted 2 lines

and use environment
variables to pass arguments to it.

The callback doesn't really need any arguments, since 'git bisect run' won't pass any arguments to it.

But you're embedding values into the temp executable script -- if you don't have that any more you'll have to pass those in.

Quoted 3 lines
Quoted 1 line

which means I can't use this in detached HEAD mode :(

Oh! I wasn't aware that git-rebase(1) supported detached HEAD mode.

Sure does!

Quoted 6 lines
Quoted 2 lines

I work in detached HEAD mode almost exclusively. I know, that's.. weird. But it works for me.

Ouch! Indeed. :)
Out of curiosity, are there any interesting reasons for such self-implied pain?

I often do:

: ; git checkout origin/master
: ; <do some work>
: ; git add ...; git commit -m '...'
: ; git push myfork HEAD:refs/heads/the-branch-name-here # <-- I name it here

then open a PR.

Now I don't have a branch here, but who cares? If I switch to other work and later want to come back to this work I'll either a) create a local branch then, and/or b) when I resume work on the first thing I'll git checkout myfork/the-branch-name-here and... once more work in detached HEAD mode.

And if I need to see "what was I doing?" then I use git log --oneline and git reflog and I quickly see the remote branch of interest.

The remote branches are the symbolic names I need to preserve, and my clone will know them, so I only need local branch names for things I work on w/o a network or over a long time.

I do exaggerate a bit. I do this a lot, but maybe not quite "almost exclusively". Often I'm forced to have a local branch by opinionated tools other than git itself.

Nico

#11 lore raw

Hi Nico,

Quoted 13 lines

Date: 2026-10-03 16:13:25-0500
From: Nico Williams <[email protected]>

Quoted 8 lines · Alejandro Colomar
Quoted 2 lines

I also have a getopts_long-like function (see my gists) for bash if you like.

I think getopts(1) is not usable for git(1)-related scripts, because getopts(1) interprets '--' as the end of the options, but git(1) uses it for distinguishing commits from paths. If anyone shows me how it can be used, I'd be interested, because I've hit this issue in the past with other script.

Quoted 3 lines

https://gist.github.com/nicowilliams/f3fe2b10b380aecdef403acb246dced2

Though there's many ways to do this.

That one still consumes the '--', but we don't want to consume it. We want it to remain there in $@ after the options have been parsed.

Quoted 9 lines
Quoted 9 lines
Quoted 8 lines
Quoted 5 lines

cat >"$callback" <<__EOF__
#!/bin/bash
...
__EOF__
chmod +x "$callback";

Here what might be better is to have a command-line option to execute this callback without having to write it to a file,

Quoted 6 lines
Quoted 1 line

How would you do it?

I'd have an option or sub-command of the main script that says "do the callback thing", then when you run git bisect run ... put in the name of this script as the command and the "do the callback thing" option next.

I'd need to see some code. I'm not seeing it. :)

Quoted 8 lines
Quoted 5 lines
Quoted 2 lines

and use environment
variables to pass arguments to it.

The callback doesn't really need any arguments, since 'git bisect run' won't pass any arguments to it.

But you're embedding values into the temp executable script -- if you don't have that any more you'll have to pass those in.

But why would we want to not have it?
That would complicate the script, no?

Quoted 31 lines
Quoted 3 lines
Quoted 1 line

which means I can't use this in detached HEAD mode :(

Oh! I wasn't aware that git-rebase(1) supported detached HEAD mode.

Sure does!

Quoted 6 lines
Quoted 2 lines

I work in detached HEAD mode almost exclusively. I know, that's.. weird. But it works for me.

Ouch! Indeed. :)
Out of curiosity, are there any interesting reasons for such self-implied pain?

I often do:

: ; git checkout origin/master
: ; <do some work>
: ; git add ...; git commit -m '...'
: ; git push myfork HEAD:refs/heads/the-branch-name-here # <-- I name it here

then open a PR.

Now I don't have a branch here, but who cares? If I switch to other work and later want to come back to this work I'll either a) create a local branch then, and/or b) when I resume work on the first thing I'll git checkout myfork/the-branch-name-here and... once more work in detached HEAD mode.

And if I need to see "what was I doing?" then I use git log --oneline and git reflog and I quickly see the remote branch of interest.

Quoted 7 lines

The remote branches are the symbolic names I need to preserve, and my clone will know them, so I only need local branch names for things I work on w/o a network or over a long time.

I do exaggerate a bit. I do this a lot, but maybe not quite "almost exclusively". Often I'm forced to have a local branch by opinionated tools other than git itself.

Hmmm, actually resembles what I do. I use branches, then push to a remote, and once it's in the remote, I remove the local branch. I try to remove the local branches as soon as I can, because that way I don't need to remember whether there was something I forgot to push, or I wanted to explicitly discard it. If there's no local branch, there's no confusion. Since I work with two local computers, having the source of truth be the remote makes it less ambiguous. But while working locally, the branch helps a lot.

Anyway, I've patched it to work with detached HEAD. (I need to remember to add two traps, now.)

	diff --git i/src/bin/git-brebase w/src/bin/git-brebase
	index d652c37ef08d..a57acc350d7e 100755
	--- i/src/bin/git-brebase
	+++ w/src/bin/git-brebase
	@@ -50,8 +50,16 @@ if test $# -gt 1; then
	 fi;
	 git rev-list -1 "$1" \
	 | read -r tgt;
	-git rev-parse --abbrev-ref HEAD \
	+
	+mktemp \
	 | read -r branch;
	+{
	+       git rev-parse --abbrev-ref HEAD;
	+       git rev-list -1 HEAD;
	+} \
	+| sed '/^HEAD$/d' \
	+| sed '1!d' \
	+>"$branch";
	 
	 # Set up the callback script for 'git rebase run'.
	 mktemp \
	@@ -91,7 +99,8 @@ cat >"$callback" <<__EOF__
			fi;
		fi;
	 
	-       git switch '$branch' >/dev/null 2>/dev/null;
	+       cat '$branch' \
	+       | xargs -I{} git checkout {} >/dev/null 2>/dev/null;
		git rev-list -1 HEAD \
		| read -r old_head;
		printf '%s' 'Rebase: ';
	@@ -103,6 +112,13 @@ cat >"$callback" <<__EOF__
			git checkout --detach "\$bisect_head" 2>/dev/null;
			exit 1;
		fi;
	+       {
	+               git rev-parse --abbrev-ref HEAD;
	+               git rev-list -1 HEAD;
	+       } \
	+       | sed '/^HEAD$/d' \
	+       | sed '1!d' \
	+       >"$branch";
	 
		if test -n '$post'; then
			printf '%s' 'Post-rebase exec: ';
	@@ -146,7 +162,8 @@ fi;
	 # shellcheck disable=SC2248  # gbopts may hold multiple options
	 git bisect start $gbopts >/dev/null;
	 git bisect bad "$tgt" >/dev/null;
	-git merge-base "$branch" "$tgt" \
	+cat "$branch" \
	+| xargs -I{} git merge-base {} "$tgt" \
	 | xargs -I{} git bisect good {};
	 git bisect run "$callback";
	 git rev-list -1 bisect/bad \
	@@ -154,7 +171,8 @@ git rev-list -1 bisect/bad \
	 git bisect reset >/dev/null 2>/dev/null;
	 
	 # Perform the conflicting rebase
	-git switch "$branch";
	+cat "$branch" \
	+| xargs -I{} git checkout {};
	 # shellcheck disable=SC2086  # gropts may hold multiple options
	 git rebase $gropts "$bad";
	 if test -v post; then

I've tested it, and it works fine with a detached HEAD.

Cheers,
Alex

Quoted 3 lines

Nico

Signature · 1 line
<https://www.alejandro-colomar.es>
#12 lore raw
Quoted 6 lines · Alejandro Colomar
Quoted 4 lines

I'd have an option or sub-command of the main script that says "do the callback thing", then when you run git bisect run ... put in the name of this script as the command and the "do the callback thing" option next.

I'd need to see some code. I'm not seeing it. :)

Warning: NOT TESTED.

Warning: I did not first adopt your other patch to support detached HEAD mode.

Look, no temp file in sight:

	@@ -1,164 +1,164 @@
	 #!/bin/bash
	 # Copyright 2026, Alejandro Colomar <[email protected]>
	 # SPDX-License-Identifier: GPL-3.0-or-later
	 
	 set -Eeufo pipefail;
	 shopt -s lastpipe;
	 
	 err()
	 {
	 	>&2 printf '%s\n' "$(basename "$0"): error: $*";
	 	exit 1;
	 }
	 
	 fp='';
	 other='';
	 pre='';
	 post='';
	+callback=false;
	 while test $# -ge 1; do
	 	case "$1" in
	 	--first-parent)
	 		fp='--first-parent';
	 		;;
	 	--pre-exec=*)
	 		echo "$1" \
	 		| sed 's/--pre-exec=//' \
	 		| read -r pre;
	 		;;
	 	--post-exec=*)
	 		echo "$1" \
	 		| sed 's/--post-exec=//' \
	 		| read -r post;
	 		;;
	+	--bisect-run-callback)
	+		callback=true
	+		break;;
	 	-*)
	 		other="$other $1";
	 		;;
	 	*)
	 		break;
	 		;;
	 	esac;
	 	shift;
	 done;
	-gbopts="$fp";
	-gropts="$other";
	 
	-if test $# -lt 1; then
	-	err 'Missing target commit.';
	-fi;
	-if test $# -gt 1; then
	-	err 'Too many arguments.';
	-fi;
	-git rev-list -1 "$1" \
	-| read -r tgt;
	-git rev-parse --abbrev-ref HEAD \
	-| read -r branch;
	-
	-# Set up the callback script for 'git rebase run'.
	-mktemp \
	-| read -r callback;
	-cat >"$callback" <<__EOF__
	-#!/bin/bash
	-
	-	set -Eeufo pipefail;
	-	shopt -s lastpipe;
	+if $callback; then
	+	# Positional arguments to the bisect run callback
	+	branch="$1"
	+	gropts="$2"
	+	pre="${3:-}"
	+	post="${4:-}"
	 
	 	git rev-list -1 HEAD \
	 	| read -r bisect_head;
	 
	-	if test -n '$pre'; then
	+	if test -n "$pre"; then
	 		printf '%s' 'Pre-rebase exec: ';
	-		pre='$pre';
	 		if
	-			\$pre;
	-			x="\$?";
	+			$pre;
	+			x="$?";
	 			true;
	 		then
	-			case "\$x" in
	+			case "$x" in
	 			0)
	 				echo 'success';
	 				;;
	 			125)
	 				echo 'skip';
	-				git checkout --detach "\$bisect_head" 2>/dev/null;
	+				git checkout --detach "$bisect_head" 2>/dev/null;
	 				exit 125;
	 				;;
	 			*)
	-				echo "failure (\$x)";
	-				git checkout --detach "\$bisect_head" 2>/dev/null;
	-				exit "\$x";
	+				echo "failure ($x)";
	+				git checkout --detach "$bisect_head" 2>/dev/null;
	+				exit "$x";
	 				;;
	 			esac;
	 		fi;
	 	fi;
	 
	-	git switch '$branch' >/dev/null 2>/dev/null;
	+	git switch "$branch" >/dev/null 2>/dev/null;
	 	git rev-list -1 HEAD \
	 	| read -r old_head;
	 	printf '%s' 'Rebase: ';
	-	if git rebase $gropts "\$bisect_head" >/dev/null 2>/dev/null; then
	+	if git rebase $gropts "$bisect_head" >/dev/null 2>/dev/null; then
	 		echo 'success';
	 	else
	 		echo 'conflict';
	 		git rebase --abort >/dev/null;
	-		git checkout --detach "\$bisect_head" 2>/dev/null;
	+		git checkout --detach "$bisect_head" 2>/dev/null;
	 		exit 1;
	 	fi;
	 
	-	if test -n '$post'; then
	+	if test -n "$post"; then
	 		printf '%s' 'Post-rebase exec: ';
	-		post='$post';
	 		if
	-			\$post;
	-			x="\$?";
	+			$post;
	+			x="$?";
	 			true;
	 		then
	-			case "\$x" in
	+			case "$x" in
	 			0)
	 				echo 'success';
	 				;;
	 			125)
	 				echo 'skip';
	-				git reset --hard "\$old_head";
	-				git checkout --detach "\$bisect_head" 2>/dev/null;
	+				git reset --hard "$old_head";
	+				git checkout --detach "$bisect_head" 2>/dev/null;
	 				exit 125;
	 				;;
	 			*)
	 				echo "failure (\$x)";
	-				git reset --hard "\$old_head";
	-				git checkout --detach "\$bisect_head" 2>/dev/null;
	-				exit "\$x";
	+				git reset --hard "$old_head";
	+				git checkout --detach "$bisect_head" 2>/dev/null;
	+				exit "$x";
	 				;;
	 			esac;
	 		fi;
	 	fi;
	-	git checkout --detach "\$bisect_head" 2>/dev/null;
	+	git checkout --detach "$bisect_head" 2>/dev/null;
	 	exit 0;
	-__EOF__
	-chmod +x "$callback";
	+fi
	+
	+gbopts="$fp";
	+gropts="$other";
	+
	+if test $# -lt 1; then
	+	err 'Missing target commit.';
	+fi;
	+if test $# -gt 1; then
	+	err 'Too many arguments.';
	+fi;
	+git rev-list -1 "$1" \
	+| read -r tgt;
	+git rev-parse --abbrev-ref HEAD \
	+| read -r branch;
	 
	 # Try the target first.
	 git checkout --detach "$tgt" 2>/dev/null;
	-if "$callback"; then
	+if "$callback" "$branch" "$gropts" "$pre" "$post"; then
	 	exit 0;
	 fi;
	 git status;
	 
	 # Bisect.
	 # shellcheck disable=SC2248  # gbopts may hold multiple options
	 git bisect start $gbopts >/dev/null;
	 git bisect bad "$tgt" >/dev/null;
	 git merge-base "$branch" "$tgt" \
	 | xargs -I{} git bisect good {};
	-git bisect run "$callback";
	+git bisect run "$0" --bisect-run-callback "$branch" "$gropts" "$pre" "$post";
	 git rev-list -1 bisect/bad \
	 | read -r bad;
	 git bisect reset >/dev/null 2>/dev/null;
	 
	 # Perform the conflicting rebase
	 git switch "$branch";
	 # shellcheck disable=SC2086  # gropts may hold multiple options
	 git rebase $gropts "$bad";
	 if test -v post; then
	 	echo 'Running post-rebase exec.';
	 	$post;
	 fi;
Quoted 11 lines
Quoted 8 lines
Quoted 5 lines
Quoted 2 lines

and use environment
variables to pass arguments to it.

The callback doesn't really need any arguments, since 'git bisect run' won't pass any arguments to it.

But you're embedding values into the temp executable script -- if you don't have that any more you'll have to pass those in.

But why would we want to not have it?
That would complicate the script, no?

Because I don't want it writing temp files unless absolutely necessary. Even with a trap this can leave garbage behind. Better to avoid it.

Plus I... just don't like that style of bash scripting, and sure, that's just personal preference.

Nico

#13 lore raw

[RFC v3] git-bisect-rebase

In reply to Alejandro Colomar

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

Signature · 1 line
<https://www.alejandro-colomar.es>

#!/bin/bash
# Copyright 2026, Alejandro Colomar <[email protected]> # 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' \

Quoted 1 line

"$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 <[email protected]> # 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' \

Quoted 1 line

"$dir/head-name";

exit 0;

#!/bin/bash
# Copyright 2026, Alejandro Colomar <[email protected]> # 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;