In this post I discuss how to use a new Git rebasing feature,
--update-refs, which was included in Git 2.38, in October 2022. This makes working with "stacked" branches a lot easier.
Stacked what now?
I'm a big fan of small git commits. Especially when you're creating a big feature. I like to create a "story" with my commits, adding bits of a larger feature commit by commit. The idea is to make it as simple as possible for others to review by walking through a commit at a time.
As an extension to that, I often create separate PRs for each couple of commits in a feature. This, again, is to make it easier for people to review. GitHub's PR review pages really don't cope well with large PRs, even if you have "incremental" commits. Creating separate branches and PRs for each unit of functionality makes it easier for people to consume and follow the "story" of the commits.
This approach, where you have lots of separate branches/PRs which build on top of one another, is called stacked branches/PRs. This makes sense when you think of the git graph of the branches: each branch is "stacked" on top of the previous one.
For example, in the following repo I have 6 commits as part of a feature,
feature-xyz, and have broken those down into 3 logical units, with a branch for each. I can then create a PR for each of those branches:
For the first PR, for branch
andrew/feature-xyz/part-1, I would create a PR requesting to merge to
dev (in this example). For the second PR, for branch
andrew/feature-xyz/part-2, I would create a PR requesting to merge to
andrew/feature-xyz/part-1, and for the
part-3 branch the PR would request to merge into
Each PR only includes the commits specific to that branch, which makes for a much nicer reviewing experience (IMO).
Don't think that I just naturally perfectly segment these commits when creating the feature. I heavily rebase and edit the commits before creating a PR.
This all works great, until someone actually reviews
part-1 and requests changes. Then we run into the gnarly side of stacked branches.
Rebasing…so much rebasing
Let's imagine someone comments that I've missed something important in the
part-1 branch. Great. I can check out that branch, make the change, commit, and push it. Now the git log looks something like the following:
Argh, what a mess. It's no longer a "stack". If we want the
part-3 branches to include the
PR Feedback commit (we almost certainly do), then we have to rebase the branches on top of
rebase, and basically never
merge. I find the constant criss-cross of merged branches really hard to reason about. This is flame-war territory though, so I'm not going to go into it any more than that!
To rebase the
part-3 branches, we would have to run something like this:
# Rebase commit 4 and commit 5 on top of the part-1 branch git checkout andrew/feature-xyz/part-2 git rebase HEAD~2 --onto andrew/feature-xyz/part-1 # Rebase commit 6 on top of the (now rebased) part-2 branch git checkout andrew/feature-xyz/part-3 git rebase HEAD~ --onto andrew/feature-xyz/part-2
After running these commands we're back to a nice stacked list
But still, that's pretty arduous. There's a lot of finicky
rebase commands to run there, you have to get the different "base" and
--onto references right (they're different for each branch), and so it's (understandably) hard to convince people that this is a worthwhile endeavour.
--update-refs comes in.
Rebasing stacked branches with --update-refs
Git version 2.38 introduced a new option to the
--update-refs. As per the documentation, when set, this option will:
Automatically force-update any branches that point to commits that are being rebased. Any branches that are checked out in a worktree are not updated in this way.
So what does that mean? In this section I'll show a few scenarios, and how
--update-refs can help in each case.
Rebasing a stack of branches
Our PRs are looking good, but there has subsequently been a commit to the
dev branch, and I need to incorporate that into all my branches by rebasing on top of the latest commit:
--update-refs, this is a pain. Using the approach from the previous section, I would need to checkout
part-1, work out how to rebase it correctly onto
dev and then repeat for each branch in the stack.
An alternative approach would be to rebase the "top" of the stack,
part-3 on top of
dev. We could then
reset each of the branches to the "correct" commit in the newly-rebased branch, something like this:
# Rebase the tip of the stack first git checkout andrew/feature-xyz/part-3 git rebase dev # Set part-2 branch to the new location git checkout andrew/feature-xyz/part-2 git reset 782b4db --hard # <-- Need to grab the correct commit for this # Set part-1 branch to the new location git checkout andrew/feature-xyz/part-1 git reset 0d976a1 --hard # <-- Need to grab the correct commit for this
This is essentially what
--update-refs does, but it makes things a lot simpler; it rebases a branch, "remembers" where all the existing (local) branches point, and then
resets them to the correct point afterwards. Instead of doing our manual rebasing of each branch, we can "fix" the above example by running:
git checkout andrew/feature-xyz/part-3 git rebase dev --update-refs
Switched to branch 'andrew/feature-xyz/part-3' Successfully rebased and updated refs/heads/andrew/feature-xyz/part-3. Updated the following refs with --update-refs: refs/heads/andrew/feature-xyz/part-1 refs/heads/andrew/feature-xyz/part-2
As you can see, git has automatically updated the
andrew/feature-xyz/part-2 branch when it rebased the
This is really handy for keeping multiple branches up to date with your main branch.
Rebasing stacked branches on a changed branch
Let's go back to the original scenario— the first PR, based on
part-1 has changes, and we need to rebase
part-3 on top.
The good news is that no matter how many branches we have stacked, we only need to run two commands: checkout the tip branch, and rebase:
# Checkout the "top" branch in the stack git checkout andrew/feature-xyz/part-3 # Rebase the tip and all intermediate branches git rebase andrew/feature-xyz/part-1 --update-refs
This has multiple benefits:
- We only need to do a single rebase
- We don't need to use
--ontoand pick only the commits specific to each intermediate branch, or do any
After running this command, the branches look as expected:
You'll still need to checkout the intermediate branches and force-push them etc, but at least a big part of the work is done.
Interactive rebase with --update-refs
In the final scenario, we have our stack of branches:
While working on
part-3, we notice a typo that needs fixing. We make the commit in the
part-3 branch initially, as shown below:
Unfortunately, we need to include that commit in the
part-1 branch. Without
--update-refs we'd have to do multiple checkouts, cherry-picking and rebasing, but with
--update-refs, we can use an interactive rebase instead:
git rebase dev -i --update-refs
This pops up the editor to choose how to do the rebase. As you can see in the following example, there's an extra option as well as
pick d323fff Commit 1 pick 45768bc Commit 2 pick 9b97cc6 Commit 3 update-ref refs/heads/andrew/feature-xyz/part-1 pick 31ab2ab Commit 4 pick 48cdb40 Commit 5 update-ref refs/heads/andrew/feature-xyz/part-2 pick 2338145 Commit 6 pick 9d698f5 Fix typo # <-- need to move this
update-ref defines the point that the branches will be updated to after the rebase is complete. So we can move the
Fix typo branch after
Commit 3, but include it in the
pick d323fff Commit 1 pick 45768bc Commit 2 pick 9b97cc6 Commit 3 pick 9d698f5 Fix typo # <-- Moved to here update-ref refs/heads/andrew/feature-xyz/part-1 # <-- Above this, so will be included in this branch pick 31ab2ab Commit 4 pick 48cdb40 Commit 5 update-ref refs/heads/andrew/feature-xyz/part-2 pick 2338145 Commit 6
After running the rebase, the branches look like this:
Again, you'll need to take care of pushing all the branches up, but it's still a lot simpler than the dance you would have to do otherwise.
Enabling --update-refs by default
At this point, you might be wondering if there's any time you don't want to use
--update-refs. While it's not always necessary (if there are no intermediate branches, for example), I can't think of a time when I wouldn't want to do this. So the good news, is you can enable
--update-refs by default!
You can enable
--update-refs by default for all repos by running the following:
git config --global --add --bool rebase.updateRefs true
Alternatively, you can add the following to your
[rebase] updateRefs = true
If, for a specific rebase, you don't want to use
--update-refs, you can disable it instead, using
git rebase --no-update-refs.
I'm pretty excited by this simple improvement. There are various tools and approaches to improve the stacked PR experience, but you really can't beat "built-in"!
In this post I described the new
--update-refs feature for rebasing in Git 2.38. I introduced the concept of stacked branches and stacked PRs, and why I like them for feature development. Unfortunately, keeping stacked branches up to date can be quite arduous, requiring a lot of tricky rebasing. with
--update-refs the commands are significantly simplified, as I showed in various scenarios. You can even enable
--update-refs by default, so that all your
rebases use it!