I spent several years working with a public sector software team in the Netherlands—first as an employee, later as a freelance consultant. The team used git for everything: code, server configuration, infrastructure automation—you name it.
One of the main projects we built was an open-source (EUPL-licensed) application used by local governments. While pairing with a colleague one day, he noticed my git tooling: aliases, shell scripts, and a few handy helpers I'd put together over time. After a quick demo, he said, “You should open-source this.”
At the time, my tools lived inside a private dotfiles repo. So I pulled them out and created bum: a standalone git toolkit. While the first standalone release of bum was in 2019, many of its scripts, aliases, and helpers trace back to early 2014. You can check it out on GitLab or clone it directly:
git clone git@gitlab.com:waterkip/bum.git
TL;DR
Bum is a collection of shell scripts, written in zsh
(my daily shell), that help me work more efficiently with git.
Git supports custom aliases and external commands. If you create a script and name it with a git-
prefix (e.g. git-foo
), you can run it as git foo
. Bum builds on that convention, offering tools like git nb
, git wip
, and more.
It also uses git’s configuration system. You can create your own namespace by running something like:
git config --set bum.foo bar
This allows you to create your own toolset, with its own configuration.
Starting a new branch
When starting a new branch, many of us will use some form of git checkout
, which I have aliased to git co
. If you are using git switch
, you probably use git switch -c
or with the capital C
.
I got tired of writing branch names, so I developed a way to start branches quickly. I created the git-new-branch
script to do just that. It utilizes several custom configuration items and is aliased to nb
:
git nb 1234 add foo to the bar
This now does several things:
- Builds the branch name from prefix + issue + description
- Checks if the name already exists
- Creates the branch
- Sets the correct upstream
- Pushes the branch to your remote
You end up with a branch called OPN-1234-add_foo_to_the_bar
.
There is hardly any configuration needed; we only need two items in our git config: bum.issue.prefix
and bum.issue.regexp
. The prefix is OPN
and the format is, for example, OPN-[0-9]+
. That way, if you try something like git nb something is fishy
, you’ll get a warning.
Under the hood
There are a few tricks we use to support nearly zero-config branch creation. First, we determine the default remote by checking which remotes are available. We prefer upstream
, but fall back to origin
if needed. You can override this by setting bum.upstream
in your git config.
Once we’ve picked the remote (say, upstream
), we look for the file refs/remotes/upstream/HEAD
. This file tells us which branch is considered the default (usually master
or main
).
If that file doesn’t exist—say, the remote was just added—we run:
git remote set-head upstream --auto
That tells git to discover the remote’s default branch automatically. Now we have all the information needed to set the correct tracking branch.
Speaking of tracking branches
Tracking a branch is just a convenience setting stored in git config. For example, it adds something like branch.my-feature.merge = refs/heads/master
. When you run git checkout -t upstream/master -b my-feature
, you're telling git: use upstream/master
as the starting point and set it as the upstream for the new branch.
Accessing internal git paths
In bum we use the alias git path
to determine the path of a file. Git itself has git rev-parse --git-path
, but this is a lot of typing. This lets us find out where internal files like HEAD, refs, or config actually live. People assume incorrectly that this is always in .git
, but that is wrong. If you work with worktrees, it is somewhere else. I'm not too familiar with worktrees, as I don't use them, but from my understanding, this is how it works and might surprise you. You setup a worktree somewhere, eg, your git repo is in /path/to/repo
and your worktree is in /opt/foo
. Now git path config
says your git config is located in /path/to/repo/.git/config
. Whereas git rev-parse --top-level
might show /opt/foo/worktreeA
or something along those lines. git rev-parse --git-path
can tell you the correct location of your config, always.
Pushing safely: remote.pushDefault
Since git-new-branch
sets up tracking branches automatically, we also make sure pushing behaves predictably. By default, git tries to push to the upstream
branch, which may be something like upstream/master
. But that’s probably not where you want your feature branch to go. To fix this, we check if remote.pushDefault
is set. If remote.pushDefault
isn’t set, it’ll warn you once as a heads-up and configure it to point to origin
:
git config --local --set remote.pushDefault origin
This tells git: “Whenever you push, assume origin is the remote I mean (unless I say otherwise).” This way, a plain git push
won’t try to push to a branch you’re tracking from upstream.
Getting interrupted and how to continue
Let's say I'm working on a feature, and suddenly someone says, "Drop whatever you are doing. You need to fix this bug immediately!" Git has a very nice feature called stash. Every uncommitted change is pushed onto the stack when you issue the command git stash
. While useful, I also have a love-hate relationship with it. More often than not, I need to remember that I have stashed some changes. Therefore, I created a small script called git-wip
, which commits all changes with a simple commit message: "WIP: You want to edit this commit message". It also accepts arguments, git wip working on X when Y interrupted me
, which renders my commit message to (you can guess): "WIP: working on X when Y interrupted me". I can now switch to fixing the bug: git nb ZS-23456 urgent bug which needs fixing asap
.
There is also another solution to this. A Reddit post inspired me to create git-stash-check
, a tool that checks if I have uncommitted changes in the stash for a particular branch. It works from within a post-checkout
hook. And before this, I used git stashed
to see what was in my stash. I borrowed that alias from Ovid.
Committing your changes
Having a good commit message is crucial; people sometimes forget that, including me. My future self and others need to know what has changed and why that change was made. You can add a commit.template
to your git config, and it will use that template when creating commit messages. An example of such a message can be found in bum and originates from https://gist.github.com/adeekshith/cd4c95a064977cdc6c50. Years ago, when we tried to improve the commit hygiene of our team, we used this template. I eventually stopped using it myself and moved on to other ways of improving commit hygiene.
I now use two hooks that prefill parts of my commit messages — pulling config from the same source as git-new-branch
. The first hook, prepare-commit-msg
, automatically adds the issue number — which is easy, since git-new-branch
already enforces that pattern in branch names.
Then I have a second hook, commit-msg
, that stops me from committing a message that’s just the issue number and nothing else — like ZS-12345:
, To make this happen, I save the MD5 checksum of the modified message in the prepare-commit-msg
and check it in the commit-msg
hook. When running git commit -m
(aliased to git cm
) the hooks run and the message is prepended with the issue number.
Aliases for committing
To make it easier to see what I’m committing, I've aliased commit to c, with a few helpful flags: git config --global alias.c 'commit -uno -v'
. The -u
/ --untracked-files
flag tells git what to do with untracked files — in my case, I don’t want to see them, so I use -uno
. The -v
option shows a unified diff at the bottom of your editor.
I've also added an alias ca
for git commit --amend
, which internally uses my c
alias — I like nesting aliases this way to keep consistency. Now, amend has all the features commit has. If you don't want to edit the commit message, just use git ca --no-edit
, which is aliased to git amend
.
It’s not technically an alias, but git prepend-msg
lets you update the commit message with a different issue number. For example, git prepend-msg ZS-123
replaces the existing one. Handy if you split off a fix and need the commit to reflect its new issue.
Once I’ve pushed my branch and submitted a PR/MR, I usually get feedback from a reviewer. For years, I handled this by creating new commits with messages like “Processed feedback from reviewer”. That clutters history. Then I came across a neat feature: commit --fixup
. Fixup allows you to tie a new commit directly to an earlier one. I find the original commit, then run:
git commit --fixup <commit-ish>
(I’ve aliased this to git fixup
.)
This produces a commit message like:
fixup! ZS-12345: Adds a new method to ZS::Foo::Bar
I do this for every bit of feedback, push those fixup commits, and once everything is approved, I run:
git pull --rebase -i --autosquash
(aliased to git autosquash
)
This pulls in any upstream changes, reorders the fixup commits under the originals they target, and squashes them in automatically. One clean history, no manual squashing, and no “addressed feedback” noise.
I also use fixup if a tester finds a bug, or if I find something broken midway through a multi-commit feature. Because I always autosquash
before merging into master, the history stays clean and linear, without losing the benefits of frequent, focused commits.
Pushing to development
At my previous client, we used a development
branch to merge all features and bugfixes into, for the tester to test. The flow was something like this, but with more error handling:
# Push to my remote
git po HEAD && \
# Checks out development
git co development && \
# Fetches the upstream
git fetch upstream && \
# Reset the local version of development to the upstream version
git reset --hard upstream/development && \
# Merge without getting an editor. It stays something like this:
# Merge branch 'branch name' into development
git merge --no-edit - && \
# Push development to the upstream repo
git push upstream HEAD && \
# And go back to the branch
git co -
This is also scripted to make life easier. Maybe I should call this git-merge-dev
and include it in bum. At the moment, I do not need it, and thus, I am not improving on it.
branch selecting
The next git-script: git-go-branch
(aliased to git gb
) which deals with not having to run git branch | grep ZS-12345
(git branch
is aliased to git br
), copy the branch name and check that branch out: git gb ZS-12345
. I don't always know the issue number by heart, but go-branch
will find any part of the branch name that matches: git gb useful
goes to the same branch. If go-branch
finds the term in multiple branches, it will print out the names, and I must copy/paste the branch name for the checkout
command. Bum doesn't use fzf for fuzzy finding branches or anything.
SEE ALSO
Bum has a lot more helpers — for cleaning branches, managing stashes, and navigating histories. But this post covers my core loop: from branch to commit to merge. I’ll dive into more advanced features in a future post.
Top comments (0)