Abhinav Gupta   About

Automatically Restacking Git Branches

Introduction

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.

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.

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

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.

o master
 \
  A---B f1
       \
        C---D f2
             \
              E f3

We have three feature branches in this scenario. The changes in f3 have a hard dependency on changes in f2, which in turn have a hard dependency on f1.

A problem with this workflow is that it’s tedious to rebase these branches. For example, moving master requires rebasing all three branches.

$ git checkout master     o---X---Y master'
$ git pull

$ git checkout f1         o---X---Y master
$ git rebase master                \
                                    A'---B' f1

$ git checkout f2         o---X---Y
$ git rebase f1                    \
                                    A'---B' f1
                                          \
                                           C'---D' f2

$ git checkout f3         o---X---Y
$ git rebase f2                    \
                                    A'---B' f1
                                           \
                                            C'---D' f2
                                                  \
                                                   E' f3

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

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 f3
$ git rebase master
                          o---X---Y master
                          |        \
                          |         A'---B'
                           \              \
                            A---B f1       C'---D'
                                 \               \
                                  C---D f2        E' f3

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

$ git branch -f f1 bbbbbb
                          o---X---Y master
                          |        \
                          |         A'---B' f1
                           \              \
                            A---B          C'---D'
                                 \               \
                                  C---D f2        E' f3

$ git branch -f f2 dddddd
                          o---X---Y master
                                   \
                                    A'---B' f1
                                          \
                                           C'---D' f2
                                                 \
                                                  E' f3

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

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 master on f3 will give us an instruction list like the following.

pick aaaaaa Add functionality
pick bbbbbb Bind configuration
pick cccccc Create handlers
pick dddddd Delete placeholders
pick eeeeee 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 aaaaaa Add functionality
 pick bbbbbb Bind configuration
+exec git branch -f f1
 pick cccccc Create handlers
 pick dddddd Delete placeholders
+exec git branch -f f2
 pick eeeeee Exercise corner cases

Each exec git branch -f instruction will update the branch to include the prior picked commits.

$ git checkout f3
$ git rebase -i master
pick aaaaaa Add functionality
pick bbbbbb Bind configuration
exec git branch -f f1
pick cccccc Create handlers
pick dddddd Delete placeholders
exec git branch -f f2
pick eeeeee Exercise corner cases

                          o---X---Y master
                                   \
                                    A'---B' f1
                                          \
                                           C'---D' f2
                                                 \
                                                  E' f3

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.

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 f3 on top of master now looks like this.

pick aaaaaa Add functionality
pick bbbbbb Bind configuration (f1)
pick cccccc Create handlers
pick dddddd Delete placeholders (f2)
pick eeeeee 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 f3
$ git rebase -i master
 pick aaaaaa Add functionality
 pick bbbbbb Bind configuration (f1)
+exec git branch -f f1
 pick cccccc Create handlers
 pick dddddd Delete placeholders (f2)
+exec git branch -f f2
 pick eeeeee Exercise corner cases

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

Making it Automatic

Let’s give the reconfigured instruction list another glance.

pick aaaaaa Add functionality
pick bbbbbb Bind configuration (f1)
pick cccccc Create handlers
pick dddddd Delete placeholders (f2)
pick eeeeee 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 the same.

#!/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 f3
$ git rebase -i master
pick aaaaaa Add functionality
pick bbbbbb Bind configuration (f1)
exec git branch -f f1
pick cccccc Create handlers
pick dddddd Delete placeholders (f2)
exec git branch -f f2
pick eeeeee 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.

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. For more, see https://github.com/abhinav/restack.

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.

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 May 13, 2019.