Gitgit replace

git replace: Transparent Object Substitution

git replace makes Git use one object as a transparent stand-in for another. When Git reads an object that has a replacement registered, it silently reads the replacement instead. Commands like git log, git show, and git diff behave as if the original object has the replacement's content — without any rewriting of commit hashes in the repository. This is a powerful but obscure mechanism that should be used sparingly and deliberately.

How git replace Works Internally

Replacements are stored as refs under refs/replace/. Each ref is named after the SHA of the original object and points to the SHA of the replacement object. When Git looks up the original hash during any traversal, it first checks refs/replace/ and transparently substitutes the replacement if one is found.

Replace mechanism diagram

Text
Normal object lookup:
  request hash ABC123 → return object ABC123

With a replacement registered:
  request hash ABC123 → check refs/replace/ABC123 → found: DEF456 → return object DEF456

Storage in .git/:
  .git/refs/replace/
    └── ABC123abc123...  →  DEF456def456...
         (original)              (replacement)
Note
The replacement is transparent only locally unless you explicitly push refs/replace/* to a remote. Other users must have the same refs/replace/* refs in their repository to see the same substitution.
Basic Syntax

Core git replace commands

Bash
# Register a replacement: make Git use REPLACEMENT whenever ORIGINAL is requested
git replace <original-object> <replacement-object>

# List all active replacements
git replace -l
git replace --list

# Delete a specific replacement
git replace -d <original-object>
git replace --delete <original-object>

Output of git replace -l

Text
a3f1c2d83e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b → 9e2b0f1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a
Primary Use Case: Grafting Disconnected Histories

The most compelling reason to use git replace is to graft two disconnected repository histories together. This happens when a project migrated from one repository to another and you want the full chronological history to appear continuous — without the old repo's commits being present in the new repo's object store originally.

The problem: two disconnected histories

Text
Old repo (archived, read-only):
  A --- B --- C --- D   (last commit before migration)

New repo (active development):
  E --- F --- G --- H   (root commit = "initial commit", no parent)

Goal after grafting:
  A --- B --- C --- D --- E --- F --- G --- H

Step-by-step grafting with git replace

Bash
# 1. Add the old repo as a remote and fetch it
git remote add old-repo git@github.com:org/old-repo.git
git fetch old-repo

# 2. Find the last commit in the old history
OLD_LAST=$(git rev-parse old-repo/main)
echo "Last old commit: $OLD_LAST"

# 3. Find the root commit of the new repo (has no parents)
NEW_FIRST=$(git rev-list --max-parents=0 HEAD)
echo "New repo root commit: $NEW_FIRST"

# 4. Create a replacement for the new root commit that has the old tip as its parent
REPLACEMENT=$(git commit-tree   $(git rev-parse ${NEW_FIRST}^{tree})   -p $OLD_LAST   -m "$(git log -1 --format=%B $NEW_FIRST)")

echo "Replacement commit: $REPLACEMENT"

# 5. Register the replacement
git replace $NEW_FIRST $REPLACEMENT

# 6. Verify the graft worked — old history now visible
git log --oneline | tail -10

History now appears continuous

Text
...new commits...
e9f0a1b (replaced) Initial commit in new repo
d4e5f6a Last commit in old repo        ← old history visible here
c3d2e1f Previous old commit
b2c1d0e Earlier old commit
a1b2c3d Very first commit (2017)
Sharing Replacements with Your Team

Push replacements to remote

Bash
# Push all replacements
git push origin "refs/replace/*"

# Team members must fetch them explicitly
git fetch origin "refs/replace/*:refs/replace/*"

# Or configure automatic fetching in .git/config
git config --add remote.origin.fetch "+refs/replace/*:refs/replace/*"
Bypassing Replacements

When debugging or auditing, you may need to see the real, un-replaced history. Use the --no-replace-objects flag before any Git command to disable replacements for that invocation.

Run commands without applying replacements

Bash
# See the real unreplaced history
git --no-replace-objects log --oneline

# See the true content of a replaced object
git --no-replace-objects cat-file -p <original-hash>

# Show the real commit the branch points to
git --no-replace-objects show HEAD
Use Cases

Use Case

What You Do

Benefit

History grafting

Connect two repo histories via parent replacement

Seamless history without force-push rewriting

Fix a bad commit message

Create corrected commit, replace original

Message fixed without rebasing all downstream commits

Add missing author info

Replace commit with corrected metadata

Corrects identity without full history rewrite

Testing history tools

Mock specific historical commits

Test tooling against synthetic history scenarios

Swap a large binary

Replace tree with cleaned version

Appears removed without destroying others' clones

Practical Example: Fix a Bad Commit Message

Use git replace to fix a committed typo without rebasing

Bash
# Identify the commit with the bad message
git log --oneline | grep "typo"
# abc1234 fix: typo in messge

# Create a corrected replacement commit (same tree, same parent, new message)
REPLACEMENT=$(git commit-tree   abc1234^{tree}   -p abc1234^   -m "fix: typo in message")

# Register the replacement
git replace abc1234 $REPLACEMENT

# Verify the log now shows the corrected message
git log --oneline | head -5
# (shows corrected message for abc1234)
Common Errors
  • "error: replace ref already exists" — a replacement is already registered for that hash. Use git replace -d first.

  • Replacement not visible to teammates — they need to fetch refs/replace/* explicitly and configure the refspec.

  • git log shows different history than a teammate — one of you has replacements the other does not. Compare with git replace -l.

  • Replacement orphaned after rebase — rebase rewrites commit hashes; the original hash no longer exists and the replacement has no effect.

Warning
git replace is a powerful and obscure feature. Replacements shared via refs/replace/* cause everyone who fetches those refs to see a different history than the actual stored objects — without any obvious indication of this substitution. This can be deeply confusing during debugging. Always document why a replacement exists, where it is stored, and who needs it. Prefer transparent solutions like git filter-repo for permanent history changes.
Tip
Use git replace -l in your onboarding scripts to warn new developers if unexpected replacements are active: if git replace -l | grep -q '.'; then echo 'Warning: history replacements are active in this repository'; fi