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
.
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.
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