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.

Advanced Rebase: Handling Conflicts in Test Branch Updates

Why This Matters

As a test automation engineer working in a collaborative environment, you’ve likely experienced this scenario: You’ve been working on a feature branch with your automated test suite for several days. Meanwhile, your teammates have merged multiple updates to the main branch—new test utilities, framework improvements, configuration changes. When you finally try to integrate your work, you face a messy merge commit that obscures what changes you actually made to the test suite.

The Real-World Problem: Merge commits create a non-linear history that makes it difficult to understand the evolution of your test codebase. When debugging a failing test or reviewing what changed in your automation framework, you need a clear, chronological story. Traditional merging creates a “spaghetti history” with interleaved commits from multiple branches, making it challenging to track down when a specific test behavior was introduced or why a particular test configuration changed.

When You’ll Use This Skill:

  • Before Pull Requests: Clean up your experimental test commits, combining multiple “trying different selector strategies” commits into one coherent change
  • Keeping Feature Branches Current: Update your long-running test development branch with the latest framework changes from main without creating merge commits
  • Team Collaboration: Present your test automation work in a logical, reviewable sequence that tells a clear story
  • Release Preparation: Ensure your test suite changes integrate smoothly with the latest application code before deployment

Common Pain Points Addressed:

  • Cryptic Commit History: “Which commit actually fixed the flaky login test?” becomes impossible to answer when history is cluttered with merge commits and “WIP” messages
  • Difficult Code Reviews: Reviewers struggle to understand your test changes when commits are out of order or mixed with temporary debugging code
  • Integration Headaches: Resolving conflicts during a final merge is more stressful than handling them incrementally during rebase
  • Lost Context: Important test automation decisions get buried in noisy commit history, making future maintenance difficult

Mastering rebase puts you in control of your project’s history, transforming it from a chaotic record of every keystroke into a curated narrative that serves your team’s needs.

Learning Objectives Overview

In this lesson, you’ll develop advanced rebasing skills specifically tailored for test automation workflows. Here’s what you’ll accomplish:

1. Execute Safe Rebases of Feature Branches
You’ll learn the complete workflow for rebasing your test automation feature branch onto an updated main branch. We’ll cover the git rebase command syntax, understanding what’s happening during a rebase, and safety measures like creating backup branches. You’ll practice rebasing a branch containing Selenium tests onto a main branch that has new test utilities.

2. Identify and Resolve Rebase Conflicts
When rebasing test code, conflicts are inevitable—especially in shared configuration files, page object classes, or test data. You’ll learn to recognize conflict markers, understand why conflicts occur during rebase (versus merge), and systematically resolve them. We’ll work through realistic scenarios like conflicting changes to test framework initialization code and overlapping modifications to API test assertions.

3. Use Interactive Rebase for Commit Cleanup
The interactive rebase (git rebase -i) is your power tool for crafting perfect commit history. You’ll learn to squash multiple “fix typo in test” commits into one, reorder commits to group related test changes logically, edit commit messages for clarity, and even split commits that changed too many things at once. This section includes hands-on practice cleaning up a messy branch of API test development.

4. Apply Rebase Best Practices in Team Environments
Rebasing has critical “don’t do this” rules, especially around shared branches. You’ll learn the golden rule of never rebasing public history, when to use --force-with-lease versus --force for pushing rebased branches, how to communicate with your team about rebased branches, and strategies for handling situations where others have based work on your branch.

5. Recognize Rebase vs Merge Scenarios
Not every situation calls for rebase. You’ll develop judgment for choosing the right integration strategy: when rebase maintains clarity for feature work, when merge commits are actually beneficial (like recording integration points), how your team’s workflow influences the decision, and special considerations for test automation branches versus application code branches.

By the end of this lesson, you’ll confidently maintain clean, linear history in your test automation projects, making your work more reviewable, debuggable, and professional.


Core Content

Core Content: Advanced Rebase: Handling Conflicts in Test Branch Updates

Core Concepts Explained

Understanding Git Rebase in Test Automation Context

Rebase is a powerful Git operation that moves or combines a sequence of commits to a new base commit. In test automation, rebasing is crucial when you need to update your test branch with the latest changes from the main branch while maintaining a clean, linear commit history.

Key Difference: Merge vs. Rebase

graph LR
    A[main: A-B-C] --> B[feature: A-B-D-E]
    B --> C[After Merge: A-B-C-D-E-M]
    B --> D[After Rebase: A-B-C-D'-E']
    style C fill:#f9f,stroke:#333
    style D fill:#9f9,stroke:#333
  • Merge creates a new “merge commit” combining histories
  • Rebase rewrites history by replaying your commits on top of the updated base

When to Use Rebase for Test Branches

Ideal Scenarios:

  • Updating feature test branch with latest main branch changes
  • Cleaning up test commits before code review
  • Synchronizing parallel test development work
  • Preparing test branches for CI/CD integration

Avoid Rebasing When:

  • The branch is already pushed and shared with team members
  • You’re working on a public/protected branch
  • Others have based work on your commits

Step-by-Step Rebase Workflow

1. Prepare Your Environment

Before starting a rebase, ensure your repository is clean:

# Check current status
$ git status
On branch test/login-automation
nothing to commit, working tree clean

# Fetch latest changes from remote
$ git fetch origin

# View current branch structure
$ git log --oneline --graph --all
* 7a3d9e2 (HEAD -> test/login-automation) Add password validation tests
* 5b8c1f4 Add username validation tests
* 2e9a7d3 (origin/main, main) Update test framework config
* 1c5f8a2 Initial test structure

2. Start the Interactive Rebase

Interactive rebase gives you control over each commit:

# Rebase current branch onto latest main
$ git rebase -i origin/main

# Alternative: Rebase with conflict resolution strategy
$ git rebase -i origin/main -X theirs  # Prefer incoming changes

This opens your editor with a rebase todo list:

pick 5b8c1f4 Add username validation tests
pick 7a3d9e2 Add password validation tests

# Rebase 2e9a7d3..7a3d9e2 onto 2e9a7d3 (2 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

3. Handling Merge Conflicts During Rebase

When conflicts occur, Git pauses the rebase:

$ git rebase -i origin/main
Auto-merging tests/login.test.js
CONFLICT (content): Merge conflict in tests/login.test.js
error: could not apply 5b8c1f4... Add username validation tests

Conflict Resolution Process:

# Step 1: Check which files have conflicts
$ git status
interactive rebase in progress; onto 2e9a7d3
Last command done (1 command done):
   pick 5b8c1f4 Add username validation tests
Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   tests/login.test.js

# Step 2: View the conflict markers in your file
$ cat tests/login.test.js

Example Conflict in Test File:

// tests/login.test.js
const { test, expect } = require('@playwright/test');

test.describe('Login Tests', () => {
<<<<<<< HEAD (Current Change - main branch)
  test('should validate email format', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#email', 'invalid-email');
    await page.click('#submit');
    await expect(page.locator('.error')).toContainText('Invalid email format');
  });
=======
  test('should validate username format', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#username', 'test@user');
    await page.click('#login-btn');
    await expect(page.locator('.error-msg')).toBeVisible();
  });
>>>>>>> 5b8c1f4 (Incoming Change - your commit)
});

Resolving the Conflict:

// tests/login.test.js - RESOLVED
const { test, expect } = require('@playwright/test');

test.describe('Login Tests', () => {
  // Keep updated selector from main branch
  test('should validate email format', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#email', 'invalid-email');
    await page.click('#submit');
    await expect(page.locator('.error')).toContainText('Invalid email format');
  });

  // Add your new test with updated selectors
  test('should validate username format', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#username', 'test@user');
    await page.click('#submit'); // Updated to match new selector
    await expect(page.locator('.error')).toBeVisible();
  });
});

Continue the Rebase:

# Stage the resolved file
$ git add tests/login.test.js

# Continue rebase process
$ git rebase --continue

# If you made a mistake, abort and start over
$ git rebase --abort

4. Squashing Test Commits

Clean up multiple small test commits into logical units:

# In the interactive rebase editor
pick 5b8c1f4 Add username validation tests
squash 7a3d9e2 Add password validation tests
squash 9c2f1a8 Fix typo in username test
squash 3e7b4d9 Update test assertions

# This combines all into one commit with a new message

Practical Real-World Example

Scenario: Updating Test Suite After Framework Migration

Initial State:

# Your test branch
$ git log --oneline
4d9e2f1 (HEAD -> test/checkout-flow) Add payment method tests
8c3a1b7 Add cart validation tests
2e9a7d3 (origin/main) Initial tests

# Meanwhile, main branch was updated
$ git log --oneline origin/main
9f7e4c2 (origin/main) Migrate from Selenium to Playwright
6a8d3e1 Update test configuration
2e9a7d3 Initial tests

Rebase Process:

# Start rebase
$ git rebase origin/main

# Conflict in cart validation tests
CONFLICT (content): Merge conflict in tests/cart.test.js

Before Rebase (Selenium syntax):

// tests/cart.test.js - Your branch
const { Builder, By, until } = require('selenium-webdriver');

describe('Cart Tests', () => {
  it('should add items to cart', async () => {
    const driver = await new Builder().forBrowser('chrome').build();
    await driver.get('https://practiceautomatedtesting.com/cart');
    await driver.findElement(By.id('add-to-cart')).click();
    const count = await driver.findElement(By.className('cart-count')).getText();
    expect(count).toBe('1');
  });
});

After Rebase (Playwright syntax from main):

// tests/cart.test.js - Resolved
const { test, expect } = require('@playwright/test');

test.describe('Cart Tests', () => {
  test('should add items to cart', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/cart');
    await page.click('#add-to-cart');
    const count = await page.locator('.cart-count').textContent();
    expect(count).toBe('1');
  });
});

Complete Resolution:

# Update all tests to new framework
$ git add tests/cart.test.js tests/payment.test.js

# Continue rebase
$ git rebase --continue

# Force push updated branch (only if not shared)
$ git push --force-with-lease origin test/checkout-flow

Advanced Conflict Resolution Strategies

Using Merge Tools

# Configure merge tool (VS Code)
$ git config --global merge.tool vscode
$ git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

# Launch merge tool during conflict
$ git mergetool

Choosing Conflict Resolution Strategy

# Keep all changes from main branch
$ git checkout --theirs tests/config.js
$ git add tests/config.js

# Keep all your changes
$ git checkout --ours tests/login.test.js
$ git add tests/login.test.js

Common Mistakes and Debugging

Mistake 1: Forgetting to Fetch Before Rebase

Problem:

$ git rebase origin/main
Current branch test/api-tests is up to date.
# But you know there are new commits!

Solution:

# Always fetch first
$ git fetch origin
$ git rebase origin/main

Mistake 2: Losing Work During Conflict Resolution

Problem: Accidentally accepting wrong changes during conflict resolution.

Solution:

# Use reflog to find your commits
$ git reflog
7a3d9e2 HEAD@{0}: rebase: abort
5b8c1f4 HEAD@{1}: rebase: Add username tests

# Restore to before rebase
$ git reset --hard HEAD@{1}

Mistake 3: Rebasing Published Branches

Problem: Force pushing rebased commits that others depend on.

Solution:

# Use merge instead for shared branches
$ git merge origin/main

# If you must rebase, use --force-with-lease
$ git push --force-with-lease origin test/feature-branch

Debugging Rebase Issues

# Check rebase status
$ cat .git/rebase-merge/head-name
refs/heads/test/login-automation

# View remaining commits to rebase
$ cat .git/rebase-merge/git-rebase-todo

# If stuck, abort and try again
$ git rebase --abort

# Skip problematic commit
$ git rebase --skip

Best Practices Checklist

✅ Always fetch latest changes before rebasing
✅ Work on a local branch, not shared/protected branches
✅ Commit your work before starting rebase
✅ Test your code after resolving conflicts
✅ Use --force-with-lease instead of --force when pushing
✅ Keep commits atomic and logically organized
✅ Write clear commit messages when squashing
✅ Document complex conflict resolutions in commit messages


Pro Tip: Create a backup branch before complex rebases:

$ git branch backup-before-rebase
$ git rebase -i origin/main
# If things go wrong: git reset --hard backup-before-rebase

Hands-On Practice

EXERCISE AND CONCLUSION

🏋️ Hands-On Exercise

Exercise: Resolve Rebase Conflicts in a Test Automation Suite

Scenario: You’re working on a feature branch that adds new API tests. Meanwhile, the main branch has been updated with refactored test utilities and new authentication methods. You need to rebase your feature branch onto the updated main branch and resolve conflicts.

Task

Rebase your feature branch with conflicting test files and successfully integrate changes from both branches while maintaining test functionality.

Step-by-Step Instructions

Setup Phase

  1. Clone the practice repository:
git clone https://github.com/your-org/test-automation-rebase-exercise.git
cd test-automation-rebase-exercise
  1. Examine the branches:
git checkout main
# Review the updated test utilities in utils/auth_helper.py
git checkout feature/api-tests
# Review your new API tests in tests/api/test_user_endpoints.py

Exercise Phase

  1. Initiate the rebase:
git checkout feature/api-tests
git rebase main
  1. Identify and analyze conflicts:

    • Check which files have conflicts using git status
    • Expected conflicts will be in:
      • tests/api/test_user_endpoints.py
      • conftest.py
      • utils/auth_helper.py
  2. Resolve each conflict:

    • Open conflicting files in your editor
    • Identify conflict markers (<<<<<<<, =======, >>>>>>>)
    • For utils/auth_helper.py: Merge the new token-based auth method with your API key additions
    • For conftest.py: Combine fixture updates from both branches
    • For test_user_endpoints.py: Update your tests to use the new auth helper methods
  3. Stage resolved files:

git add tests/api/test_user_endpoints.py
git add conftest.py
git add utils/auth_helper.py
  1. Continue the rebase:
git rebase --continue
  1. Handle any additional conflicts:

    • If more conflicts appear, repeat steps 5-7
    • If you make a mistake, use git rebase --abort and start over
  2. Verify the resolution:

# Run the test suite
pytest tests/api/test_user_endpoints.py -v

# Check test coverage
pytest --cov=utils --cov=tests/api

# Verify linting passes
flake8 tests/ utils/

Expected Outcome

After successfully completing the rebase:

  • ✅ All commits from your feature branch are replayed on top of main
  • ✅ Conflicts are resolved, preserving functionality from both branches
  • ✅ Tests pass with 100% success rate
  • ✅ No merge commits in history (clean linear history)
  • ✅ Your new API tests use the refactored authentication utilities
  • ✅ Code style and linting checks pass

Solution Approach

For utils/auth_helper.py conflict:

# WRONG - Keeping only one side
def authenticate():
    return get_api_key()  # Old method only

# CORRECT - Integrating both approaches
def authenticate(method='token'):
    """Support both token and API key authentication"""
    if method == 'token':
        return get_auth_token()  # From main branch
    elif method == 'api_key':
        return get_api_key()      # From feature branch
    else:
        raise ValueError(f"Unknown auth method: {method}")

For test_user_endpoints.py conflict:

# Update imports to use refactored utilities
from utils.auth_helper import authenticate

# Modify test setup to use new auth method
@pytest.fixture
def auth_headers():
    token = authenticate(method='token')  # Updated call
    return {"Authorization": f"Bearer {token}"}

def test_get_user(auth_headers):
    response = requests.get(API_URL + "/user", headers=auth_headers)
    assert response.status_code == 200

Recovery commands if needed:

# If you need to abort and restart
git rebase --abort

# If you need to skip a problematic commit
git rebase --skip

# To view the rebase in progress
git status
git log --oneline --graph

🎓 Key Takeaways

  • Rebase creates linear history by replaying your commits on top of the target branch, avoiding unnecessary merge commits and keeping the project history clean and readable

  • Conflict resolution during rebase requires careful analysis of both branches’ changes to preserve intended functionality; use git status, diff tools, and test execution to validate your resolutions

  • Interactive rebase (git rebase -i) is powerful for cleaning up commit history before sharing your work, allowing you to squash, reorder, edit, or drop commits strategically

  • Always run your test suite after resolving conflicts to ensure that integrated changes work correctly together; automated tests are your safety net during complex rebases

  • The rebase workflow suits feature branch development particularly well for test automation projects, where clean history and up-to-date integration with main branch changes are crucial for team collaboration


🚀 Next Steps

Practice These Skills

  1. Daily rebase practice: Before starting work each day, rebase your feature branches onto the latest main branch
  2. Interactive rebase cleanup: Practice using git rebase -i HEAD~5 to clean up your last 5 commits before creating pull requests
  3. Complex conflict scenarios: Intentionally create conflicts between test utilities, fixtures, and test cases to build confidence in resolution strategies
  4. Rebase with CI/CD: Set up pre-push hooks that run tests to catch integration issues early
  • Git Cherry-Pick: Selectively apply specific commits from one branch to another without full rebasing
  • Git Reflog: Recover from rebase mistakes by understanding the reference log and commit history
  • Merge Strategies: Learn when to use git merge --no-ff vs rebase for different workflow scenarios
  • Git Bisect: Use binary search through commit history to identify which commit introduced a test failure
  • Test Quarantine Strategies: Managing flaky tests during branch integration and rebase operations
  • CI/CD Pipeline Integration: Configuring automated test runs on rebased branches before merging
  • Git Hooks for Test Automation: Setting up pre-rebase and post-rebase hooks to enforce testing standards
  • Pro Git Book - Chapter 7: Advanced Rebasing Techniques
  • Practice repository: Create your own “conflict playground” with intentionally conflicting test scenarios
  • Git workflow documentation for your team’s specific branching strategy (Git Flow, GitHub Flow, Trunk-Based Development)