Rewriting History¶
Git's commit history is immutable in a cryptographic sense - every commit is identified by a hash of its contents, parents, and metadata. But Git gives you tools to create new commits that replace old ones, effectively rewriting the visible history. This is one of Git's most powerful capabilities and one of its most dangerous. Understanding when rewriting is safe, what tools are available, and how to recover from mistakes is essential.
The Golden Rule¶
Never rewrite commits that have been pushed to a shared branch.
When you rewrite history, you create new commit objects with new hashes. Anyone who based work on the old commits now has a diverged history. They'll get confusing merge conflicts when they try to pull, and the team will waste time untangling the mess.
Rewrite freely on:
- Local branches that only you work on
- Feature branches before they're merged
- Commits you haven't pushed yet
Leave shared history alone. Use git revert (which creates new commits) instead of rewriting when working on branches others depend on.
Amending the Last Commit¶
The simplest form of rewriting. git commit --amend replaces the most recent commit:
# Fix the commit message
git commit --amend -m "Correct message"
# Add a forgotten file (keep the same message)
git add forgotten-file.py
git commit --amend --no-edit
# Change both the content and message
git add extra-fix.py
git commit --amend -m "Updated message with extra fix"
Under the hood, --amend creates a brand-new commit with a new hash and points the branch at it. The old commit still exists in the object database (reachable via the reflog) until garbage collection removes it.
Interactive Rebase¶
git rebase -i (interactive rebase) is the Swiss army knife for history cleanup. It lets you reorder, squash, split, edit, and drop commits from a branch.
Starting an Interactive Rebase¶
# Rebase the last 4 commits
git rebase -i HEAD~4
# Rebase all commits since branching from main
git rebase -i main
Git opens your editor with a list of commits (oldest first):
pick a1b2c3d Add user model
pick b2c3d4e Add user controller
pick c3d4e5f Fix typo in user model
pick d4e5f6a Add user tests
Rebase Commands¶
Change the word pick to one of these to control what happens to each commit:
| Command | Short | Effect |
|---|---|---|
pick |
p |
Keep the commit as-is |
reword |
r |
Keep the commit, edit the message |
edit |
e |
Pause at this commit so you can amend it |
squash |
s |
Combine with the previous commit, edit the combined message |
fixup |
f |
Combine with the previous commit, discard this commit's message |
drop |
d |
Delete the commit entirely |
You can also reorder lines to reorder commits, or delete a line to drop a commit.
Example: Squashing Cleanup Commits¶
You made three commits but the middle one is just a typo fix. Squash it into the first:
pick a1b2c3d Add user model
fixup c3d4e5f Fix typo in user model
pick b2c3d4e Add user controller
pick d4e5f6a Add user tests
Notice the reordering: the typo fix is moved directly below the commit it fixes, then marked as fixup so its message is discarded. The result is three clean commits instead of four.
Example: Editing a Commit¶
Mark a commit as edit to pause the rebase at that point:
Git stops after replaying a1b2c3d. You can now:
# Make changes to files
git add .
git commit --amend # Modify the commit
# Continue the rebase
git rebase --continue
Autosquash¶
If you know while committing that a change should be squashed into an earlier commit, use --fixup:
# Create a fixup commit targeting a specific commit
git commit --fixup=a1b2c3d
# Later, rebase with autosquash to automatically reorder and squash
git rebase -i --autosquash main
Git creates a commit with a message like fixup! Add user model. When you run rebase -i --autosquash, it automatically reorders the fixup commit and marks it as fixup.
To always autosquash during interactive rebase:
Cherry-Pick¶
git cherry-pick copies a specific commit from one branch to another. It creates a new commit with the same changes but a different hash and parent:
# Apply a specific commit to the current branch
git cherry-pick a1b2c3d
# Cherry-pick without committing (stage the changes instead)
git cherry-pick --no-commit a1b2c3d
# Cherry-pick a range of commits
git cherry-pick a1b2c3d..d4e5f6a
Cherry-pick is useful when you need a specific fix from another branch but don't want to merge the entire branch. But use it sparingly - duplicated commits (same changes, different hashes) can cause confusion when the branches are eventually merged.
Revert vs Reset¶
Both undo changes, but in fundamentally different ways.
git revert - Safe Undo (Creates New Commits)¶
git revert creates a new commit that undoes the changes from a specified commit. The original commit remains in history:
# Revert a specific commit
git revert a1b2c3d
# Revert without auto-committing (stage the undo changes)
git revert --no-commit a1b2c3d
# Revert a merge commit (must specify which parent to keep)
git revert -m 1 a1b2c3d
Use revert on shared branches. It's safe because it adds to history rather than rewriting it. Everyone can pull the revert without conflicts.
git reset - Rewrite History (Moves HEAD)¶
git reset moves the branch pointer backward, effectively removing commits from the branch's visible history. The three modes control what happens to the changes from those commits:
| Mode | HEAD moves? | Index changes? | Working dir changes? | Effect |
|---|---|---|---|---|
--soft |
Yes | No | No | Commits removed, changes stay staged |
--mixed (default) |
Yes | Yes | No | Commits removed, changes unstaged but in working dir |
--hard |
Yes | Yes | Yes | Commits removed, changes discarded entirely |
# Soft reset: uncommit but keep changes staged
git reset --soft HEAD~1
# Mixed reset: uncommit and unstage, keep changes in working dir
git reset HEAD~1
# Hard reset: uncommit and discard all changes
git reset --hard HEAD~1
# Reset to a specific commit
git reset --hard a1b2c3d
git reset --hard deletes uncommitted work
--hard discards changes from your working directory. If those changes were never committed or stashed, they're gone permanently. Always check git status and git stash before using --hard.
flowchart TD
A["Want to undo commits?"] --> B{"Are they on a shared branch?"}
B -->|Yes| C["git revert<br/>(safe, creates undo commit)"]
B -->|No| D{"What should happen to the changes?"}
D -->|Keep staged| E["git reset --soft"]
D -->|Keep unstaged| F["git reset --mixed"]
D -->|Discard completely| G["git reset --hard"]
The Reflog: Your Safety Net¶
The reflog (reference log) records every time a branch tip or HEAD changes. Every commit, reset, rebase, checkout, merge, and amend is recorded. Even after you rewrite history, the old commits are findable through the reflog.
# View the reflog for HEAD
git reflog
# View reflog for a specific branch
git reflog show main
# View with dates
git reflog --date=iso
Sample output:
e5f6a7b (HEAD -> main) HEAD@{0}: rebase (finish): returning to refs/heads/main
d4e5f6a HEAD@{1}: rebase (start): checkout HEAD~3
d4e5f6a HEAD@{2}: commit: Add tests
c3d4e5f HEAD@{3}: commit: Fix typo in application
b2c3d4e HEAD@{4}: commit: Update application logic
a1b2c3d HEAD@{5}: commit (initial): Add application
Each entry has a reference like HEAD@{3} that you can use in any Git command:
# See what HEAD pointed to 3 moves ago
git show HEAD@{3}
# Reset to where HEAD was before a bad rebase
git reset --hard HEAD@{1}
# Create a branch at an old position
git branch recovery HEAD@{5}
Reflog entries expire
By default, reflog entries for reachable commits expire after 90 days and unreachable commits after 30 days. If you need to recover something, don't wait months. You can adjust the expiration: git config gc.reflogExpire 180.days.
Rebasing onto Another Branch¶
Beyond interactive rebase for cleanup, git rebase is used to move a branch to start from a different base:
Before rebase:
After git switch feature && git rebase main:
D' and E' are new commits (new hashes) with the same changes as D and E, but they now build on C instead of B. The original D and E become unreachable.
Rebase vs Merge¶
| Merge | Rebase | |
|---|---|---|
| History | Preserves branch topology | Creates linear history |
| Commits | Adds a merge commit | Rewrites existing commits |
| Safety | Non-destructive | Rewrites history (dangerous for shared branches) |
| Conflicts | Resolve once | May resolve per-commit |
| Use when | Integrating shared branches | Cleaning up local/feature branches before merge |
Exercises¶
Further Reading¶
- Pro Git - Chapter 7.6: Rewriting History - interactive rebase, amending, filter-branch
- Pro Git - Chapter 3.6: Rebasing - rebase fundamentals and the golden rule
- Official git-rebase documentation - complete reference for rebase modes and options
- Official git-reset documentation - the three reset modes explained
- Official git-reflog documentation - reflog usage and expiration
Previous: Remote Repositories | Next: Stashing and the Worktree | Back to Index