test && commit || revert
Iβve been playing with Kent Beckβs TCR idea for the past couple of weeks, and itβs grown into this fun little shell script I want to share:
#!/bin/bash
# This script watches for file changes (via fswatch) in your
# git repository, runs your configured test command(s), and
# commits if they pass, reverts if they fail.
#
# This is Kent Beck's TCR idea:
# - test && commit || revert - Kent Beck - Medium
# https://medium.com/@kentbeck_7670/test-commit-revert-870bbd756864
# - TCR test && commit || revert -- Rope in Python
# https://www.youtube.com/playlist?list=PLlmVY7qtgT_nhLyIbeAaUlFOWbWT5y53t
#
# I'm interested in TCR as a forcing function for:
# 1. Keeping me focused
# 2. Keeping me oriented toward small changes
# 3. Keeping the tests really really fast
set -euo pipefail
# Let's clear the screen before we get started.
tput reset
# Now, TCR makes a lot (a LOT!) of small commits with
# meaningless messages.
#
# Maybe we don't worry about them! See Limbo:
# https://medium.com/@kentbeck_7670/limbo-on-the-cheap-e4cfae840330
#
# But for me, for now, on my projects, I'd like to squash a
# session's work into one commit.
#
# There's git tooling to help:
# - `git commit --fixup`
# - `git rebase --interactive --autosquash`
#
# So we start by making an empty base commit (with a fun
# message!) here before we enter the TCR loop.
#
# (Perhaps we should just run `git commit --amend` instead,
# avoiding the rebase altogether. But something about
# retaining short-term access to the commit history feels
# comforting to me.)
emoji=("π»" "π¦" "π―" "π" "πΈ" "π" "π¦" "π©" "π¦")
message="${emoji[$RANDOM % ${#emoji[@]} ]} $(date '+%A, %B %e')"
git commit --message "$message" --allow-empty
base=$(git rev-parse HEAD)
test_and_commit_or_revert() {
local base=${1}
# Clear the screen at the beginning of each run.
tput reset
# A cyan heading and a newline help me scan the output.
run() {
echo "$(tput setaf 6)$*$(tput sgr0)"
eval $*
echo
}
# When the code gets reverted, I want to see what I did:
run git --no-pager diff
# Now, let's run the tests.
# As soon as any test fails, this subshell exits with
# failure. We don't want that to kill the whole script, so
# we jankily hop in & out of `set -e`.
set +e; ( set -e
# (And how to emit `run`'s lovely balancing newline on
# failure? Like this. So cool!)
set -E; trap echo ERR
# Where to store the project-specific test commands?
# Scripts or config files are messy -- and we're already
# using git!
git config --get-all tcr.command \
| while read -r command; do run $command; done
)
result=$?; set -e
if [ $result -eq 0 ]; then
run git commit --all --fixup $base --allow-empty
else
run git reset --hard
fi
}
# Why not just use fswatch's own looping?
# Because the revert would otherwise trigger another burst of
# fswatch events, and we want to leave the bad diff and the
# test failures on the screen.
running=1; trap "running=0" INT
while [ $running -eq 1 ]; do
fswatch --one-event --latency 0.1 $(git ls-files) \
| read && test_and_commit_or_revert $base
done
# You can get fswatch via `brew install fswatch`
# http://emcrisostomo.github.io/fswatch/getting.html