The nightmare that is squash merge ft Github

Squash merging causes merge conflicts after PRs are merged. Learn why this happens and how to fix it with interactive rebase—plus an automated git alias that drops merged commits with a single command, saving you from repetitive conflict resolution.

The nightmare that is squash merge ft Github
Photo by Mitchell Luo / Unsplash

Git Squash Merge Workflow Guide

A comprehensive guide to handling merge conflicts when using squash merging in your git workflow.


Table of Contents

  1. The Problem with Squash Merging
  2. The Wrong Approach (Don't Do This)
  3. How to Identify Which Commits Are Already Merged
  4. The Right Approach: Interactive Rebase
  5. Automating the Process with a Shell Script
  6. Workflow Summary
  7. Commands Reference
  8. Key Takeaway
  9. Disclaimer

The Problem with Squash Merging

When teams use squash merging for PRs, this creates a common conflict scenario:

  1. Create feature branch
  2. Make commits C1, C2, C3
  3. Raise PR after C2
  4. PR gets squash merged to master
  5. Continue working on same branch with C4, C5
  6. Try to pull origin/master → CONFLICTS

The conflicts happen because:

  • Local branch has C1, C2, C3, C4, C5
  • Master has [C1+C2]* (squashed commit with different hash)
  • Git sees C1 and C2 as different from [C1+C2]*

The Wrong Approach (Don't Do This)

# ❌ This causes conflicts
git pull origin master

This creates merge conflicts even though the code is identical.


How to Identify Which Commits Are Already Merged

This is the key question: which commits should you drop (rebase) or skip (cherry-pick)?

The simplest approach: Open your PR on GitHub/GitLab

  1. Open your merged PR in the web browser
  2. Go to the "Commits" tab
  3. Note ALL commits in the PR (from first to last)
  4. During interactive rebase, delete lines for ALL those commits (not just first and last - everything in between too!)

Example:

Open PR #42 on GitHub:
Commits tab shows:
  ✓ aaa111 - Setup auth routes       ← First commit in PR
  ✓ bbb222 - Add middleware           ← Middle commit in PR
  ✓ ccc333 - Add login form           ← Last commit in PR

You must delete ALL three commits (aaa111, bbb222, ccc333)!

During git rebase -i origin/master, the editor shows:
pick aaa111 Setup auth routes
pick bbb222 Add middleware
pick ccc333 Add login form
pick ddd444 Fix auth redirect         ← NEW (keep)
pick eee555 Add session timeout       ← NEW (keep)
pick fff666 Add logout button         ← NEW (keep)

Option A - Delete entire lines (easiest):
pick ddd444 Fix auth redirect         ← Keep
pick eee555 Add session timeout       ← Keep
pick fff666 Add logout button         ← Keep

Option B - Change "pick" to "drop":
drop aaa111 Setup auth routes         ← Drop
drop bbb222 Add middleware             ← Drop
drop ccc333 Add login form             ← Drop
pick ddd444 Fix auth redirect         ← Keep
pick eee555 Add session timeout       ← Keep
pick fff666 Add logout button         ← Keep

Option C - Comment out with #:
# pick aaa111 Setup auth routes      ← Drop
# pick bbb222 Add middleware          ← Drop
# pick ccc333 Add login form          ← Drop
pick ddd444 Fix auth redirect         ← Keep
pick eee555 Add session timeout       ← Keep
pick fff666 Add logout button         ← Keep

All three methods work the same! Deleting the line is usually fastest.


The Right Approach: Interactive Rebase

# Fetch latest changes
git fetch origin

# Interactive rebase - drop merged commits
git rebase -i origin/master

# In editor, drop merged commits by either:
# 1. Deleting the entire line (easiest)
# 2. Changing "pick" to "drop"
# 3. Commenting out with #
# Keep (pick) new commits that aren't in master yet

How the commit history looks:

BEFORE interactive rebase:
feature branch: A → B → C1 → C2 → C3 → C4
master:         A → B → [C1+C2]* (squashed)

During rebase editor (before your edits):
pick C1  # Already in master
pick C2  # Already in master
pick C3  # New work
pick C4  # New work

After your edits (just delete C1 and C2 lines):
pick C3  # New work
pick C4  # New work

AFTER interactive rebase:
feature branch: A → B → [C1+C2]* → C3 → C4
master:         A → B → [C1+C2]*

Result: Your feature branch now has the same base as master (including the squashed commit), plus only your new commits (C3, C4). The original C1 and C2 are gone, replaced by the squashed version from master.


Automating the Process with a Shell Script

Instead of manually editing the rebase file, you can automate the entire process with a single command.

How It Works

The automation follows these steps:

  1. Fetch the latest changes from origin/master
  2. Get commit hashes from the PR using gh pr view command
  3. Create a temporary editor script that automatically modifies the rebase todo file
  4. Run interactive rebase with a custom GIT_SEQUENCE_EDITOR that:
    • Finds all commits from the PR in the rebase todo list
    • Comments them out (effectively dropping them)
    • Keeps all other commits unchanged
  5. Clean up temporary files - Delete the temporary editor script that was created in step 3

Why a Temporary Editor Script?

When you run git rebase -i origin/master, git creates a todo file with all commits and opens it in your editor (vim, nano, etc.). You manually edit the file, save, and close it. Git then reads your changes and performs the rebase.

To automate this:

  • We can't directly edit the file because git hasn't created it yet
  • We need to intercept the moment when git opens the editor
  • We use GIT_SEQUENCE_EDITOR to tell git: "Instead of opening vim/nano, run THIS script"
  • Our script acts as a fake "editor" that:
    • Receives the todo file path from git
    • Reads it, modifies it (drops the commits), saves it
    • Exits immediately (no human interaction needed)
  • Git continues with the rebase using our modified file

The temporary script exists only during the rebase operation, then gets deleted.

Prerequisites

  • gh CLI must be installed and authenticated
  • You must be in a git repository
  • The PR must be accessible (merged or open)

Option 1: Standalone Shell Script

Create a file called rebase-drop-pr.sh:

#!/bin/bash

# Usage: ./rebase-drop-pr.sh <PR-number>
# Example: ./rebase-drop-pr.sh 42

set -e

if [ -z "$1" ]; then
  echo "Usage: $0 <PR-number>"
  exit 1
fi

PR_NUMBER=$1

echo "Fetching commits from PR #$PR_NUMBER..."
COMMITS=$(gh pr view "$PR_NUMBER" --json commits --jq '.commits[].oid' | cut -c1-7)

if [ -z "$COMMITS" ]; then
  echo "Error: Could not fetch commits from PR #$PR_NUMBER"
  exit 1
fi

echo "Commits to drop: $COMMITS"

EDITOR_SCRIPT=$(mktemp)
cat > "$EDITOR_SCRIPT" << 'EDITORSCRIPT'
#!/bin/bash
while IFS= read -r line; do
  should_drop=false
  for commit in $COMMITS_TO_DROP; do
    if [[ "$line" == pick\ $commit* ]]; then
      should_drop=true
      break
    fi
  done
  if [ "$should_drop" = true ]; then
    echo "# $line  # AUTO-DROPPED"
  else
    echo "$line"
  fi
done < "$1" > "$1.new"
mv "$1.new" "$1"
EDITORSCRIPT

chmod +x "$EDITOR_SCRIPT"
export COMMITS_TO_DROP="$COMMITS"
git fetch origin
echo "Starting interactive rebase..."
GIT_SEQUENCE_EDITOR="$EDITOR_SCRIPT" git rebase -i origin/master
rm "$EDITOR_SCRIPT"

echo "✅ Done! Commits from PR #$PR_NUMBER have been dropped."

Setup:

# Save the script and make it executable
chmod +x ~/rebase-drop-pr.sh

# Use it
~/rebase-drop-pr.sh 42

Add it as a git alias to your ~/.gitconfig for easier access:

git config --global alias.drop-pr '!f() {
  if [ -z "$1" ]; then
    echo "Usage: git drop-pr <PR-number>"
    return 1
  fi
  echo "Fetching commits from PR #$1..."
  COMMITS=$(gh pr view "$1" --json commits --jq ".commits[].oid" | cut -c1-7)
  if [ -z "$COMMITS" ]; then
    echo "Error: Could not fetch commits from PR #$1"
    return 1
  fi
  echo "Commits to drop: $COMMITS"
  EDITOR_SCRIPT=$(mktemp)
  cat > "$EDITOR_SCRIPT" << "EDITORSCRIPT"
#!/bin/bash
while IFS= read -r line; do
  should_drop=false
  for commit in $COMMITS_TO_DROP; do
    if [[ "$line" == pick\ $commit* ]]; then
      should_drop=true
      break
    fi
  done
  if [ "$should_drop" = true ]; then
    echo "# $line  # AUTO-DROPPED"
  else
    echo "$line"
  fi
done < "$1" > "$1.new"
mv "$1.new" "$1"
EDITORSCRIPT
  chmod +x "$EDITOR_SCRIPT"
  export COMMITS_TO_DROP="$COMMITS"
  git fetch origin
  GIT_SEQUENCE_EDITOR="$EDITOR_SCRIPT" git rebase -i origin/master
  rm "$EDITOR_SCRIPT"
  echo "✅ Done! Commits from PR #$1 have been dropped."
}; f'

This command adds the alias to your global git configuration. You only need to run it once.

Usage

Once the alias is added, simply run:

git drop-pr 42 

Example output:

$ git drop-pr 42
Fetching commits from PR #42...
Commits to drop: aaa111 bbb222 ccc333
Fetching latest changes from origin/master...
Starting interactive rebase...
Successfully rebased and updated refs/heads/feature-auth.
✅ Done! Commits from PR #42 have been dropped.

That's it! One command automatically drops all merged commits from the specified PR.


Commands Reference

# Safe fetch without conflicts
git fetch origin

# Check what commits are in master but not in your branch
git log HEAD..origin/master --oneline

# Check what commits are in your branch but not in master
git log origin/master..HEAD --oneline

# Interactive rebase to clean up
git rebase -i origin/master

# Create fresh branch from master
git checkout -b new-branch origin/master

# Abort rebase if needed
git rebase --abort

Key Takeaway

Never use git pull origin master when working on a branch that has had a PR squash merged.

Instead:

  1. git fetch origin to get latest changes
  2. git rebase -i origin/master to interactively handle conflicts
  3. Drop commits that were already merged
  4. Keep commits that are new

Pro tip: Use the automation script above to drop commits with a single command!


Acknowledgment

💡 This guide was enhanced with AI assistance to provide thorough documentation and practical solutions for squash merge challenges.