test && commit || revert (TCR) is a coding workflow proposed by extreme programming guru Kent Beck. I just tried it out. Here’s what I learned.

1. Starting out is somewhat paradoxical

You have to write tests that pass, otherwise they get deleted. So you end up aiming for silly things, like tests that test nothing or test stubs. After that, it seems to work well enough for something like a Fibonacci function.

The idea is not that this workflow is better, only that it has something to teach us:

2. Small commits are better

When your change being wrong means it being deleted, you become more cautious. Does that sound harsh?

Curious if it’s making a difference in your commit sizes? Here’s a script to compute Git diff size histograms. Choose between logarithmic or constant bin size.

#!/bin/sh -e
# This command prints out a histogram of diff size.
git log --pretty=format:'%p %h' |
while read pair; do
    git diff --shortstat $pair
done |
mawk '
function constant_bin(diff_size, bin_size) {
    return bin_size * int(diff_size / bin_size)
}

function log_bin(diff_size, base) {
    return exp(log(base) * int(log(diff_size) / log(base)))
}

{
    diff_size = $4 + $6
    freq[log_bin(diff_size, 2)]++
    #freq[constant_bin(diff_size, 2)]++
}

END {
    for (diff_size in freq) {
        print diff_size, freq[diff_size]
    }
}' | sort -nr

3. It’s dangerous

The basic command is

./test && git commit -a || git reset --hard

I’ve shot myself in the foot a few times with this form. I deleted it when I got tired of losing hours of good work. See if you can figure out what the risks are.

  • If the ./test command is missing, your changes are deleted
  • If you abort the commit, your changes are deleted
  • git commit -a is bad: all changes to tracked files are added, but you can’t review the diff before committing

This is a more careful approach:

#!/bin/sh

if [ ! -x ./test ]; then
    printf 'no ./test to run\n'
    exit 1
fi

if ./test; then
    git commit -v -a
    exit 0
fi

git reset --hard

Here, we simply exit with a nonzero status if the test command doesn’t exist. If it does exist, we run it. If it succeeds, we commit all changes, but show the diff in the message template, so you know exactly what has changed. If you abort the commit, the script simply returns. Only if the test fails are the changes deleted.

4. It’s incompatible with TDD

One ambiguity in TCR is whether it is meant to apply to tests as well as to source code.

In TDD, tests are written first, and their failure unreflectively determines what the code should do. In TCR, failing tests cause changes to revert, preventing you from committing them!

TCR does not prevent you from writing untested code. For that, you’d have to compute test coverage. But being a slave to coverage metrics does not necessarily produce better code, as it can encourage lots of indirection.

5. It’s kind of fun

It presents a novel challenge. I experienced a flow state while using it.

But it’s not fun enough to continue using it after a month. There are definitely cases where it’s frustrating, like when you can’t figure out how to refactor something without tests breaking somewhere. But it’s useful for encouraging small, strategic changes.

6. I’ve used it for real work

There are a few occasions where I’ve actually used TCR on production codebases. You can spot these by big spikes in the number of commits.