Rename rebase with git


Want to perform a git rebase in which you rename a file without pesky conflicts?

tl;dr

1
2
git filter-branch -f --tree-filter "git mv ORIGINAL_FILE_NAME NEW_FILE_NAME || true" -- $(git merge-base origin/master HEAD)..HEAD
git rebase origin/master

Ever wanted to rename a file you created/modified in a pull request but also wanted to keep the pristine history you worked hard for? Well tough luck you can either push a new commit to rename it (and keep old commits with the old file name) or resolve a bunch of pointless conflicts. There must be a better way…

Turns out you can have your commit history and eat it too! You can tell the git to go through your history and do the rename on each commit and then rebase.

Let’s walk through the steps. First we need to figure out which commits are affected. To start of we need to find what git calls a “merge base” - the nearest common ancestor with the target branch. This is the point from which commits will be considered by e.g. GitHub when you open a pull request.

1
git merge-base origin/master HEAD

Assuming the target repo is origin and the target branch is master. hEAD just points to whatever is currently checked out this will give use the merge base commit hash.

We get the full commit range with the .. operator. We can put the merge-base command into a subshell to build up our one-liner

1
$(git merge-base origin/master HEAD)..HEAD

NONE this is not valid bash but a git construct to be used in further parameters.

And here comes the main star - git command to rewrite history: git filter-branch. It has many options and modes - read the docs for more info, here we’ll just focus on our use case.

1
git filter-branch -f --tree-filter "git mv ORIGINAL_FILE_NAME NEW_FILE_NAME || true" -- $(git merge-base origin/master HEAD)..HEAD

Let’s walk through each part.

Then wait a bit (yes filter-branch may be quite slow)…..And we’re done. Well, almost. Still need to do the actual rebase - thus far we’ve only done the rename.

1
git rebase origin/master

My original use case was rebasing migrations that use sequence numbers as file names - you run into conflicts whenever somebody else merges any migration. So I’ve created an alias and put it into my ~/.gitconfig

1
2
[alias]
  rebase-migration = "!f() { git filter-branch -f --tree-filter \"git mv migrations/$1.sql migrations/$2.sql || true\" -- $(git merge-base $3 HEAD)..HEAD; git rebase $3; }; f"

Now I can simply run

1
git rebase-migration 76 77 origin/develop

Last modified on 2020-04-20

Previous Moving to Hugo
Next Surprising ease of gaming on Linux in 2020