Forking Workflow

The forking workflow is how most open-source contributions happen on GitHub. Instead of getting direct push access to someone else’s repository, you create your own personal, server-side copy of it — a fork — make your changes there, and propose them back to the original project with a pull request. It lets anyone in the world contribute to a project without ever touching the original repository directly, while the maintainers stay in full control of what actually gets merged. This lesson walks through forking a repository, cloning it locally, keeping your fork in sync with the original, and submitting your work as a pull request.

Overview: How the Forking Workflow Works

A fork is not a Git concept — it’s a GitHub feature. When you click “Fork” on a repository (or run gh repo fork), GitHub creates a brand-new repository under your own account that starts out as a complete, exact copy of the original: every commit, every branch, every tag. Because it’s a full copy of the same commit history, the SHA-1 hashes of existing commits, trees, and blobs are identical between the fork and the original — only new commits you make will have new hashes. This matters because it means Git’s object model works exactly the same way on a fork as on any other repository: a commit object still points to a tree (a snapshot of the file structure), the tree still points to blobs (file contents) and further trees for subdirectories, and a branch is still just a movable pointer (a 40-character SHA-1 stored in a small file) to a commit. Forking simply gives you a second, independent copy of that whole object graph that you’re allowed to push to.

Once you have a fork, you clone your fork to your machine, not the original repository, because only your fork accepts pushes from your account. Convention names your fork’s remote origin (Git’s default name for the remote you cloned from) and adds the original repository as a second remote, conventionally named upstream. You do your work on a feature branch, push that branch to origin (your fork), and then open a pull request on GitHub asking the original repository’s maintainers to pull your branch into their main. This is the opposite of the shared-repository (“feature branch”) workflow, where every contributor has push access to one central repository and just pushes branches directly — forking exists specifically for cases where you don’t have (or shouldn’t have) that access, which is nearly always true for open-source projects with contributors outside the core team.

Syntax

There is no single git fork command, because forking happens on GitHub’s servers, not in Git itself. You either click the “Fork” button in the GitHub web UI, or use the GitHub CLI:

gh repo fork original-owner/awesome-project --clone=true

The Git commands you’ll actually use throughout the workflow are:

  • git clone <url> — download a repository (your fork) and check out its default branch.
  • git remote add <name> <url> — register another remote repository, typically upstream for the original project.
  • git remote -v — list configured remotes and their URLs.
  • git fetch <remote> — download new commits/branches from a remote into remote-tracking branches, without touching your working tree.
  • git switch <branch> / git switch -c <branch> — move to (or create and move to) a branch.
  • git merge <remote>/<branch> — integrate a remote-tracking branch into your current branch.
  • git push <remote> <branch> — upload your branch’s commits to a remote.
  • gh pr create — open a pull request from your fork’s branch against the upstream repository’s branch.
Remote name Points to You can push?
origin Your fork on GitHub Yes, always
upstream The original repository Usually no — contribute via pull request instead

Examples

Example 1: Forking and cloning

After forking original-owner/awesome-project on GitHub (or via gh repo fork), clone your fork and register the original as upstream:

git clone https://github.com/yourusername/awesome-project.git
cd awesome-project
git remote add upstream https://github.com/original-owner/awesome-project.git
git remote -v

Output:

Cloning into 'awesome-project'...
remote: Enumerating objects: 128, done.
remote: Counting objects: 100% (128/128), done.
remote: Compressing objects: 100% (94/94), done.
Receiving objects: 100% (128/128), 42.10 KiB | 2.63 MiB/s, done.
Resolving deltas: 100% (51/51), done.
origin	https://github.com/yourusername/awesome-project.git (fetch)
origin	https://github.com/yourusername/awesome-project.git (push)
upstream	https://github.com/original-owner/awesome-project.git (fetch)
upstream	https://github.com/original-owner/awesome-project.git (push)

Cloning downloaded your fork’s full object graph and set up origin automatically. The second command adds upstream manually — at this point Git only stores its URL in .git/config; nothing has been fetched from it yet.

Example 2: Branch, commit, push to your fork, open a pull request

git switch -c feature/fix-typo-readme main
# ...edit README.md...
git add README.md
git commit -m "docs: fix typo in installation section"
git push -u origin feature/fix-typo-readme
gh pr create --base main --head yourusername:feature/fix-typo-readme --title "docs: fix typo in installation section" --body "Fixes a small typo in the installation instructions."

Output:

[feature/fix-typo-readme 8a1c9de] docs: fix typo in installation section
 1 file changed, 1 insertion(+), 1 deletion(-)
Enumerating objects: 5, total.
To https://github.com/yourusername/awesome-project.git
 * [new branch]      feature/fix-typo-readme -> feature/fix-typo-readme
branch 'feature/fix-typo-readme' set up to track 'origin/feature/fix-typo-readme'.

Creating pull request for yourusername:feature/fix-typo-readme into main in original-owner/awesome-project
https://github.com/original-owner/awesome-project/pull/342

The branch and commit only ever existed in your local repository until the push, which uploaded the new commit object (and its tree and blob) to origin (your fork) and created a new branch ref there. gh pr create then talks to the GitHub API to register a pull request comparing your fork’s branch against upstream‘s main — it doesn’t move any Git refs itself.

Example 3: Syncing your fork with upstream

While your pull request is open, the original project keeps moving. Before starting new work (or before rebasing your PR branch), bring your fork’s main up to date:

git fetch upstream
git switch main
git merge upstream/main
git push origin main

Output:

remote: Enumerating objects: 15, done.
remote: Counting objects: 100% (15/15), done.
Unpacking objects: 100% (9/9), done.
From https://github.com/original-owner/awesome-project
 * [new branch]      main       -> upstream/main
Updating a1b2c3d..e4f5g6h
Fast-forward
 CHANGELOG.md | 8 ++++++++
 1 file changed, 8 insertions(+)
To https://github.com/yourusername/awesome-project.git
   a1b2c3d..e4f5g6h  main -> main

git fetch upstream downloaded the new commits into the remote-tracking ref refs/remotes/upstream/main without touching your local main or working tree. Because your local main had no commits of its own that upstream didn’t have, git merge upstream/main was able to fast-forward — it just moved the main pointer forward to match upstream/main, no merge commit needed. The final push updates your fork on GitHub to match.

How It Works Step by Step

  • Fork (on GitHub): GitHub copies the entire repository — every commit, tree, and blob object, plus every branch and tag ref — into a new repository under your account.
  • Clone: Git downloads that fork’s objects to .git/objects on your machine and creates a local main branch pointing at the same commit as origin/main; HEAD is set to point at your local main.
  • git remote add upstream: stores a URL under a new remote name in .git/config. No network activity happens yet.
  • git fetch upstream: connects to the original repository, downloads any objects you don’t already have, and updates remote-tracking refs like refs/remotes/upstream/main. Your local branches and working tree are untouched.
  • git switch -c feature/x main: creates a new ref refs/heads/feature/x pointing at the same commit as main, and moves HEAD to point at that new branch.
  • git commit: writes a new blob for each changed file, a new tree describing the snapshot, and a new commit object pointing at that tree and at the previous commit as its parent; the current branch ref is updated to point at the new commit.
  • git push origin feature/x: uploads the new objects to your fork and creates (or updates) the feature/x ref there.
  • gh pr create / merge upstream/main: pull requests and syncing are metadata and pointer operations — no file content is duplicated beyond the actual new commits involved.

Common Mistakes

Mistake 1: Pushing straight to the upstream remote.

git push upstream feature/fix-typo-readme
remote: Permission to original-owner/awesome-project.git denied to yourusername.
fatal: unable to access 'https://github.com/original-owner/awesome-project.git/': The requested URL returned error: 403

You don’t have write access to a repository you don’t own. Push to origin (your fork) instead, then open a pull request so a maintainer can pull your changes in.

Mistake 2: Cloning the original repository instead of your fork. You can make commits locally, but any git push fails with the same permission error above. Double-check the URL you clone — it should point at github.com/yourusername/..., not github.com/original-owner/....

Mistake 3: Branching off a stale main. If you never run git fetch upstream and merge before creating a feature branch, your branch is based on an outdated snapshot of the project. By the time you open a pull request, your changes may conflict unnecessarily with everything that’s landed on upstream since you forked. Always sync main with upstream/main before starting new work.

Mistake 4: Force-pushing over a reviewed branch with bare --force. If a maintainer has already fetched or commented on specific commits in your PR branch and you rewrite history (e.g. after an interactive rebase) and run git push --force, you can silently discard commits they were looking at. Use git push --force-with-lease origin feature/x instead — it refuses to overwrite the remote branch if it has commits you haven’t seen, protecting against clobbering someone else’s concurrent work.

Best Practices

  • Sync your fork’s main with upstream/main before starting each new feature or fix, so you branch from the latest code.
  • Never commit directly on your fork’s main branch — keep it a clean mirror of upstream and do all work on feature branches like feature/login-page or fix/null-pointer-on-logout.
  • Write commit messages in Conventional Commits style (feat:, fix:, docs:, refactor:) so history and changelogs stay readable.
  • Rewriting history with git rebase is fine on a feature branch only you are working on, but never rebase a branch that other people have already pulled or built work on top of — that’s Git’s golden rule of rebasing.
  • Check “Allow edits from maintainers” when opening a pull request so a maintainer can push small fixes directly to your PR branch instead of going back and forth.
  • Delete your feature branch (locally and with gh pr merge --delete-branch, or the button on GitHub) once the pull request is merged, to keep your fork tidy.
  • Use SSH keys or a Personal Access Token for authentication — GitHub no longer accepts plaintext passwords over HTTPS.
  • Keep pull requests focused: one feature or fix per branch and per PR makes review far easier than one giant branch touching many things.

Practice Exercises

  • Exercise 1: Pick a small public repository on GitHub. Fork it (web UI or gh repo fork owner/repo --clone=true), then run git remote -v in the cloned copy and confirm you see both an origin remote pointing at your fork and an upstream remote pointing at the original (you’ll need to add upstream manually if you used the web UI).
  • Exercise 2: Create a branch named docs/update-readme, make a small, harmless change to a text file, commit it with a Conventional Commits message, push it to origin, and open a pull request with gh pr create. Expected end state: a pull request visible on the upstream repository comparing your fork’s branch to its main.
  • Exercise 3: Simulate the original project moving forward: edit a file directly on GitHub in the upstream repository (if you have access) or just note its latest commit. Locally, run git fetch upstream, then merge upstream/main into your local main, and push the result to origin. Expected end state: your fork’s main on GitHub matches upstream’s main exactly.

Summary

  • A fork is a full, independent copy of a repository created on GitHub, owned by your account — forking is a GitHub feature, not a native Git command.
  • Clone your fork (not the original), then add the original repository as a second remote conventionally named upstream.
  • Do work on feature branches, push them to origin (your fork), and open pull requests against upstream.
  • git fetch only downloads and updates remote-tracking refs; git merge (or rebase) is what actually moves your local branch pointer.
  • Sync your fork’s main with upstream/main regularly to avoid stale branches and unnecessary conflicts.
  • Never force-push over commits others have reviewed; prefer --force-with-lease and never rebase a branch others are already building on.