What Is a Pull Request?
A pull request (PR) is GitHub’s mechanism for proposing that changes you made on one branch be reviewed and merged into another branch, usually main. Instead of merging locally and pushing straight to the shared branch, you push your work to its own branch and ask GitHub to open a structured conversation around it: a diff, inline comments, automated checks, and an explicit approval step. Pull requests are the backbone of collaborative workflows on GitHub — almost nothing lands on a shared branch without going through one.
Overview / How Pull Requests Work
It helps to be precise about what a pull request actually is. Git itself has no concept of a "pull request" — it is not a commit, a branch, or any object in Git’s object model (blobs, trees, commits). A PR is a piece of GitHub-side metadata: a record that says "compare ref A (the base) against ref B (the head), and let people discuss and eventually merge the difference." Everything you see on a PR page — the diff, the comment threads, the list of commits, the checkmarks from CI — is GitHub rendering a comparison between two branches (or a branch and a fork) using ordinary Git history underneath.
Recall that a branch is just a movable pointer to a commit, and HEAD normally points to whichever branch you have checked out. When you create a feature branch, make commits, and push it to GitHub, you now have two diverging pointers on the remote: main and feature/login-page, both eventually sharing a common ancestor commit. A pull request asks GitHub to compute the commits reachable from the head branch but not from the base branch (conceptually the same thing git log main..feature/login-page would show you locally) and display them as the proposed change.
The typical flow looks like this:
- Create a branch off
mainand commit your work there. - Push the branch to GitHub.
- Open a pull request, choosing a base branch (where the change should land) and a head branch (your work).
- Reviewers read the diff, leave comments, request changes, or approve. Automated checks (tests, linters, GitHub Actions workflows) run against the head branch.
- Once approved and passing, the PR is merged into the base branch using one of several merge strategies.
- The head branch is typically deleted, since its commits now live in
main‘s history (or have been squashed/replayed into it).
Same-repo branches vs. forks
Team members with write access usually open PRs from a branch inside the same repository. External contributors, who don’t have push access, first fork the repository (their own full copy on GitHub) and open a PR from a branch in their fork back to the upstream repository. Git and GitHub don’t actually care which case you’re in — a fork’s branch is just another ref that can serve as a head branch, GitHub just tracks the ownership boundary.
Draft pull requests
If you want early feedback or want CI to run before your work is finished, you can open a PR as a draft. Draft PRs show the same diff and run the same checks, but are visually marked as not-ready and normally cannot be merged until you click "Ready for review."
Merge strategies, briefly
When a PR is merged, GitHub offers three strategies. A merge commit creates a new commit with two parents, preserving every individual commit and the branch topology. Squash and merge collapses all of the PR’s commits into a single new commit on the base branch, giving a clean, linear history at the cost of losing the individual commit steps. Rebase and merge replays each of the PR’s commits individually onto the tip of the base branch, keeping them separate but avoiding a merge commit. None of these is universally "correct" — merge commits preserve full context, squash keeps main tidy, and rebase keeps granularity without merge noise. Many teams standardize on one strategy per repository via branch settings.
Syntax
There is no single Git command for opening a pull request, because a PR is a GitHub feature, not a Git one. You can open one from the GitHub web UI, or from the command line using GitHub’s official gh CLI. The general form of the relevant gh subcommands is:
gh pr create [--base <branch>] [--head <branch>] [--title <text>] [--body <text>] [--draft]
gh pr list [--state open|closed|merged|all]
gh pr view <number>
gh pr checkout <number>
gh pr merge <number> [--merge|--squash|--rebase] [--delete-branch]
| Flag / subcommand | Meaning |
|---|---|
gh pr create |
Opens a new pull request from your current (or specified) branch. |
--base |
The branch the change should be merged into (default: the repository’s default branch). |
--head |
The branch containing your changes (default: the currently checked-out branch). |
--title, --body |
The PR’s title and description text. |
--draft |
Opens the PR in draft state. |
gh pr list |
Lists pull requests in the current repository. |
gh pr view <number> |
Shows details of a specific PR in the terminal. |
gh pr checkout <number> |
Fetches a PR’s head branch and checks it out locally. |
gh pr merge <number> |
Merges a PR using the chosen strategy, optionally deleting the head branch afterward. |
Examples
Example 1: Push a branch and get GitHub’s PR link
The simplest way to start a PR is to push a new branch. Git’s remote (via GitHub’s server-side hook) prints a ready-made URL for opening the pull request.
git switch -c feature/login-page
git add login.html login.css
git commit -m "feat: add login page markup and styles"
git push -u origin feature/login-page
Output:
remote:
remote: Create a pull request for 'feature/login-page' on GitHub by visiting:
remote: https://github.com/your-org/your-repo/pull/new/feature/login-page
remote:
To github.com:your-org/your-repo.git
* [new branch] feature/login-page -> feature/login-page
branch 'feature/login-page' set up to track 'origin/feature/login-page'.
The branch now exists on the remote, but nothing has been proposed for merge yet — a PR is a separate GitHub object you still have to create by visiting that link (or using gh pr create, below).
Example 2: Open a PR from the terminal with gh
gh pr create --base main --head feature/login-page --title "feat: add login page" --body "Adds the initial markup and styles for the login page. Closes #42."
Output:
https://github.com/your-org/your-repo/pull/57
This creates PR #57, comparing feature/login-page (head) against main (base). Writing Closes #42 in the body links the PR to issue #42 and will automatically close that issue once the PR is merged — a small but very useful GitHub convention.
Example 3: Check out someone else’s PR locally to review it
Reviewing code is often easier with it actually checked out, so you can run it, search it, or test edge cases. Every open PR’s commits live on a special ref GitHub exposes on the remote.
git fetch origin pull/57/head:pr-57
git switch pr-57
Output:
From github.com:your-org/your-repo
* [new ref] refs/pull/57/head -> pr-57
Switched to branch 'pr-57'
This works because GitHub stores every PR’s head commit under refs/pull/<number>/head on the remote, even for PRs opened from forks you don’t otherwise have direct branch access to. The equivalent, shorter command is gh pr checkout 57, which does the same fetch-and-switch for you.
How It Works Step by Step
When you open a pull request, GitHub does not create any new Git objects for the comparison itself. Under the hood it performs the equivalent of a three-dot diff: it finds the merge base (common ancestor) of the base and head branches and shows everything the head branch has added since that point, which is exactly what running the commands below locally would show you.
git merge-base main feature/login-page
git log --oneline main..feature/login-page
When a reviewer approves and the PR is merged, real Git history is finally written: a merge commit is created (pointing to both branch tips as parents), or a new squash commit is created (a single commit whose tree is the final state, with only main‘s previous tip as parent), or the head commits are individually rebased and appended to main. In every case, the main branch pointer on GitHub’s copy of the repository moves forward to the newly created commit. Your local main only catches up once you run git fetch or git pull.
Common Mistakes
Mistake 1: Opening the PR against the wrong base branch
It’s easy to leave the base branch on GitHub’s default suggestion when you actually meant to target a release branch or another feature branch, producing a diff full of unrelated commits.
# Wrong: PR silently compares against the default branch
gh pr create --title "feat: add login page"
Always double-check the base before creating the PR, or fix it afterward:
gh pr edit 57 --base develop
Mistake 2: Force-pushing a PR branch other people are also using
Rewriting a PR branch’s history (for example after an interactive rebase to clean up commits) and force-pushing can silently discard commits a reviewer or collaborator already pulled.
# Risky: overwrites the remote branch unconditionally
git push --force origin feature/login-page
Prefer --force-with-lease, which refuses to push if the remote branch has commits you haven’t fetched yet, protecting collaborators from a silent overwrite:
git push --force-with-lease origin feature/login-page
Mistake 3: Letting the PR branch drift far from main
Leaving a PR open for weeks while main moves on produces large, conflict-ridden merges. Periodically bring the base branch’s changes into your PR branch (via merge or rebase, depending on team convention) instead of doing it all at once right before merging.
Mistake 4: Forgetting to delete the branch after merge
Merged branches that are never deleted clutter the branch list and make it harder to tell what’s still active. GitHub can delete the head branch automatically when you merge — use it.
Best Practices
- Keep pull requests small and focused on one logical change — they’re easier and faster to review.
- Write PR titles and commit messages in Conventional Commits style, e.g.
feat: add login pageorfix: correct off-by-one in pagination. - Use a draft PR when you want early CI feedback or visibility but aren’t ready for review.
- Reference related issues in the PR body (
Closes #42) so they close automatically on merge. - Request specific reviewers rather than leaving review ownership ambiguous.
- Keep your branch reasonably up to date with its base branch to avoid painful late-stage conflicts.
- Agree on one merge strategy (merge commit, squash, or rebase) per repository and enforce it in the repo’s merge settings.
- Delete the head branch immediately after merging, either via the "Delete branch" button or
gh pr merge --delete-branch. - Use
--force-with-leaseinstead of bare--forcewhenever you must rewrite a pushed PR branch.
Practice Exercises
- In a repository you own on GitHub, create a branch named
feature/readme-update, make a small edit toREADME.md, commit it with a Conventional Commits message, push it, and open a pull request againstmainusinggh pr create. Expected end state: a new open PR whose diff shows only yourREADME.mdchange. - Find any open-source PR number in a public repository you have read access to (or one of your own), and check it out locally using
gh pr checkout <number>. Confirm withgit branch --show-currentthat you’re now on the PR’s head branch. - Open a PR, then push an additional commit that amends your very first commit and force-push it. Try pushing first with plain
--forceand then, after resetting the scenario, with--force-with-lease. Hint: to see the protective behavior of--force-with-lease, have a "collaborator" (a second local clone) push a commit to the branch first, then attempt your force-push without re-fetching.
Summary
- A pull request is GitHub metadata comparing a head branch against a base branch — it is not part of Git’s own object model.
- The typical flow is: branch, commit, push, open PR, review and run checks, merge, delete branch.
- External contributors usually open PRs from a fork; team members usually use a branch in the same repository.
- Merging offers three strategies — merge commit, squash and merge, and rebase and merge — each with different history tradeoffs.
gh pr create,gh pr checkout, andgh pr mergelet you manage the entire PR lifecycle from the terminal.- Every open PR’s commits are fetchable via
refs/pull/<number>/headon the remote, even from forks. - Prefer
--force-with-leaseover bare--forcewhen rewriting a pushed PR branch.
