The pull request flow

The overview page introduces pull requests and code review; this page goes deeper. A pull request (PR) is not a Git feature but a feature of the hosting platform (GitLab calls the same thing a merge request). Git itself only knows branches and merges; GitHub adds a page per proposed merge where people discuss the change, review it line by line, and see the results of automated checks. Nothing reaches the target branch until someone presses the merge button (or a rule does it for them).

The pull request flow: create a branch and push it from your machine, open a pull request on GitHub, where review and automated checks happen side by side, then merge and delete the branch. When a reviewer requests changes, you push more commits to the same branch and the pull request updates. Branch Push Open PR Review Checks Merge Delete branch changes requested: push more commits on your machine on GitHub
Review and checks run side by side. Every new commit pushed to the branch updates the pull request and runs the checks again.
  1. Branch. Start a topic branch from an up-to-date main, for example feature/ntp (see Branches).
  2. Push. Commit your work and push the branch (git push -u origin feature/ntp; see Remotes and collaboration).
  3. Open the pull request. On GitHub you choose the base branch (where the change should go, usually main) and the compare branch (your branch), then write a title and description.
  4. Review and checks. Reviewers comment and approve; workflows (see GitHub Actions) build and test the branch and report a status.
  5. Merge. When the rules are satisfied, someone with write access merges, using one of the merge methods.
  6. Delete the branch. Its commits now live in main; the branch name is just clutter. The repository setting "Automatically delete head branches" does this for you.

The same flow from the terminal with the GitHub CLI (command-line interface) (gh), which talks to GitHub over the network and is therefore not run on this page:

git switch -c feature/ntp
# ... edit router.cfg, commit ...
git push -u origin feature/ntp
gh pr create --draft --title "Add NTP servers to R1" --body "Closes #42"
gh pr ready              # mark the draft ready for review
gh pr checks             # status of the automated checks
gh pr review --approve   # run by the reviewer, not the author
gh pr merge --squash --delete-branch

Draft pull requests

A draft pull request is a pull request marked as work in progress: everyone can see it and the checks run, but it cannot be merged until the author marks it "Ready for review", and code owners are not asked to review it automatically while it is a draft. Open a draft early when you want feedback on a direction, or want the pipeline to run on your branch, before the work is finished. Converting back to draft is possible at any time.

Writing a good pull request

A reviewer has only what you give them: the diff and your words. Make it easy for them to say yes for the right reasons.

  • Keep it small and about one thing. A change of a few dozen lines gets a careful review; a change of two thousand lines gets a skim and an approval. Split refactoring, formatting and behavior changes into separate pull requests.
  • A clear title. "Add NTP servers to R1 base config" beats "updates". Many teams squash on merge, so the title often becomes the commit message on main.
  • A description that answers why. What problem does this solve, what did you change, what did you consider and reject, and how did you test it? Point reviewers to the risky part.
  • Link the issue. Writing Closes #42 (or Fixes #42) in the description links the issue and closes it automatically when the pull request is merged into the default branch.
  • Show, don't tell. For visible changes, add screenshots; for configuration, paste the relevant show output or a dry-run diff; for a bug, the before-and-after.
  • Review it yourself first. Read your own diff on the pull request page before asking anyone. You will find the debug print and the commented-out block.

A repository can offer a template for the description: a file .github/pull_request_template.md whose content is filled into every new pull request, for example with headings "What", "Why" and "How tested".

Reviewing well

Code review is the practice of having someone other than the author read a change before it is merged, to catch defects, share knowledge and keep the code base consistent. Its value is as much in the second point as in the first: after a review, two people understand the change instead of one.

What to look for, roughly in order of importance:

  1. Does it do the right thing? Does it solve the linked issue, and does it handle the edge cases (an empty list, a missing file, a device that does not respond)?
  2. Is it safe? No secrets in the diff, input validated, permissions not widened without a reason. See Securing repositories.
  3. Is it tested? Are there tests for the new behavior, and did the checks pass for the right reason?
  4. Can the next person understand it? Names, structure, comments where the why is not obvious.
  5. Style comes last, and ideally is not a human job at all: a formatter and a linter in the pipeline settle it automatically.

Write comments that are kind and specific. Comment on the code, not the person; say what you see and why it matters; and make clear whether a point blocks the merge. Many teams prefix optional remarks with "nit:" (a small, non-blocking point). Compare "This is wrong" with "If the device is unreachable, connect() raises and the loop stops for all remaining devices. Should we catch the timeout and continue?" The second one can be acted on.

Approve, comment or request changes

A review on GitHub is a set of line comments submitted together, with one of three outcomes:

Outcome Meaning Effect on merging
Comment Feedback or questions, no verdict None
Approve "I have reviewed this and it can be merged" Counts toward the required number of approvals
Request changes "This must change before it is merged" Blocks merging while required reviews are enforced, until that reviewer approves or the review is dismissed

Suggested changes

For small fixes, a reviewer can write a suggestion: a code block marked suggestion inside a line comment, containing the replacement lines. The author applies it with one click ("Commit suggestion"), which creates a commit on the branch. Several suggestions can be applied together as one commit.

The second server should be the backup at the other site.

```suggestion
ntp server 198.51.100.11
```

CODEOWNERS

CODEOWNERS is a file that maps path patterns in a repository to the people or teams responsible for them; GitHub automatically requests a review from the matching owners when a pull request changes those paths. GitHub looks for the file in .github/, then in the repository root, then in docs/, and uses the first one it finds. The patterns follow .gitignore syntax, owners need write access to the repository, and the file must be smaller than 3 MB.

# Each line: a path pattern, then one or more owners.
# When several patterns match a file, the LAST one wins.

# Fallback owners for everything
*                  @example-corp/netops

# Router and switch configurations
/configs/          @example-corp/netops @amara-okafor

# Python tools, wherever they are
*.py               @joost-bakker

# Workflows and this file itself
/.github/          @example-corp/platform

Which owners are requested for a few changed files:

Changed file Last matching pattern Review requested from
README.md * @example-corp/netops
configs/r1.cfg /configs/ @example-corp/netops and @amara-okafor
configs/render.py *.py @joost-bakker only
.github/workflows/ci.yml /.github/ @example-corp/platform

On its own, CODEOWNERS only requests reviews. To make an owner's approval mandatory, enable "Require review from Code Owners" in a branch protection rule or ruleset (below); approval from any one of the listed owners is then enough. Protect the CODEOWNERS file itself with an owner, as the last line does, otherwise anyone can remove themselves from review.

Protecting branches

Without rules, anyone with write access can push straight to main, merge without a review, or force push and rewrite history. GitHub has two systems to prevent that.

Branch protection rules

A branch protection rule is the classic GitHub setting that enforces conditions on one branch name pattern, such as required reviews and passing checks before merging; it is configured under Settings → Branches. Only one branch protection rule applies to a given branch. Typical settings:

  • Require a pull request before merging, with a number of approvals.
  • Require review from Code Owners.
  • Require status checks to pass before merging.
  • Require signed commits (see Securing repositories).
  • Require linear history (no merge commits on the branch).
  • Block force pushes and branch deletion.

Rulesets

A repository ruleset is a named list of rules that applies to the branches or tags matching its target patterns; several rulesets can apply to the same branch, and they combine with each other and with branch protection rules, the most restrictive rule winning. Rulesets live under Settings → Rules → Rulesets and are the newer, more flexible system:

Branch protection rule Ruleset
Rules per branch One rule applies Several rulesets layer; the most restrictive wins
Switching off Delete or edit the rule Set enforcement to Disabled and back to Active, without deleting it
Who may skip it Administrators, depending on settings An explicit bypass list of roles, teams or apps
Visibility Repository administrators Anyone with read access can see active rulesets
Scope One repository One repository, or many at once from the organization settings
Common rules Required reviews, required status checks, signed commits, linear history and blocking force pushes exist in both

Availability depends on the plan (as of 2026): rulesets work on public repositories on GitHub Free, and on public and private repositories on Pro, Team and Enterprise Cloud. A repository can have up to 75 rulesets. Organizations on Team can also use push rulesets on private repositories, which block certain file paths, extensions or file sizes from being pushed at all. For new setups, prefer rulesets; existing branch protection rules keep working alongside them.

Required status checks

A required status check is an automated check, usually a CI (continuous integration) job, that must report success on the latest commit of a pull request before it can be merged. You pick the checks by name (for example the job test from a workflow in GitHub Actions). An extra option requires the branch to be up to date with the base branch before merging, so the checks ran against the code that will actually land.

Merge methods

The merge button has three variants; a repository can enable or disable each one under Settings → General, in the "Pull Requests" section. They produce different histories on main. To see the difference, here is a pull request in the making: feature/ntp has two commits, and meanwhile someone added a README to main. (The examples on these pages use a fixed author and date, so the hashes you see match the output.)

git init -q netconf && cd netconf
printf 'hostname R1\n' > router.cfg
git add router.cfg && git commit -q -m "Add R1 base config"
git switch -q -c feature/ntp
printf 'ntp server 192.0.2.10\n' >> router.cfg && git commit -q -am "Add NTP server"
printf 'ntp server 192.0.2.11\n' >> router.cfg && git commit -q -am "Add second NTP server"
git switch -q main
printf '# Router configs\n' > README.md
git add README.md && git commit -q -m "Add README"
git log --graph --oneline --all
* 5c0926b Add second NTP server
* f93e6eb Add NTP server
| * 15186d5 Add README
|/
* 3672ce5 Add R1 base config

GitHub performs the merge on its servers, but each method has a local Git equivalent. The next example builds the same repository, makes three clones (as if three people each pressed a different button) and merges origin/feature/ntp in each:

# The same starting point as above
git init -q netconf && cd netconf
printf 'hostname R1\n' > router.cfg
git add router.cfg && git commit -q -m "Add R1 base config"
git switch -q -c feature/ntp
printf 'ntp server 192.0.2.10\n' >> router.cfg && git commit -q -am "Add NTP server"
printf 'ntp server 192.0.2.11\n' >> router.cfg && git commit -q -am "Add second NTP server"
git switch -q main
printf '# Router configs\n' > README.md
git add README.md && git commit -q -m "Add README"
cd ..

# Three copies, one per merge method
git clone -q netconf merge-commit
git clone -q netconf squash
git clone -q netconf rebase

echo "== Create a merge commit"
cd merge-commit
git merge -q --no-ff -m "Merge pull request #12 from feature/ntp" origin/feature/ntp
git log --graph --oneline
cd ..

echo "== Squash and merge"
cd squash
git merge -q --squash origin/feature/ntp
git commit -q -m "Add NTP servers (#12)"
git log --graph --oneline
cd ..

echo "== Rebase and merge"
cd rebase
git switch -q -c ntp origin/feature/ntp
git rebase -q main
git switch -q main
git merge -q --ff-only ntp
git log --graph --oneline
== Create a merge commit
*   aaebbe2 Merge pull request #12 from feature/ntp
|\
| * 5c0926b Add second NTP server
| * f93e6eb Add NTP server
* | 15186d5 Add README
|/
* 3672ce5 Add R1 base config
== Squash and merge
Automatic merge went well; stopped before committing as requested
Squash commit -- not updating HEAD
* ba236aa Add NTP servers (#12)
* 15186d5 Add README
* 3672ce5 Add R1 base config
== Rebase and merge
* 3ad0f8d Add second NTP server
* 6878372 Add NTP server
* 15186d5 Add README
* 3672ce5 Add R1 base config
  • Merge commit (git merge --no-ff): the original commits f93e6eb and 5c0926b are kept unchanged, and a new commit with two parents records the merge. --no-ff forces that merge commit even when a fast-forward would be possible; GitHub always creates one with this method.
  • Squash and merge (git merge --squash plus a commit): a squash merge combines all changes of a branch into one new commit with a single parent on the target branch; the individual commits of the branch do not appear in its history. --squash only stages the combined result, which is why Git says it stopped before committing and did not update HEAD: the commit is yours to make.
  • Rebase and merge (git rebase, then a fast-forward): the two commits are replayed on top of main. Same messages and changes, but new hashes (6878372, 3ad0f8d), because each commit now has a different parent. See Rebasing and rewriting history.
Method History on main Good for Watch out for
Create a merge commit (default) All branch commits plus a merge commit; branch shape visible Seeing exactly what was merged when, and from where A busy, non-linear graph; work-in-progress commits end up on main
Squash and merge One new commit per pull request; linear Clean history of one commit per change; easy to revert as a unit The branch commits are gone from main; keep pull requests small so one commit is meaningful. Git no longer sees the branch as merged, so delete it
Rebase and merge Every branch commit, replayed with new hashes; linear Linear history that keeps well-crafted individual commits New hashes, so GitHub cannot show the original signatures as verified; commits on main no longer match those on the branch

A rule "Require linear history" allows only squash and rebase merges. Which method a team uses is part of its branching strategy; consistency matters more than the choice.

The merge queue

Required checks have a gap on busy repositories. Pull requests A and B both pass their checks against the same main. A is merged. B is still "green", but nobody has tested B together with A, and the combination can break. Requiring branches to be up to date closes the gap, but then every merge forces everyone else to update and wait for a new run.

A merge queue is a GitHub feature that merges approved pull requests one after another and, before each merge, runs the required checks on the pull request combined with the latest base branch and every pull request queued ahead of it. Instead of pressing merge, you add the pull request to the queue; GitHub builds a temporary branch with the combination, runs the checks, and merges only if they pass. A failing pull request is removed from the queue and the ones behind it are tested again without it.

  • Workflows must also listen to the merge_group event (next to pull_request); otherwise the required checks never report for the queue and nothing merges.
  • Availability (as of 2026): public repositories owned by an organization, and private organization repositories on Enterprise Cloud.
  • A merge queue cannot be combined with branch protection rules that use wildcard patterns.
on:
  pull_request:
  merge_group:   # also run the checks for the merge queue

Review etiquette and common mistakes

  • Huge pull requests. Review quality drops sharply with size. If a change cannot be small, split it into a series: preparation and refactoring first, behavior change after.
  • Rubber-stamping. An approval with no comments within a minute of opening a 600-line diff is not a review; it is a signature on something unread. If you cannot review properly now, say so, or review part and say which part.
  • Letting pull requests rot. A pull request that waits three days drifts away from main and collects conflicts. Many teams agree to respond within a working day.
  • Arguing about style in comments. Agree on a formatter once, run it in the pipeline, and spend review time on behavior.
  • Force pushing during review. Rewriting the branch after reviews makes earlier comments point at commits that no longer exist. Push new commits during review; tidy up (or squash on merge) at the end.
  • Approving your own work through a second account or merging with admin rights "just this once" defeats the purpose of the rules; use the bypass list deliberately and rarely.