Practical Rebasing of Chained Branches

As most readers of this blog post would be well aware of, git is a tool used essentially daily by all software developers. I would wish to claim that we lived in a world where everyone also used its rebase command to make sure their commit history is understandable and easy to follow, but that is unfortunately not always the case. I would urge anyone who is not yet proficient with using git rebase in interactive mode, to please consider to spending a moment to learn it properly. To gain any value from what follows, it is likely a prerequisite to have a clear understanding of why one wants to spend effort on cleaning-up messy work prior to submitting it. Junior developers might want to read something like Anna Shipman's blog post How to raise a good pull request first.

While git is great at making interactive rebases of single branches, it quickly gets out of hand when working with multiple branches stacked or chained together. Say for example that we have a situation like the one in the left image below. With topic/some_branch, topic/other_branch and topic/yet_another_branch being actively worked upon and unfinished, while main has moved since work started on this stack of branches. Our goal is to gain a repository like the one in the image on the right, where all work branches have been rebased upon main.

branches behind branches rebased

One would of course wish to merge everything early, never needing to end up with having to rebase entire trains of branches. Yet, reality has a tendency to mess with ideal models. That which can not be avoided, needs to instead be handled. Some searching seemed to suggest that people tend to either roll their own scripts or adapt super-heavy workflows to solve this problem. After having attempted to reuse someone else's unmaintained and broken script, I decided to do like everyone else and reinvent the wheel.

Meet git-rb, a script which aids in rebasing related branches by creatively wrapping the editor, by setting the GIT_SEQUENCE_EDITOR environment variable to pre-populate the sequence with labels affected by the rebase:

#!/bin/sh -eu

# Version 2.18 (commit 9055e4) of git added the required 'label' functionality.
_min_major_version=2
_min_minor_version=18

_git_version="$( git version )"
_git_major_version="${_git_version##* }"
_git_minor_version="${_git_version#*.}"
_git_major_version="${_git_major_version%%.*}"
_git_minor_version="${_git_minor_version%%.*}"

unset _git_version

if [ "${_git_major_version}" -lt "${_min_major_version}" ] ||
    [ "${_git_major_version}" = "${_min_major_version}" ] &&
    [ "${_git_minor_version}" -lt "${_min_minor_version}" ];
then
  echo "Git ${_git_major_version}.${_git_minor_version} detected." \
     "Minimum ${_min_major_version}.${_min_minor_version} required." >&2
  exit 1
fi

unset _git_major_version _git_minor_version \
    _min_major_version _min_minor_version

_script_dir="$( dirname "${0}" )"
GIT_SEQUENCE_EDITOR="${_script_dir}/branch-moving-wrapper" git rebase "${@}"

unset _script_dir

That other script, branch-moving-wrapper, does the heavy lifting. It suggests to move every chained branch together with the rebase, and it looks like:

#!/bin/sh -eu

_head=$( git rev-parse --short HEAD )

if ! grep -q '^exec git branch' "${1}"; then
  for _hash in IFS=" " $( awk < "${1}" '$1=="pick" { print $2 }' ); do
    [ "${_hash}" != "${_head}" ] || continue

    for _branch in $( git branch --points-at "${_hash}" | cut -c3- ); do
      _sequence="$( awk <"${1}" \ '{
          if ($0 ~ /^pick '"${_hash}"'/) {
            print; print "label '"${_branch}"'"
          } else {
            print
          }
        }' | awk '{
          if ($0 ~ /^# Rebase /) {
            print "exec " \
                "git branch --force '"${_branch} refs/rewritten/${_branch}"'";
            print
          } else {
            print
          }
        }'
      )"
      echo "${_sequence}" >"${1}"
    done
  done
fi

unset _branch _hash _head _sequence

${VISUAL:-vi} "${@}"

Given that the scripts gets placed in some directory in PATH, one should be able to use git rb --interactive main to have dependant branches move together with the rebase of the leaf branch. Please see the images below for a rebase resulting in a transition between the example from the images above. The lines starting with label and exec are the interesting ones added by the script.

cli

Beware, the above relies on replacing the editor with a script injecting commands into the sequence list and wrapping the editor. It lacks error checking for a bunch of possible scenarios. Only use it if you understand how it works.

By placing the following in ~/.zshrc, zsh can be made to inherit tab completion of options and arguments from git-rebase.

_git-rb() {
  _git-rebase "${@}"
}

compdef _git-rb git-rb

2022-08-21 19:25:36 +0000
Thoughts and feedback may be directed at me using the channel listed on my contact page.

Previous post Next post