Git History Rewriting
Introduction
Git's history is normally considered immutable - once commits are made, they should remain unchanged. However, sometimes you need to modify your Git history to fix mistakes, improve commit organization, or clean up before sharing your code. This guide will walk you through common scenarios for rewriting Git history and how to do it safely.
Rewriting history that has already been pushed to a shared repository can cause problems for other contributors. Only rewrite history that exists solely in your local repository or coordinate carefully with your team.
Understanding Git History
Before we dive into rewriting techniques, let's understand what Git history actually is:
Each commit in Git contains:
- A snapshot of all tracked files
- Author and committer information
- A message describing the changes
- References to parent commit(s)
When you rewrite history, you're creating new commits with new SHA-1 hashes, effectively abandoning the old ones.
Common History Rewriting Commands
Amending the Last Commit
The simplest form of history rewriting is amending the most recent commit. This is useful for:
- Fixing a typo in a commit message
- Adding forgotten files to the last commit
- Changing author information
Example: Fix a commit message
# Make your change
git commit --amend -m "Fixed feature X correctly"
Input/Output:
# Before
$ git log --oneline -1
a1b2c3d Implement feature X with bug
# After amending
$ git log --oneline -1
e4f5g6h Fixed feature X correctly
Note that the commit hash changed (a1b2c3d
→ e4f5g6h
).
Example: Add forgotten files
# Stage the forgotten files
git add forgotten-file.js
# Amend the commit (keeps the same message)
git commit --amend --no-edit
Interactive Rebase
Interactive rebase is a powerful tool that allows you to modify multiple commits. You can:
- Reorder commits
- Edit commit messages
- Combine multiple commits (squash)
- Split commits
- Remove commits entirely
Basic syntax:
git rebase -i <commit>~
Where <commit>~
is the parent of the earliest commit you want to modify.
Example: Squashing multiple commits
Let's say we have the following history:
$ git log --oneline
a1b2c3d Fix typo in header
e4f5g6h Update CSS for header
i7j8k9l Add new header
m0n1o2p Initial commit
And we want to combine all the header-related commits into one:
git rebase -i m0n1o2p
This will open your text editor with:
pick i7j8k9l Add new header
pick e4f5g6h Update CSS for header
pick a1b2c3d Fix typo in header
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's message
# ...
Change it to:
pick i7j8k9l Add new header
squash e4f5g6h Update CSS for header
squash a1b2c3d Fix typo in header
Save and close. Git will prompt for a new combined commit message.
Final result:
$ git log --oneline
f9d8c7b Add new header with styling and fixes
m0n1o2p Initial commit
Changing Multiple Commit Messages
To change multiple commit messages, use interactive rebase with the reword
option:
git rebase -i HEAD~3 # To modify the last 3 commits
Then change pick
to reword
for any commits whose messages you want to edit:
reword a1b2c3d Commit message to change
pick e4f5g6h Leave this commit as is
reword i7j8k9l Another commit message to change
Git will open your editor for each commit marked with reword
.
Removing Sensitive Data
If you accidentally committed sensitive data (passwords, API keys, large files, etc.), you can use filter-branch
or the BFG Repo Cleaner:
Using filter-branch to remove a file from all commits:
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch PATH-TO-FILE" \
--prune-empty --tag-name-filter cat -- --all
filter-branch
is powerful but complex. For removing sensitive data, consider using the BFG Repo-Cleaner which is faster and simpler.
Reordering Commits
To change the order of commits, use interactive rebase and simply rearrange the lines:
git rebase -i HEAD~3
Original order:
pick a1b2c3d First commit
pick e4f5g6h Second commit
pick i7j8k9l Third commit
New order:
pick i7j8k9l Third commit
pick a1b2c3d First commit
pick e4f5g6h Second commit
Practical Examples
Example 1: Fixing a Feature Across Multiple Commits
Imagine you're developing a login feature across multiple commits, but discover a security issue that affects several of these commits.
$ git log --oneline
a1b2c3d Add password reset
e4f5g6h Add login form validation
i7j8k9l Add login form UI
m0n1o2p Initial project setup
To fix the security issue in each commit:
git rebase -i m0n1o2p
Change from:
pick i7j8k9l Add login form UI
pick e4f5g6h Add login form validation
pick a1b2c3d Add password reset
To:
edit i7j8k9l Add login form UI
edit e4f5g6h Add login form validation
edit a1b2c3d Add password reset
Git will stop at each commit, allowing you to make changes:
# Git stops at first commit
# Make your security fix
git add .
git commit --amend --no-edit
git rebase --continue
# Repeat for each commit
Example 2: Preparing a Clean Pull Request
You've made several commits with exploratory work, debugging code, and fixed issues. Before creating a pull request, you want to organize these into logical, clean commits:
Original history:
$ git log --oneline
a1b2c3d Remove debug logs
e4f5g6h Fix broken test
i7j8k9l Add debug logs
m0n1o2p Implement feature
r2s3t4u More debugging
v5w6x7y Initial test implementation
Using interactive rebase:
git rebase -i v5w6x7y~
Restructure into clean, logical commits:
pick v5w6x7y Initial test implementation
squash r2s3t4u More debugging
squash i7j8k9l Add debug logs
squash a1b2c3d Remove debug logs
pick m0n1o2p Implement feature
pick e4f5g6h Fix broken test
Final history:
$ git log --oneline
f9d8c7b Fix broken test
e6g5h4i Implement feature
k3l2m1n Initial test implementation
Safely Rewriting Public History
If you must rewrite history that others have already pulled, follow these steps to minimize disruption:
- Coordinate with your team and agree on a time for the rewrite
- Have everyone push their changes and stop working on the branch
- Perform the rewrite
- Force push with the
--force-with-lease
option:bashgit push --force-with-lease origin branch-name
- Have everyone run:
bash
git fetch
git reset --hard origin/branch-name
Avoid rewriting public history whenever possible. If you must do it, --force-with-lease
is safer than --force
as it prevents overwriting others' work.
Best Practices
- Never rewrite history on main/master branches of shared repositories
- Create a backup branch before rewriting:
bash
git branch backup-before-rewrite
- Use
--force-with-lease
instead of plain--force
when pushing rewritten history - Communicate with your team before rewriting shared history
- Keep rewrites small and focused on recent history when possible
Summary
Git history rewriting is a powerful set of techniques that allow you to maintain a clean, logical commit history. The main commands we've covered are:
git commit --amend
for fixing the most recent commitgit rebase -i
for modifying multiple commitsgit filter-branch
for advanced history rewriting
When used carefully and judiciously, these tools can help you maintain a cleaner, more useful Git history that better documents your project's evolution.
Additional Resources
- Git Documentation on Rewriting History
- Pro Git Book - Chapter 7.6
- BFG Repo-Cleaner for efficiently removing large files or sensitive data
Exercises
- Create a test repository and practice amending the last commit with different file changes
- Practice interactive rebase by squashing multiple commits into one
- Try reordering commits using interactive rebase
- Create a commit with a deliberately bad message, then reword it
- Create a branch and try these operations to get comfortable with how they work:
- Split one commit into multiple commits
- Remove a file from history using filter-branch
- Fix a bug that spans multiple commits
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)