Abhinav Gupta | About

Restack Git branches automatically

Table of Contents

In this post I will discuss a Git workflow I have been using to manage interdependent feature branches. With it, each branch maps to a Pull Request (or analogous abstraction in the Git service of your choice). You use git rebase to move around or "restack" dependent branches as you make new changes.

1. Safety

People can be wary of git rebase, and with good reason. If you are unsure about what you are doing, it’s easy to mess up your checkout. To that end, remember the following.

Never rebase shared branches

Do not rebase any remote branch that other people are committing to. Feel free to rebase local branches and remote branches that you are certain you own. I find it useful to prefix branches with my name to inform collaborators that they are "private" (git checkout -b $(whoami)/myfeature).

Never force push on shared branches

Rebasing a branch rewrites its history. If you rebase a branch after pushing it to a remote, you need to use git push --force to update it. Doing so for shared branches breaks your collaborators’ local checkouts.

Generally speaking, understand what git rebase does and be mindful of your collaborators.

2. Rebasing stacks

My coworkers and I often have feature branches that depend on each other. Newer branches depend on changes made in older branches. Each branch corresponds to its own Pull Request, making for individually reviewable and mergeable units of code. We refer to these as review stacks.

For example, consider the following.

mainfeature1feature2feature3

We have three feature branches in this scenario. The changes in feature3 have a hard dependency on changes in feature2, which in turn have a hard dependency on feature1.

A problem with this workflow is that it’s tedious to rebase these branches. For example, suppose that main moves and we need to move all of these on top of that.

feature1feature2feature3main

Doing that is quite a manual process:

$ git checkout main
$ git pull
main
$ git checkout feature1
$ git rebase main
mainfeature1
$ git checkout feature2
$ git rebase feature1
mainfeature1feature2
$ git checkout feature3
$ git rebase feature2
mainfeature1feature2feature3

An obvious improvement we can make here is to perform this work in a single rebase call.

3. Rebasing once

Rather than rebasing branches individually, we can start at the top of the stack and rebase all the commits at once. This is insufficient, though, because it does not update the other branches.

$ git checkout feature3
$ git rebase main
feature1feature2mainfeature3

We must manually update those with git branch, passing -f/--force to move the existing branches.

$ git branch -f feature1 bbbbb2
feature2mainfeature1feature3
$ git branch -f feature2 ddddd2
mainfeature1feature2feature3

This is still tedious. We can improve on this by moving the branches during the rebase rather than afterwards.

4. Moving during the rebase

We can move branches while rebasing with -i/--interactive. Interactive rebase gathers the affected commits as a list of pick instructions. For example, git rebase -i main on feature3 will give us an instruction list like the following.

pick aaaaa1 Add functionality
pick bbbbb1 Bind configuration
pick ccccc1 Create handlers
pick ddddd1 Delete placeholders
pick eeeee1 Exercise corner cases

Interactive rebase supports the x/exec instruction, which lets you execute shell commands. By interleaving exec git branch -f instructions between pick instructions, we can move branches as we rebase their commits.

 pick aaaaa1 Add functionality
 pick bbbbb1 Bind configuration
+exec git branch -f feature1
 pick ccccc1 Create handlers
 pick ddddd1 Delete placeholders
+exec git branch -f feature2
 pick eeeee1 Exercise corner cases

Each exec git branch -f instruction will update the branch to include the prior `pick`ed commits.

$ git checkout feature3
$ git rebase -i main
pick aaaaa1 Add functionality
pick bbbbb1 Bind configuration
exec git branch -f feature1
pick ccccc1 Create handlers
pick ddddd1 Delete placeholders
exec git branch -f feature2
pick eeeee1 Exercise corner cases
mainfeature1feature2feature3

This improves on the previous version but it requires remembering all the branches and matching them to commit titles. We can make it more convenient.

5. Making it convenient

Git’s rebase.instructionFormat configuration allows changing the format of the Rebase instruction list.

rebase.instructionFormat
    A format string, as specified in git-log(1), to be used for the todo list
    during an interactive rebase. [...]

The default for this is %s, the commit subject. The template also supports %d:

%d: ref names, like the --decorate option of git-log(1)

This will print the names of branches and tags associated with a commit, if any. We can configure Git to use it like so,

$ git config --global rebase.instructionFormat "%s%d"

Our instruction list for rebasing feature3 on top of main now looks like this.

pick aaaaa1 Add functionality
pick bbbbb1 Bind configuration (feature1)
pick ccccc1 Create handlers
pick ddddd1 Delete placeholders (feature2)
pick eeeee1 Exercise corner cases

We no longer need to remember the names and positions of the other feature branches. Git tells us where we need to add exec git branch -f instructions.

$ git checkout feature3
$ git rebase -i main
 pick aaaaa1 Add functionality
 pick bbbbb1 Bind configuration (feature1)
+exec git branch -f feature1
 pick ccccc1 Create handlers
 pick ddddd1 Delete placeholders (feature2)
+exec git branch -f feature2
 pick eeeee1 Exercise corner cases

This helps but it requires manual work. We can automate this.

6. Making it automatic

Let’s give the reconfigured instruction list another glance.

pick aaaaa1 Add functionality
pick bbbbb1 Bind configuration (feature1)
pick ccccc1 Create handlers
pick ddddd1 Delete placeholders (feature2)
pick eeeee1 Exercise corner cases

We can write a program to search this text for pick lines ending with (…​) and add exec git branch -f instructions. Here’s a simplified version of a Perl script authored by Kris Kowal to do this.

#!/usr/bin/env perl

while (<>) {
    # An empty line indicates the end of the instruction list.
    last if /^\n$/;
    print;

    # Look for pick instructions ending with (...).
    next unless /^pick /;
    next unless m/\(([^)]+)\)$/g;

    # The parentheses can contain multiple branch names. All these
    # branches must be updated.
    for (split /, ?/, $1) {
        next if / /;  # no spaces in branch names

        # Verify that the name refers to a known branch.
        next unless `git show-ref --verify "refs/heads/$_" 2>/dev/null`;

        print "exec git branch -f $_\n";
    }
}

# Print the remaining content as-is.
while (<>) { print; }

Place this script on your $PATH with the name restack, and configure your editor to run this script when it detects an interactive rebase.

In vim, this takes the following form.

autocmd Filetype gitrebase silent %!restack

With this configuration, interactive rebase instruction lists come pre-populated with branch updates.

$ git checkout feature3
$ git rebase -i main
pick aaaaa1 Add functionality
pick bbbbb1 Bind configuration (feature1)
exec git branch -f feature1
pick ccccc1 Create handlers
pick ddddd1 Delete placeholders (feature2)
exec git branch -f feature2
pick eeeee1 Exercise corner cases

Our workflow is now fully automated but there’s a smell to configuring your editor for a change in Git. We should fix that.

7. Making it nice: Restack

The sequence.editor configuration allows changing the editor used for interactive rebases.

sequence.editor
    Text editor used by git rebase -i for editing the rebase instruction file.
    [...]

We can stick a program in there that handles adding the exec branch -f instructions before calling the real editor. We can also have it look up branch names for commit hashes, obviating the need to change rebase.instructionFormat.

I did this and packaged it all up into Restack. With Restack, the setup above abbreviates to a single command.

$ restack setup

Following that, your interactive rebases will include branch updates in the instruction list.

8. Conclusion

What started off as a tedious workflow ended up in my preferred way to interact with interdependent feature branches and their corresponding Pull Requests.

I recommend giving this workflow a try if you find yourself submitting massive Pull Requests, or Pull Requests with unrelated commits in them. Try it out with Restack or set it up by hand with the Perl script above.

9. Credits

Credit for optimizing this workflow all the way down to the Perl script goes to Kris Kowal. I was a rubber duck along the way, and packaged it up into Restack at the end.

Written on .