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

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:

  1. Start an interactive rebase from main:

    git fetch origin
    git rebase -i origin/main
    
  2. 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
  3. 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:

  1. When conflict occurs:

    # Git will pause and show:
    # CONFLICT (content): Merge conflict in tests/login.spec.js
    
  2. Resolve the conflict:

    # Open the file and look for conflict markers
    code tests/login.spec.js
    
  3. 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
  4. Continue the rebase:

    git add tests/login.spec.js
    git rebase --continue
    
  5. 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:

  1. Update your local main:

    git checkout main
    git pull origin main
    
  2. Rebase your feature branch onto latest main:

    git checkout feature/messy-login-tests
    git rebase main
    
  3. If conflicts arise, resolve them:

    # Resolve conflicts
    git add .
    git rebase --continue
    
  4. 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:

  1. Verify your branch is ready:

    git log --oneline origin/main..HEAD
    # Should show only your clean commits
    
  2. 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
    
  3. Force push with lease:

    git push --force-with-lease origin feature/messy-login-tests
    
  4. 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

  1. Daily Rebase Habit - Before starting work each day, rebase your feature branches onto the latest main to catch integration issues early

  2. Commit Message Refinement - Practice writing conventional commit messages during interactive rebase sessions

  3. Complex Conflict Resolution - Set up practice scenarios with intentional conflicts in test fixtures, page objects, and test utilities

  • Git Reflog Recovery - Learn how to recover from rebase mistakes using git reflog and git 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
  • “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