I like Merge Bubbles
I like git. I use it for all my programming projects of course, and everything
from ~/.ssh/
to ~/.claude/
to my Godot game (coming soon, I promise)
to my book formatted in LaTeX,
and nearly any directory that’s got custom-edited text files in it.
I generally work alone on my projects,
so I don’t need PRs like
[github-flow](https://docs.github.com/en/get-started/using-github/github-flow)
and especially don’t need the original
[git-flow](https://nvie.com/posts/a-successful-git-branching-model/).
But I do like to keep groups of commits together. For that, I like merge bubbles.
For example:
* e3f9cca DONE admin set existing user password
|\
| * 0a6dd90 Admin user_edit: hide set-password panel on self + server-side guard
| * d5dc655 AdminSetPasswordCest: end-to-end round trip on abc
| * ba74df9 placeholder for AdminSetPasswordCest
| * bbb4013 Admin user_edit: set-password form + handler
| * 384f4ba locale: admin user_edit set-password strings (EN + JA)
|/
* 7cd2527 BEGIN admin set existing user password
* 658cc93 DONE admin-driven brand manager registration
|\
| * 6e1f640 register.php: admin-driven success message names the new manager + login URL
| * bcda5fd register.php: gate to admin only post-bootstrap
| * 94b8c63 Admin users list: '+ Add brand manager' link to /login/register.php
|/
* 8eb82ba BEGIN brand manager registration UX
Every set of related commits lives inside a bubble. I start with a BEGIN commit, which generally has either nothing (via --allow-empty) or a minimum change, like updoot the the version of the code.
From there, I stack up a bunch of related commits. When it’s time to close the commit, I just:
- Look up the hash of the BEGIN commit
- Copy the hash to my paste buffer
git checkout [paste]git merge --no-ff active-branch -m "DONE with my awesome change"gitl, which for me meansgit log --oneline --graph --decorate --all- Look up the newly created hash for the DONE commit
- Copy the hash to my paste buffer
git branch -f active-branch [paste]git checkout active-branch
It’s a lot! It’s a mess; it’s annoying; it’s fragile, but it’s repeatable and I love it.
I asked my AI about it and it was like “yeah no worries mate” (or something like that), and presented me with this script which I have saved in ~/.local/bin/git-close-bubble:
git close-bubble
#!/usr/bin/env bash
# Close a merge bubble started with a "BEGIN ..." commit.
# Usage: git close-bubble <message> [BEGIN-commit]
# git close-bubble --dry-run [BEGIN-commit]
set -euo pipefail
DRY_RUN=0
if [ "${1:-}" = "--dry-run" ]; then
DRY_RUN=1
shift
fi
if [ "$DRY_RUN" -eq 0 ] && [ $# -lt 1 ]; then
echo "usage: git close-bubble <message> [BEGIN-commit]" >&2
echo " git close-bubble --dry-run [BEGIN-commit]" >&2
exit 1
fi
if [ "$DRY_RUN" -eq 1 ]; then
MSG=""
BEGIN_ARG="${1:-}"
else
MSG="$1"
BEGIN_ARG="${2:-}"
fi
B=$(git branch --show-current)
if [ -n "$BEGIN_ARG" ]; then
S=$(git rev-parse --verify "$BEGIN_ARG")
else
CLOSED=$(git log --merges --format='%P' | awk '{print $1}' | sort -u)
S=""
while IFS= read -r c; do
if ! printf '%s\n' "$CLOSED" | grep -qx "$c"; then
S="$c"; break
fi
done < <(git log --grep='^BEGIN ' --format='%H')
if [ -z "$S" ]; then
echo "error: no unclosed BEGIN commit found" >&2
exit 1
fi
fi
echo "Would close bubble starting at: $(git log -1 --format='%h %s' "$S")"
if [ "$DRY_RUN" -eq 1 ]; then
echo "(dry-run; no changes made)"
exit 0
fi
git checkout "$S"
git merge --no-ff "$B" -m "$MSG"
M=$(git rev-parse HEAD)
git checkout "$B"
git merge "$M"
echo "Done. Branch '$B' now points at $M. Inspect, then 'git push' when satisfied."
It near-magically handles the fragile command line stuff with a single line:
git close-bubble "DONE my cool code"
It does all the 9 steps above without me having to copy hashes and remembering all the incantations.
Dry Run
There is a dry-run option you can use to see what hash it would target as the BEGIN commit.
git close-bubble --dry-run
