You might also like
TL;DR
Bum is built on three principles: small composable scripts that do one thing
well, guardrails that protect you from destructive operations, and gitconfig as
a runtime database. Bum uses bum.* as its own config namespace, meaning it
gets repo-awareness, includeIf support, and per-project configuration
completely for free because git already does all of that. Under the hood, bum
leans almost exclusively on plumbing commands rather than porcelain, making it
stable across git versions. The .git directory isn’t always where you think
it is, and bum handles that too. None of this is magic. It’s just git, used
deliberately.
How bum is built
Bum exists because git left little gems, breadcrumbs to use for customisation
and extensions. As explained in git-xxx, and aliases. So how
does bum make use of this? Bum uses small scripts and functions that do one
thing. Bum limits the amount of parameters a tool accepts, you won’t see a lot
of getopts in the suite. It is often, do what I say.
Most of the things bum does are sourced from a library, or just by calling another script. This way we compose bigger scripts in doing exactly what we want.
Small scripts that just work
Bum has 43 scripts (at time of writing) that all do something, most of them
small. In order to get the default remote branch: git-default-remote-branch.
In order to see if two branches are the same: git-equals. Want to find out if
a branch is an ancestor of another: git-is-ancestor. They all do what it says
on the tin. Most of the scripts don’t use the porcelain commands. Instead they
lean on the plumbing commands.
Porcelain vs Plumbing
Plumbing commands have stable interfaces and predictable output, porcelain is
for humans. That’s exactly why you don’t want to parse git status output in a
script but git status --porcelain is fine.
It is not just scripts, but also aliases. They are a lightweight version of
script in my git vocabulary. git lb to me is git log-branch, and it tells
you exactly which commits the branch introduces based on the tracking branch
you have configured. The same for git sl, which is an alias that is similar
to git log --oneline with specific formatting.
git body and git title are small helpers so I can quickly create merge
requests from the CLI to gitlab via their glab tool.
Bum contains roughly 70 aliases, some like most logging aliases are just there
to prevent me from having to remember all the formatting syntax, others are to
shorten frequently used terms, co, eq, s, br, among others.
Safety first
Bum tries to automate a lot of things, and it automates even the scary and potentially destructive things. Deletion of branches, force-pushes, mass-rebasing among other things. But it comes with some guardrails, we try to protect certain branches, master, main, develop, and development. Bum checks if your branch is protected and refuses to perform a destructive action. Other scripts check if the branch is an ancestor before resetting/fast-forwarding it. You can tell bum to prefer fast-forward by setting a configuration item, protection is almost always built in.
The protective layer
So how do we add all these protective layers? Well, via gitconfig. Git has a
beautiful configuration system, and bum relies on this from almost day one. Why
do I say beautiful for a tool called git in use by a toolkit called bum? It’s
because you can namespace your configuration so you don’t clash with git’s
configuration. Bum uses bum.*, below you’ll find a small overview from a
repository I used to work on, the repository many of bum’s features were
developed for:
Bum uses these config settings to figure out intent and to define the rules of engagement. You can of course always look at native git config units to infer behaviour for your own tools, but don’t be afraid to claim your own namespace. For tooling I make that cannot be found in bum, I use local or my online handle waterkip, it depends a bit on context.
Plumbing commands and the .git directory
Bum also does a lot of things with zero-config. The briefly mentioned
git default-remote-branch is one of these smart things. Git stores the remote
default branch in .git/refs/remotes/origin/HEAD. If this file does not exist
you can grab it with git remote set-head $1 --auto. In bum this is done
automatically by get_default_remote_branch and if you really want to force
our hand: git update-default-remote-branch.
Don’t think .git is always where you think it is. In a worktree it is
elsewhere, if you are in a submodule you’d be surprised where it resides. Bum
exposes a lot of these different variants by using git path, git root, git dir:
git pathis an alias forgit rev-parse --git-pathand is worktree safegit diris the same asgit pathgit rootis an alias forgit rev-parse --show-toplevelgit commonis an alias forgit rev-parse --git-common-dir(also worktree safe)
These aliases are used by bum to validate if branches are remote or local, or if remotes actually exist.
There are some things you need to be aware of, even when using plumbing
commands. git status for example, weirdo’s (like me) may have
status.showuntrackedfiles=no set. In order for you to see untracked files you
must call git -c status.showuntrackedfiles=yes status --porcelain to be
one hundred percent sure your worktree doesn’t contain untracked files:
Similarly, you can always exclude untracked files by using git -c status.showuntrackedfiles=no status --porcelain. The point is this, be aware
how configuration can influence your call sites.
Repo awareness
Bum isn’t repo aware, until it is. Because bum relies on the normal gitconfig
rules (system, global, repo) and thus follows all of the rules, including
includeIfs, bum can be configured differently for each repo or all of them at
once. And that I think is the thing most git tooling seems to ignore, they
implement their own configfiles, which makes repo-aware configs almost
impossible or just very difficult at best. Bum is kept simple by utilizing
everything that git exposes, including the configuration.