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

Re: [RFC] git-brebase

From
ACAlejandro Colomar <alx@kernel.org>
Date
Oct 3, 2026, 20:13 UTC
Message-ID
<asFhyVfiG9RTlIG-@debian>
In-Reply-To
<asFRVdMTpshsazgM@debian>
(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

Show 357 quoted lines
> Date: 2026-10-03 22:11:54+0200
> From: Alejandro Colomar <alx@kernel.org>
>
> 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 <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;
> }
> 
> 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;
> 
> 
> -- 
> <https://www.alejandro-colomar.es>
-- 
<https://www.alejandro-colomar.es>
Previous: Alejandro ColomarNext: Nico Williams
Message 2 of 13 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

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.