If you've only ever committed straight to main, the first time you join a team or contribute to open source, you'll hit a wall of jargon: branch, fork, draft PR, conflict, rebase, squash, force-push, ready for review, approve, request changes. None of it is hard, but it's never explained in one place — people assume you picked it up from somewhere.
This chapter is that one place. By the end you'll know what a pull request actually is, why every team uses them, the four commands that make up the entire workflow, the three merge strategies and which to pick, how to fix a merge conflict without panicking, and what makes a PR description that a reviewer actually wants to read.
What a Pull Request Actually Is
A pull request (or PR — sometimes called a merge request or MR on GitLab) is a proposal:
"Here's a branch with some changes. Please review them and, if you approve, merge them into the main branch."
That's it. It's not a special git feature — it's a GitHub/GitLab/Bitbucket UI on top of two regular git concepts (branches and merges). The PR adds a place to:
- See the diff between your branch and
mainin a web UI. - Run CI (Ch 15) automatically on every push to the branch.
- Have reviewers leave inline comments on specific lines.
- Block the merge until checks pass and reviewers approve.
- Keep a permanent record of why the change was made, separate from the code itself.
The shape of every PR-based workflow is the same:
Figure 1 — The PR loop. Branch off main, push it, open a PR, get reviewed and CI-checked, merge back. The branch dies after merge; main moves forward by one commit (or one merge commit, depending on strategy).
Why Not Just Push to Main?
For a solo project on day one, you absolutely can. Most weekend hacks live their whole life with git commit && git push on main. There's no rule against it.
The reasons to switch to PRs as the project grows:
- You're not the only committer. Two people pushing to
mainwill eventually push at the same time and one of you will be very confused. - You want CI to gate the merge. Without a PR, CI runs after the broken commit is already on
main. With a PR, CI runs before and blocks the merge. - You want a second pair of eyes. Even brilliant developers ship bugs that a 30-second skim by a colleague would have caught.
- You want a paper trail. Six months from now you'll need to know why a function was changed. The PR description is where that lives.
- You're contributing to someone else's project. You can't push to their
main— you can only open a PR.
For a one-person side project: skip PRs until the first of those reasons applies. For anything with a teammate, customer, or future-you who'll forget: PRs from day one.
The Four Commands That Are 95% of the Workflow
Open a terminal. Here's the full lifecycle of one PR using nothing but git + the gh CLI (Ch 15 installed it, otherwise brew install gh and gh auth login):
# 1. Start a new branch off latest main
git switch main
git pull
git switch -c fix/typo-in-header
# 2. Make changes, commit
# …edit files…
git add .
git commit -m "Fix typo in header"
# 3. Push the branch + open the PR
git push -u origin fix/typo-in-header
gh pr create --fill
# 4. Address review feedback, push more commits to the same branch
# …edit files…
git commit -am "Address review: rename variable"
git pushThat's the whole core workflow. Let's name what each step does.
Step 1 — git switch -c: create a branch
A branch is a movable label pointing at a commit. main is a branch. fix/typo-in-header is now a new branch starting from the same commit main is on. Commits you make are added to whatever branch you're currently on.
git switch -c <name> creates a new branch and switches to it. (The older spelling git checkout -b does the same.)
Branch naming: most teams use type/short-description. Common types: feat/, fix/, chore/, refactor/, docs/. It's a convention, not a rule — pick what your team uses and stop worrying about it.
Step 2 — git commit: save a snapshot
A commit is a snapshot of every tracked file at one moment, plus a message and a parent commit. You'll have one or many commits on your branch.
Commit message convention worth adopting: write the subject line as if completing the sentence "If applied, this commit will __". So Fix typo in header, not Fixed typo or Header typo.
Step 3 — git push -u origin <branch> and gh pr create
git push uploads your local commits. The -u origin <branch> says "this is the first time, set up the upstream tracking so plain git push works from now on".
gh pr create --fill opens the PR using your last commit's message as title and body. For multi-commit branches, use gh pr create (no --fill) to write a proper description in an editor — covered later.
Step 4 — Address feedback by pushing more commits
This is the part that surprises beginners. Once a PR is open, pushing more commits to the same branch updates the PR automatically. There's no "submit again" button. The PR is the branch — push to the branch, the PR refreshes, CI re-runs.
So the review loop is:
- Reviewer leaves comments.
- You make changes locally, commit, push.
- PR shows the new commits and a "fixed" diff. Reviewer re-checks. Approves or asks for more.
- Once approved + CI green, merge.
Forks vs Branches — When You Need Each
Two ways to propose a change:
- Branch in the same repo. You have write access. Push branches directly. This is the default for teams.
- Fork the repo, then branch in your fork. You don't have write access (open source, or a team where outsiders contribute). You clone-by-clicking-Fork on GitHub, push branches to your copy, then open a PR pointing from your fork's branch back to the original repo's
main.
The PR mechanics are identical — only the origin is different. gh pr create figures out which one you're in and does the right thing.
Figure 2 — Two paths to the same PR. Team members push branches in the main repo. External contributors fork first and push to their own copy. Both end at "open a PR".
Draft PRs — "Look but Don't Merge Yet"
A draft PR is a PR explicitly marked as "work in progress". GitHub disables the merge button and (usually) tells reviewers not to review it yet.
Use one when:
- You want CI to run while you finish the work.
- You want to share your direction with a colleague before going deep.
- The change is half-done but you want a public branch with a comment thread.
Open as draft from the CLI:
gh pr create --draftOr click the dropdown arrow next to "Create pull request" on GitHub. When ready, click "Ready for review" — that flips it to a normal PR.
Anatomy of a Good PR Description
A PR is a half-tweet, half-changelog, half-spec. There's no universal format, but the structure that consistently works:
## Summary
One paragraph: what this PR changes and why.
## Changes
- Bullet list of the noteworthy edits
- Don't list every file — link to the diff for that
## Why
Context the diff doesn't show. The bug report, the design doc,
the prod incident, the customer ask.
## Test plan
- How you tested this manually
- Any new automated tests added
- Any cases reviewers should specifically check
## Screenshots / Demo
For UI changes only.The most undervalued section is Why. The diff shows what changed. The branch name and commit messages show roughly what. But why this fix and not the obvious other one? Why now? Why this approach over the simpler one a reviewer might suggest? That's the part that saves five round-trips of "but couldn't we just…".
GitHub remembers your last PR description as a template. To make a real one per repo, drop .github/pull_request_template.md with the shape above — every new PR will start with that skeleton.
Keeping Your Branch Up to Date
Long-lived PRs eventually fall behind main. Reviewers ask "can you rebase?". You have two options.
Option A: Merge main into your branch
git switch fix/typo-in-header
git fetch origin
git merge origin/main
# resolve any conflicts (see next section)
git pushAdds a merge commit to your branch. History becomes a small diamond. Safe and reversible. Some teams prefer this for ongoing collaboration where multiple people share a branch.
Option B: Rebase your branch on top of main
git switch fix/typo-in-header
git fetch origin
git rebase origin/main
# resolve any conflicts
git push --force-with-leaseReplays your commits one by one on top of latest main. Result: a linear history with no merge commits — looks like you just wrote your changes today against the freshest main.
The cost: rebase rewrites your commits' SHAs, so the branch on your machine no longer matches the branch on the server. You have to force-push. Always use --force-with-lease, not --force — it refuses to overwrite if someone else pushed while you were rebasing, saving you from clobbering their work.
Which to use? Personal preference per project. The common modern default: rebase your feature branch, merge into main — feature branches stay clean, the merge into main is a single squash or merge commit.
Fixing a Merge Conflict (Without Panic)
A conflict happens when two branches changed the same line of the same file. Git can't decide which version wins, so it pauses and asks you.
After a conflicting git merge or git rebase, your file looks like this (the marker lines are indented here for annotation — in your real file they sit flush against the left margin, at column 1, with no indentation):
function getUser() {
<<<<<<< HEAD ← start of YOUR version
return fetchUser({ id, includeAvatar: true });
======= ← divider
return fetchUser({ id, includeProfile: true });
>>>>>>> origin/main ← end of THEIR version
}Three markers:
<<<<<<< HEAD— top: your branch's version=======— separator>>>>>>> origin/main— bottom: the other branch's version
You manually pick the right thing — often a combination — and delete the markers:
function getUser() {
return fetchUser({ id, includeAvatar: true, includeProfile: true });
}Then:
git add path/to/the/file.js
git commit # for merge
# OR
git rebase --continue # for rebaseRepeat for each conflict. VS Code/Cursor/most editors show inline buttons ("Accept current", "Accept incoming", "Accept both") that handle the markers for you.
The one rule: when in doubt, ask the person whose code you're conflicting with. They wrote it for a reason. Five minutes of "can you check this merge?" beats half a day of debugging a silent merge mistake.
Merge, Squash, or Rebase — Picking a Merge Strategy
When the PR is approved + green, GitHub gives you three buttons on the merge dropdown:
| Strategy | What happens | Best for |
|---|---|---|
| Create a merge commit | Keeps every commit on the branch + adds a merge commit on main | Long-lived feature branches with meaningful commit history you want to preserve |
| Squash and merge | Collapses all branch commits into one new commit on main | Most cases. PR title becomes the commit message. Clean history. |
| Rebase and merge | Replays branch commits onto main without a merge commit | Branches with already-clean per-commit history (rare in practice) |
For 90% of teams: default to squash and merge. Reasons:
- One PR = one commit on
main. Easy to revert, easy to read ingit log. - The branch commits ("WIP", "Fix lint", "Address review") are noise once the PR is done — no one wants them in
main. - The PR description becomes the commit body, which is exactly what
git blameshould surface six months later.
If you set the default in repo Settings → General → Pull Requests, the button auto-picks the right strategy and your team stops thinking about it.
Commit SHAs — Why Every Commit Has a 40-Character Hash
Open git log. Every commit has a long hexadecimal string next to it:
461f450e9c8a8f9b1d7c… web tutorials: add Chs 16-19
32be3848… Retire legacy /api/credits/add endpoint (#2)
b4d690f2… Centralize website Stripe links (#1)That string is the commit's SHA — short for SHA-1 hash, the cryptographic algorithm git uses to fingerprint every commit. (Newer git versions are migrating to SHA-256; same idea, longer string.) The full hash is 40 characters; most tools show the first 7-10 because that's enough to be unique in any normal repo.
What it's a hash of
Git feeds these inputs through SHA-1 to produce the commit's ID:
- The full snapshot of every tracked file at that moment (via tree object hashes)
- The author's name + email
- The commit message
- The timestamp
- The parent commit's SHA (or two parents, for a merge commit)
Change any one of those — even a single space in the message — and you get a completely different SHA. That's the whole point: the SHA is the commit. Two commits with the same SHA are literally the same commit.
What the SHA is used for
A SHA is an unforgeable, globally-unique pointer to one specific snapshot of the codebase. Once you have one, you can:
git show 461f450e # see the full commit
git checkout 461f450e # check out that exact snapshot (detached HEAD)
git diff 461f450e 32be3848 # diff between two commits
git revert 461f450e # undo just that commit
git cherry-pick 461f450e # apply that commit to your current branch
git blame -L 10,20 file.js # see which SHA last touched each lineThis is why PRs link to SHAs instead of "the third commit Alice made on Tuesday". 461f450e means one thing forever. Branch names (fix/typo) and human descriptions are mutable; SHAs are not.
Where you'll see SHAs in a PR
Open any PR on GitHub. You'll see SHAs in three places:
- Each commit in the PR is a row with its 7-character SHA prefix, message, and author. Click it for the full diff of that one commit.
- The "head" SHA (the tip commit of your branch) is what CI ran against. Status checks like "✅ test passed for
461f450e" name it explicitly. - The merge commit (if you use "Create a merge commit" instead of squash) gets a new SHA after merge — the commit that joined your branch into main.
When CI fails or someone files a bug "we noticed weird behaviour after 461f450e", that SHA is the link back to exactly the change that caused it.
Short SHAs and uniqueness
git log --oneline shows 7-character prefixes (461f450e). Git accepts any unambiguous prefix — as few as 4 characters often works on small repos. The risk: collisions become possible on huge repos (the Linux kernel auto-uses 12 characters for this reason). Always copy from git log rather than typing from memory.
"Detached HEAD" — what happens when you check out a SHA directly
git checkout 461f450e jumps you to that exact snapshot. But you're no longer on a branch — git calls this detached HEAD and warns about it. Useful for debugging ("does the bug exist at this old SHA?"). Don't commit there — the commit would be on no branch and easy to lose. To work from that point, branch off it: git switch -c investigate-bug.
SHAs in commit messages
A common convention: when you fix a bug introduced in a specific commit, mention the SHA in the message:
Fix off-by-one in pagination
Introduced by 461f450e9c8a, which mis-counted the offset
for the last page.That gives the next person reading git blame an immediate breadcrumb back to the original context — much faster than "fix bug".
After the Merge — Cleaning Up
Once your PR is merged:
git switch main
git pull
git branch -d fix/typo-in-header # delete local branchGitHub usually deletes the remote branch automatically (Settings → General → "Automatically delete head branches"). Turn that on — there's no reason to keep merged branches around, they just clutter the dropdown.
Reverting a PR
Production caught fire. You need to undo a PR.
gh pr view 123 # find the merge commit SHA
git revert <merge-sha> -m 1
git push-m 1 tells git "when reverting a merge commit, treat the first parent (main) as the canonical side". This produces a new commit that exactly undoes the PR.
GitHub also has a Revert button on every merged PR. Click it, GitHub opens a new PR with the revert. Review it, merge it, you're back to the previous good state. The original PR remains in history but its effects are undone.
This is much safer than git reset --hard or force-pushing. Always revert by adding a commit, never by deleting one.
The PR Etiquette Nobody Tells You
A handful of small habits that make teams much happier to merge your PRs:
- Keep PRs small. A 100-line PR gets reviewed today. A 2000-line PR sits for a week and ships with bugs. If a feature is large, split it into stacked PRs.
- Don't mix concerns. "Adds new feature + fixes 3 unrelated bugs + reformats whole file" is three PRs, not one. Each can be reviewed and reverted independently.
- Self-review first. Open your own PR on GitHub before requesting reviewers. Read the diff like you've never seen it. You'll catch debug
console.logs, stale comments, and bad variable names every time. - Mention people you need.
@alice can you check the SQL?is much better than silence. Reviewers don't get notified by your PR's existence — they get notified by@s and review requests. - Respond to every comment. Even "good catch, fixed" or "intentional, see line X" closes the loop. Silent unresolved comments make reviewers ghost.
The Mac App Workflow — One Concrete Example
The SimpleAppShipper repo uses PRs for anything non-trivial. The flow is exactly the chapter:
git switch -c feat/aso-keyword-researchoffmain.- Build the feature across 3-5 commits.
gh pr createwith a description that links the design notes.- CI runs lint, type-check, build, tests. Branch protection blocks merge until green.
- One reviewer approves.
- Squash and merge. Branch auto-deleted.
npm run cf:build && npm run cf:deployto ship.
That's it. Same loop for every feature shipped in 2026. The whole point of a workflow is that it becomes invisible — you stop thinking about steps and start thinking about the work.
Mental Model — Three Sentences
- A pull request is a branch + a web UI for reviewing and gating its merge into
main— the unit of work every modern codebase changes by. - The core workflow is four commands:
git switch -c,git commit,git push -u,gh pr create— then more commits + pushes until approved. - Squash-and-merge is the default modern strategy because one PR becoming one commit on
mainmakes history readable, blames meaningful, and reverts cheap.
Try It Yourself (15 Minutes)
- In any repo (yours or a fork of a friend's), create a branch:
git switch -c try/my-first-pr. - Edit one file. Commit. Push with
-u. gh pr create --fill. Open the URL it prints.- On GitHub, click "Files changed" to see the diff. Add a comment on any line.
- Push another commit. Watch the PR refresh.
- Squash-and-merge. Watch your branch disappear.
- Locally:
git switch main && git pull && git branch -d try/my-first-pr. You've done the entire cycle.
Repeat 50 times and you're a senior. The mechanics never change; only the size and stakes of what you put inside the PR.
Where This Lands in the Series
Ch 15-17 covered the robots in the pipeline — CI, lint, tests. This chapter covers the humans — how a change gets proposed. The next chapter, Code Review, covers the other side of that conversation: how to read a PR somebody else opened and leave feedback that actually helps.
Ship your apps faster
When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.
Try Simple App Shipper