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.
Git Squash Merge Workflow Guide
A comprehensive guide to handling merge conflicts when using squash merging in your git workflow.
Table of Contents
- The Problem with Squash Merging
- The Wrong Approach (Don't Do This)
- How to Identify Which Commits Are Already Merged
- The Right Approach: Interactive Rebase
- Automating the Process with a Shell Script
- Workflow Summary
- Commands Reference
- Key Takeaway
- Disclaimer
The Problem with Squash Merging
When teams use squash merging for PRs, this creates a common conflict scenario:
- Create feature branch
- Make commits C1, C2, C3
- Raise PR after C2
- PR gets squash merged to master
- Continue working on same branch with C4, C5
- 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
- Open your merged PR in the web browser
- Go to the "Commits" tab
- Note ALL commits in the PR (from first to last)
- 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:
- Fetch the latest changes from
origin/master - Get commit hashes from the PR using
gh pr viewcommand - Create a temporary editor script that automatically modifies the rebase todo file
- Run interactive rebase with a custom
GIT_SEQUENCE_EDITORthat:- Finds all commits from the PR in the rebase todo list
- Comments them out (effectively dropping them)
- Keeps all other commits unchanged
- 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_EDITORto 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
ghCLI 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
Option 2: Git Alias (Recommended)
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:
git fetch originto get latest changesgit rebase -i origin/masterto interactively handle conflicts- Drop commits that were already merged
- 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.