I was midway through working on a project I had been calling commentate
, but
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
is the .git
repository.
(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
.gitignored
filesAccording 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 --tree-filter
does.
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
did.
So what weâll need is a shell command that replaces the word commentate
with
the word ahnnotate
. We also need to be careful to be case sensitive, that it
doesnât replace Commentate
with ahnnotate
or some variation of the sort.
Note: These examples all use the BSD sed
. On Linux (using GNU sed
), you
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 commentate
with ahnnotate
.
git filter-branch --tree-filter \
'ag --files-with-matches --case-sensitive commentate | xargs sed -i "" "s/commentate/ahnnotate/g"' \
HEAD
Next, weâll replace Commentate
with Ahnnotate
. However, git will complain
something about a backup already existing, so we need to add the -f
flag.
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 -f
flag.
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
lines in .git/info/refs
and had to delete all the lines except the one
containing refs/heads/master
.
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'