Terminal, Git & GitHub
Terminal, Git & GitHub

The pull-request workflow, from draft to drained stack

Lesson · ~35 min
After this unit you can
  • Scope a PR to one reviewable purpose and write a commit message and description that say why — ≤50-character imperative subject, blank line, body; Fixes #N closes issues only on the default branch
  • Run the review loop — draft vs ready, Comment / Approve / Request changes, pending review comments — and say what actually blocks a merge
  • Read required status checks as gates: a job skipped by its condition counts as success, while a workflow that never runs blocks with "Waiting for status to be reported"
  • Choose merge commit, squash or rebase-and-merge knowing what each does to main's history and to branches built on the PR
  • Drain a hand-built stack bottom-up: retarget the child before its base branch is deleted, then rebase --onto and force-push with a lease

Read

Simple
1 / 2

A pull request is a request for someone's attention

Flag

A pull request asks a reviewer to understand a change well enough to share responsibility for it. Everything about a good PR makes that cheaper.

One purpose per PR

GitHub's own advice: "Small, focused pull requests are easier to review and safer to merge." The test is whether you can state the purpose in one sentence. "Bump the ORM, refactor the repository layer, and add order export" is three PRs:

PR 1  Bump the ORM to 7.2                     (mechanical; reviewer skims)
PR 2  Move order queries into OrderRepository (refactor; no behaviour change)
PR 3  Add CSV export for orders               (the feature; the real review)

Each is easy to say yes to, easy to revert alone, and fails CI for one reason. If PR 3 needs PR 2, that's a stack — the complex example covers merging one.

Before asking anyone else, review it yourself: read the whole diff in the Files changed tab, look for debug output, stray files and unrelated formatting, and check that CI is green.

Commit messages that survive

Git's own manual: "begin the commit message with a single short (no more than 50 characters) line summarizing the change, followed by a blank line and then a more thorough description." Pro Git adds: write it in the imperative — "Fix bug", not "Fixed bug" — and use the body for the motivation.

Round cart totals once, after summing

Line totals were rounded individually and then summed, so a cart of
three $0.333 items showed $0.99 instead of $1.00. Sum the exact
amounts and round the result.

The subject is what git log --oneline, git revert and every review tool show. The body is for the person running git blame on this line in two years: why, not a list of files.

A description that does the reviewer's first five minutes

  • Why — the problem, or a link to the issue.
  • What — the approach, in a few lines.
  • Where to look — "the logic is in pricing.ts; the rest is fixture updates".
  • How it was checked — tests added, what you ran by hand.

Fixes #88 (or Closes, Resolves) in the description links the issue and closes it when the PR merges — but only when the PR targets the repository's default branch. Against any other base, GitHub's docs say the keywords are ignored.

Medium
1 / 2

What actually gates a merge

Flag

A green "Merge" button means the repository's rules are satisfied — not that the change is right. Those rules live in branch protection (or the newer rulesets) on the target branch. The settings you'll meet most, as GitHub's docs describe them:

SettingEffect
Require pull request reviewsN approvals from people with write access; a write-access "Request changes" blocks until that reviewer approves
— dismiss stale approvalsa push that changes the diff withdraws existing approvals
— require review from code ownersfiles with a CODEOWNERS entry need their owner's approval
Require status checksnamed checks must pass on the PR's latest commit
— require branches to be up to date ("strict")the PR must also contain the base branch's latest commit
Require conversation resolutionevery review thread resolved
Require linear historyonly squash or rebase merges

By default a protection rule also blocks force-pushes and deletion of that branch — which is why git push --force to a protected main fails no matter what you type. And by default the rules don't apply to repository admins unless "Do not allow bypassing the above settings" is on.

Status checks: the CI gate

A status check is a pass/fail/pending result attached to a commit, usually posted by CI. Only checks marked required gate the merge; the rest are information.

% gh pr checks 42 --required --watch      # wait for the required checks on PR #42

Strict vs loose. With "require branches to be up to date", a PR that was green yesterday must take in the latest main (Update branch, or rebase and push) and pass again before it can merge. That catches two PRs that each pass alone and break together, at the cost of more CI runs. A merge queue gives the same guarantee without making every author update by hand.

Auto-merge: say yes now, merge when it's ready

gh pr merge 42 --squash --auto

When reviews or checks are still pending, --auto enables auto-merge: GitHub merges as soon as every requirement is met. GitHub switches it off again if someone without write access pushes to the branch or changes its base branch.

Why this matters to you as an author

  • A PR that's "approved" but red can't merge; a PR that's green but unreviewed can't either. Read the merge box — it lists exactly which requirement is missing.
  • A required check is only as good as the job behind it. The complex example shows two ways a required check reports "passing" or "blocked" for reasons unrelated to your code.
Complex
1 / 2

Draining a stack of pull requests, bottom-up

Flag

A hand-built stack: PR #1 merges feature-a into main; PR #2 merges feature-b into feature-a, so it shows only b's diff. Reviews happen in parallel. Merging is where stacks go wrong, and there are two separate failure modes.

Failure 1: the base branch disappears and takes PR #2 with it

GitHub's docs describe the happy path: "If you delete a head branch after its pull request has been merged, GitHub checks for any open pull requests in the same repository that specify the deleted branch as their base branch" and changes their base to the merged PR's base. The docs don't say whether every way of deleting the branch triggers that.

Not every way does. gh pr merge 1 --delete-branch deletes the remote branch through the API, and the GitHub CLI issue tracker has an open bug from 2020 (cli/cli#1168) in which the child PR is closed instead of retargeted; a commenter there traced it to the API call that deletes the branch. A similar report from August 2026 (cli/cli#14223) is labelled a platform bug and still open. One commenter on #1168 found that turning on the repo's "Automatically delete head branches" setting avoided it in 2020 and no longer did by 2024. Once closed, the PR can't be retargeted (Cannot change the base branch of a closed pull request.), and in #14223 reopening it was refused too — so PR #2 may have to be recreated, losing its review history.

Don't depend on which deletion path you took. Retarget first, then merge and delete:

gh pr edit 2 --base main          # PR #2 now targets main (its diff briefly includes a's commits)
gh pr merge 1 --squash            # merge the bottom PR; if gh offers to delete the branch, say no

Failure 2: after a squash, the upper branch still carries the old commits

main now has one squash commit; feature-b still has A1, A2 underneath B1. Rebase only b's own commits onto main — your local feature-a branch marks where they start, which is why it's worth keeping until this step is done:

git fetch
git switch feature-b
git rebase --onto origin/main feature-a     # replay feature-a..feature-b
git push --force-with-lease                 # PR #2 now shows only b's diff again
git push origin --delete feature-a          # safe now: nothing targets it

With a merge commit instead of a squash, a plain git rebase origin/main does the same job, because A1, A2 are already in main's history. With a one-commit feature-a, rebase skips the duplicate on its own.

The rule

Drain bottom-up, one PR at a time: retarget the next PR to main → merge the bottom → rebase the next branch onto main (with --onto after a squash) → force-push with a lease → delete the old base. Merging from the top down doesn't work: merging PR #2 first lands b in feature-a, not in main.

Quiz

Your branch bumps the ORM, refactors the order queries into a repository class, and adds CSV export (1,800 lines). How should it reach review?

Flag

Which commit message follows the git manual's and Pro Git's guidance?

Flag

A PR into release-2.4 (not the default branch) says Fixes #88 and has merged. Issue #88 is still open. Why?

Flag

A reviewer chose Request changes, yet the author merged five minutes later without approval. How?

Flag

You left twelve line comments on a teammate's PR an hour ago. They say they can't see any. What happened?

Flag

A docs-only PR is blocked: the required check build says "Waiting for status to be reported." The build workflow only runs when files under src/ change. Why is it blocked?

Flag

The required check test is green on a PR — but its job has a condition that was false for this PR, so it never ran. Can the PR merge?

Flag

PR #10 was squash-merged. You kept committing on the same branch and opened PR #11 from it. PR #11 lists PR #10's commits again. Why?

Flag

PR #2 (feature-b → feature-a) is stacked on PR #1 (feature-a → main). Both are approved. What's the safe order?

Flag

Fill in the blank

Open the PR as a draft, then later mark it ready for review.

Flag
gh pr create --fill 
gh pr 

In a PR to the default branch, make issue 88 close automatically when the PR merges.

Flag
 #88

Before merging the bottom PR of a stack, point the PR above it at main.

Flag
gh pr edit 2  main

Squash-merge PR 42 automatically as soon as its required reviews and checks pass.

Flag
gh pr merge 42 --squash 

PR 1 (feature-a) was squash-merged. Replay only feature-b's own commits onto main, then update its PR safely.

Flag
git rebase --onto origin/main  feature-b
git push 

Further reading