Tracking renamed files in Git
Git famously doesn’t track file renames. That is, Git doesn’t store the information “file A has been renamed to B in commit X”.
Instead, Git stores snapshots of the repository at each commit. It then uses a (customizable) heuristic during diffing to guess at likely renames: “File B in commit X is new, and file A has been deleted. B is 90 % identical to A’s previous contents, so A was probably renamed to B.”
This behavior is very much by design:
- Git FAQ: Why does Git not “track” renames?
- Linus Torvald’s explanation why Git doesn’t track renames (2005-04-15) (archived copy)
Linus Torvald’s email is worth reading. It’s well-reasoned and I agree with his arguments:
- Tracking renames is a superficial solution that fixes only part of the actual problem: how do you track the history of a particular piece of information, which may be much smaller (a single line) or larger (the design of an entire subsystem) than a file, depending on context.
- Shifting the task of history tracking from commit time to search time allows the search algorithm to do a much better job, because it can be tweaked to the structure of the underlying data.
And yet, I still miss the ability to explicitly register a rename operation with Git. Maybe this is because the history tracking tools we have are not as good as what Linus Torvalds envisioned in 2005. Or because sometimes the file is a good enough unit of granularity for history tracking, even if imperfect.
Use a separate commit for the rename
Git’s heuristics work great if renaming a file is all you do in a commit. Tracking only becomes a problem if the renaming coincides with substantial changes to the file’s contents in the same commit. Unfortunately, this happens very frequently in my experience: more often than not, my reason for renaming a file is that I made substantial edits and now the filename no longer represents the file’s contents.
The golden rule: To track a file’s identity across renames, perform the rename in a standalone commit, separate from any edits to the file.
git mv stages the rename but not the edits
Git has the promisingly named git mv command, but since Git doesn’t track renames, git mv is mostly no different than doing the renaming in some other way and then staging the change (the deleted and newly created file). The FAQ answer I linked to above even says so:
Git has a rename command git mv, but that is just for convenience. The effect is indistinguishable from removing the file and adding another with different name and the same content.
But there is an important difference: git mv will stage the rename, but crucially it keeps any edits in the renamed file unstaged. This is exactly what I want since it allows me to commit the rename and the edits separately.
Example: Let’s create a fresh repository with a single file and then make some edits to the file:
mkdir testrepo
cd testrepo
git init
# Create a file
echo "Hello" > A.txt
# Commit the file
git add .
git commit -m "Create A"
# Make edits to the file
echo "World" > A.txt
If we now rename A.txt to B.txt and stage the changes, Git won’t track this as a rename:
# Variant 1 (bad):
# Rename A to B
mv A.txt B.txt
# Stage changes
git add .
git status
Changes to be committed:
deleted: A.txt
new file: B.txt
But, if we instead use git mv to rename the edited file, Git will stage the rename and keep the edits unstaged:
# Variant 2 (good):
# Use git mv for renaming
git mv A.txt B.txt
git status
Changes to be committed:
renamed: A.txt -> B.txt
Changes not staged for commit:
modified: B.txt
Now we can commit the rename, and then stage and commit the edits:
git commit -m "Rename A to B"
git add .
git commit -m "Edit B"
Great!
If you can’t use git mv
I’m often in situations where I can’t use git mv to rename a file because it’s important to perform the rename in some other tool. For example, I write my notes (tracked in Git) in Obsidian. Obsidian can automatically update links to a note when you rename it, but only if you do the renaming in Obsidian.
The workaround I came up with in this case:
- Rename the renamed file back to its original name. I do this in Terminal using the normal
mvcommand. - Redo the intended renaming, but this time with
git mv. This puts the repository into the desired state where I can commit the rename operation separately from edits to the file’s contents.
I wrote myself a shell script to perform these steps:
#!/bin/bash
# git-fix-rename
if [ "$#" -ne 2 ]; then
echo "Allow git to track a rename even when the renamed file has been edited."
echo "Usage: $0 <new_filename> <old_filename>"
exit 1
fi
old="$2"
new="$1"
# Situation: we renamed $old to $new. But Git can’t track the rename
# because we made changes to $new at the same time. `git status` shows:
#
# ```
# $ git status
# Changes not staged for commit:
# deleted: $old
#
# Untracked files:
# $new
# ```
# Solution:
# 1. Undo the rename temporarily:
if [ -e "$old" ]; then
echo "Error: Destination file '$old' already exists. Aborting."
exit 1
fi
mv "$new" "$old"
# 2. Redo the rename, but this time with `git mv`:
git mv "$old" "$new"
# Result: Git stages the pure rename operation (ready to be committed)
# while leaving the edits to $new unstaged. You can now commit the
# the rename and edit steps separately, allowing Git to track the rename.
#
# ```
# $ git status
# Changes to be committed:
# renamed: $old -> $new
#
# Changes not staged for commit:
# modified: $new
# ```
If you name the script e.g. git-fix-rename (no file extension) and make it executable, you can even call it like any built-in Git command:
# We have renamed A.txt to B.txt and made edits to B.txt.
# Now we want to record the rename in Git.
git fix-rename B.txt A.txt
So far, this has worked well for me. But beware: I wrote the script for myself and it doesn’t have robust edge case handling. It might mess up your uncommitted files/edits.