Deepen PracticeOrdered learning track

Rename Detection, Copy Detection, and Review Risk

Learn Git In Action - Part 084

Rename detection, copy detection, break-rewrite behavior, directory rename detection, and how large refactors hide semantic review risk.

10 min read1926 words
PrevNext
Lesson 84126 lesson track69–103 Deepen Practice
#git#diff#rename-detection#code-review+2 more

Part 084 — Rename Detection, Copy Detection, and Review Risk

Git does not store “rename events” as first-class history objects.

Git stores snapshots.

A commit contains a tree. A tree maps path names to object IDs and modes. If a file path changes, the new tree simply has a different path entry. Rename detection is inferred later by comparing deleted files and added files.

This is the first invariant:

Rename is not stored. Rename is detected.

That one sentence explains many advanced Git surprises:

  • git mv does not create a special rename object.
  • git diff may show delete/add or rename depending on similarity detection.
  • Review UIs may disagree with CLI output.
  • A file can be “renamed” and heavily modified, but Git may not detect it as rename.
  • Copy detection is even more expensive and less commonly enabled by default.
  • Large mechanical refactors can hide behavior changes.

The official gitdiffcore documentation describes diffcore-rename as the transformation used to detect renames and copies. It is controlled by -M for renames and -C for copies. The git diff documentation exposes related options like --find-renames, --find-copies, --break-rewrites, and similarity thresholds.

References:


1. Git Stores Snapshots, Not File Histories

A simplified Git commit:

commit
  parent: <parent-commit>
  tree:   <root-tree>

A simplified tree:

100644 blob a1b2... src/case/CaseService.java
100644 blob c3d4... src/case/CaseRepository.java

After a rename:

100644 blob a1b2... src/enforcement/CaseService.java
100644 blob c3d4... src/case/CaseRepository.java

The blob object may be identical. The path changed.

Git can infer:

src/case/CaseService.java -> src/enforcement/CaseService.java

But the commit did not store a rename operation.

This is why rename detection is configurable at diff time.


2. git mv Is Convenience, Not History Semantics

Command:

git mv old/path/File.java new/path/File.java

This is roughly equivalent to:

mv old/path/File.java new/path/File.java
git add new/path/File.java
git rm old/path/File.java

After commit, Git stores the resulting tree.

There is no durable “rename metadata” object created by git mv.

This matters for review:

# Depending on similarity detection, both may show rename.
git diff --find-renames HEAD~1 HEAD

And for history tracing:

git log --follow -- path/to/file

--follow asks Git to continue history across renames for a single path. It is useful, but it is still based on detection and has limitations.


3. Rename Detection Basics

Use:

git diff -M A B

or:

git diff --find-renames A B

With a threshold:

git diff -M90% A B

Meaning:

Only classify as rename if similarity is high enough.

Common examples:

# Default rename detection threshold.
git diff -M main...HEAD

# More strict.
git diff -M90% main...HEAD

# More permissive.
git diff -M30% main...HEAD

Review interpretation:

OutputMeaning
R100rename with identical content
R095rename with 95% similarity
D + Adelete/add; Git did not classify as rename under current options

Inspect:

git diff --name-status -M main...HEAD

Example output:

R100    src/case/CaseService.java    src/enforcement/CaseService.java
R087    src/case/CasePolicy.java     src/enforcement/CasePolicy.java
M       src/enforcement/CasePolicy.java

Note that actual output formatting can vary by command/options.


4. Copy Detection Basics

Use:

git diff -C A B

or:

git diff --find-copies A B

Copy detection asks:

Did an added file appear to come from an existing file, while the original still exists?

Example:

src/case/CasePolicy.java
src/escalation/EscalationPolicy.java

If similar enough, Git may infer a copy.

Use:

git diff --name-status -C main...HEAD

Possible output:

C082    src/case/CasePolicy.java    src/escalation/EscalationPolicy.java

Copy detection is generally more expensive than rename detection because the source candidate set can be larger.

Operational rule:

Enable copy detection when copy provenance matters. Do not assume it is always enabled in every UI/tool.

5. Break-Rewrite Detection

Some changes are technically modifications but semantically full rewrites.

Example:

git diff -B A B

or:

git diff --break-rewrites A B

This can split a total rewrite into delete/create style output, making review clearer.

Use when:

  • a file was heavily rewritten,
  • the normal diff is unreadable,
  • you want to separate “old implementation removed” from “new implementation added”,
  • rename detection is confused by large modifications.

Combined with rename detection:

git diff -B -M main...HEAD

This is useful when a file was both rewritten and moved.

Review warning:

A rewrite is high risk even when the diff is small or elegantly presented.

6. The Review Problem: Mechanical Move + Semantic Change

Dangerous PR shape:

Move package from src/case to src/enforcement
Change authorization rule during the move
Update imports everywhere
Reformat moved files

Diff output may say:

R065 src/case/CasePolicy.java -> src/enforcement/CasePolicy.java

A reviewer may think:

Mostly rename. Low risk.

But inside the 35% difference may be the real production change:

- return user.hasRole("CASE_MANAGER");
+ return user.hasPermission("ENFORCEMENT_ESCALATE");

This is the main rule of this part:

Rename detection reduces visual noise, but it can also reduce reviewer suspicion.

7. Safe Review Strategy for Rename/Move PRs

7.1 First classify the shape

git diff --name-status -M -C main...HEAD

Look for:

  • many R100 entries,
  • many R<low> entries,
  • delete/add pairs,
  • high file count,
  • sensitive path movement.

7.2 Inspect pure move vs modified move

# Show summary of renames.
git diff --summary -M main...HEAD

# Show full patch with rename detection.
git diff --histogram -M main...HEAD

7.3 Compare ignoring whitespace

git diff -w --histogram -M main...HEAD

If exact diff is huge but whitespace-ignore diff is small, the move may include formatting noise.

7.4 Inspect behavior-sensitive paths separately

git diff --histogram -M main...HEAD -- 'src/**/auth*' 'src/**/policy*' 'src/**/permission*'

7.5 Require split commits when needed

Best structure:

commit 1: move files only
commit 2: update package/import paths only
commit 3: apply behavior change
commit 4: update tests

If the move and behavior are mixed, ask for split unless urgent production constraints justify accepting higher risk.


8. Mechanical Refactor Protocol

A safe large move/refactor protocol:

Step 1 — Snapshot current behavior

git switch -c refactor/move-case-module

Run tests before changes:

./gradlew test
# or npm test / mvn test / go test ./...

Step 2 — Mechanical move only

git mv src/case src/enforcement

Update only imports/package declarations if required.

Commit:

git add -A
git commit -m 'Move case module under enforcement package'

Step 3 — Verify move-only diff

git show --name-status -M --stat HEAD
git show -w --histogram -M HEAD

The whitespace-ignore diff should be boring.

Step 4 — Behavior change separately

# edit behavior
git add -p
git commit -m 'Require enforcement escalation permission for case escalation'

Step 5 — Review each commit independently

git log --oneline origin/main..HEAD
git show --histogram -M <move-commit>
git show --histogram <behavior-commit>

Reviewer can now answer:

Was the move mechanical?
Was the behavior change correct?

Those are different questions and should not be blended.


9. Directory Rename Detection

Git can reason about directory renames in merge machinery by aggregating individual file rename detection in some scenarios.

Example:

Branch A:
  src/case/* -> src/enforcement/*

Branch B:
  modifies src/case/DeadlinePolicy.java

During merge, Git may be able to place Branch B's modification under the new directory:

src/enforcement/DeadlinePolicy.java

But directory rename detection is not magic.

Failure modes:

  • partial directory moves,
  • files split across multiple destinations,
  • new files added to old directory after the move,
  • heavy modifications that reduce similarity,
  • tooling/UI views that only show file-level diff.

Operational rule:

After a directory move, check active branches that touched the old path.

Commands:

# Find branches with changes under old path.
git branch --all --contains origin/main

git log --oneline --all -- src/case/

# Compare old and new path activity.
git log --oneline --all -- src/case/ src/enforcement/

For teams, announce directory moves before landing large refactors.


10. Rename Detection and Merge/Rebase

Rename detection affects merge/rebase outcomes because Git must decide which paths correspond across versions.

Potential cases:

CaseRisk
One side renames, other side editsGit may apply edit to renamed file.
Both sides rename differentlyConflict or surprising path choice.
One side deletes, other side editsModify/delete conflict.
One side copies, other side edits originalCopy detection may not protect semantics.
Directory moved, other branch adds file in old dirDirectory rename handling may be ambiguous.

During conflict:

git status
git diff --name-status --diff-filter=U
git ls-files -u

Read conflict as topology + path mapping problem, not only line conflict.


11. --follow and File History Across Renames

Use:

git log --follow -- path/to/file

This asks Git to continue history across renames for a single path.

Good for:

  • tracing a file that moved,
  • investigating ownership over time,
  • finding original introduction of a function in a moved file.

Limitations:

  • works on one path at a time,
  • detection-based,
  • can be confused by heavy rewrites,
  • does not mean Git stores durable rename history,
  • may miss complex copy/split/merge file evolution.

For archaeology, combine:

git log --follow --stat -- path/to/file
git log -M -C --find-copies-harder -- path/to/file
git log -S 'permissionName' --all -- path/to/file

12. Rename Detection in Release Audit

Release audit question:

Did a sensitive file change, or was it merely moved?

Bad audit:

git diff --name-only v1.2.0..v1.3.0

This may show old deletion/new addition without context.

Better:

git diff --name-status -M -C v1.2.0..v1.3.0

Then inspect sensitive moves:

git diff --histogram -M v1.2.0..v1.3.0 -- 'src/**/policy/**' 'src/**/auth/**'

Audit statement should distinguish:

File moved from A to B with no content change.

from:

File moved from A to B and authorization condition changed.

Those are not equivalent.


13. Rename Detection in Monorepos

Monorepos make rename detection harder because:

  • many files are similar,
  • package moves are broad,
  • generated files are common,
  • refactors touch thousands of paths,
  • copy/paste patterns produce false copy candidates,
  • CI may use path-based affected-project detection.

Guidelines:

Keep mechanical moves isolated

Move-only PRs should not change behavior.

Use ownership-aware review

If files move between ownership boundaries, review both old and new owners.

Preserve import-only commits

Example:

commit 1: move module directory
commit 2: update import/package declarations
commit 3: adjust build configuration
commit 4: behavior change

Recompute affected projects carefully

A path move may require testing:

  • old owner area,
  • new owner area,
  • downstream dependents,
  • build graph roots,
  • release packaging.

Do not rely only on changed final paths.


14. Rename Detection and Path-Based CI

Path-based CI usually asks:

Which files changed?

But after rename:

old/path/File.java -> new/path/File.java

Which owner should run tests?

Safe answer:

Both old and new ownership surfaces may be affected.

Example script:

git diff --name-status -M origin/main...HEAD > /tmp/changed.txt

awk '$1 ~ /^R/ { print $2; print $3; next } { print $2 }' /tmp/changed.txt | sort -u

This emits both old and new paths for rename entries.

For ownership routing, use both.

For release audit, use both.

For migration planning, use both.


15. Copy Detection and Security Risk

Copying security-sensitive code can be risky.

Example:

Copy CasePermissionEvaluator into LegacyCasePermissionEvaluator
Modify small condition

Risks:

  • old bug copied into new surface,
  • old exception copied without context,
  • future fix only applied to original,
  • divergence begins silently,
  • duplicated policy logic becomes inconsistent.

Use:

git diff -C --find-copies main...HEAD -- 'src/**/permission*' 'src/**/policy*'

For stronger search:

git log -S 'hasPermission' --all -- 'src/**'

Review question:

Should this be copied, extracted, or intentionally forked?

Copy detection is not just convenience. It reveals architectural debt.


16. Similarity Threshold Is a Review Lever

A rename detected at 95% similarity is different from one detected at 35%.

Commands:

git diff --name-status -M90% main...HEAD
git diff --name-status -M50% main...HEAD
git diff --name-status -M20% main...HEAD

Interpretation:

If rename appears only at low thresholdMeaning
File moved and heavily changedReview as rewrite plus move.
Similar boilerplate confused detectionInspect manually.
Generated files dominate similarityDo not infer semantic continuity.
File was split/mergedReview lineage as architectural change.

For high-risk files, low-similarity rename should trigger more review, not less.


17. Large Refactor Review Playbook

Use this when a PR has many moved/renamed files.

Step 1 — Shape

git diff --name-status -M -C origin/main...HEAD

Step 2 — Count status categories

git diff --name-status -M -C origin/main...HEAD \
  | awk '{print $1}' \
  | sed 's/[0-9].*$//' \
  | sort \
  | uniq -c

Step 3 — Inspect move-only equivalence

git diff -w --stat -M origin/main...HEAD

Step 4 — Identify non-mechanical hunks

git diff -w --histogram -M origin/main...HEAD

Step 5 — Audit sensitive files

git diff --name-status -M -C origin/main...HEAD \
  | grep -Ei 'auth|permission|policy|migration|crypto|secret|deploy|workflow'

Step 6 — Ask for split if behavior is buried

Policy:

A large move/refactor PR must prove which commits are mechanical and which commits are semantic.

18. Case Study: Moving an Enforcement Workflow Module

Initial structure:

src/case/workflow/CaseStateMachine.java
src/case/workflow/EscalationPolicy.java
src/case/workflow/DeadlineCalculator.java

Target structure:

src/enforcement/workflow/CaseStateMachine.java
src/enforcement/workflow/EscalationPolicy.java
src/enforcement/workflow/DeadlineCalculator.java

Bad PR:

Move workflow package and improve escalation behavior

Problems:

  • reviewers see many renamed files,
  • import changes create noise,
  • escalation behavior change is buried,
  • tests might pass but audit trail is weak,
  • release note cannot isolate behavior change.

Good commit series:

1. Move case workflow package under enforcement
2. Update imports for enforcement workflow package
3. Rename CaseEscalationPolicy to EnforcementEscalationPolicy
4. Require supervisor approval for severe enforcement escalation
5. Add regression tests for severe escalation approval

Review commands:

# Mechanical move commit
git show --name-status -M <commit-1>
git show -w --histogram -M <commit-1>

# Behavior commit
git show --histogram <commit-4>

# Test commit
git show --histogram <commit-5>

Now audit evidence is clear:

The behavior change is commit 4, not hidden inside the package move.

19. False Sense of Safety: R100

R100 means identical file content under rename detection.

It does not mean:

  • build behavior is identical,
  • package/class identity is identical,
  • runtime resource lookup is identical,
  • ownership is identical,
  • deployment packaging is identical,
  • import paths are correct,
  • reflection/classpath behavior is unchanged,
  • configuration references were updated.

Example:

R100 config/dev/rules.yaml -> config/prod/rules.yaml

Content identical, but risk is huge because environment boundary changed.

Review rule:

Rename similarity is content similarity, not system equivalence.

20. False Sense of Danger: Delete/Add

Delete/add does not always mean unrelated files.

A file can be renamed and heavily edited enough that Git shows:

D old/path/RuleEngine.java
A new/path/RuleEngine.java

Investigate manually:

git diff -M20% --name-status main...HEAD

or compare files directly:

git diff --no-index old-copy.java new-copy.java

If source exists in history:

git show main:old/path/RuleEngine.java > /tmp/old.java
git show HEAD:new/path/RuleEngine.java > /tmp/new.java
git diff --no-index --histogram /tmp/old.java /tmp/new.java

This is useful when Git's automatic detection is insufficient for review.


21. Tooling Guardrail: Detect Dangerous Mixed Rename PRs

A simple script idea:

#!/usr/bin/env bash
set -euo pipefail

base=${1:-origin/main}

renames=$(git diff --name-status -M "$base"...HEAD | awk '$1 ~ /^R/ { print $0 }')

if [ -z "$renames" ]; then
  exit 0
fi

echo "Rename summary:"
echo "$renames"

echo

echo "Non-whitespace patch after rename detection:"
git diff -w --histogram -M "$base"...HEAD --stat

echo

echo "Sensitive renamed paths:"
echo "$renames" | grep -Ei 'auth|permission|policy|migration|workflow|deploy|secret|crypto' || true

This does not approve or reject. It surfaces risk.

Advanced version:

  • compare exact vs -w stats,
  • enforce max non-whitespace churn in move-only PR,
  • require owner review for old and new paths,
  • block behavior changes in refactor/move-* branches unless explicitly labeled.

22. Git Attributes and Rename Review

For generated/binary files, rename detection may be unhelpful.

.gitattributes can reduce bad signal:

*.png binary
*.pdf binary
generated/** -diff
*.lock diff=lockfile

For domain files, custom diff drivers can improve hunk context:

*.policy diff=policy
*.workflow diff=workflow

Config:

[diff "workflow"]
  xfuncname = "^(state|transition|guard|action)[[:space:]].*$"

This matters when files move across directories but contain domain-specific rule changes.


23. Operational Policy for Teams

A strong Git workflow defines rename/refactor rules.

Recommended policy:

1. Pure move/rename commits must not change behavior.
2. Formatting-only changes must not be mixed with behavior changes.
3. High-risk files moved across ownership boundaries require old and new owner review.
4. Large rename PRs must include a review note explaining mechanical vs semantic commits.
5. CI affected-path logic must consider both old and new paths for renames.
6. Release audit must classify moved sensitive files separately from modified sensitive files.
7. Low-similarity rename of sensitive file must be reviewed as rewrite.

This is not bureaucracy. It is signal preservation.


24. Exercises

Exercise 1 — Rename-only vs rename-plus-change

mkdir /tmp/git-rename-lab
cd /tmp/git-rename-lab
git init
mkdir -p src/case src/enforcement
cat > src/case/policy.txt <<'TXT'
rule: case_manager_can_escalate
condition: user.role == CASE_MANAGER
TXT
git add .
git commit -m 'Add case policy'

Rename only:

git mv src/case/policy.txt src/enforcement/policy.txt
git commit -am 'Move policy under enforcement'

git show --name-status -M HEAD
git show -w --histogram -M HEAD

Then modify:

sed -i.bak 's/user.role == CASE_MANAGER/user.permission == ENFORCEMENT_ESCALATE/' src/enforcement/policy.txt
rm src/enforcement/policy.txt.bak
git add .
git commit -m 'Require enforcement escalation permission'

git show --histogram HEAD

Compare with a single mixed commit by resetting and doing both together.

Questions:

  1. Which history is easier to review?
  2. Which history is easier to audit?
  3. Which history is safer to revert?

Exercise 2 — Threshold impact

git diff --name-status -M90% HEAD~2..HEAD
git diff --name-status -M50% HEAD~2..HEAD
git diff --name-status -M20% HEAD~2..HEAD

Question:

At which threshold does Git classify movement as rename?

Exercise 3 — Manual compare when detection fails

git show HEAD~2:src/case/policy.txt > /tmp/old-policy.txt
git show HEAD:src/enforcement/policy.txt > /tmp/new-policy.txt
git diff --no-index --histogram /tmp/old-policy.txt /tmp/new-policy.txt || true

25. Key Takeaways

Git stores snapshots. Rename and copy detection are inferred views.

git mv is useful, but it does not create permanent rename metadata.

-M detects renames. -C detects copies. -B can make rewrites easier to review.

Rename detection is helpful because it reduces noise.

Rename detection is dangerous because it can reduce suspicion.

A moved file with 70% similarity may contain the one 30% that matters.

Large refactors must preserve review signal by separating:

  • move,
  • formatting,
  • import/package adjustment,
  • behavior,
  • tests,
  • generated output.

For high-risk systems, the question is never merely:

Did Git detect a rename?

The real question is:

Can reviewers and auditors clearly see whether behavior changed?

References

Lesson Recap

You just completed lesson 84 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.