I was midway through working on a project I had been calling
then one of my coworkers suggested a much better name,
ahnnotate. Instead of
a single large commit to rename each file and each class name, I thought it’d be
cool to rewrite history and pretend that it had the cool name all along. But I
have to admit, it wasn’t quite as straightforward as I had expected.
Use at your own risk!
Before we go any further, it would probably be wise to make a backup using
cp -r. I personally made several mistakes and had to undo my changes;
thankfully I had copied my repository prior to starting.
We’ll be using several
--force flags, deleting git’s automatic backups, etc.
Since we’ll be operating on git’s own objects, what we really want a backup of
(In other words, the git repository you’re working on is NOT your backup! The
operations we’ll use will overwrite your git repository, so a
git reflog might
not help you.)
I’d personally recommend deleting all “pointers” to your git commits. That includes branches, remotes, and tags. I’m not sure if this is necessary, but it makes it easier when there are fewer things to keep track of.
When done correctly, each of the following commands shouldn’t return any results.
git branch --list git tag --list git remote -v
According to the
git filter-branch docs,
--tree-filter will add whatever
files are present in the working directory, even if they’re ignored. Since those
files were probably ignored for a reason, we’ll need to delete them. (I hope you
backed these files up with that
cp -r because git won’t be able to recover
files it doesn’t know about 😬)
Thankfully it’s fairly simple to get a list of what’s ignored. I went through the list and moved or deleted each file as necessary.
git status --ignored
We’ll mostly be using
git filter-branch. It’s similar to
git rebase in that
it allows you to rewrite history, but it’s quite a bit more destructive.
I found this article
quite helpful in understanding exactly what
git filter-branch --tree-filter takes a shell command and runs it against
every commit between the first commit and
HEAD (or whichever other commit you
specify). It then replaces the contents of that commit with whatever the command
So what we’ll need is a shell command that replaces the word
ahnnotate. We also need to be careful to be case sensitive, that it
ahnnotate or some variation of the sort.
Note: These examples all use the BSD
sed. On Linux (using GNU
won’t need the empty quotes in these examples; you can use
sed -i "s/pattern/replace/g". I’m not sure what happens with GNU sed if it is given
the empty quotes.
First, we’ll replace
git filter-branch --tree-filter \ 'ag --files-with-matches --case-sensitive commentate | xargs sed -i "" "s/commentate/ahnnotate/g"' \ HEAD
Next, we’ll replace
Ahnnotate. However, git will complain
something about a backup already existing, so we need to add the
This flag is pretty picky about where it is.
You can also optionally delete the backup with
rm -rf .git/refs/original and
run it without the
git filter-branch -f --tree-filter \ 'ag -l -s Commentate | xargs sed -i "" "s/Commentate/Ahnnotate/g"' HEAD
We’ll need to do this over and over for each capitalization variation you care
about. One thing that bit me though is that I used “commentate” as a verb,
“commentating”. I had to do an additional search/replace for that since the
absence of the
e broke the pattern.
I found this article quite helpful, and this following example is basically copied from there.
git filter-branch -f --index-filter \ 'git ls-files -s | sed "s/commentate/ahnnotate/g" | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"' \ HEAD
Lastly, we’ll update the commit message contents. You’ll need to repeat this for whatever capitalization combinations you have.
git filter-branch -f --msg-filter 'sed s/commentate/ahnnotate/g' HEAD
Cool! Hopefully we’re done now. Now we just need to check.
At this point, it’s clearer to delete the backups that git created. In order to check if the renaming is complete, we’ll run some searches that look at every commit that git knows about. Leaving these backups means that git will search through them and provide false positives.
We’ll know that the backups are deleted once the following commands return the same result:
# Lists the number of commits in master git rev-list --count master # Lists the number of commits in the entire repository git rev-list --all --count
An easy first step is to delete the
.git/refs/original directory. Hopefully,
this will be the only step you have to do. For some reason, I had a bunch of
.git/info/refs and had to delete all the lines except the one
I also found
git reflog expire --expire-unreachable=now --all and
git gc --prune=now helpful in removing commits that weren’t reachable through
the master branch.
Last is the actual verification. None of the following commands should give you any output. If it does, you’ll probably need to go back and re-run some commands.
# Search file contents git grep commentate $(git rev-list --all) # Search file/directory names git log --all --full-history --oneline -- '*commentate*' # Search commit message contents git log --all --grep='commentate'