Topic · Collaboration & Remotes

Forks & Pull Requests

A fork is a server-side clone of a repository under your namespace. A pull request is a request to merge a branch from one repository into another. Together they form the contribution workflow that runs nearly every open-source project on a public forge.

What a fork actually is

From Git's point of view, a fork is nothing special. A fork is a server-side copy of a repository placed under a different namespace on the same forge. The forge (GitHub, GitLab, Bitbucket, Gitea, Codeberg, and so on) remembers which repository it was copied from and which branch is the default; Git itself does not.

Fork

A copy of a repository on a hosting platform, owned by your user or organization namespace, with a tracked "parent" relationship to the source. The fork relationship is a forge feature layered on top of plain Git, not a Git concept.

You fork because you do not have push access to the upstream repository. The fork is yours. You can clone it, push to it, break it, and rewrite its history without anyone else caring. When you have something worth offering back, you ask the upstream maintainers to pull from your fork. That request is the pull request.

Forking is the answer to a permissions question, not a technical one. If you were a committer on the upstream repository, you would just push a branch to it directly. Forking exists because most projects do not hand push access to strangers, and forging an independent copy lets contribution happen anyway.

The two-remote topology

Once you fork and clone, your local repository ends up connected to two remotes. By convention:

  • origin is your fork. You push to it. You pull from it.
  • upstream is the original repository. You only fetch from it. You almost never push to it.

This triangle is what most contribution diagrams are really drawing.

Three repositories. Two remotes. One contributor. upstream github.com/project/repo the original origin (your fork) github.com/you/repo your copy on the forge local clone on your laptop fork (one-time, forge) push / pull origin fetch upstream

Pull requests close the triangle: your branch lives on origin, and the PR asks the forge to merge it into upstream. The forge does the actual merge; you never push to upstream yourself.

Setting up origin and upstream

After clicking "Fork" on the forge UI, clone your fork. The clone gives you origin for free; you add upstream by hand.

fork setupshell
# 1. Clone YOUR fork (not the upstream)
git clone git@github.com:you/repo.git
cd repo

# 2. Confirm origin points at your fork
git remote -v
# origin  git@github.com:you/repo.git (fetch)
# origin  git@github.com:you/repo.git (push)

# 3. Add the upstream remote
git remote add upstream https://github.com/project/repo.git

# 4. Verify both remotes
git remote -v
# origin    git@github.com:you/repo.git         (fetch)
# origin    git@github.com:you/repo.git         (push)
# upstream  https://github.com/project/repo.git (fetch)
# upstream  https://github.com/project/repo.git (push)

Some contributors disable pushing to upstream entirely so they cannot fat-finger a push to the wrong place. It is a one-line guard:

disable upstream pushshell
git remote set-url --push upstream no_push

Any later git push upstream will now fail loudly instead of silently trying to write to a repository you do not own.

Topic branches off main

Every contribution lives on a topic branch. Branch off main (or whatever the upstream default is called), do the work, and push the branch to your fork. Do not commit directly to main on your fork.

Keeping main on your fork as a clean mirror of upstream/main has two payoffs. First, you always have a reliable starting point for the next branch. Second, when you open a PR, the diff is exactly your topic-branch work rather than a tangle of "merge upstream" commits.

starting a topic branchshell
# Fetch upstream and make sure main is current
git fetch upstream
git checkout main
git merge --ff-only upstream/main

# Branch off main for the work
git checkout -b fix-readme-typo

# Edit, commit, push to YOUR fork
git add README.md
git commit -m "Fix typo in installation section"
git push -u origin fix-readme-typo

The --ff-only on the merge is a guardrail. It refuses to create a merge commit, so if your local main has drifted from upstream you find out immediately instead of pasting an accidental merge into history.

The pull request, anatomically

A pull request, despite the name, does not pull anything by itself. It is a forge-managed conversation about a proposed merge between two refs. The forge calls them the base branch (where the change is going) and the compare or head branch (where the change is coming from). Underneath, the forge does little more than a git fetch from your fork plus a calculated merge preview.

Pull request

A request, mediated by a forge, to merge commits from one branch into another. Surfaces a diff, a discussion thread, line-level review comments, CI results, and a merge button. GitHub and Bitbucket call them pull requests; GitLab calls them merge requests; Gerrit calls them changes. Same underlying Git operation.

The PR page surfaces, at minimum:

  • A title and description. The description is where you say what the change does and why. Reviewers read it before they read the diff.
  • A diff between base and head, with line-level review affordances.
  • A commits list. Each push to the branch updates this list.
  • Checks — the status of CI workflows triggered by the branch.
  • A merge button gated on review approval, passing checks, and any branch protection rules the upstream has configured.

The PR lifecycle

From the moment you push your branch to the moment your topic branch is gone, a pull request travels through six states.

Open push + create Review comments Update push more CI tests, lint Merge forge button Cleanup delete branch re-review

Opening

Push your branch to origin (your fork), then click "New pull request" on the forge. Select base = upstream/main, compare = your-fork/your-branch. Fill out the title and description with intent: what changes, why, and how to verify.

Review

Reviewers leave line-level comments, request changes, or approve. On GitHub the Conversation tab is the central thread; line comments are anchored to specific diff hunks. Draft PRs let you publish work-in-progress without notifying code owners or asking for review until you flip it to ready.

Update, CI, merge, cleanup

Push follow-up commits to the same branch — the PR updates automatically and CI re-runs. Once approvals are in and checks are green, the maintainer clicks the merge button. Then you delete the topic branch (the forge usually offers a button for this) and resync your fork's main.

Staying current with upstream

Long-lived PRs often need to absorb new commits from upstream/main — either because reviewers ask for it, because of a conflict, or because branch protection requires the branch be up to date. There are two ways, and the choice is partly project culture and partly your taste.

Strategy 1: merge upstream into your branch

This is what GitHub's "Update branch" button does by default. It creates a merge commit on your branch that combines the new upstream history with yours.

merge upstream into branchshell
git fetch upstream
git checkout fix-readme-typo
git merge upstream/main
# resolve conflicts if any, then:
git push origin fix-readme-typo

The history is preserved verbatim. The downside: the branch now contains commits that are not yours, and the diff in the PR can look messier. The upside: it is safe, non-destructive, and never requires a force push.

Strategy 2: rebase onto upstream

Rebasing replays your commits on top of the latest upstream/main, producing a linear branch that looks as if you started your work from today. Because rebase rewrites commit hashes, you must force-push.

rebase onto upstreamshell
git fetch upstream
git checkout fix-readme-typo
git rebase upstream/main
# resolve conflicts during the rebase if any, then:
git push --force-with-lease origin fix-readme-typo

Use --force-with-lease, not plain --force. It refuses to clobber the remote if someone else has pushed in the meantime — a small but real safety net. The mechanics of rebasing are covered in detail in Rebasing.

Which one?

Follow the project's stated convention. Many projects prefer rebase-style updates because the eventual merge produces a clean, linear history. Others forbid force pushes during review because they invalidate reviewer link anchors and make it harder to see what changed since the last review.

Merge strategies on the forge

When a maintainer hits the merge button, the forge offers up to three strategies. Each makes a different promise about the resulting history on main.

Strategy What lands on main Pros Cons
Merge commit Every commit from the branch plus a merge commit (--no-ff) that joins them. Full history. Preserves the branch topology. Easy to revert by reverting the merge commit. Main history fills with work-in-progress commits. Topology can become busy.
Squash & merge One single new commit on main containing the cumulative diff. Clean, scannable main. One PR = one commit. Hides messy intermediate commits. Lossy: branch history disappears. SHAs differ from your branch; subsequent merges from the same branch can re-conflict.
Rebase & merge Each branch commit replayed linearly onto main, no merge commit. Linear history. Per-commit granularity preserved. Bisect-friendly. Requires each commit in the branch to be coherent on its own. New SHAs differ from your branch.

The three strategies are not interchangeable. Pick squash if your branches are scratch-pad style and only the final state matters. Pick rebase if you take care to write commits as a curated narrative. Pick merge commit if the branching topology itself has signal — release branches, long-lived feature branches with multiple authors.

Squash & long-lived branches

If you squash-merge a long-lived branch and then keep working on it, you will likely fight repeated conflicts. The common ancestor on main looks unrelated to your branch because the squash collapsed everything into a new commit. Delete and re-branch after a squash-merge, or use rebase/merge for branches that outlive a single PR.

After the merge

When the PR is merged, two small chores keep your fork from drifting.

post-merge cleanupshell
# 1. Delete the topic branch locally and on your fork
git checkout main
git branch -D fix-readme-typo
git push origin --delete fix-readme-typo

# 2. Resync your fork's main with upstream
git fetch upstream
git merge --ff-only upstream/main
git push origin main

Most forges also expose a "Sync fork" button on your fork's main page that does the same thing through the web UI. It is a fast-forward on the server side — identical in result to the commands above, but you still need a git fetch locally afterward to see it.

The gh and glab CLIs

For anything you do more than twice, the official CLIs are faster than the browser. GitHub's gh covers the full PR lifecycle; GitLab's glab mirrors it for merge requests.

gh from the terminalshell
# Open a PR for the current branch
gh pr create --base main --title "Fix typo" --body "..."

# List open PRs in the upstream repo
gh pr list

# Check out someone else's PR locally to test it
gh pr checkout 1234

# See the diff, comments, and CI status without leaving the terminal
gh pr view 1234
gh pr checks 1234

# Merge with a chosen strategy
gh pr merge 1234 --squash
Tip

The killer feature of gh pr checkout <number> is reviewing other people's PRs locally. It creates a branch tracking the contributor's fork, so you can run their code, poke at it, and even push fixes back to their branch if they enabled "Allow edits from maintainers" on the PR.

Common pitfalls

Pitfall 1

Committing directly to main on your fork. Now your fork's main has diverged from upstream/main, and resyncing requires a real merge instead of a fast-forward. Reset main to upstream/main (git reset --hard upstream/main after a fetch), move your work onto a topic branch, and resume.

Pitfall 2

Letting your fork go stale. If you forget to pull from upstream for months, branching off your fork's main branches off ancient history. Every contribution starts behind. Fetch upstream and fast-forward main before each new branch — it costs nothing.

Pitfall 3

Force-pushing in the middle of a review. Rebasing and force-pushing rewrites the commits reviewers were looking at. Their inline comments lose their anchors, and "see what changed since I last reviewed" stops working. Either don't force-push during active review, or coordinate with reviewers so they know to re-read from scratch.

Pitfall 4

Opening a PR against the wrong base branch. The forge's default base is the upstream default branch, but on long-lived release branches it might need to be something else. Check the base ref before submitting — a PR against the wrong base will look enormous and confuse reviewers, even if the diff itself is fine.

Sources & further reading

  • About forks Reference GitHub Docs

    Definition of a fork on GitHub, the parent/fork relationship, namespace rules, and how the fork stays linked to its upstream on the web UI.

  • About pull requests Reference GitHub Docs

    The anatomy of a PR: base and compare refs, the Conversation, Commits, and Checks tabs, draft PRs, and how the merge button gates on review and CI.

  • About pull request merges Reference GitHub Docs

    Side-by-side comparison of merge commit, squash-and-merge, and rebase-and-merge, including the squash-then-keep-working hazard around shared common ancestors.

  • Pro Git, 5.2: Contributing to a Project Textbook Scott Chacon and Ben Straub

    The Git-native view of the fork-and-contribute workflow: cloning, topic branches, pushing to a fork remote, and the git request-pull command beneath the forge UI.

  • Merge requests Reference GitLab Docs

    GitLab's vocabulary for the same workflow. Useful when you switch forges and need a translation of pull request to merge request, plus GitLab-specific features like merge trains.

  • gh pr Reference GitHub CLI Manual

    Subcommand reference for gh pr: create, list, view, checkout, checks, merge, and the rest. The terminal-native PR workflow.