The GitHub Flow & Pull Requests
How teams ship changes together without chaos — the GitHub Flow loop, what a pull request really is, and how to choose between merge, squash, and rebase.
What you'll learn
- The GitHub Flow loop: branch → commit → push → PR → review/CI → merge → delete
- What a pull request is: a merge request plus a review surface plus a CI gate
- Merge options on GitHub: when to pick squash-and-merge over a plain merge commit
Before you start
The last lesson ended on the missing piece: raw git push puts code straight onto main with nobody watching, and no real team works that way. The fix is to wrap that push in a review step — and the wrapper has a name and a rhythm. It is the GitHub Flow, and a pull request is the object at its heart. Here is the whole loop, and what a PR really is underneath the button.
The loop at a glance
GitHub Flow is deliberately minimal. It has exactly one rule: main is always deployable. Everything else follows from that constraint.
The loop looks like this:
- Branch off
main. - Do your work in commits on that branch.
- Push the branch to GitHub.
- Open a pull request (PR) targeting
main. - Teammates review; CI runs automated checks.
- Address feedback with more commits on the same branch.
- Merge when approved and green.
- Delete the branch.
Repeat.
What a pull request actually is
A pull request is three things bundled together:
- A merge request. It says: “please merge branch
feat/loginintomain.” - A review surface. Teammates can comment on individual lines, approve, or request changes.
- A CI gate. GitHub runs status checks (tests, linters, security scans) against the PR branch before anyone can merge.
Nothing about a PR is magic. Under the hood it is just two branch names and a diff. The power comes from making the conversation and the checks happen before the code touches main.
Walking through the loop
1. Create a branch off main
git switch main
git pull # make sure you start from the latest
git switch -c feat/add-dark-mode # short, descriptive name
Branch names are cheap. Use them freely.
2. Commit your work
# ... edit files ...
git add src/theme.ts
git commit -m "add dark-mode token set"
# ... more edits ...
git add src/toggle.tsx
git commit -m "wire ThemeToggle to token set"
Keep commits focused. Each one should leave the code in a working state.
3. Push the branch
git push -u origin feat/add-dark-mode
The -u flag sets the upstream so future git push calls on this branch need no arguments.
4. Open a pull request
GitHub prints a URL to open a PR immediately after your first push. Click it, or use the GitHub CLI:
gh pr create \
--base main \
--title "Add dark-mode support" \
--body "Closes #42. Adds token set and wires the toggle."
Write a PR description that explains why, not just what. Reviewers who understand the context give faster, better feedback.
5. Review, CI, and iteration
While the PR is open:
- CI runs your test suite and linters automatically.
- Teammates leave comments or approve.
- You push more commits to address feedback — they appear in the same PR automatically.
# Address a reviewer's comment, then push
git add src/toggle.tsx
git commit -m "fix toggle aria-label per review"
git push
You never need to close and reopen the PR. The new commits show up in the existing thread.
6. Merge and delete
Once the PR is approved and all checks are green, merge it on GitHub. Then clean up:
git switch main
git pull # pull the merged commit
git branch -d feat/add-dark-mode # delete local branch
git push origin --delete feat/add-dark-mode # delete remote branch
GitHub can be set to delete the remote branch automatically after merge — turn that on in repository settings.
Fork-based contribution
When you do not have write access to a repository — as is common in open source — you cannot push a branch directly. Instead you fork the repo: GitHub copies it under your account, giving you full write access to your copy.
The flow is the same, but the push target is your fork, and the PR targets the original (upstream) repository’s main.
# After forking on github.com:
git clone https://github.com/YOUR-USERNAME/the-project.git
git switch -c fix/typo-in-readme
# ... edit ...
git push -u origin fix/typo-in-readme
gh pr create --repo original-owner/the-project --base main
For repos where you are a collaborator, skip the fork — push your branch directly.
Keeping a PR branch current
If main receives new commits while your PR is open, your branch falls behind. You have two options:
Option A — merge main in (safer, preserves history):
git switch feat/add-dark-mode
git fetch origin
git merge origin/main
git push
Option B — rebase onto main (cleaner history, rewrites commits):
git switch feat/add-dark-mode
git fetch origin
git rebase origin/main
git push --force-with-lease # required after rebase
For shared PR branches where a teammate may also have pushed, prefer the merge option. Rebase is fine on branches only you are using.
Merge options on GitHub
When you click the merge button, GitHub offers three strategies:
| Strategy | What it does | History shape |
|---|---|---|
| Merge commit | Creates a merge commit with two parents | Preserves every commit from the branch |
| Squash and merge | Collapses all PR commits into one commit on main | Flat, one commit per PR |
| Rebase and merge | Replays PR commits linearly onto main | Linear, multiple commits |
When to pick squash-and-merge: your PR branch has noisy intermediate commits (“fix typo”, “add back missing semicolon”, “WIP”) and you want main’s history to read as one clean unit of work per feature. Most teams standardize on squash-and-merge for this reason.
When to keep a merge commit: you want the full commit story preserved — useful on projects where git bisect needs granular commits to pinpoint regressions.
Reviewing etiquette
A few practices that make reviews faster and kinder:
- Reviewer: distinguish blocking feedback (“this will break X”) from suggestions (“we could simplify this, but it works either way”).
- Author: respond to every comment, even if just to say you pushed a fix or explain why you kept the original approach.
- Everyone: small PRs review faster. A 200-line diff gets thorough review in minutes. A 2000-line diff gets skimmed.
This connects directly to the merging lesson — understanding fast-forward vs. merge commit behavior there explains exactly what GitHub is doing under each of the three merge buttons here.
In one breath
The GitHub Flow is one loop built on a single rule — main is always deployable: branch off main, commit, push the branch, open a pull request, let review and CI run, address feedback with more commits on the same branch, merge when green, delete the branch. A PR is just three things bundled — a merge request, a review surface, and a CI gate — making the conversation and the checks happen before code touches main. Without write access you fork and target the upstream’s main. Keep a PR branch current by merging origin/main in (safe) or rebasing onto it (clean, needs --force-with-lease), keep PRs small, and at merge time pick a strategy: squash for one clean commit per feature (most teams’ default), a merge commit to preserve every commit, or rebase for a linear multi-commit history.
Practice
Before the quiz, plan a real contribution end to end: you are fixing a typo in an open-source repo you do not have write access to. List the sequence — fork, clone, branch, commit, push, PR — naming whose main your push targets versus whose main the PR targets. Then, mid-review, main moves ahead and CI goes red on a conflict: which one command brings your branch current, and which push flag must follow if you chose to rebase?
Quick check
A question to carry forward
Step back and look at the arc of this whole section: everything has moved forward. You learned to make commits, branch, merge, push, and open pull requests — to build and to share. But here is the truth no workflow can dodge: you will make mistakes. You will commit the wrong file, stage something half-finished, write a broken change, or — worst of all — push a bad commit the whole team has already pulled. Fluency in Git is not avoiding those moments; it is recovering from them without panic. And Git’s undo is not one command but a small family of them, each matching a different mess — because undoing a change you have not shared is a completely different problem from undoing one that is already public. Which undo for which situation is the final chapter, and it begins with the next lesson.