Stackvora.tech

Interactive Rebase to Clean History

Git's interactive rebase is a powerful yet often misunderstood tool for rewriting commit history. It allows developers to modify a series of commits,…

Git's interactive rebase is a powerful yet often misunderstood tool for rewriting commit history. It allows developers to modify a series of commits, providing fine-grained control over the project's lineage before integration into a shared branch. This deep dive covers its primary use cases, operational mechanics, and critical considerations for maintaining a clean, coherent, and useful commit history.

The core concept behind interactive rebase (git rebase -i) is to present a list of commits in a text editor, each prefixed with a command. By altering these commands or reordering the commits, you instruct Git on how to rewrite that segment of history. This is invaluable for streamlining local work, preparing feature branches for merge, or fixing mistakes made during development.

Understanding Interactive Rebase Operations

When you initiate an interactive rebase, Git performs several key steps:

  1. It identifies the range of commits to be rebased.
  2. It then rewinds the branch to the base commit (the one just before the range).
  3. It presents a temporary file in your configured text editor (e.g., Vim, Nano, VS Code) containing a list of commits and available commands.
  4. You modify this file to specify how each commit should be handled.
  5. Upon saving and closing the editor, Git attempts to reapply the commits sequentially, performing the specified operations.

The standard command to start an interactive rebase is git rebase -i [base_commit], where [base_commit] is the commit *before* the series you want to modify. Often, this is a branch head like origin/main or HEAD~N for the last N commits.

# Scenario 1: Rebase commits on top of 'origin/main'
# First, ensure your local 'main' is up-to-date or you have the remote's latest state.
git fetch origin
git rebase -i origin/main

# Scenario 2: Rebase the last 3 commits on your current branch
git rebase -i HEAD~3

The editor will display something similar to this, listing commits in chronological order (oldest first):

pick abc1234 Add initial project structure
pick def5678 Implement user authentication
pick 012abcd Fix typo in auth message
pick 345efgh Add basic user profile page

# Rebase 1f4a9b5..345efgh onto 1f4a9b5 (4 commands)
#
# 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 log message
# x, exec <command> = run command (the rest of the script continues afterwards)
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label the current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's message (or the one provided)
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here, that commit will be removed from the rebase.
#
# However, if you remove all commits, the rebase will be aborted.
#

Key Interactive Rebase Commands

  • pick (p): The default action. Keeps the commit as is.
  • reword (r): Keeps the commit's changes but stops to allow you to edit its commit message.
  • edit (e): Stops the rebase process after applying the commit. This allows you to make further changes to the commit (e.g., adding/removing files, splitting it into multiple commits) using git commit --amend. Once done, use git rebase --continue.
  • squash (s): Combines the commit with the previous one. Git will then open an editor to allow you to combine their commit messages.
  • fixup (f): Similar to squash, but it discards the commit's message, keeping only the previous commit's message. Useful for "oops" commits or minor fixes.
  • drop (d): Removes the commit entirely from the history.
  • exec (x): Runs an arbitrary shell command. For example, to run tests after a specific commit: x npm test.

Common Use Cases for Interactive Rebase

1. Combining Small, Related Commits (Squashing/Fixup)

Often, a feature development involves several small commits like "WIP", "fix typo", "add forgotten file". Before merging, these can be squashed into a single, meaningful commit.

# Original history:
pick abc1234 Initial feature scaffold
pick def5678 Add basic functionality
pick 012abcd Fix small bug in functionality
pick 345efgh Refactor some code

# To squash 012abcd and 345efgh into def5678:
pick abc1234 Initial feature scaffold
pick def5678 Add basic functionality
squash 012abcd Fix small bug in functionality
fixup 345efgh Refactor some code

After saving, Git will present a new editor for the combined commit message of def5678 and 012abcd. The 345efgh commit's message will be discarded.

2. Rewording Commit Messages

If a commit message is unclear, grammatically incorrect, or simply needs updating to reflect a better understanding of the change, reword is ideal.

# Original history:
pick abc1234 Add login

# To reword the message for abc1234:
reword abc1234 Add login

Git will open your editor with the current message, allowing you to modify it.

3. Deleting Unwanted Commits (Dropping)

Sometimes a commit introduces a feature that's later abandoned, or it's simply a mistake that shouldn't be part of the final history.

# Original history:
pick abc1234 Implement Feature A
pick def5678 Experiment with Feature B (discard)
pick 012abcd Implement Feature C

# To drop def5678:
pick abc1234 Implement Feature A
pick 012abcd Implement Feature C

Simply delete the line for the unwanted commit. Git will then apply the remaining commits.

4. Splitting a Commit (Editing)

If a single commit contains unrelated changes or is too large, it can be split into multiple, smaller, more focused commits. This requires the edit command.

# Original history:
pick abc1234 Add user management and admin dashboard

# To split abc1234:
edit abc1234 Add user management and admin dashboard

After Git stops at abc1234, you'll be in a detached HEAD state. You can then:

  1. git reset HEAD~1 (or git reset HEAD^) to unstage the changes but keep them in your working directory.
  2. Stage and commit the first logical change: git add <files for user management> && git commit -m "Add user management features"
  3. Stage and commit the second logical change: git add <files for admin dashboard> && git commit -m "Add admin dashboard"
  4. Continue the rebase: git rebase --continue

5. Reordering Commits

Sometimes the logical order of commits is different from their chronological creation order. Interactive rebase allows you to change this.

# Original history (chronological):
pick abc1234 Add UI for feature
pick def5678 Implement backend logic for feature (depends on UI)
pick 012abcd Add unit tests for backend logic

# To reorder for better logic (backend first):
pick def5678 Implement backend logic for feature
pick abc1234 Add UI for feature
pick 012abcd Add unit tests for backend logic

Simply cut and paste lines in the editor to change their order.

Advanced Techniques & Trade-offs

Conflict Resolution During Rebase

When Git reapplies commits, conflicts can arise, especially if commits are reordered or heavily modified. When a conflict occurs, Git will pause the rebase and notify you. You must then:

  1. Manually resolve the conflicts in the affected files (e.g., git status to see conflicting files, edit them, git add <resolved_file>).
  2. Stage the resolved files.
  3. Continue the rebase: git rebase --continue.

If you made a mistake or wish to abandon the rebase, use git rebase --abort.

--force-with-lease vs. --force

After an interactive rebase, your local branch history diverges from the remote. To update the remote, you'll need to force push. git push --force is dangerous as it overwrites the remote branch without checking. If someone else pushed to the branch in the interim, their work would be lost.

git push --force-with-lease is the safer alternative. It only force pushes if the remote branch is exactly as you last saw it. If someone else has pushed new commits, the push will fail, preventing accidental overwrites. This is the recommended practice for shared branches where you're rewriting history.

git push origin my-feature-branch --force-with-lease

Common Pitfalls and Best Practices

  • Never rebase commits that have already been shared: This is the golden rule. Rebasing creates new commit IDs. If others have based their work on the "old" commits, force pushing "new" commits will cause significant confusion and require them to rebase their work, potentially multiple times. Limit interactive rebase to local branches or feature branches that have not yet been integrated or widely shared.
  • Rebase frequently, but in small chunks: Don't let your feature branch diverge too much from main or develop. Rebase against the target branch frequently to avoid large, complex conflict resolution at the end.
  • Keep commit messages meaningful: Even when squashing, ensure the resulting commit message clearly describes the combined change.
  • Always git fetch before rebasing onto a remote branch: This ensures you're rebasing onto the absolute latest version of the remote branch, minimizing conflicts and keeping your history clean.
  • Back up your work (optional but recommended): Before a complex rebase, consider creating a temporary backup branch (e.g., git branch my-feature-backup) so you can easily revert if things go wrong.
  • Use git reflog: If you accidentally mess up your history with rebase, git reflog is your savior. It shows a record of where your HEAD has been, allowing you to return to previous states using git reset --hard <reflog_entry>.

Back to the knowledge base · Ask the AI assistant