Module 5: Rebasing: Creating Clean Test History
Master rebasing to maintain a linear, clean history in your test automation projects. Learn when rebasing is preferable to merging, how to rebase feature branches onto updated main branches, and practice interactive rebasing to clean up test commits before sharing with the team.
Rebase Workflows for Test Teams: PR Preparation and Branch Maintenance
Why This Matters
As test automation projects grow and more team members contribute, your Git history can quickly become a tangled mess of merge commits, work-in-progress commits, and debugging attempts. You’ve likely experienced the frustration of reviewing a pull request with 27 commits that include messages like “fix typo,” “ugh this should work,” and “actually fixed it this time.” Or you’ve pulled the latest changes from main only to have your feature branch’s history look like a subway map with intersecting lines everywhere.
Real-world scenarios where rebasing becomes essential:
-
Before creating a PR: Your test implementation branch has 15 commits spanning three days of work, including failed experiments, temporary console.log debugging, and multiple “fix lint errors” commits. Your team lead expects clean, logical commits that tell a story.
-
Keeping feature branches current: You’ve been working on a new test suite for two weeks. Meanwhile, the main branch has received updates to the test framework configuration, new helper functions, and dependency upgrades. You need these changes without creating a merge commit spaghetti.
-
Maintaining professional commit history: Your team uses Git history as documentation. Future engineers should see what changed and why, not your stream-of-consciousness debugging journey at 2 AM.
Common pain points this lesson addresses:
- Cluttered Git history that makes code archaeology nearly impossible
- Merge conflicts that seem to multiply with every pull from main
- Embarrassing commit messages that live forever in the repository
- Difficulty identifying which commits introduced test failures
- Pull requests that are hard to review because changes are scattered across dozens of tiny commits
- Team friction over messy Git practices that slow down code review
By mastering rebase workflows, you’ll produce clean, professional pull requests that your teammates actually want to review, maintain a Git history that serves as valuable documentation, and demonstrate the advanced version control skills expected of senior test engineers.
What You’ll Learn
This advanced lesson transforms you from someone who knows Git exists to someone who wields it like a precision instrument. Here’s how we’ll build your rebasing expertise:
Understanding Rebase vs Merge — We start by demystifying what rebasing actually does under the hood. You’ll learn why rebasing “replays” commits, how it differs from merging conceptually, and develop the judgment to choose the right tool for each situation. This foundation is critical before you start rewriting history.
Rebasing Feature Branches — You’ll practice the most common daily use case: updating your test branch with the latest changes from main. We’ll work through concrete examples where your teammate has pushed framework updates, and you need to incorporate them without creating merge commits. You’ll master the standard rebase workflow that keeps your branches current.
Interactive Rebasing for Commit Cleanup — This is where rebasing becomes powerful. You’ll learn to use git rebase -i
to squash those five “fix test” commits into one meaningful commit, reorder commits so related changes are together, edit commit messages to be professional and descriptive, and even split large commits into logical chunks. We’ll practice on realistic test automation scenarios.
Conflict Resolution During Rebase — Conflicts happen. You’ll learn the systematic approach to resolving them during a rebase (hint: it’s different from merge conflicts), how to preserve your test suite’s functionality while resolving conflicts, and when to abort a rebase and try a different approach.
Safe Rebasing Practices — We’ll cover the critical rules that prevent disasters: never rebase commits that have been pushed to shared branches, understanding force push and its safer alternative --force-with-lease
, and recognizing situations where merging is actually the better choice.
PR Preparation Workflow — Finally, you’ll put it all together into a professional workflow: updating your branch with main’s latest changes, cleaning up your commits interactively, writing clear commit messages, and pushing a PR-ready branch that makes reviewers happy.
By the end of this lesson, you’ll confidently prepare pull requests with clean, linear history that showcases your test automation work professionally and makes your team’s Git repository a joy to navigate.
Core Content
Core Content: Rebase Workflows for Test Teams
1. Core Concepts Explained
Understanding Rebase vs. Merge
Rebase and merge are two fundamental approaches to integrating changes from one branch into another. For test automation teams, understanding the distinction is critical for maintaining clean, linear commit histories.
Merge creates a new commit that combines two branches, preserving the complete history of both branches including all branch points.
Rebase rewrites commit history by moving commits from one branch onto another, creating a linear history as if work happened sequentially.
graph LR
A[main: A-B-C] --> B[feature: A-B-C-D-E]
A --> C[main updated: A-B-C-F-G]
B --> D[After Merge: A-B-C-F-G-M<br/>with D-E]
B --> E[After Rebase: A-B-C-F-G-D'-E']
style D fill:#f9f,stroke:#333
style E fill:#9f9,stroke:#333
Why Test Teams Need Rebase
Test automation repositories benefit from rebasing because:
- Clean history: Each test feature appears as a linear sequence
- Easier debugging:
git bisect
works more effectively with linear history - Clearer PRs: Reviewers see focused changes without merge noise
- CI/CD clarity: Test failures map directly to specific feature commits
Interactive Rebase: The Power Tool
Interactive rebase (git rebase -i
) allows you to:
- Squash commits: Combine “WIP” and “fix typo” commits into meaningful units
- Reorder commits: Organize test commits logically
- Edit messages: Clean up commit descriptions before PR submission
- Drop commits: Remove experimental or obsolete test code
2. Practical Workflows for Test Teams
Workflow 1: Preparing a Test PR with Clean History
Scenario: You’ve been working on automated tests for a login feature with multiple commits that need cleaning before PR submission.
Step 1: Check Your Current State
# View your commit history
$ git log --oneline
a1b2c3d Add password validation test
d4e5f6g fix typo in assertion
g7h8i9j WIP login test
k0l1m2n Add login page object
o3p4q5r Update test data
# Check which branch you're on
$ git status
On branch feature/login-tests
Your branch is ahead of 'origin/main' by 5 commits.
Step 2: Fetch Latest Changes from Main
# Update your local main branch
$ git checkout main
$ git pull origin main
# Return to your feature branch
$ git checkout feature/login-tests
Step 3: Interactive Rebase to Clean History
# Start interactive rebase against main
$ git rebase -i main
This opens your editor with:
pick k0l1m2n Add login page object
pick o3p4q5r Update test data
pick g7h8i9j WIP login test
pick d4e5f6g fix typo in assertion
pick a1b2c3d Add password validation test
# Rebase k0l1m2n..a1b2c3d onto main (5 commands)
#
# 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 4: Organize Your Commits
Modify the rebase plan:
pick k0l1m2n Add login page object
fixup o3p4q5r Update test data
pick g7h8i9j WIP login test
fixup d4e5f6g fix typo in assertion
reword a1b2c3d Add password validation test
Explanation:
- fixup: Merges changes into the previous commit, discarding the commit message
- reword: Lets you change the commit message for clarity
- The result will be 3 clean commits instead of 5 messy ones
Step 5: Update Commit Messages
After saving, Git will prompt you to reword commits:
# Original message for g7h8i9j
WIP login test
# Rewrite to:
Add automated tests for login functionality
- Verify successful login with valid credentials
- Test error messages for invalid credentials
- Ensure login button state management
# Original message for a1b2c3d
Add password validation test
# Rewrite to:
Add password field validation tests
- Test minimum password length requirement
- Verify special character validation
- Test password visibility toggle functionality
Step 6: Rebase onto Latest Main
# Now rebase your clean commits onto main
$ git rebase main
# If conflicts occur, resolve them
$ git status # Shows conflicting files
# Edit files to resolve conflicts
$ git add <resolved-files>
$ git rebase --continue
Step 7: Force Push to Your Branch
# Since you've rewritten history, force push is required
$ git push --force-with-lease origin feature/login-tests
⚠️ Important: Use --force-with-lease
instead of --force
. It prevents overwriting others’ work if someone else pushed to your branch.
Workflow 2: Keeping Test Branch Updated During Long PR Reviews
Scenario: Your PR has been open for several days, and main has received updates. You need to incorporate these changes.
# On your feature branch
$ git checkout feature/api-tests
# Fetch latest changes
$ git fetch origin
# Rebase your work onto the updated main
$ git rebase origin/main
# Resolve any conflicts
# ... conflict resolution if needed ...
# Push updated branch
$ git push --force-with-lease origin feature/api-tests
Workflow 3: Squashing Related Test Commits
Real-world example: You’ve added tests across multiple commits for the same feature.
# View last 4 commits
$ git log --oneline -4
f1a2b3c Add test for checkout flow
e4d5c6b Add cart validation test
d7e8f9g Add product selection test
c0a1b2d Update test configuration
# Squash last 3 commits into one
$ git rebase -i HEAD~3
In the editor:
pick d7e8f9g Add product selection test
squash e4d5c6b Add cart validation test
squash f1a2b3c Add test for checkout flow
Write a comprehensive commit message:
Add e-commerce purchase flow tests
Complete test suite for product purchase workflow:
- Product selection and variants
- Cart management and validation
- Checkout process and payment flow
Tests cover happy path and error scenarios
Includes test data fixtures and page objects
3. Practical Code Examples
Example 1: Page Object Model with Clean Commit History
Here’s how your test code should evolve with proper rebasing:
First Commit: Add login page object
// tests/pages/LoginPage.js
class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('#submit');
this.errorMessage = page.locator('.alert-error');
}
async navigate() {
await this.page.goto('https://practiceautomatedtesting.com/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage() {
return await this.errorMessage.textContent();
}
}
module.exports = LoginPage;
Second Commit: Add login functionality tests
// tests/specs/login.spec.js
const { test, expect } = require('@playwright/test');
const LoginPage = require('../pages/LoginPage');
test.describe('Login Functionality', () => {
test('should login successfully with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
// Using test data that's realistic
await loginPage.login('testuser@example.com', 'SecurePass123!');
// Verify redirect to dashboard
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.locator('.welcome-message')).toBeVisible();
});
test('should display error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('invalid@example.com', 'wrongpass');
// Verify error message appears
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
});
Third Commit: Add password validation tests
// tests/specs/password-validation.spec.js
const { test, expect } = require('@playwright/test');
const LoginPage = require('../pages/LoginPage');
test.describe('Password Field Validation', () => {
let loginPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
await loginPage.navigate();
});
test('should reject password shorter than 8 characters', async ({ page }) => {
await loginPage.passwordInput.fill('Short1!');
// Verify validation message
const validationMsg = await page.locator('#password-error').textContent();
expect(validationMsg).toContain('minimum 8 characters');
});
test('should require special characters in password', async ({ page }) => {
await loginPage.passwordInput.fill('NoSpecial123');
const validationMsg = await page.locator('#password-error').textContent();
expect(validationMsg).toContain('special character required');
});
test('should toggle password visibility', async ({ page }) => {
const passwordField = loginPage.passwordInput;
const toggleButton = page.locator('#toggle-password-visibility');
// Initially should be password type
await expect(passwordField).toHaveAttribute('type', 'password');
// Click toggle
await toggleButton.click();
await expect(passwordField).toHaveAttribute('type', 'text');
// Click again to hide
await toggleButton.click();
await expect(passwordField).toHaveAttribute('type', 'password');
});
});
Example 2: Handling Conflicts During Rebase
When rebasing, conflicts may occur if main branch changed the same files:
$ git rebase main
Auto-merging tests/specs/login.spec.js
CONFLICT (content): Merge conflict in tests/specs/login.spec.js
error: could not apply a1b2c3d... Add login functionality tests
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
Before resolution (login.spec.js):
<<<<<<< HEAD
// Changes from main branch
test('should login with email', async ({ page }) => {
await page.goto('https://practiceautomatedtesting.com/login');
await page.fill('#email', 'user@test.com');
=======
// Your changes
test('should login successfully with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('testuser@example.com', 'SecurePass123!');
>>>>>>> a1b2c3d... Add login functionality tests
After resolution:
// Combine both approaches: use page object but keep updated selector
test('should login successfully with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
// Updated to use email field instead of username (from main)
await page.fill('#email', 'testuser@example.com');
await page.fill('#password', 'SecurePass123!');
await loginPage.loginButton.click();
await expect(page).toHaveURL(/.*dashboard/);
});
# Mark as resolved
$ git add tests/specs/login.spec.js
# Continue rebase
$ git rebase --continue
Example 3: Using Git Aliases for Common Rebase Tasks
Add these to your .gitconfig
for efficiency:
# Configure useful aliases
$ git config --global alias.rb 'rebase'
$ git config --global alias.rbi 'rebase -i'
$ git config --global alias.rbc 'rebase --continue'
$ git config --global alias.rba 'rebase --abort'
$ git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
# Usage example
$ git rb main # Instead of git rebase main
$ git rbi HEAD~5 # Instead of git rebase -i HEAD~5
$ git lg # Beautiful log view
<!-- SCREENSHOT_NEEDED: IDE
IDE: VS Code
View: Terminal showing git lg output
Description: Colorful git log with branch visualization
Placement: after git aliases section -->
4. Common Mistakes and Solutions
Mistake 1: Rebasing Public/Shared Branches
❌ Wrong:
# Never do this on main or shared branches
$ git checkout main
$ git rebase feature/my-branch # Rewrites public history!
✅ Correct:
# Only rebase your own feature branches
$ git checkout feature/my-branch
$ git rebase main # This is safe
Why: Rebasing rewrites commit history. If others have pulled the branch, their history becomes incompatible.
Mistake 2: Not Using –force-with-lease
❌ Dangerous:
$ git push --force origin feature/tests
# Overwrites any changes others made
✅ Safer:
$ git push --force-with-lease origin feature/tests
# Fails if remote has changes you don't have locally
Mistake 3: Losing Work During Rebase
Emergency recovery:
# If rebase goes wrong, find your commits in reflog
$ git reflog
a1b2c3d HEAD@{0}: rebase: Add login tests
e4f5g6h HEAD@{1}: commit: WIP login test # Your lost commit!
# Restore to before rebase
$ git reset --hard HEAD@{1}
Mistake 4: Creating Conflicts by Reordering Dependent Commits
Problem: Reordering commits where later ones depend on earlier ones.
# This order causes issues:
pick a1b2c3d Add password validation test
pick k0l1m2n Add login page object # Should be first!
Solution: Keep commits in dependency order during interactive rebase.
Mistake 5: Forgetting to Pull Before Rebase
Best practice workflow:
# Always fetch latest before rebasing
$ git checkout main
$ git pull origin main # Update main
$ git checkout feature/my-tests
$ git rebase main # Now rebase with latest
<!-- SCREENSHOT_NEEDED: BROWSER
URL: https://github.com/username/repo/pulls
Description: Pull request showing clean, linear commit history after rebase
Placement: at end of common mistakes section -->
Debugging Rebase Issues
Check rebase status:
$ git status
interactive rebase in progress; onto
---
## Hands-On Practice
# Hands-On Exercise
## Scenario: Clean Up a Feature Branch Before PR Submission
You're a test automation engineer who has been working on a feature branch with multiple commits. Your test suite now passes, but your commit history is messy with WIP commits, merge commits, and fixes. Before submitting your PR, you need to clean up the branch using rebase workflows.
### Task
Clean up a test automation feature branch using interactive rebase, resolve conflicts that arise, and prepare it for a clean PR submission.
### Setup Instructions
**Step 1: Clone the practice repository**
```bash
git clone https://github.com/your-org/test-automation-practice.git
cd test-automation-practice
git checkout feature/messy-login-tests
Step 2: Examine the current state
git log --oneline -10
# You should see something like:
# a1b2c3d Fix typo in assertion
# d4e5f6g WIP - debugging
# g7h8i9j Add login timeout test
# j1k2l3m Merge main into feature
# m4n5o6p Fix test data
# p7q8r9s Add initial login tests
# ... (main branch commits)
Exercise Tasks
Task 1: Interactive Rebase to Clean Commit History (30 minutes)
Instructions:
-
Start an interactive rebase from main:
git fetch origin git rebase -i origin/main
-
In the interactive editor, restructure commits:
- Squash “Fix typo in assertion” into “Add login timeout test”
- Drop the “WIP - debugging” commit (it contains only console.logs)
- Squash “Fix test data” into “Add initial login tests”
- Keep the remaining meaningful commits
- Reorder commits so related tests are grouped logically
-
Edit commit messages to follow convention:
test: add comprehensive login page tests - Includes happy path scenarios - Includes validation error cases - Uses proper page object pattern test: add login timeout and performance tests - Verifies timeout handling - Ensures login completes within SLA
Expected Outcome:
- 2-3 clean, logical commits
- No merge commits
- Clear, descriptive commit messages
- All tests still passing
Task 2: Handle Rebase Conflicts (20 minutes)
During the rebase, you’ll encounter conflicts in tests/login.spec.js
because main branch has updated the page object structure.
Instructions:
-
When conflict occurs:
# Git will pause and show: # CONFLICT (content): Merge conflict in tests/login.spec.js
-
Resolve the conflict:
# Open the file and look for conflict markers code tests/login.spec.js
-
Edit to keep both changes intelligently:
- Main branch: Updated
LoginPage
class with new locator strategy - Your branch: Added new test methods
- Combine both changes appropriately
- Main branch: Updated
-
Continue the rebase:
git add tests/login.spec.js git rebase --continue
-
Verify tests pass:
npm test
Expected Outcome:
- Conflicts resolved without losing any functionality
- Tests use the updated page object structure
- All tests passing
Task 3: Synchronize with Updated Main Branch (15 minutes)
While you were working, main branch received several updates including critical bug fixes.
Instructions:
-
Update your local main:
git checkout main git pull origin main
-
Rebase your feature branch onto latest main:
git checkout feature/messy-login-tests git rebase main
-
If conflicts arise, resolve them:
# Resolve conflicts git add . git rebase --continue
-
Verify the complete test suite:
npm test -- --all
Expected Outcome:
- Feature branch cleanly rebased on latest main
- No regression in existing tests
- New tests integrate seamlessly
Task 4: Force Push Safely (10 minutes)
Instructions:
-
Verify your branch is ready:
git log --oneline origin/main..HEAD # Should show only your clean commits
-
Check for remote changes (safety check):
git fetch origin git log HEAD..origin/feature/messy-login-tests # Should be empty if no one else pushed
-
Force push with lease:
git push --force-with-lease origin feature/messy-login-tests
-
Create a PR and verify the diff:
- Go to your Git hosting platform
- Verify the PR shows only meaningful changes
- Check that commit history is clean
Expected Outcome:
- Clean PR with logical commits
- Reviewers can easily understand changes
- No accidental overwrites of others’ work
Validation Checklist
- No merge commits in your branch
- 2-3 clear, descriptive commits
- All commit messages follow convention
- All tests passing
- Branch is up-to-date with main
- Clean diff in PR (only your changes)
Solution Approach
Click to reveal solution steps
Interactive Rebase Commands:
# In the rebase-todo editor:
pick p7q8r9s Add initial login tests
squash m4n5o6p Fix test data
pick g7h8i9j Add login timeout test
squash a1b2c3d Fix typo in assertion
drop d4e5f6g WIP - debugging
# Save and exit
Conflict Resolution Strategy:
// In tests/login.spec.js - keep structure from main, add your tests
import { LoginPage } from '../pages/LoginPage'; // Updated import from main
describe('Login Tests', () => {
let loginPage;
beforeEach(() => {
loginPage = new LoginPage(); // Updated instantiation from main
});
// Your new tests using the updated structure
it('should handle login timeout gracefully', async () => {
// ... your test code
});
});
Complete Workflow:
# 1. Clean up commits
git rebase -i origin/main
# 2. Resolve conflicts when they appear
git status
# Edit conflicted files
git add .
git rebase --continue
# 3. Sync with latest main
git checkout main && git pull
git checkout feature/messy-login-tests
git rebase main
# 4. Force push safely
git push --force-with-lease origin feature/messy-login-tests
Key Takeaways
-
Interactive rebase is a powerful PR preparation tool - Use
git rebase -i
to squash WIP commits, reorder changes logically, and craft a clean story for reviewers before submitting PRs. -
Rebase conflicts are resolved differently than merge conflicts - During rebase, you’re reapplying your changes on top of new code. Resolve by understanding both the base changes and your modifications, then continue with
git rebase --continue
. -
Regular rebasing keeps branches maintainable - Frequently rebasing feature branches onto main prevents massive conflicts, keeps your branch up-to-date with bug fixes, and makes eventual PR merging straightforward.
-
Force push safely with
--force-with-lease
- After rewriting history with rebase, use--force-with-lease
instead of--force
to prevent accidentally overwriting collaborators’ work. -
Clean commit history improves team productivity - Well-organized commits with clear messages help code reviewers, make bisecting bugs easier, and serve as documentation of why changes were made.
Next Steps
Practice These Skills
-
Daily Rebase Habit - Before starting work each day, rebase your feature branches onto the latest main to catch integration issues early
-
Commit Message Refinement - Practice writing conventional commit messages during interactive rebase sessions
-
Complex Conflict Resolution - Set up practice scenarios with intentional conflicts in test fixtures, page objects, and test utilities
Related Topics to Explore
- Git Reflog Recovery - Learn how to recover from rebase mistakes using
git reflog
andgit reset
- Merge vs Rebase Strategies - Understand when to use rebase workflows vs. merge workflows in different team contexts
- Pre-commit Hooks for Test Teams - Automate test running and linting before commits to maintain quality
- Branch Protection Rules - Configure repository settings to require linear history and passing tests
- Cherry-picking for Test Fixes - Learn to selectively apply test fixes across multiple branches
Recommended Reading
- “Git for Test Automation Teams” - Focus on practical workflows
- “Pro Git” Chapter 3 (Git Branching) and Chapter 7 (Git Tools)
- Your team’s Git workflow documentation - Ensure alignment with team standards