Skip to main content

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.

caution

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

bash
# 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 (a1b2c3de4f5g6h).

Example: Add forgotten files

bash
# 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:

bash
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:

bash
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:

bash
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:

bash
git filter-branch --force --index-filter \
"git rm --cached --ignore-unmatch PATH-TO-FILE" \
--prune-empty --tag-name-filter cat -- --all
warning

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:

bash
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:

bash
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:

bash
# 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:

bash
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:

  1. Coordinate with your team and agree on a time for the rewrite
  2. Have everyone push their changes and stop working on the branch
  3. Perform the rewrite
  4. Force push with the --force-with-lease option:
    bash
    git push --force-with-lease origin branch-name
  5. Have everyone run:
    bash
    git fetch
    git reset --hard origin/branch-name
caution

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

  1. Never rewrite history on main/master branches of shared repositories
  2. Create a backup branch before rewriting:
    bash
    git branch backup-before-rewrite
  3. Use --force-with-lease instead of plain --force when pushing rewritten history
  4. Communicate with your team before rewriting shared history
  5. 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 commit
  • git rebase -i for modifying multiple commits
  • git 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

Exercises

  1. Create a test repository and practice amending the last commit with different file changes
  2. Practice interactive rebase by squashing multiple commits into one
  3. Try reordering commits using interactive rebase
  4. Create a commit with a deliberately bad message, then reword it
  5. 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! :)