03 · Pull Requests (create, review, merge)¶
A pull request (PR) is GitHub's wrapper around a merge: it packages a
branch's commits into a reviewable, discussable unit before they land in
main. The underlying Git operation is exactly the merges and rebases
from earlier modules — GitHub adds the collaboration layer on top.
The local half: preparing a branch¶
git switch -c feature/login-form
echo "<form>login</form>" > login.html
git add login.html
git commit -m "Add login form markup"
git push -u origin feature/login-form
git push -u sets the upstream tracking branch so future git push /
git pull on this branch need no arguments — GitHub now has the commits,
but there is no PR yet, just a branch.
Creating the PR¶
Via the GitHub CLI (this needs a real GitHub remote and network access — it cannot be demonstrated in an offline scratch repo, but the invocation is exact):
gh pr create --title "Add login form" \
--body "Adds the initial login form markup." \
--base main --head feature/login-form
Behavior, confirmed from gh pr create --help:
Create a pull request on GitHub.
Upon success, the URL of the created pull request will be printed.
When the current branch isn't fully pushed to a git remote, a prompt will ask where
to push the branch and offer an option to fork the base repository. Any fork created this
way will only have the default branch of the upstream repository. Use `--head` to
explicitly skip any forking or pushing behavior.
Equivalently, on github.com: open the repo, GitHub shows a "Compare & pull request" banner for any recently-pushed branch, or use the Pull requests → New pull request page and pick base/compare branches manually.
What a PR actually contains¶
A PR is not a copy of your commits — it's a live comparison between two
refs (base and head). Every new commit you push to feature/login-form
automatically appears on the open PR; there's no separate "update the PR"
step.
That push alone updates the already-open PR with the new commit and re-triggers any configured CI checks.
Reviewing a PR¶
Reviewers can leave three kinds of feedback:
- Comment — feedback with no verdict, just discussion.
- Approve — signals the PR is good to merge (required by branch protection rules, covered next module).
- Request changes — blocks merging until addressed and re-reviewed.
Via CLI:
gh pr review 42 --approve --body "LGTM, nice tests"
gh pr review 42 --request-changes --body "Please add a null check on line 12"
gh pr diff 42
Inline comments (attached to a specific line of the diff) are the most useful review tool — they show up directly on the line in question and threaded replies keep discussion scoped to that exact change.
Merging a PR¶
GitHub offers three merge strategies, each corresponding to a real Git operation:
| GitHub button | Git equivalent | Resulting history |
|---|---|---|
| Create a merge commit | git merge --no-ff |
Merge commit with two parents, all original commits kept |
| Squash and merge | all PR commits combined into one, then applied on top of base | One clean commit on main; original branch commits disappear from main's history |
| Rebase and merge | git rebase each commit onto base, no merge commit |
Linear history, original commits replayed with new hashes |
Squash is popular for feature branches with messy "wip", "fix typo"
commits — the branch's noisy history never touches main, only the final
squashed commit does.
Draft PRs¶
Opening a PR before it's ready for review (to get CI running early, or to share work-in-progress) uses draft mode:
Draft PRs cannot be merged until marked ready, which prevents an accidental early merge.
How It Actually Works¶
- A PR is a GitHub-side object, not a Git object — there is nothing in
.git/representing "PR #42." GitHub stores it as metadata (title, body, reviews, status) that references two git refs: the base branch and the head branch (or a specialrefs/pull/42/headref GitHub maintains server-side for every PR, fetchable withgit fetch origin pull/42/head:pr-42even without the head repo's remote configured). - The "diff" shown on a PR is computed the same way
git diff base...headis computed locally: from the merge base of the two branches, not from head's very first commit — this is why a PR only shows your changes even ifmainhas moved on since you branched, as long as your branch wasn't rebased onto a stale base. - Squash merge is literally
git merge --squashplus a manual commit under the hood: Git stages the combined diff of all PR commits against base but does not create a merge commit or record the branch as merged in the graph — one ordinary commit gets appended to base's history with a single parent, which is why the original per-commit history vanishes frommain(it still exists on the now-orphaned feature branch/ref until deleted and garbage collected). - Rebase merge performs the same commit-by-commit replay described in the rebase module, server-side, then fast-forwards the base branch — each original commit gets a new hash but keeps its individual message and diff.
- Required status checks (CI) work by GitHub recording the check result against the exact head commit SHA; if you push a new commit, the SHA changes and all prior check results become irrelevant to the new SHA, which is why pushing always re-runs checks rather than reusing old results.
Exercise¶
Push a two-commit feature branch to a real (or throwaway) GitHub repo you
control, open a PR with gh pr create, then merge it three separate times
in three separate throwaway branches — once with a merge commit, once
squashed, once rebased — and compare the resulting main history with
git log --oneline --graph for each.