Wesley Schwengle IncludeIf: A digital space written in pencil Articles Series Answerrit /dev/null About IncludeIf: A digital space written in pencil All articles How to replicate bum - building a toolkit based on git primitives

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

I showed you want bum does This one is about why it works the way it does. Bum isn’t a framework. It’s a set of principles that happen to have grown into scripts over eleven years. Understanding those principles is more useful than memorising the tools. I would love for you to use bum, but it is way more interesting to use the patterns of bum because it opens up a universe of git tools that can make your life easier. In fact I would argue more git tools should take this approach. It makes the whole git ecosystem much more coherent.

Bum exists because git left little gems, breadcrumbs to use for customisation and extensions. As explained in the original bum article , you can extend git via 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:

# define upstreams bum.upstream=zs bum.upstream=origin # define protected branches bum.branches.protected=master bum.branches.protected=preprod bum.branches.protected=development bum.branches.protected=postex # define branches a post-merge hook cannot touch bum.branches.nomerge=postex bum.branches.nomerge=development # automatic branch create rules bum.issue.prefix=MINTY bum.issue.regexp=MINTY-[0-9]+ bum.issue.format=title # new branch creation rules bum.pushnew=true bum.nowarning.upstream=true # SPDX license rules bum.license=EUPL-1.2

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 path is an alias for git rev-parse --git-path and is worktree safe
  • git dir is the same as git path
  • git root is an alias for git rev-parse --show-toplevel
  • git common is an alias for git 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:

$ git status --porcelain M content/article/git-bum-followup.md $ git -c status.showuntrackedfiles=yes status --porcelain M content/article/git-bum-followup.md ?? README.md

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.

Git in the trenches: How maintaining sanity is a graph in the making