Skip to content

Deleting and Moving Files

Deleting or renaming a tracked file changes the Working Tree first. A normal commit still records the Index, so the repository does not record that change until the Index represents it. The same three-area model used for edits and new files therefore applies to deletions and moves.

Suppose old.php exists in the latest commit and the Index:

HEAD Index Working Tree
old.php old.php old.php

Deleting it in VS Code or with a filesystem command changes only the Working Tree initially:

Terminal window
rm old.php
HEAD Index Working Tree
old.php old.php missing

Short status shows the deletion in the second column:

D old.php

The leading space means the Index has no staged change. D in the second column means the Working Tree is missing the file represented in the Index. A normal git commit does not record this deletion yet because it reads the Index rather than copying the current directory directly.

Stage the specific path after deleting it manually:

Terminal window
git add old.php

Despite its name, git add does not mean only “add a new file.” It updates the selected Index path to represent the current Working Tree state. When the path is absent, that update stages its deletion.

For tracked changes across a suitable scope, this form can also stage modifications and deletions:

Terminal window
git add -u

The staged state becomes:

HEAD Index Working Tree
old.php staged as deleted missing

and short status becomes:

D old.php

D in the first column means the deletion is selected for the next commit. The file still exists in older commits; the new snapshot records that the path is absent.

Git can combine the filesystem deletion and Index update:

Terminal window
git rm old.php

This command:

  1. removes old.php from the Working Tree;
  2. stages removal of its Index entry.

It normally produces the same final state as:

Terminal window
rm old.php
git add old.php

Inspect the result before committing:

Terminal window
git status --short
git diff --staged

For a tracked directory, recursive removal requires an explicit recursive form:

Terminal window
git rm -r obsolete/

Review the target and staged diff carefully before removing many paths.

Sometimes the repository should stop tracking a file while the application still needs a local copy:

Terminal window
git rm --cached debug.log

Compare the two forms:

Command Index Working Tree
git rm debug.log removal staged local file removed
git rm --cached debug.log removal staged local file preserved

The --cached form targets the Index boundary. Add a suitable ignore rule if the preserved local path should not return as untracked or be added again. Neither form removes versions stored in older commits.

Git can reject git rm when removing the path would discard content that is not safely represented in both HEAD and the Index. Treat that refusal as a safety signal, not an obstacle to bypass immediately.

Inspect first:

Terminal window
git status
git diff -- old.php
git diff --staged -- old.php

Decide whether the local work should be committed, copied elsewhere, or intentionally discarded. Do not add -f merely to silence the refusal; a forced removal can destroy uncommitted content.

This command moves a file in the Working Tree and stages the corresponding Index change:

Terminal window
git mv old.php UserService.php

A typical short status is:

R old.php -> UserService.php

git mv is convenient, but it is not required for Git to recognize a rename. The following workflow can reach the same staged result:

Terminal window
mv old.php UserService.php
git add -A

Before staging a manual move, status commonly exposes the raw Working Tree facts:

D old.php
?? UserService.php

After staging, Git compares the removed and added content and may present the pair as a rename.

A commit normally does not store an operation saying “rename this path.” It stores a new snapshot: the old path is absent, and the new path exists. Diff and history commands compare snapshots and infer a rename when deleted and added content is sufficiently similar.

Inspect the inference with:

Terminal window
git diff --staged --summary

Example:

rename old.php => UserService.php (100%)

100% means the compared file content is identical. If the file is heavily rewritten while being moved, Git may display a deletion and an addition instead. The committed snapshot is still correct; only the comparison’s presentation differs.

This also explains why different diff options or similarity thresholds can present the same pair of snapshots differently. Rename detection is analysis, not permanent rename metadata inside the commit.

Suppose a Laravel service is moved and renamed:

app/Services/Payment.php
→ app/Billing/PaymentService.php

A focused sequence is:

Terminal window
git mv app/Services/Payment.php app/Billing/PaymentService.php
git status --short
git diff --staged --summary
git diff --staged

Update namespaces, imports, service-container bindings, and tests as separate content edits, then stage and review them. Rename detection does not prove the application still works; run the relevant test suite after the refactor.

  • Assuming a filesystem deletion is automatically included in a normal commit.
  • Reading D file and D file as the same state.
  • Thinking git add cannot stage a deletion.
  • Forgetting that git rm deletes the Working Tree file too.
  • Using git rm when git rm --cached is required to preserve a local file.
  • Forcing removal without inspecting uncommitted work.
  • Believing git mv writes permanent rename metadata into the commit.
  • Treating a detected rename as proof that application references were updated.
filesystem delete → Working Tree changes first
git add <deleted-path> → stage the deletion
git rm <path> → delete locally and stage removal
git rm --cached <path> → stage removal; preserve local file
git mv <old> <new> → move locally and stage the result
manual move + git add -A → can produce the same staged snapshot
rename display → inferred by comparing similar content
Why does a plain commit not record a filesystem-only deletion?

A normal commit records the Index. Until the deleted path is staged, its Index entry still represents the old file.

When should git rm –cached be used?

When the next snapshot should stop tracking a path but the Working Tree copy must remain on the machine.

Does a commit contain a permanent rename operation?

Normally no. It records the new snapshot, and Git later infers renames by comparing removed and added content.