Dom Williams

The Bane of TODOs

TODOs are great. In my head they're short and concise notes that will remind future me about issues and outstanding work that he'll get to one day. They'll be ticked off one by one in small commits that paint a beautifully satisfying git log. Surely?

In reality they're meaningless splatters of incoherent noise stuffed into random places littered across the code base. Often they'll either declare the surrounding code hideous/broken/incorrect with no explanation or suggestion on what could be done better, or summarise a clear-at-the-time thought in an opaque handful of abbreviations1.

The poor man's kanban

My side projects are filled with TODOs of greatly varying detail2, which for the most part peter out before planning and task prioritisation ever become an issue. Professionally this is where tickets, sprints and retrospectives come in, but for side projects I'd prefer the middle ground without maintaining a full ticket system.

As it turns out, TODOs don't have to be scattered and unorganised, nor rigidly maintained in a faraway place. By combining the power of git hooks, dirty scripts and markdown, we can achieve a reasonable middle ground.

Git hooks to the rescue

In a nutshell, I grep for TODOs in a pre-commit hook and dump them into a version controlled markdown file. It comes in the form of a dense mass of punctuation that, just by chance, is valid Perl3.

use strict;
use warnings;

# output file to skip searching for TODOs
my $md = $ARGV[0] || "";

open LINES, "git grep -I -n -E \"TODO|FIXME\" |" or die "git grep failed: $!";

my %todos;
my @files;
my $count = 0;
while (<LINES>) {
    next if /^$md:/;
    if (my @match = m/^([^:]+):(\d+):\s*(.+)\n$/) {
        my ($file, $line_no, $todo) = @match;
        push @{$todos{$file}}, $todo;

        push @files, $file unless @files && $files[-1] eq $file;
    } else {
        die "bad line: $_\n";

print("# TODOs ($count)\n");
foreach (@files) {
    my @todos = @{$todos{$_}};
    my $count = @todos;
    print(" * [$_]($_) ($count)\n");
    foreach (@todos) {
        print("   * `$_`\n");



git add $OUTFILE


On each commit, all TODO/FIXMEs in the project excluding the TODO list itself are scraped and written to, grouped by file path. Each file name is a hyperlink, and the number of TODOs per file and total count are shown too.

The markdown is pretty when rendered on your repository host of choice, and viewing the mighty task list in a browser easily beats the plain grep output or IDE view.

Example generated markdown rendered on GitHubA snippet of the TODOs from my current gamedev project, rendered on GitHub.

I keep this script in ~/bin for easy use across projects, but you may find it better to store it in the repository e.g. $REPO/.dev/ if there are multiple developers.


In the short time I've been trialling this, I've found it's surprisingly effective motivation to write coherent and meaningful TODOs. Publishing these in a publicly accessible formatted file means there is now very little friction preventing a new visitor to the repository glancing through the outstanding task list. Garbled streams of consciousness trashing the code don't shine a good light on the project, especially for first impressions.

The fact that it is automatically kept up to date without any thought is not to be sneezed at. I have a graveyard of todos.txts and plans.mds that live externally to the repository, and always end up being a write-only brain dump that was beneficial only for that planning session. For some reason I'm always reluctant to delete anything from those files for fear of losing any deep insights they might have contained, even if the notes are long-since irrelevant.

What's more, having a running counter of the total TODOs is very satisfying, and a metric I didn't know I wanted. Seeing it decrease is a rare and enjoyable event, only if I can control myself for long enough to complete only the task at hand and not replace it with a handful more.

  1. Whatever divine inspiration inspired past me to scrawl TODO fix this and FIXME dont do this on a function is long gone; I guess I'll leave them there for a few more weeks and delete them silently in an unrelated commit. 

  2. That is, the occasional comment actually contains some information about the code, and makes sense at a glance. The majority end up as a simple marker that boils down to "code bad, redo code better". 

  3. Why Perl? It's typically present by default on most machines, and is very capable at running shell commands and processing the output. Bash and/or Python could have done just as well of a job, but Perl fits both tasks perfectly. Plus, I've been spoiling myself with too much ergonomic Rust syntax recently, one has to stay on one's toes.