Module 6: Merge vs Rebase: Choosing the Right Strategy

Compare merge and rebase strategies side-by-side using real test automation scenarios. Understand the trade-offs between preserving complete history vs. maintaining clean linear history. Learn team conventions and when each approach is most appropriate for test projects.

Merge vs Rebase: Understanding the Core Differences

Why This Matters

As a test automation engineer, you’ve likely encountered this scenario: You’re working on a feature branch developing new test cases, and when it’s time to integrate your changes back to the main branch, you face a critical decision—should you merge or rebase?

This decision isn’t just academic. It directly impacts:

  • Code review clarity: Can reviewers easily understand your test changes, or are they buried in a tangle of merge commits?
  • Bug investigation: When a test starts failing, can you quickly trace through a clean history to identify what changed, or must you navigate through dozens of merge commits?
  • Team collaboration: Does your team’s Git history tell a clear story of feature development, or does it resemble a tangled web that confuses everyone?
  • CI/CD pipeline stability: Does your branching strategy minimize integration conflicts that break automated test runs?

The real-world pain points are significant. Teams that don’t understand these strategies often end up with:

  • Test histories so cluttered that bisecting to find which commit broke tests becomes nearly impossible
  • Merge conflicts that could have been avoided with proper rebasing
  • Confusion about when changes were actually integrated versus when they were originally written
  • Disagreements and inconsistent practices across team members

Choosing the right strategy for your test automation project isn’t about one being “better” than the other—it’s about understanding the trade-offs and applying the right tool at the right time.

What You’ll Accomplish

By the end of this lesson, you’ll move from uncertainty to confidence in managing your test automation branches. Here’s how we’ll get you there:

Comparing Strategies Through Real Scenarios
You’ll work through practical test automation examples—adding new test cases, refactoring test frameworks, and handling long-running feature branches. For each scenario, you’ll see side-by-side comparisons of merge versus rebase outcomes, complete with visual representations of the resulting Git history.

Understanding the Critical Trade-offs
You’ll learn why merge preserves the complete historical context (including when branches diverged and converged) while rebase creates a cleaner, linear narrative. We’ll explore concrete examples from test projects showing when each approach shines and when it creates problems. You’ll understand concepts like “destructive” operations, shared branch considerations, and the famous “golden rule of rebasing.”

Mastering Team Conventions and Best Practices
You’ll discover common team workflows used in successful test automation projects: when to use merge for feature integration, when rebase makes sense for personal cleanup, and how to establish clear conventions. We’ll cover decision frameworks based on branch types (personal vs. shared), project phases (active development vs. release preparation), and team size considerations.

This isn’t just theory—you’ll gain actionable knowledge you can apply immediately to your test automation projects, making decisions that align with your team’s needs and project goals.


Core Content

Core Content: Merge vs Rebase - Understanding the Core Differences

Core Concepts Explained

What is Git Merge?

Git merge combines the changes from two branches by creating a new “merge commit” that ties together the histories of both branches. This approach preserves the complete history of both branches, including when they diverged and when they came back together.

How Merge Works:

  1. Git identifies the common ancestor commit of both branches
  2. Git compares the changes from both branches since that ancestor
  3. Git creates a new commit that combines these changes
  4. This new commit has two parent commits (one from each branch)
graph LR
    A[Common Ancestor] --> B[Feature Branch Commit 1]
    A --> C[Main Branch Commit 1]
    B --> D[Feature Branch Commit 2]
    C --> E[Main Branch Commit 2]
    D --> F[Merge Commit]
    E --> F

What is Git Rebase?

Git rebase rewrites commit history by moving your branch’s commits to start from a different base commit. Instead of creating a merge commit, rebase applies your commits one by one on top of the target branch, creating a linear history.

How Rebase Works:

  1. Git identifies your branch’s commits
  2. Git temporarily removes these commits
  3. Git moves your branch pointer to the target branch
  4. Git replays your commits one by one on the new base
  5. Each commit gets a new SHA (commit ID)
graph LR
    A[Common Ancestor] --> B[Main Branch Commit 1]
    B --> C[Main Branch Commit 2]
    C --> D[Feature Commit 1']
    D --> E[Feature Commit 2']
    
    style D fill:#90EE90
    style E fill:#90EE90

Key Differences at a Glance

Aspect Merge Rebase
History Non-linear with merge commits Linear, clean history
Commit IDs Original commits preserved New commit IDs created
Traceability Shows when branches merged Hides branch integration points
Safety Safe for shared branches Dangerous for shared branches
Conflicts Resolved once May resolve multiple times

Practical Code Examples

Setting Up a Practice Repository

Let’s create a realistic scenario to understand both approaches:

# Create a new test repository
mkdir git-merge-rebase-demo
cd git-merge-rebase-demo
git init

# Create initial commit on main
echo "# Test Automation Project" > README.md
git add README.md
git commit -m "Initial commit"

# Create a test file
mkdir tests
echo "describe('Login Tests', () => {
  it('should login successfully', () => {
    // test code
  });
});" > tests/login.test.js
git add tests/
git commit -m "Add login tests"

Scenario 1: Using Git Merge

# Create a feature branch for new test cases
git checkout -b feature/add-signup-tests

# Add new test file
echo "describe('Signup Tests', () => {
  it('should signup with valid data', () => {
    cy.visit('https://practiceautomatedtesting.com/signup');
    cy.get('#email').type('user@test.com');
    cy.get('#password').type('SecurePass123');
    cy.get('#submit').click();
    cy.url().should('include', '/dashboard');
  });
});" > tests/signup.test.js

git add tests/signup.test.js
git commit -m "Add signup test cases"

# Add another commit
echo "  it('should show error for invalid email', () => {
    cy.visit('https://practiceautomatedtesting.com/signup');
    cy.get('#email').type('invalid-email');
    cy.get('#submit').click();
    cy.get('.error').should('contain', 'Invalid email');
  });" >> tests/signup.test.js

git add tests/signup.test.js
git commit -m "Add email validation test"

# Meanwhile, someone updates main branch
git checkout main
echo "module.exports = { testTimeout: 30000 };" > jest.config.js
git add jest.config.js
git commit -m "Add Jest configuration"

# Now merge feature branch into main
git merge feature/add-signup-tests

Result of Merge:

$ git log --oneline --graph
*   a1b2c3d (HEAD -> main) Merge branch 'feature/add-signup-tests'
|\  
| * e4f5g6h Add email validation test
| * h7i8j9k Add signup test cases
* | l0m1n2o Add Jest configuration
|/  
* p3q4r5s Add login tests
* t6u7v8w Initial commit

Scenario 2: Using Git Rebase

# Create another feature branch
git checkout -b feature/add-api-tests

# Add API test file
echo "describe('API Tests', () => {
  it('should fetch user data', async () => {
    const response = await fetch('https://practiceautomatedtesting.com/api/users/1');
    const data = await response.json();
    expect(data.id).toBe(1);
  });
});" > tests/api.test.js

git add tests/api.test.js
git commit -m "Add API tests"

# Add another commit
echo "  it('should handle API errors', async () => {
    const response = await fetch('https://practiceautomatedtesting.com/api/users/999');
    expect(response.status).toBe(404);
  });" >> tests/api.test.js

git add tests/api.test.js
git commit -m "Add API error handling test"

# Someone updates main again
git checkout main
echo "CI=true npm test" > .github/workflows/test.yml
git add .github/
git commit -m "Add CI workflow"

# Rebase feature branch onto updated main
git checkout feature/add-api-tests
git rebase main

Result of Rebase:

$ git log --oneline --graph
* z9y8x7w (HEAD -> feature/add-api-tests) Add API error handling test
* w6v5u4t Add API tests
* c3b2a1n (main) Add CI workflow
* a1b2c3d Merge branch 'feature/add-signup-tests'
|\  
| * e4f5g6h Add email validation test
| * h7i8j9k Add signup test cases
* | l0m1n2o Add Jest configuration
|/  
* p3q4r5s Add login tests
* t6u7v8w Initial commit

Handling Merge Conflicts

During Merge:

# If conflicts occur during merge
git merge feature/conflicting-branch

# Output:
# Auto-merging tests/login.test.js
# CONFLICT (content): Merge conflict in tests/login.test.js
# Automatic merge failed; fix conflicts and then commit the result.

# View conflicted files
git status

# Edit the conflicted file to resolve
# File will show conflict markers:
<<<<<<< HEAD
describe('Login Tests', () => {
  it('should login with email', () => {
=======
describe('Login Test Suite', () => {
  it('should authenticate user', () => {
>>>>>>> feature/conflicting-branch

# After resolving, mark as resolved
git add tests/login.test.js
git commit -m "Merge feature/conflicting-branch and resolve conflicts"

During Rebase:

# If conflicts occur during rebase
git rebase main

# Output:
# CONFLICT (content): Merge conflict in tests/login.test.js
# error: could not apply abc123... Update login tests

# Resolve conflict in the file, then:
git add tests/login.test.js
git rebase --continue

# If you want to abort:
git rebase --abort

# Note: You may need to resolve conflicts for EACH commit being rebased

Interactive Rebase for Clean History

Interactive rebase is powerful for cleaning up your commits before merging:

# View last 3 commits
git log --oneline -3

# Start interactive rebase
git rebase -i HEAD~3

# Editor opens with:
pick abc123 Add initial test structure
pick def456 Fix typo in test
pick ghi789 Add more test cases

# You can reorder, squash, or edit commits:
pick abc123 Add initial test structure
squash def456 Fix typo in test  # Combines with previous commit
pick ghi789 Add more test cases

# Save and close editor
# Git will combine commits and ask for new commit message

Practical Example: Complete Test Automation Workflow

# Feature development workflow using rebase
git checkout -b feature/login-automation

# Create test file
echo "import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
  test('successful login with valid credentials', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#username', 'testuser@example.com');
    await page.fill('#password', 'Test123!');
    await page.click('button[type=\"submit\"]');
    
    // Verify successful login
    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('.welcome-message')).toBeVisible();
  });

  test('login fails with invalid credentials', async ({ page }) => {
    await page.goto('https://practiceautomatedtesting.com/login');
    await page.fill('#username', 'wrong@example.com');
    await page.fill('#password', 'wrongpass');
    await page.click('button[type=\"submit\"]');
    
    // Verify error message
    await expect(page.locator('.error-message')).toContainText('Invalid credentials');
  });
});" > tests/login.spec.js

git add tests/login.spec.js
git commit -m "Add login automation tests"

# Keep your branch updated with main
git fetch origin main
git rebase origin/main

# Clean up commits before merging
git rebase -i origin/main

# Push to remote (force push needed after rebase)
git push origin feature/login-automation --force-with-lease

# Create pull request, then merge using GitHub/GitLab UI

Common Mistakes Section

Mistake 1: Rebasing Shared/Public Branches

❌ Don’t do this:

# Never rebase commits that others are working on
git checkout main
git rebase feature/some-branch  # DANGEROUS if others have pulled main

✅ Do this instead:

# Use merge for shared branches
git checkout main
git merge feature/some-branch

Why: Rebase changes commit IDs, causing confusion for anyone who already has the old commits.

Mistake 2: Forgetting to Push After Rebase

Problem:

git rebase main
git push origin feature-branch
# Error: Updates were rejected because the tip of your current branch is behind

Solution:

# Use force-with-lease for safety
git push origin feature-branch --force-with-lease

# Never use plain --force unless you're absolutely sure

Mistake 3: Losing Work During Rebase Conflicts

Recovery steps:

# If you mess up during rebase
git rebase --abort  # Cancels rebase and returns to original state

# If you already completed a problematic rebase
git reflog  # Shows history of HEAD changes
git reset --hard HEAD@{2}  # Reset to before rebase

Mistake 4: Using Merge When History Should Be Clean

Scenario: Feature branch has many “WIP” and “fix typo” commits.

❌ Direct merge creates messy history:

git merge feature/messy-branch  # Brings all messy commits into main

✅ Clean up first with interactive rebase:

git checkout feature/messy-branch
git rebase -i main  # Squash unnecessary commits
git checkout main
git merge feature/messy-branch  # Now history is clean

Debugging Common Issues

Issue: “Cannot rebase: You have unstaged changes”

# Solution 1: Stash changes
git stash
git rebase main
git stash pop

# Solution 2: Commit changes first
git add .
git commit -m "WIP: Save current work"
git rebase main

Issue: Rebase creates duplicate commits

# This happens when commits were already merged
# Prevention: Don't rebase commits that exist on main

# Fix: Reset to origin
git reset --hard origin/feature-branch

Issue: Lost commits after rebase

# Check reflog to find lost commits
git reflog

# Restore lost commits
git cherry-pick abc123  # Pick specific commit
# or
git reset --hard HEAD@{5}  # Reset to before rebase

Best Practices Summary

  1. Use Merge when:

    • Working on shared/public branches
    • You want to preserve complete history
    • Multiple people collaborate on same branch
  2. Use Rebase when:

    • Updating your personal feature branch with main
    • Cleaning up commits before creating PR
    • Creating linear history for easier debugging
  3. Golden Rule:

    • Never rebase commits that have been pushed to shared branches
    • Always use --force-with-lease instead of --force
    • Communicate with team when force-pushing

Hands-On Practice

EXERCISE

Hands-On Exercise: Merge vs Rebase in Action

Objective

Practice both merge and rebase workflows to understand their effects on Git history and learn when to use each approach.

Scenario

You’re working on a feature branch while your teammate has made updates to the main branch. You’ll experience both workflows to see how they handle integrating changes differently.

Prerequisites

  • Git installed on your system
  • Basic familiarity with Git commands
  • A terminal/command prompt

Step-by-Step Instructions

Part 1: Setup the Repository

# Create a new directory and initialize Git
mkdir git-merge-vs-rebase
cd git-merge-vs-rebase
git init

# Create initial commit on main
echo "# Project README" > README.md
git add README.md
git commit -m "Initial commit"

# Create and commit a base file
echo "Version 1.0" > version.txt
git add version.txt
git commit -m "Add version file"

Part 2: Experience Merge Workflow

# Create a feature branch
git checkout -b feature-merge
echo "function calculateTotal() {}" > calculator.js
git add calculator.js
git commit -m "Add calculator function"

# Switch back to main and make competing changes
git checkout main
echo "Version 1.1" > version.txt
git add version.txt
git commit -m "Update version to 1.1"

echo "## Contributors" >> README.md
git add README.md
git commit -m "Add contributors section"

# Now merge feature-merge into main
git merge feature-merge

Expected Outcome:

  • A merge commit is created
  • Run git log --oneline --graph --all to see the history
  • Note the parallel branches that converge

Part 3: Experience Rebase Workflow

# Reset to before the merge
git reset --hard HEAD~1

# Create another feature branch
git checkout -b feature-rebase
echo "function calculateTax() {}" > tax.js
git add tax.js
git commit -m "Add tax calculation"

echo "function formatCurrency() {}" >> tax.js
git add tax.js
git commit -m "Add currency formatter"

# Rebase onto main
git rebase main

Expected Outcome:

  • Your commits are replayed on top of main
  • Run git log --oneline --graph to see linear history
  • No merge commit created

Part 4: Compare the Histories

# Check out the merged branch
git checkout main
git merge feature-merge  # Complete the earlier merge
git log --oneline --graph --all > merge-history.txt

# Now look at rebased history
git checkout feature-rebase
git log --oneline --graph > rebase-history.txt

# Compare both files
cat merge-history.txt
cat rebase-history.txt

Challenge Tasks

  1. Identify the differences: What do you notice about commit hashes before and after rebase?
  2. Conflict resolution: Create a deliberate conflict and resolve it using both merge and rebase
  3. Interactive rebase: Try git rebase -i main on a feature branch with multiple commits

Expected Results

Merge Output:

  • Branch history shows divergence and convergence
  • Original commit hashes preserved
  • Merge commit visible with two parents
  • Complete historical context maintained

Rebase Output:

  • Linear commit history
  • New commit hashes for rebased commits
  • Clean, straight-line progression
  • No merge commit

Solution Approach

The exercise demonstrates that:

  • Merge preserves complete history, including when branches diverged
  • Rebase creates a clean linear history by rewriting commits
  • Commit hashes change after rebase (important for shared branches!)
  • Both achieve the same end result in terms of file contents

CONCLUSION

Key Takeaways

Merge creates a merge commit that preserves the complete branching history, showing exactly when and where branches diverged and converged. Use merge for:

  • Public/shared branches
  • Preserving complete project history
  • Feature branches in collaborative environments

Rebase rewrites commit history by replaying commits on top of another branch, creating a linear progression. Use rebase for:

  • Private/local branches before pushing
  • Cleaning up local commits
  • Maintaining a readable, linear project history

Commit hashes change during rebase because commits are being recreated with new parent references. This is why you should never rebase commits that have been pushed to a shared repository.

Both methods integrate changes from one branch to another but with different philosophical approaches: merge is about preserving truth, rebase is about telling a clear story.

Choose based on context: Use merge for transparency and collaboration; use rebase for cleanliness and clarity in your local work before sharing.

Next Steps

Practice These Skills

  1. Daily workflow practice: Use rebase for your local feature branches, merge when integrating to main
  2. Conflict resolution: Intentionally create conflicts and practice resolving them in both workflows
  3. Interactive rebase: Master git rebase -i for squashing, reordering, and editing commits
  4. Team scenarios: Practice on a repository with multiple contributors to see real-world implications
  • Git rebase –onto: Advanced rebasing for complex branch scenarios
  • Git merge strategies: Fast-forward, recursive, and ours/theirs options
  • Pull request workflows: How merge vs rebase affects PR reviews
  • Git reflog: Recovering from rebase mistakes
  • Squash merging: Combining multiple commits when merging PRs
  • Cherry-picking: Applying specific commits across branches
  • Forking workflows: How rebase applies in open-source contributions
  • Practice in a safe environment before using on production code
  • Set up Git aliases for common merge/rebase workflows
  • Learn your team’s Git workflow conventions
  • Explore git log options for visualizing different histories