Skip to content
ExponaBLOG
GIT & GITHUB FOR PROFESSIONAL DEVELOPERS · DAY 030 / 100

Forks vs branches: Git & GitHub best practices explained

Two collaboration models.

Expona Admin7 min read

Forks vs branches: Git & GitHub best practices explained

When you're working with a distributed team or managing open-source contributions, understanding when to use branches versus forks becomes critical to your workflow. We often see confusion around these two collaboration models, even in mature development teams—mainly because both create separate lines of development, but they solve different problems. In this article, we'll clarify the distinction and show you when and how to use each one effectively.

What makes branches ideal for team collaboration

Branches are your go-to for team members who have direct write access to the same repository. When everyone works on the same codebase and trusts each other to follow established conventions, branches provide simplicity and speed. They live in the same Git repository, share the same history, and integrate seamlessly with your CI/CD pipeline without extra configuration.

Think of a typical sprint scenario: your backend team is working on a payment module while the frontend team builds the checkout UI. Rather than everyone pushing directly to main, you create feature branches like feature/payment-processing and feature/checkout-ui. Team members pull the latest from main, create a local branch, commit their work, and open a pull request.

Here's how you'd set this up:

bash
git checkout -b feature/payment-processing
# Make your changes
git add .
git commit -m "Add Stripe integration for payment processing"
git push origin feature/payment-processing

The branch approach works because your team is coordinated. You're all watching the same remote, CI checks run automatically, and code review feedback comes from colleagues you interact with daily. There's no question about who owns the upstream repository—it's yours.

When forks are the right choice for outside contributors

Forks solve a different problem: how do you accept contributions from developers who don't have direct write access to your repository? A fork is a complete copy of a repository under a contributor's own GitHub account. It gives them full control in their namespace while keeping the original project clean.

Imagine you're maintaining an open-source logging library. A developer in another company finds a bug and wants to fix it. You can't give them direct access—and you shouldn't need to. Instead, they fork your repository, work on the fix in their fork, then submit a pull request back to your original repository. The pull request mechanism bridges the gap, allowing you to review and merge their work without granting them write permissions.

Creating and working with a fork follows this pattern:

bash
# Developer clicks "Fork" on GitHub, then clones their fork
git clone https://github.com/their-username/logging-library.git
cd logging-library

# Create a feature branch in their fork
git checkout -b fix/null-pointer-exception
# Make changes and commit
git add .
git commit -m "Fix null pointer in JSON formatter"
git push origin fix/null-pointer-exception

# Then open a pull request from their fork to the original repo

The fork model scales to unlimited contributors without compromising repository security. Each contributor's fork is isolated, and the original project maintainers review everything before it lands in the main codebase.

Setting up and maintaining the upstream remote

One challenge with forks: how do contributors keep their copy synchronized with the original project? That's where the upstream remote comes in. When you fork a repository and clone it locally, you only have origin pointing to your fork. To stay synchronized with the original, you add upstream as an additional remote.

Let's say you're a contributor to a popular React component library. The project evolves rapidly, and you want your fork to reflect the latest changes:

bash
# After cloning your fork
git remote add upstream https://github.com/original-owner/react-components.git

# Verify both remotes
git remote -v
# Output:
# origin    https://github.com/your-username/react-components.git (fetch)
# origin    https://github.com/your-username/react-components.git (push)
# upstream  https://github.com/original-owner/react-components.git (fetch)
# upstream  https://github.com/original-owner/react-components.git (fetch)

Now you can sync your fork with the original project before starting new work:

bash
# Fetch latest from upstream
git fetch upstream

# Rebase your main branch on upstream's main
git checkout main
git rebase upstream/main

# Push the updated main back to your fork
git push origin main

This pattern keeps your fork in sync and prevents your pull requests from having merge conflicts caused by diverging histories. It's especially important when the original project gets lots of activity.

How pull requests work with both models

The pull request mechanism is your unified interface for code review, regardless of whether you're using branches or forks. The underlying mechanics are identical: you propose changes, reviewers examine the diff, discussion happens in comments, and eventually the maintainer merges or closes the request.

For an internal team using branches, a pull request might look like this:

bash
# Developer on the team pushes a branch
git push origin feature/database-migration

# On GitHub, they open a PR from feature/database-migration to main
# Team members review, comment, request changes
# Once approved, the maintainer merges with a commit message

For open-source contributors using forks, the flow is nearly identical—except GitHub handles the cross-repository reference:

  • Contributor pushes to their fork
  • They open a PR requesting to merge their fork's branch into the original repository's main branch
  • Maintainers review and merge

Both scenarios run the same checks: status checks from your CI pipeline, branch protection rules, and required reviewers. The difference is purely organizational—branches are for your team, forks are for external collaborators.

Branches and forks in real production scenarios

In practice, many mature projects use both. A SaaS company might have internal branches for employee engineers and accept community contributions via forks. Here's how that shapes the workflow:

Internal development: Your team creates feature branches off main, opens PRs, gets internal review, and merges. CI runs automatically. Deployments happen from main.

Community contributions: External developers fork the repository, work on fixes or features, and submit PRs. Your team reviews—sometimes more carefully than internal PRs because the contributor may not be familiar with your architecture. Automated tests still run, and you maintain code quality standards.

A practical example: suppose you maintain a trading system library. Your team works in feature branches to add WebSocket reconnection logic. Simultaneously, a contributor from a partner company forks your repository to fix a bug in order parsing. Both create PRs against your main branch. GitHub shows you both pull requests in the same interface. Your CI pipeline runs tests on both. Your review process is consistent.

Common mistakes to avoid

Mixing the models inconsistently: Don't create a fork for someone on your team who already has write access. It adds unnecessary indirection and makes it harder to track who's working on what. Branches are faster and more transparent for trusted team members.

Forgetting to sync forks: Contributors often open pull requests without rebasing on the latest upstream. This leads to merge conflicts, failed CI checks, and frustration. Include fork synchronization in your contributor documentation.

Unclear branch naming: When your team uses many branches, poor naming leads to confusion. Use a consistent convention: feature/, bugfix/, hotfix/, release/. Everyone should understand what each branch is for at a glance.

Leaving stale branches unclean: After a PR merges, delete the branch on GitHub. Accumulated dead branches clutter the repository and confuse new team members about which branches are active.

Pushing directly to main: Even with branch protection rules, it's tempting to skip the PR process for "quick fixes." Resist this. Every commit to main should have gone through review and CI checks. It protects your production deployments.

Key takeaways

  • Use branches for team members with repository write access; they're simpler and faster for coordinated teams.
  • Use forks for external contributors and open-source projects; they provide isolation and security.
  • Always add an upstream remote in forked repositories to keep your copy synchronized with the original project.
  • Pull requests work consistently across both models—they're your interface for code review regardless of the collaboration method.
  • Most production projects use both: branches internally, forks for external contributors.
  • Follow naming conventions for branches, delete merged branches promptly, and never bypass pull requests for main branch commits.
  • Educate your team on when to use each model; unclear workflows lead to mistakes and slower development cycles.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles