Git has no rename

Monday, October 19, 2026 · 5 min read

gitversion control

Recently, I was cleaning up a Nest.js project and moved the Prisma files into their own folder. prisma.module.ts and prisma.service.ts were sitting directly in src/, so I moved both of them into src/prisma/. While moving them, I also renamed the classes from MetaPrismaService and MetaPrismaModule to PrismaService and PrismaModule, since the Meta prefix was no longer needed. Both files were moved together, in the same commit, into the same folder.

When I opened the diff, it looked like this:

+  src/prisma/prisma.module.ts      added⇄  src/prisma/prisma.service.ts     renamed±  src/app.module.ts                modified±  src/main.ts                      modified−  src/prisma.module.ts             deleted

Git showed the service as a rename, which is what I expected. But the module showed up twice: once as a brand new file and once as a deleted one, as if the old module was removed and an unrelated new one was created. I had moved both files in exactly the same way, so I wanted to understand why Git treated them differently.

It turns out that Git doesn’t store renames at all. A commit is just a snapshot of your files, a list of paths and the content at each path. When you move a file, the next snapshot simply has a different path than the previous one. There is nothing in the commit that says “this file was moved from here to there”. Even git mv doesn’t record anything special; it just removes the old path and adds the new one, which is the same thing you’d get by moving the file yourself.

So when Git shows you a rename, it is actually guessing. Every time you look at a diff, Git takes all the paths that disappeared and all the paths that appeared, and tries to pair them up. If a deleted file and an added file have exactly the same content, they are paired right away. For everything else, Git compares the content of each deleted file with each added file and calculates a similarity score from 0 to 100%. If the score is 50% or more, Git reports it as a rename. Anything below that stays a separate delete and add. The 50% is just the default, and you can change it with the -M option.

Git also shows this similarity score in the diff, so I checked what each file got. The service scored 72%, but the module only scored 32%, well below the threshold.

The reason was the class rename I did in the same commit. In the service, removing the Meta prefix changed a single line in a file with plenty of other code:

- export class MetaPrismaService extends PrismaClient …
+ export class PrismaService extends PrismaClient …

The module, on the other hand, is a tiny file, only nine lines. The same rename touched four of them:

- import { MetaPrismaService } from "./prisma.service";
+ import { PrismaService } from "./prisma.service";
 
  @Global()
  @Module({
-   providers: [MetaPrismaService],
-   exports: [MetaPrismaService],
+   providers: [PrismaService],
+   exports: [PrismaService],
  })
- export class MetaPrismaModule {}
+ export class PrismaModule {}

Similarity is measured against the size of the file. For a small file, changing a few lines changes most of its content, so Git no longer recognizes it as the same file. The exact same edit in a bigger file barely affects the score. I tried lowering the threshold with --find-renames one step at a time, and the module only showed up as a rename once I got down to 32%.

At first this felt like a cosmetic issue in the diff view, but the same rename detection is used in more important places. When you merge or rebase, Git uses it to figure out that a file was moved, so that changes made to the old path can be applied to the new one. If detection fails, you can end up with a conflict, or with someone’s changes left behind on a path that no longer exists. git log --follow and git blame also rely on it, so the history of the module now effectively starts at this commit. On large refactors, Git can even skip rename detection entirely if there are too many files to compare, which is controlled by diff.renameLimit.

If you want Git to keep track of moved files, move them in one commit and change their contents in the next one.

git mv src/prisma.module.ts src/prisma/prisma.module.ts
git mv src/prisma.service.ts src/prisma/prisma.service.ts
git commit -m "refactor: move prisma files into their own folder"
 
# then rename the classes
git commit -am "refactor: drop Meta prefix from prisma classes"

A pure move has 100% similarity, so Git always detects it as a rename, and the second commit is a small, easy-to-review diff on files that are already in their new place. If you are looking at a commit where this already happened, you can lower the threshold for that one command, like git diff -M30% or git log -M30%, and use git log --follow -- path/to/file to follow a file’s history across renames.

This was a good reminder that Git tracks content, not intentions. A rename is something Git figures out after the fact, not something it remembers. Once I understood that, the diff that confused me at first made complete sense.