It’s 6:47 p.m. The office is quiet, the coffee is cold, and you’re one merge away from calling it a day. You open the PR, skim the diff, checks are green, your brain is… not. Click. Merge. Slack lights up: “Hey, did we just break staging?” Your stomach drops. Wrong commits. Wrong branch. Wrong end of the day.
If that scene feels familiar, you’re not alone. PRs go sideways for all kinds of human reasons: fatigue, branch drift, last‑minute refactors. The good news is that Git gives you several safe escape hatches—and you don’t need to rewrite history (or your evening plans) to use them. The trick is choosing the right path for the situation: is the PR still open, or was it merged? If merged, was it a merge commit, a squash, or a rebase & merge? Each answer points to a clean, auditable undo.
This article walks you through those decisions and gives you copy‑paste commands using GitHub CLI (gh). We’ll show how to spot the merge style, create a revert PR (the safest fix on protected branches), and—only when you truly must—roll back with a controlled history rewrite. Keep it handy as a playbook for the next “oh no” moment.
TL;DR
- Open PR: close it or fix the PR branch (reset/revert on the branch, then push).
- Merged PR: prefer a revert commit (keeps history, works on protected branches).
- Only rewrite history (hard reset + force-push) if your team agrees and the branch allows it.
Prerequisites
- Git installed (
git --version) - GitHub CLI installed and logged in:
gh auth status -h github.com - Know the PR number and the base branch (the branch the PR merged into)
|
1 2 3 4 5 |
# Optional: set variables to avoid typos PR=713 BASE=main # or staging, release/x.y, etc. REMOTE=origin |
Step 1 — Is the PR open or merged?
Before you type a single git revert, pause and classify the situation. If the PR is still open, you can change what the PR contains by editing its branch—or simply close it—without touching shared history. If it’s already merged, you’re no longer editing the PR; you’re repairing the base branch. From here on, the goal is to minimize blast radius: choose fixes that are reviewable, auditable, and compatible with protected-branch rules.
If the PR is open (not merged yet)
Option A — Close it
|
1 2 3 |
# Close and optionally delete the branch in one command gh pr close $PR --delete-branch |
Option B — Keep it open but change its contents
Reset the PR branch to a safe point or revert problematic commits on that branch, then push. The PR diff updates automatically.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# Example: reset PR branch to match BASE, making the PR effectively empty BRANCH=$(gh pr view $PR --json headRefName -q .headRefName) git fetch $REMOTE git checkout $BRANCH # Either hard reset... git reset --hard $REMOTE/$BASE # ...or selectively revert specific SHAs on the PR branch: # git revert <sha1> [<sha2> ...] git push --force-with-lease |
If the PR is merged
You have three merge styles to consider because the undo steps differ slightly:
- Merge commit (one new commit with two parents)
- Squash merge (one new single‑parent commit)
- Rebase & merge (no new merge commit; multiple single‑parent commits landed)
Step 2 — Determine how the PR was merged
Undo work is only as clean as your diagnosis. A merge commit has two parents and can be reverted with one command; a squash or rebase & merge lands single-parent commit(s) that you’ll revert by SHA. Use the PR page and gh so you don’t guess: identify the exact shape, then choose the matching play below. Getting this right prevents double-reverts, orphaned changes, and mysterious conflicts later.
Fastest: GitHub UI
Open the merged PR page and look for the label: “Merged”, “Squashed and merged”, or “Rebased and merged.”
From the CLI:
|
1 2 3 4 5 6 |
# See base/head branch names (sanity check) gh pr view $PR --json baseRefName,headRefName -q '"base=\(.baseRefName) head=\(.headRefName)"' # Try to fetch the merge commit directly (present only for merge-commit strategy) gh pr view $PR --json mergeCommit -q .mergeCommit.oid |
If the mergeCommit OID exists → it was a merge commit. If not, it was squash or rebase.
You can also search the base branch history:
|
1 2 3 4 |
git fetch $REMOTE # Merge-commit style creates a message like "Merge pull request #<PR>" commit_merge=$(git log $REMOTE/$BASE --merges --grep="Merge pull request #$PR" -n1 --format=%H) && echo $commit_merge |
Step 3 — Undo the merged PR (safe way: make a revert commit)
Think of a revert PR as an antidote: it introduces a commit that mathematically cancels the original change, preserves linear history, and keeps auditors and protected-branch policies happy. It’s also reversible—if you later decide the original change was fine, you can “revert the revert” and roll forward cleanly. Pick the case that matches the merge style you found in Step 2.
This creates a new commit that reverses the PR. It’s the preferred approach on protected branches.
Case A) The PR was a merge commit
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# 1) Identify the merge commit SHA MERGE_SHA=$(gh pr view $PR --json mergeCommit -q .mergeCommit.oid) # If that returns empty, fall back to git log as shown above # 2) Create a revert branch and revert the merge git checkout $BASE git pull git checkout -b revert/pr-$PR # -m 1 tells Git the mainline (parent) is the base branch git revert -m 1 "$MERGE_SHA" # 3) Push and open a PR for the revert git push -u $REMOTE HEAD gh pr create --fill --base $BASE |
Notes
- If you hit conflicts: resolve them,
git add -A, thengit revert --continue. -m 1is required so Git knows which parent reflects the base branch.
Case B) The PR was a squash merge (single commit)
|
1 2 3 4 5 6 7 8 9 10 11 |
# 1) Find the squash commit on the base branch (from PR page or git log) SQUASH_SHA=<the_squash_commit_sha> # 2) Revert it on a revert branch git checkout $BASE && git pull git checkout -b revert/pr-$PR git revert "$SQUASH_SHA" git push -u $REMOTE HEAD gh pr create --fill --base $BASE |
Case C) The PR was rebase & merge (several single-parent commits)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# 1) Identify the commit range introduced by the PR (from the PR page or git log) # 2) Revert them (newest → oldest is safest) git checkout $BASE && git pull git checkout -b revert/pr-$PR # If you have the SHAs (newest first): git revert <sha_newest> <sha_older> <sha_oldest> # Or stage them without committing one by one: # git revert --no-commit <sha_newest> <sha_older> <sha_oldest> # git commit -m "Revert PR #$PR" git push -u $REMOTE HEAD gh pr create --fill --base $BASE |
GitHub UI shortcut
On the merged PR page, click Revert (if available). GitHub opens a new PR that undoes the changes. You’ll still need to resolve conflicts if they exist.
Step 4 — Verify the undo
A good undo includes proof. Before you merge the revert PR, diff the branch to confirm you’re removing exactly what you intend and nothing more; kick off CI and any smoke tests. After merge, scan the short log and sanity-check your environment. If something else moved underneath you—another PR merged, config changed—you’ll catch it now instead of rediscovering it at 6:47 p.m. tomorrow.
|
1 2 3 4 5 6 |
# Before opening/merging the revert PR git diff $REMOTE/$BASE...HEAD # After merging the revert PR, ensure the base is clean git fetch $REMOTE git log $REMOTE/$BASE --oneline -n 5 |
Optionally run your test suite or CI locally to be extra sure.
Advanced: the destructive option (history rewrite)
This is the fire alarm behind glass. A hard reset + force-push rewrites commit hashes for everyone, can break open PRs, and may surprise teammates who’ve already pulled. Only use it with explicit coordination (ideally during a freeze window). If you must, protect collaborators with --force-with-lease and immediately communicate the new tip SHA.
Avoid unless your team explicitly approves and the branch allows force-push.
|
1 2 3 4 5 6 7 8 9 10 |
# Move the base branch pointer back to before the merge SAFE_SHA=<commit_before_the_PR> git checkout $BASE git pull git reset --hard "$SAFE_SHA" # Protect collaborators: use --force-with-lease, not --force git push --force-with-lease |
When to use: hotfixing a broken merge on a non‑protected branch or a release branch where linear history is required and the team agrees to rewrite.
Common pitfalls & fixes
- Conflicts during revert: resolve like any merge; continue with
git revert --continue. - Reverting the wrong commit:
git revert <bad_revert_sha>reverts the revert (restores the original change). - PR was partially reverted earlier: prefer
git revert --no-commit <sha...>for all SHAs, then one cohesive commit. - Submodules: ensure submodule commits exist on remotes; may need to update submodule pointers separately.
- CI/workflows: reverting a workflow file may require re‑running checks; use
gh runor the Actions UI.
At‑a‑glance cheat sheet
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
# Detect merge method MERGE_SHA=$(gh pr view $PR --json mergeCommit -q .mergeCommit.oid) if [ -n "$MERGE_SHA" ]; then METHOD=merge else METHOD=$(gh pr view $PR --json state,mergeable,headRefName,baseRefName -q '"squash_or_rebase"') fi git fetch $REMOTE git checkout $BASE && git pull git checkout -b revert/pr-$PR if [ "$METHOD" = merge ]; then git revert -m 1 "$MERGE_SHA" else # fall back: pick the squash/rebase SHAs from git log or PR page # git revert <sha_newest> ... echo "Identify the landed SHAs and revert them (see guide)." fi git push -u $REMOTE HEAD gh pr create --fill --base $BASE |
Appendix: find the right SHAs quickly
- Merge commit SHA:
1234gh pr view $PR --json mergeCommit -q .mergeCommit.oid# orgit log $REMOTE/$BASE --merges --grep="Merge pull request #$PR" -n1 --format=%H - Squash commit SHA: check the PR page (“squashed and merged” shows the new commit) or:
12git log $REMOTE/$BASE --grep="(#$PR)" -n1 --format=%H - Rebase & merge SHAs: enumerate commits from the PR’s head branch that landed on
$BASEin the PR timeframe; the PR page shows them explicitly.




