Bum - the git equivalent of kiss
TL;DR
Bum is built from small git helpers that do one thing well. Instead of guessing or parsing porcelain command output, they rely on git’s plumbing and internal data. By composing tiny primitives into scripts, you can build reliable tooling that works across any repository without configuration.
Introduction
I promised you in the first article to show you bum had more helpers. In the second article we went over how bum uses git primitives. Today we’re gonna build a bum tool. Welcome to my TED talk.
Default branches
Back in 2020 when the the world didn’t want master/slave notations anymore in software land, git became agnostic to how one called their default branch. A lot of tooling assumed master and nowadays a lot of tooling assumes main. The problem with these assumptions: you can change the default branch on any forge with just the press of a button and some project, like Perl, use blead as their default branch. Configuring the default branch in your config is certainly an option. You don’t need to do so however. In bum the problem is solved by adding two helpers. And while I already have them in bum, we’re gonna build it from scratch as an exercise for the user :)
For the tool git-new-branch I needed a way to determine what the default
branch was, so I could use it track it for my newly created branches. Git
stores the default branch information on disk. You can query it and there is no
reason for you to configure it in a configuration file.
Git stores the information in a file called .git/refs/remotes/<remote>/HEAD.
You need to retrieve the information once and it is cached on disk forever.
In order for us to get to the correct path we need to be aware of how .git,
aka the $GIT_DIR can be reached. A popular way of working is via worktrees
and .git behaves, or is different here. It is a file referencing the actual
.git dir of the repository. Your tooling cannot assume native .git
directories. In order to figure out where your actual $GIT_DIR exists, you
can use git rev-parse --git-path. If you give paramenters to the this
command you can find the exact location of the file you are looking for. In
bum this command is aliased to git path, and we can use it everywhere, so
getting a default branch from a remote becomes trivial:
get_default_branch_from_remote() {
cat $(git path refs/remotes/$1/HEAD)
}
Now, the file doesn’t need to exist, so we need to find a way to get the
information from a remote. This can be done via git remote set-head $1 --auto.
get_default_branch_from_remote() {
local path=$(git path refs/remotes/$1/HEAD)
[ ! -f $path ] && git remote set-head $1 --auto >/dev/null
cat $(git path refs/remotes/$1/HEAD)
}
Now the ref we find is a fully qualified ref:
$ cat $(git path refs/remotes/origin/HEAD)
ref: refs/remotes/origin/master
We are only interested the origin/master bit, or, because we already operate on
the origin branch, the master bit:
get_default_branch_from_remote() {
local path=$(git path refs/remotes/$1/HEAD)
[ ! -f $path ] && git remote set-head $1 --auto >/dev/null
awk -F/ '{print $NF}' < "$path"
}
Now we can use this function in our script, git-default-branch
#!/usr/bin/env zsh
source /path/to/file.zsh
get_default_branch_from_remote ${1:-origin}
Now you have one primitive to get a default branch, from every git repo you ever run this script in.
There is one thing I’d like to change and that is to add an update call. You can have a change of heart and want to change master to main. Right now the tooling doesn’t allow updating it:
set_default_branch_from_remote() {
git remote set-head $1 --auto >/dev/null
}
get_default_branch_from_remote() {
local path=$(git path refs/remotes/$1/HEAD)
[ ! -f $path ] && set_default_branch_from_remote $1
awk -F/ '{print $NF}' < "$path"
}
Now we can add a second script git-set-default-branch-from-remote:
#!/usr/bin/env zsh
source /path/to/file.zsh
remote=${1:-origin}
set_default_branch_from_remote $remote
get_default_branch_from_remote $remote
Now bum tries to be helpful, so we give users a way to do it for one or more remotes, or just all of them. Depending on how they invoke the script:
if [ -z "$*" ]
then
for i in $(git remote)
do
echo "$i/$(get_default_branch_from_remote $i)"
done
else
for i in $@
do
if is_remote $i
then
echo "$i/$(get_default_branch_from_remote $i)"
else
echo "$i isn't a configured remote!" >&2
fi
done
fi
And we also create a similar thing for the git-set-default-branch-from-remote
script. For those wondering, is_remote is another helper:
is_remote() {
local remote=$1
# This is what bum uses
[ -d "$(git path refs/remotes/$remote)" ] && return 0
# alternatively, and probably required once git 3.0 lands
[ -n $(git for-each-ref refs/remotes/$remote --format="%(refname:rstrip=1)" --count 1) ] && return 0
return 1;
}
With a couple of aliases, some plumbing commands, and some porcelain (git remote) we are able to query git about what the default branch is on the
remote and always have the correct value. Zero config, mostly offline and 100%
reusable across any project you build.
Conclusion
Bum is filled with helpers like this, primitives are stashed away in library files and the scripts expose them. This way we don’t need to execute code, we just source the logic and get the functions for free. I don’t have a hard line of when to call a script and when to source. It’s often more by feel. Both are building bigger scripts based on primitives. Which is the take home message of this article I think.
The next time you script against git, remember this: do one thing, do it well, and then: reuse it.