Lesson 11 of 15

Pull Requests

What a Pull Request Is For

A pull request — everyone says PR — is a proposal: "I have made these changes on this branch, please review them and merge them into that branch." It is not a Git feature at all. Git only knows about merging. The pull request is a layer GitHub adds around a merge, giving it a discussion thread, a reviewable diff, a place for automated checks to report, and a permanent record of the conversation.

The value is the pause. Without pull requests, whoever merges into main decides alone whether the change is correct. With them, somebody else reads the code before it becomes everybody's problem. In a four-person college project this is what stops one person's untested change from breaking the build the night before submission.

The record matters almost as much as the review. A year later, the question "why does the cart total round this way?" is answered by the pull request, where you can read the original description, the reviewer's objection and your reply. Chat messages are gone; the pull request is attached to the code forever.

Open pull requests even when you are working alone. It costs nothing, it gives you a diff of your own work to read before merging — which catches a surprising number of mistakes — and it builds the habit before you are in a team where it is required.

  • A reviewable diff of everything the branch changes, comparable at a glance
  • Line-by-line comments, so feedback points at specific code instead of describing it
  • Automated checks that run on every push and report pass or fail on the PR
  • Approval requirements — a branch protection rule can demand one before merging
  • Linked issues, so the change and the reason it exists stay connected
  • A permanent, searchable record of the discussion behind every merged change
Notes
  • The name comes from the request being aimed at the maintainer: you are asking them to pull your branch into theirs. GitLab calls the same thing a merge request, which is arguably the clearer name for it.

Opening One

The Git part is what you already know: branch, commit, push. The pull request itself is created afterwards, from the branch you pushed. GitHub notices a newly pushed branch and shows a Compare & pull request button on the repository page for a while, which is the quickest route.

When you open it you choose two branches: the base, which will receive the changes (usually main), and the compare branch, which contains your work. Check these before submitting. Getting them backwards on a fork is common and produces a pull request proposing to overwrite your work with the original — harmless, but confusing enough that people abandon the attempt.

Write the description as if the reviewer knows nothing about what you have been doing, because they usually do not. Say what the change does, why it was needed, and how you tested it. Add a screenshot for anything visual — a reviewer looking at a CSS diff cannot tell whether the result looks right. Link the issue with Closes #42, which makes GitHub close that issue automatically when the PR is merged.

If the work is not finished but you want early feedback, open it as a draft pull request. A draft cannot be merged accidentally and signals clearly that you are not asking for a final review yet.

Example
# 1. Branch from an up-to-date main and do the work
git switch main && git pull
git switch -c feature/user-auth
# ... write code ...
git add src/auth.js src/login.html
git commit -m "Add email and password login"

# 2. Push the branch to GitHub
git push -u origin feature/user-auth

# 3a. Open the PR on the GitHub website (it offers a button), or
# 3b. Use the GitHub CLI
gh pr create \
  --base main \
  --head feature/user-auth \
  --title "Add email and password login" \
  --body "Adds a login form and session handling. Closes #42."

# Open it as a draft while still working
gh pr create --draft --title "WIP: password reset"

# Check on it later
gh pr status
gh pr view --web

# Review someone else's PR locally
gh pr checkout 17          # switches to their branch so you can run it
Notes
  • Any commit you push to the same branch afterwards appears in the pull request automatically. You do not close and reopen a PR to update it — you just push more commits, and reviewers can see exactly what changed since their last look.

Reviewing, and Being Reviewed

Reviewing is a skill worth practising deliberately, and student teams usually treat it as a formality — clicking Approve without reading. That wastes the whole mechanism. A useful review asks whether the code does what the description claims, whether it breaks anything nearby, whether the edge cases are handled, and whether someone new could understand it in six months.

Comment on the code, not the person. "This crashes when the cart is empty" is useful; "you always forget edge cases" is not. Ask questions when you are unsure rather than asserting: "what happens if the API times out here?" is a genuine question that often finds a genuine bug, and costs nothing if the answer is that it is already handled.

GitHub gives a review three outcomes. Comment leaves feedback without a verdict. Approve says this can be merged. Request changes blocks the merge until you are satisfied. Use Request changes for real problems, not for style preferences you could raise as a comment and let the author decide on.

On the receiving end: reply to every comment, even if only to say you have fixed it. Push fixes as new commits rather than amending and force-pushing, because a reviewer who has already read half the diff needs to see what changed since — force-pushing mid-review destroys that view and is the one behaviour that reliably annoys reviewers.

  • Read the description first, then the diff — you are checking the code against a claim
  • Look for bugs and missing cases before style; a reviewer is not a linter
  • Point at specific lines, and suggest a fix where you can
  • Approve when it is good enough to merge, not when it is perfect
  • As the author, respond to everything and push fixes as new commits
  • Do not force-push once review has started — it discards the reviewer's place in the diff
  • Merge only when checks pass; a red cross on the PR means something is genuinely broken
Notes
  • Keep pull requests small. Reviewers read a fifty-line PR carefully and a fifteen-hundred-line PR not at all — they skim it and approve, which is worse than no review because everyone believes it was checked. Splitting a large piece of work into three sequential pull requests gets all three properly reviewed.

Three Ways to Merge, and Conflicts in a PR

When a pull request is approved, GitHub offers up to three merge buttons and they produce genuinely different histories. Create a merge commit keeps all your branch's commits and adds a merge commit joining them — the full record, at the cost of a busier graph. Squash and merge combines every commit on the branch into one new commit on main, which is ideal when your branch history is a dozen commits reading "fix", "fix again", "actually fix". Rebase and merge replays your commits onto main individually with no merge commit, giving a linear history that keeps the individual commits.

For student projects, squash and merge is usually the best default. It gives main one clean commit per feature, and it means messy work-in-progress commits on your branch cost nothing. Note that after a squash merge your branch's original commits are not ancestors of main, so delete the branch afterwards rather than continuing to work on it.

Sometimes GitHub reports that the branch has conflicts and cannot be merged automatically. This is the same merge conflict you already know about, discovered on the server. The fix happens on your machine: update your branch with the latest main, resolve the conflicts locally, and push. The pull request re-checks itself and the button turns green again.

GitHub also offers a web editor for simple conflicts, which is fine for a README but a poor idea for code — you cannot run anything to check that your resolution actually works.

Example
# Resolve a PR conflict locally (the reliable way)
git switch feature/user-auth
git fetch origin
git merge origin/main
#   ... resolve conflict markers, then
git add src/auth.js
git commit
git push
#   the pull request updates itself and becomes mergeable

# Merging from the command line
gh pr merge 17 --squash --delete-branch
gh pr merge 17 --merge
gh pr merge 17 --rebase

# After it is merged, tidy up locally
git switch main
git pull
git branch -d feature/user-auth
git fetch --prune          # drop the stale origin/feature/user-auth entry
Notes
  • Turn on branch protection for main in the repository settings on any project with more than one contributor: require a pull request before merging, require checks to pass, and block force-pushes. It takes a minute and removes the possibility of somebody pushing broken code straight to main at 2 a.m.
Ask AI