Skip to main content

Time Travel in Git: How to Add Code to Past Commits and Affect All Future Ones

Β· 10 min read
Mahmut Salman
Software Developer

Ever wished you could go back to your first commit, add that test class you forgot, and have it magically appear in all subsequent commits? With Git's interactive rebase, you can! Let's explore how Git actually stores history and how to safely rewrite it.

The Question​

"In Git, can I add code to my first commit and have it appear in all later commits? Like, I wrote a test class in commit 1, and I want to see it in commit 2, 3, 4, etc."

Short answer: Yes! Using git rebase -i (interactive rebase).

Important caveat: This rewrites history, so use carefully!


Understanding Git Commits First​

Commits Are Snapshots, Not Diffs​

Common misconception:

Commit 1: Added file A
Commit 2: Modified file A
Commit 3: Added file B

What Git actually stores:

Commit 1: Complete snapshot of project (contains: file A)
Commit 2: Complete snapshot of project (contains: file A modified)
Commit 3: Complete snapshot of project (contains: file A modified, file B)

Each commit = Full snapshot of your project at that moment!

Why This Matters​

When you modify Commit 1, you're changing the entire snapshot. This affects all subsequent commits because they're based on the previous state.

Before modification:
Commit 1 [snapshot: A, B, C]
↓
Commit 2 [snapshot: A, B, C, D]
↓
Commit 3 [snapshot: A, B, C, D, E]

After adding file X to Commit 1:
Commit 1' [snapshot: A, B, C, X] ← Modified
↓
Commit 2' [snapshot: A, B, C, X, D] ← Automatically includes X
↓
Commit 3' [snapshot: A, B, C, X, D, E] ← Automatically includes X

All subsequent commits now include X!


Interactive Rebase: Your Time Machine​

What is git rebase -i?​

Interactive rebase lets you:

  • ✏️ Edit past commits
  • πŸ”„ Reorder commits
  • 🎯 Combine (squash) commits
  • ❌ Delete commits
  • πŸ“ Change commit messages

The Command​

git rebase -i HEAD~n

Where:

  • HEAD~n = Go back n commits
  • -i = Interactive mode

Examples:

git rebase -i HEAD~3   # Last 3 commits
git rebase -i HEAD~5 # Last 5 commits
git rebase -i abc123 # Back to commit abc123

Real-World Scenario: Adding a Forgotten Test Class​

The Problem​

Your commit history:

Commit 3: Implement feature X
Commit 2: Add User class
Commit 1: Initial project setup ← Forgot to add UserTest.java here!

You realize you should have added UserTest.java in Commit 1, and you want it to appear in all commits.

The Solution: Step-by-Step​

Step 1: Start Interactive Rebase​

# Go back 3 commits
git rebase -i HEAD~3

Interactive editor opens:

pick abc123 Initial project setup
pick def456 Add User class
pick ghi789 Implement feature X

# Rebase commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# d, drop = remove commit

Step 2: Mark First Commit for Editing​

Change pick to edit for the first commit:

edit abc123 Initial project setup  ← Changed to 'edit'
pick def456 Add User class
pick ghi789 Implement feature X

Save and close the editor.

Step 3: Add Your Forgotten File​

Git pauses at Commit 1:

Stopped at abc123... Initial project setup
You can amend the commit now, with

git commit --amend

Once you are satisfied with your changes, run

git rebase --continue

Now add your test file:

# Create the test file
cat > UserTest.java << 'EOF'
import org.junit.Test;

public class UserTest {
@Test
public void testUser() {
// Test code
}
}
EOF

# Stage the file
git add UserTest.java

# Amend the commit (add to existing commit)
git commit --amend --no-edit

Step 4: Continue Rebase​

git rebase --continue

Git replays all subsequent commits on top of your modified commit.

Step 5: Verify​

# Check each commit
git log --oneline

# See that UserTest.java exists in all commits
git show abc123:UserTest.java # Commit 1
git show def456:UserTest.java # Commit 2
git show ghi789:UserTest.java # Commit 3

Result: UserTest.java now exists in Commit 1, 2, and 3! ✨


Visual Walkthrough​

Before Rebase​

Commit 1: [User.java, Main.java]
↓
Commit 2: [User.java, Main.java, Config.java]
↓
Commit 3: [User.java, Main.java, Config.java, Feature.java]

Missing: UserTest.java in all commits

During Rebase (Adding UserTest.java to Commit 1)​

Rebase paused at Commit 1
↓
Add UserTest.java
↓
Amend Commit 1
↓
Commit 1': [User.java, Main.java, UserTest.java] ← Updated!

After Rebase (Git Replays Commits 2 & 3)​

Commit 1': [User.java, Main.java, UserTest.java]
↓
Commit 2': [User.java, Main.java, UserTest.java, Config.java]
↓
Commit 3': [User.java, Main.java, UserTest.java, Config.java, Feature.java]

UserTest.java is now in ALL commits! βœ…


Interactive Rebase Operations​

1. Edit (e) - Modify a Commit​

edit abc123 Add User class

Use when:

  • Adding forgotten files
  • Removing files
  • Changing code

2. Reword (r) - Change Commit Message​

reword abc123 Add User class

Use when:

  • Fixing typos in commit messages
  • Making messages more descriptive

3. Squash (s) - Combine Commits​

pick abc123 Add User class
squash def456 Fix typo in User class
squash ghi789 Add User tests

Result: All three commits become one!

4. Fixup (f) - Like Squash but Discard Message​

pick abc123 Add User class
fixup def456 Fix typo
fixup ghi789 Fix another typo

Result: Keeps only the first commit message

5. Drop (d) - Remove Commit​

pick abc123 Add User class
drop def456 Experimental code (didn't work)
pick ghi789 Add tests

Result: Commit 2 is deleted from history

6. Reorder - Change Commit Order​

pick ghi789 Add tests
pick abc123 Add User class
pick def456 Add Config

Result: Commits applied in new order


Common Scenarios​

Scenario 1: Split One Commit Into Multiple​

Current:

Commit 1: Add User class and tests (too big!)

Goal:

Commit 1: Add User class
Commit 2: Add User tests

Solution:

git rebase -i HEAD~1

# Editor:
edit abc123 Add User class and tests

# Git pauses at commit
git reset HEAD^ # Unstage all changes

# Now commit separately
git add User.java
git commit -m "Add User class"

git add UserTest.java
git commit -m "Add User tests"

git rebase --continue

Scenario 2: Combine Multiple Small Commits​

Current:

Commit 1: Add User class
Commit 2: Fix typo in User
Commit 3: Fix another typo
Commit 4: Add User tests

Goal:

Commit 1: Add User class with tests

Solution:

git rebase -i HEAD~4

# Editor:
pick abc123 Add User class
squash def456 Fix typo in User
squash ghi789 Fix another typo
squash jkl012 Add User tests

Scenario 3: Remove Sensitive Data from History​

Problem: Accidentally committed API key in Commit 2

Solution:

git rebase -i HEAD~5

# Editor:
pick abc123 Initial commit
edit def456 Add config (contains API key!)
pick ghi789 Add feature
pick jkl012 Add tests
pick mno345 Update README

# Git pauses
git rm config/api-keys.txt
git commit --amend --no-edit
git rebase --continue

Important Warnings βš οΈβ€‹

Never Rebase Published Commits!​

# Local commits only (not pushed) βœ…
git rebase -i HEAD~3

# Published commits (already pushed) ❌ DANGER!
git rebase -i HEAD~3
git push --force # Don't do this on shared branches!

Why?

  • Rewriting published history confuses collaborators
  • Creates divergent histories
  • Causes merge conflicts
  • Can lose work

When It's Safe to Rebase​

βœ… Safe:

  • Your feature branch (not yet merged)
  • Local commits (not pushed)
  • Your personal fork

❌ Dangerous:

  • main or master branch
  • Commits other people have based work on
  • Public repositories (with collaborators)

The Golden Rule​

If commits have been pushed to a shared repository, don't rebase them!


Handling Conflicts During Rebase​

When Conflicts Occur​

git rebase -i HEAD~3

# ...Git replays commits...
Auto-merging User.java
CONFLICT (content): Merge conflict in User.java
error: could not apply def456... Add User tests

Git pauses and asks you to resolve conflicts.

Resolution Steps​

Step 1: See Conflicted Files​

git status

Output:

Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: User.java

Step 2: Resolve Conflicts​

Open User.java:

public class User {
<<<<<<< HEAD
private String name;
=======
private String username;
>>>>>>> def456 (Add User tests)
}

Edit to resolve:

public class User {
private String username; // Keep this version
}

Step 3: Mark as Resolved​

git add User.java

Step 4: Continue Rebase​

git rebase --continue

Abort If Things Go Wrong​

# Undo everything and return to original state
git rebase --abort

Practical Workflow​

Before Starting Rebase​

# 1. Ensure working directory is clean
git status

# 2. Create backup branch (safety!)
git branch backup-before-rebase

# 3. Start rebase
git rebase -i HEAD~5

If Something Goes Wrong​

# Option 1: Abort and start over
git rebase --abort

# Option 2: Return to backup
git reset --hard backup-before-rebase

# Option 3: Use reflog (Git's safety net)
git reflog
git reset --hard HEAD@{5} # Go back to previous state

Best Practices​

βœ… Do This​

1. Rebase before pushing

# Clean up local commits before sharing
git rebase -i HEAD~3
git push origin feature-branch

2. Use descriptive commit messages

# Good
git commit -m "Add User authentication with JWT tokens"

# Bad
git commit -m "fix stuff"

3. Create backup branch before rebase

git branch backup
git rebase -i HEAD~5

4. Rebase frequently during development

# Keep feature branch updated with main
git checkout feature-branch
git rebase main

❌ Avoid This​

1. Don't rebase public branches

# ❌ Never do this
git checkout main
git rebase -i HEAD~10
git push --force

2. Don't rebase if unsure

# Create backup first!
git branch backup

3. Don't force push to shared branches

# ❌ Avoid
git push --force origin main

# βœ… Only on personal branches
git push --force origin my-feature-branch

Common Commands Reference​

Interactive Rebase​

# Last n commits
git rebase -i HEAD~n

# Since specific commit
git rebase -i <commit-hash>

# Since branch point
git rebase -i main

During Rebase​

# Amend current commit
git commit --amend

# Continue after resolving conflicts
git rebase --continue

# Skip this commit
git rebase --skip

# Abort rebase
git rebase --abort

Viewing History​

# See commit history
git log --oneline

# See what changed in each commit
git log -p

# Graphical view
git log --oneline --graph --all

# See file in specific commit
git show <commit-hash>:<file-path>

Safety Commands​

# Create backup
git branch backup

# View reflog (Git's history of your actions)
git reflog

# Return to previous state
git reset --hard HEAD@{n}

Summary​

Key Concepts​

  1. Git commits are snapshots, not diffs
  2. Modifying an early commit affects all later commits
  3. Interactive rebase (git rebase -i) is your time machine
  4. Only rebase unpublished commits

The Pattern​

# 1. Start interactive rebase
git rebase -i HEAD~n

# 2. Mark commit for editing
edit abc123 Initial commit

# 3. Make your changes
git add forgotten-file.java
git commit --amend --no-edit

# 4. Continue rebase
git rebase --continue

# Result: forgotten-file.java now in all commits! ✨

When to Use​

βœ… Good use cases:

  • Cleaning up messy commit history
  • Adding forgotten files to past commits
  • Combining related commits
  • Removing sensitive data

❌ Avoid when:

  • Commits are already pushed to shared branch
  • Working on main/master branch
  • Collaborators have based work on your commits

Remember: With great power comes great responsibility. Interactive rebase is powerful, but use it wisely. When in doubt, create a backup branch first! πŸ›Ÿ

Tags: #git #version-control #rebase #best-practices #tutorial