The New Git Option For Rebasing Multiple Branches At Once

What used to take several commands can now be done with one. What used to be several problems is now one.

A version of this article appeared on viget.com

A flowering shrub: branch with yellow flowers on old wood. Watercolour
A flowering shrub: branch with yellow flowers on old wood. Watercolour. Public Domain Mark. Source: Wellcome Collection.

In version Git v2.38 (released Oct 3 2022), git-rebase learned a new --update-refs option. With --update-refs, rebasing will “Automatically force-update any branches that point to commits that are being rebased” (docs).

A standard rebase results in up to one branch pointing at a different commit than it did before

text
A < B(main)
\
C - D(feature)
text
A < B(main)
\
C - D(feature)

rebasing C off B — for example with

shell
shell
git checkout feature
git rebase main
shell
git checkout feature
git rebase main

—or the convenient form git rebase main feature— results in

text
A < B(main) < C < D(feature)
text
A < B(main) < C < D(feature)

Not much room for improvement there. But what if there are intermediate branches between the specified branch’s fork point (A in the above example) and the branch you’re rebasing?

The new capability

Here’s what happens in a multi-branch situation with a standard rebase:

text
A < B(main)
\
C(other) - D(feature)
text
A < B(main)
\
C(other) - D(feature)

followed by

shell
shell
git rebase main feature
shell
git rebase main feature

results in

text
A < B(main) < C' < D'(feature)
\
C(other)
text
A < B(main) < C' < D'(feature)
\
C(other)

With --update-refs, git-rebase will also update all branches which start out pointing to commits that then get rebased. Here’s the difference:

text
A < B(main)
\
C(other) - D(feature)
text
A < B(main)
\
C(other) - D(feature)

followed by

shell
shell
git rebase --update-refs main feature
shell
git rebase --update-refs main feature
text
A < B(main) < C'(other) < D'(feature)
text
A < B(main) < C'(other) < D'(feature)

Real life uses

I’m excited about this enhancement because of two scenarios I run into:

The first real-life scenario is during development. I sometimes build several branches upon each other. Maybe they are for dependent features; maybe it’s one large feature, and I’m splitting it up to make code review more feasible.

text
A(main) < B(first) < C(second-requires-first) < D(third-requires-second)
text
A(main) < B(first) < C(second-requires-first) < D(third-requires-second)

(I’m keeping each branch to only one commit for legibility.)

I’m working at third-requires-second and make a change that belongs in first. I’m at

text
A(main) < B(first) < C(second-requires-first) < D < goes-before-B(third-requires-second)
text
A(main) < B(first) < C(second-requires-first) < D < goes-before-B(third-requires-second)

and want to be to

text
A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)
text
A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)

Before Git v2.38 my solution was

shell
shell
# Step 1.
git rebase --interactive first~
shell
# Step 1.
git rebase --interactive first~

which would result in

text
A(main) < goes-before-B' < B' < C' < D'(third-requires-second)
\
B(first) - C(second-requires-first)
text
A(main) < goes-before-B' < B' < C' < D'(third-requires-second)
\
B(first) - C(second-requires-first)

Git surgery is required to point first to B' and to point second-requires-first to C'. You probably have your workflow. Maybe git-log or a Git graph UI to figure what B' and C' are relative to third-requires-second, or to look up their IDs, and then some git reset --hards or git branch -fs.

With Git v2.38, my solution is

shell
shell
git rebase --interactive --update-refs first~
shell
git rebase --interactive --update-refs first~

which results in

text
A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)
text
A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)

The second real-life scenario is during the review/approval phase. Say I put these branches up for review at the same time (this isn’t a standard practice on my team, but our process is okay with it and it comes up from time to time).

  • Pull request 1: main ← first
  • Pull request 2: first ← second-requires-first
  • Pull request 3: second-requires-first ← third-requires-second

Say a colleague requests a change in the first pull request. In the past, I might have added a commit to that branch

shell
shell
git checkout first
# hack
git commit
shell
git checkout first
# hack
git commit

which would leave me at

text
A(main) < goes-before-B < B < E(first)
\
C(second-requires-first) < D(third-requires-second)
text
A(main) < goes-before-B < B < E(first)
\
C(second-requires-first) < D(third-requires-second)

To get to the goal

text
A(main) < goes-before-B < B < E(first) < C(second-requires-first) < D(third-requires-second)
text
A(main) < goes-before-B < B < E(first) < C(second-requires-first) < D(third-requires-second)

we could run

shell
shell
git rebase --onto first first~ third-requires-second
git branch -f second-requires-first third-requires-second~
shell
git rebase --onto first first~ third-requires-second
git branch -f second-requires-first third-requires-second~

or my preference before git rebase --update-refs, git rebase --fork-point:

shell
shell
git rebase --fork-point first second-requires-first
git rebase --fork-point second-requires-first third-requires-second
shell
git rebase --fork-point first second-requires-first
git rebase --fork-point second-requires-first third-requires-second

followed by pushing first and force pushing (--with-lease!) second-requires-first and third-requires-second.

With git rebase --update-refs we can instead run one command

shell
shell
git rebase --fork-point --update-refs third-requires-second
shell
git rebase --fork-point --update-refs third-requires-second

(Or git rebase --onto first first~ third-requires-second. Shorter… but requires more knowledge of the context.)

A new era

The “during development” scenario doesn’t only happen during development, and the “during review” scenario doesn’t only happen during review. But without git rebase --update-refs it is reasonable to think of them as closely related but distinct problems. But with git rebase --update-refs the solutions are identical 👀 By adopting git rebase --update-refs you reduce the set of branch management problems you need to have solutions for.

You can use git rebase --update-refs today. If you’re locked into an outdated version because you’re using a copy of Git that ships with your OS, you’ll need to switch to a copy you manage yourself (for example, Homebrew users can brew install git).

Bonus

--update-refs is smart about squashed commits! Say you want to make a fix to the first of several stacked branches

command line
shell
# A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)
# Need to change B
git checkout third-requires-second
# hack
git commit
git rebase -i first~
# in the interactive rebase todo editor
# make the new commit a 'fixup' commit
# and move it follow B
shell
# A(main) < goes-before-B < B(first) < C(second-requires-first) < D(third-requires-second)
# Need to change B
git checkout third-requires-second
# hack
git commit
git rebase -i first~
# in the interactive rebase todo editor
# make the new commit a 'fixup' commit
# and move it follow B

followed by pushes.

Bonus bonus

Instead of git commit that could be git commit --fixup=first. And with rebase.autoSquash set to true, Git will move the fixup commit to immediately following its target for you. More efficiency… but that’s beyond the scope of this article. checkout 🥁 the links!

Articles You Might Enjoy

Or Go To All Articles