Renaming a Git Project for Real

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!

0. Make a backup!

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.)

1. Delete all branches, remotes, and tags

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

2. Delete all .gitignored files

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

3. Update the file contents

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.

4. Update the file/directory names

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

5. Update the commit message contents

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

6. Confirm it worked (and clean up)

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'

References

Posted on 2019-02-14 11:12 AM
Contact
hello(at)zachahn(dot)com
© Copyright 2008–2021 Zach Ahn