Why This Matters
Git does not just record history -- it lets you reshape it. Messy commit messages, accidental commits, experiments that went nowhere: you can clean all of these up before sharing your work. Rebase replays your commits on top of a different base, creating a clean linear history. Cherry-pick lets you copy individual commits from one branch to another. Interactive rebase lets you reorder, squash, edit, or drop commits. And reset lets you move your branch pointer backward to undo commits.
These are power tools. They rewrite commit history, which means they change the commit hashes that other people might be relying on. The golden rule: never rewrite history that has already been pushed and shared with others. But for local work or your own feature branch before it is merged, these tools let you craft a clean, meaningful history that tells a coherent story.
Understanding history rewriting separates beginners from confident Git users. When you can rebase, squash, and cherry-pick with confidence, you stop fearing Git and start using it as a precise instrument for managing your codebase.
Define Terms
Visual Model
The full process at a glance. Click Start tour to walk through each step.
Merge preserves branch topology. Rebase creates a linear history by replaying commits on a new base.
Code Example
// Rewriting Git history
// 1. Rebase feature branch onto latest main
// $ git checkout feature-branch
// $ git rebase main
// This replays your feature commits on top of main
// 2. Interactive rebase: edit last 3 commits
// $ git rebase -i HEAD~3
// Opens editor with:
// pick abc1234 Add login form
// pick def5678 Fix typo in login
// pick ghi9012 Add login tests
//
// Change to squash commits:
// pick abc1234 Add login form
// squash def5678 Fix typo in login
// pick ghi9012 Add login tests
// 3. Cherry-pick a specific commit
// $ git checkout main
// $ git cherry-pick abc1234
// Copies that one commit onto main
// 4. Amend the last commit message
// $ git commit --amend -m "Better commit message"
// 5. Reset (undo commits)
// $ git reset --soft HEAD~1 # Undo commit, keep staged
// $ git reset --mixed HEAD~1 # Undo commit, unstage
// $ git reset --hard HEAD~1 # Undo commit, discard changes
// 6. Reflog: recover from mistakes
// $ git reflog
// $ git reset --hard abc1234 # Go back to a specific state
// Simulating commit history management in JavaScript
class CommitHistory {
constructor() {
this.commits = [];
}
addCommit(id, message) {
this.commits.push({ id, message });
}
log() {
return this.commits.map(c => c.id + ": " + c.message);
}
rebase(newBaseCommits) {
// Replay our commits on top of a new base
const result = [...newBaseCommits];
for (const commit of this.commits) {
// New hash simulated by adding r- prefix
result.push({
id: "r-" + commit.id,
message: commit.message
});
}
this.commits = result;
}
squash(startIdx, endIdx, newMessage) {
// Combine commits from startIdx to endIdx
const before = this.commits.slice(0, startIdx);
const squashed = this.commits.slice(startIdx, endIdx + 1);
const after = this.commits.slice(endIdx + 1);
const combined = {
id: squashed[0].id,
message: newMessage
};
this.commits = [...before, combined, ...after];
}
reset(count) {
// Remove last N commits
this.commits = this.commits.slice(0, -count);
}
cherryPick(commit) {
this.commits.push({
id: "cp-" + commit.id,
message: commit.message
});
}
}
const history = new CommitHistory();
history.addCommit("a1", "Add feature");
history.addCommit("b2", "Fix typo");
history.addCommit("c3", "Add tests");
history.squash(0, 1, "Add feature with fix");
console.log(history.log());Interactive Experiment
Try these exercises in a real Git repository (use a test repo -- not a project you care about):
- Create a branch with three commits. Then run
git rebase -i HEAD~3to squash them into one. Notice how the commit hash changes. - Create two branches from main. Add different commits to each. Cherry-pick one commit from branch A onto branch B using
git cherry-pick <hash>. - Practice
git reset --soft HEAD~1to undo a commit but keep the changes staged. Thengit reset --mixed HEAD~1to unstage. Finallygit reset --hard HEAD~1to discard entirely. - After a
git reset --hard, rungit reflogto find the lost commit. Usegit reset --hard <hash>to recover it.
Quick Quiz
Coding Challenge
Build a `CommitHistory` class that supports history rewriting operations. Methods: `addCommit(id, message)` appends a commit, `log()` returns an array of strings in format 'id: message', `squash(startIdx, endIdx, newMessage)` combines commits from startIdx to endIdx (inclusive) into one commit using the first commit id and the new message, `reset(count)` removes the last N commits (throw 'Nothing to reset' if count is greater than the number of commits), and `cherryPick(id, message)` appends a new commit with id 'cp-<id>' and the given message.
Real-World Usage
History rewriting tools are used daily by professional developers:
- Squash before merge: Most teams squash feature branch commits into a single clean commit when merging a PR, keeping main's history readable. GitHub's "Squash and merge" button automates this.
- Rebase onto main: Developers rebase their feature branches onto the latest main before opening a PR to avoid merge conflicts and keep a linear history.
- Amend typos:
git commit --amendis the fastest way to fix a commit message or add a forgotten file to the last commit. - Cherry-pick hotfixes: When a critical bug fix lands on a development branch, teams cherry-pick it onto the release branch to ship it immediately without waiting for the full release.
- Reflog as safety net: The reflog records every position your HEAD has been at, making it possible to recover from almost any Git mistake -- even a
reset --hard.