The Short Answer
The following solution was modified from multiple sources:
filter-branch --index-filter always failing with "fatal: bad source".
Renaming The Past With Git.
Here is a filter-branch invocation that uses an index-filter to rewrite the commits without a working copy, so it should run really fast. Note that, as an example, I'm renaming the file alpha/beta/foo.cpp
to alpha/beta/Foo.cpp
.
As with any potentially destructive Git operation, it is highly recommended that you make a backup clone of your repo before you use this:
git filter-branch --index-filter '
git ls-files --stage | \
sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:" | \
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD
Note that HEAD
is optional, because it should be the default for filter-branch
. It will rewrite all commits from the root commit to the commit pointed to by HEAD. If you want to increase the speed of the filter-branch even more, you can pass a range of commits instead of HEAD
, such as
HEAD~20..HEAD
to rewrite just the last 20 commits. The beginning of the range is exclusive, i.e. it's not rewritten, only its children are, and the ending HEAD
is again optional, since its a default.
Verification
It's a good idea to do some quick sanity-checks to verify that the filter-branch did what you expected it to do. First, compare the current history with the previous history:
git diff --name-status refs/original/refs/heads/master
D foo.cpp
A Foo.cpp
Notice that when the previous history is compared relative to the current one, the current history no longer has foo.cpp
(it's deleted), while Foo.cpp
was added to it.
Now confirm that foo.cpp
contains the exact same content as Foo.cpp
:
git diff refs/original/refs/heads/master:foo.cpp Foo.cpp
The output should be empty, meaning that there are no differences between the two versions.
Detailed Explanation
The following breakdown is also available in more detail from the blog post "Renaming The Past With Git". I am summarizing it here. The basic idea of the script is to create a new index file that contains the new name for the file foo
(i.e. foo
becomes Foo
), and then replace the old index with the new one.
Step 1: Get the Index File Contents
First, the current index file contents are output in a form that can then be fed into git update-index
, using the --stage
option:
git ls-files --stage
100644 195ff081f7d0d37a60181de790ae1c6b9f177be8 0 alpha/beta/foo.cpp
100644 0504de8997941bf10bcfb5af9a0bf472d6c061d3 0 LICENSE
100644 6293167f0eb7389b2f6f6b73e838d3a547787cbf 0 README.md
...etc...
Step 2: Rename the File
Since we want to rename foo.cpp
to Foo.cpp
, we use sed
with a regular expression to replace the string foo
with Foo
:
"s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:"
In the above command, I'm using a colon :
to delimit the regexes in the sed
command, but you can use other characters as delimiters too, such as pipe |
. I chose a colon instead of the more standard forward-slash /
as a delimeter so that it wasn't necessary to escape the forward-slashes used in the file paths.
After piping git ls-files --stage
through sed
, you should get the following:
git ls-files --stage | sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:"
100644 195ff081f7d0d37a60181de790ae1c6b9f177be8 0 alpha/beta/Foo.cpp
100644 0504de8997941bf10bcfb5af9a0bf472d6c061d3 0 LICENSE
100644 6293167f0eb7389b2f6f6b73e838d3a547787cbf 0 README.md
...etc...
Step 3: Create a New Index with the Renamed File
Now the modified output of git ls-files --stage
can be piped into git update-index --index-info
to rename the file in the index. Because we want to create an entirely new index to replace the old one, some environment variables for the path to the index file need to be set first, before invoking the git update-index
command:
GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info
Step 4: Replace the Old Index
Now we just replace the old index with the new one, which effectively "renames" the file:
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
Summary
Here's the whole command again, when everything is put together:
git filter-branch --index-filter '
git ls-files --stage | \
sed "s:alpha/beta/foo.cpp:alpha/beta/Foo.cpp:" | \
GIT_INDEX_FILE=$GIT_INDEX_FILE.new \
git update-index --index-info && \
mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE"
' HEAD
Documentation
git filter-branch
.
git ls-files
.
git update-index
.
Git environment variables.