Clean slate refactoring

October 1st, 2021

Clean slate refactoring

a slab of slate, clean of course

Thanks to my tinkering instincts and chronic indecisions, I’m terrible at is picking a technology and sticking with it. Which means that I’m quite prone to utter those dread words: “screw it, I’m going to rewrite it. FROM SCRATCHES!“.

But it’s rare that “from scratches” really means “from scratches scratches”. I’m likely to want to keep some files as-is. Most will be modified, sure, but I might still want to at least be inspired by the code I wrote before.

What to do, then?

One option is to create a new empty repository beside the old one, and manually copy (and alter) files from the old to the new.

Another is to create a new branch, and progressively convert the files within that branch.

Both options are… not optimal. With the side-by-side repos, as the transition goes on keeping track of what already has been done and what remains to be addresses gets harder and harder. As for doing in situ migrations, we end up with the old and the new all intermixed in what can easily turn into a huge confusing mess.

But wait, there is a third option! Leveraging some of Git’s features, we can follow a “clean slate refactoring” procedure that will keep for us a living laundry list of all that is left to do. Here, let me show you.

The use case: git-vaudeville

It’s better to show than to tell, so let’s pick a concrete example: git-vaudeville. This package is currently written in TypeScript. For giggles, let’s say I want to change it back to pure JavaScript. How would I go about it?


I’m calling this “clean slate refactoring”, so let’s give ourselves a fresh new start. First by creating a work branch for our rewrite.



Earmarking the beginning of the rewrite branch. It’ll will come in handy later on.


And now creating the refactoring branch proper.


Oh yeah, small note: git cob is one of my aliases, here standing for checkout -b. I debated putting the long form of the commands instead. Unarguably be more pedagogical, but not as fun. In any case, I’ll point out when I’m using one of those yenzie specials, and all aliases used in this articles will be listed at the end — I might be a vain showoff, but I’m not a monster.

Back to our example. Let’s now remove everything from the working tree.



Remove everything. Spare no one.


Commit what will become our todo list.


Welcome to Tabula Rasa!


That wip-clean-slate commit is going to be our todo list of files yet to be processed, and our end goal is going to be to turn it into an empty commit. Until then, at any time we can peek at it to see what is left to do.

$ git show --compact-summary :/wip-clean-slate
commit 98a520965f220e7c0a14b1c42f7573747fa86ace (HEAD -> js-rewrite)
Author: Yanick Champoux <>
Date:   Wed Sep 29 14:06:53 2021 -0400


 .gitignore (gone)                       |   4 --
 Changes (gone)                          |  24 ------- (gone)                        | 138 --------------------------------------
 dist/cli.js (gone)                      |   8 ---
 dist/colors.js (gone)                   |  12 ----
 dist/command.js (gone)                  |  38 -----------
 dist/commands/info.js (gone)            |  39 -----------
 dist/commands/install.js (gone)         |  56 ----------------
 dist/commands/run.js (gone)             |  31 ---------
 dist/commands/run.test.js (gone)        |  16 -----
 dist/hook.js (gone)                     |  67 ------------------
 dist/hook.test.js (gone)                |  12 ----
 dist/index.js (gone)                    |   8 ---
 dist/vaudeville.js (gone)               |  68 -------------------
 jest.config.js (gone)                   |   9 ---
 package.json (gone)                     |  55 ---------------
 src/@types/simple-git/index.d.ts (gone) |   3 -
 src/@types/yurnalist/index.d.ts (gone)  |   4 --
 src/cli.ts (gone)                       |   6 --
 src/colors.ts (gone)                    |  23 -------
 src/command.ts (gone)                   |  40 -----------
 src/commands/info.ts (gone)             |  43 ------------
 src/commands/install.ts (gone)          |  72 --------------------
 src/commands/run.test.ts (gone)         |  16 -----
 src/commands/run.ts (gone)              |  50 --------------
 src/hook.test.ts (gone)                 |   7 --
 src/hook.ts (gone)                      | 127 -----------------------------------
 src/index.ts (gone)                     |   2 -
 src/vaudeville.ts (gone)                |  91 -------------------------
 tsconfig.json (gone)                    |  66 ------------------
 30 files changed, 1135 deletions(-)

Keeping files wholesale

There are some files we want to keep from the old incarnation unchanged. Here, that’d be and .gitignore.

There are a few ways to skin that cat. Here’s a simple one:



Add back the files we want to keep wholesale.


fix is an alias for commit --fixup.


The big trick I’m using is the fix variant of the commit. Because with that, next time we do an interactive rebase, the fix will be merged with the original wip-clean-slate commit and it’ll be just like we never deleted those files at all.



After the commit we have two commits in our rewrite branch.


We rebase interactively, retcon is my alias for rebase -i.


The fix is now part of the (now altered) original commit.


And that new clean slate commit actually spared our two files.

Throwing files in the oubliettes

For files that we want to really discard, we want to extract their removal from the wip-clean-slate commit so that we know it’s not part of the todo list but a deliberate kill.

To do so, we can do a two-step dance where we bring the file back like in the previous section, and then kill it dead.



Bring the file back from the dead.


Set that as an edit on the clean slate commit.


Now kill it fo’ realz.


Rebase to merge the fix with the clean slate commit.


The removal of the file is now in its own commit.


A different approach would be to use something like my git resplit custom command (included at the end of the article), which does a rebase where all changes done to specified paths are extracted from the bigger commits.



Run the magic command.


At this point the removal of src/@types/* is extracted from wip-clean-slate.


Now I can do an interactive rebase and, say, merge the split commit with the “don’t need types anymore” commit.


We’re back to two commits.


And the type deletes are indeed together.

Everything else in-between

Between the two extremes of keeping as-is and deleting, we have all the other files we want to keep with modifications. For those we’ll follow the same procedure: pluck them from the wip-clean-slate commit via a fix, then do the edits proper.



Pluck from wip-clean-slate.


Edit the file.


Commit in the edits. And yeah, cm is another of my aliases.

Alternate picking using the stash

So far what we’ve done is starting from an empty plate, and incrementally picking the files from the wip-clean-slate. If you prefer to still have all files presents, we can also do that via our old friend the stash.



Bring all the files to the working tree, but don’t commit any of them.


All the files are there, and yes, s is an alias.


First make the ‘fix’ commit for wip-clean-slate.


Do whatever you wanna do.


Commit those changes.


Before we rebase, we have to stash the untracked files.


Now the files are out of wip-clean-slate and their changes are all in the last commit.


Finally re-apply the stash to continue the good work…

Addendum: git aliases and commands

Git aliases

cob    = checkout -b
fix    = !f() { git commit --fixup ${1:-HEAD};  }; f
retcon = rebase -i
s      = status


#!/usr/bin/env fish

set -l base $argv[1]
set -e argv[1]

git rebase $base --exec "git split $argv"


#!/usr/bin/env fish

if test -z $argv
    echo "must provide at least one path"
    exit 1

set -l sha1 ( git rev-parse HEAD )

git reset HEAD^ -- $argv

if test -n ( echo ( git diff --name-only --cached ) )
    git commit --amend --no-edit

git add $argv

if test -n ( echo ( git diff --name-only --cached ) )
    echo (git log -1 $sha1 --pretty=%B) "(split $argv)" | git commit -F -


#!/usr/bin/env perl

use 5.24.0;
use strict;
use warnings;

use List::MoreUtils qw/ first_index /;

my $index = first_index { $_ eq '--' } @ARGV;

$index = 0 if $index== -1;

push @ARGV, '--message', join " ", grep { $_ ne '--' } splice @ARGV, $index;

exec 'git', 'commit', @ARGV;