Skip to content

Team Workflows: Shared Main and Feature Branches

Git permits a team to share one branch or work on separate branches. These are team policies, not different versions of Git. The decision affects when unfinished code reaches the branch everyone relies on.

In a centralized-style workflow, Ahmed, Sara, and Omar each have a local clone and commit directly on main. They all push to the same hosted main.

Ahmed/main ─┐
Sara/main ─┼──> GitHub/main
Omar/main ─┘

This can be simple for a tiny, closely coordinated exercise. Nobody has to name feature branches. It becomes awkward when developers work independently or when main is expected to remain deployable.

Omar and Sara both start from commit C1. Omar pushes O1; Sara creates S1 without fetching Omar’s work:

GitHub main: C1──O1
Sara main: C1──S1

Sara’s ordinary push cannot fast-forward GitHub’s branch to S1 because that would remove O1 from its reachable tip. Git rejects the non-fast-forward push to protect Omar’s work.

Sara should fetch, inspect, then integrate according to team policy before pushing:

Terminal window
git fetch origin
git log --oneline --graph --decorate --all
git switch main
git merge origin/main
dotnet test
git push origin main

A merge may produce a conflict, but divergence alone does not guarantee one. A team could also choose a carefully scoped rebase for unpublished local commits; do not force-push shared main to bypass the rejection.

Ahmed may want help with a half-built feature. Git technically lets him push an unfinished commit to a writable shared main, but that exposes broken code to everyone who updates from it. He is not blocked by Git merely because the feature is incomplete; the team’s stability rule is the issue.

If Sara also has local edits, a pull may refuse when integrating would overwrite them. She should inspect git status, then commit or stash as appropriate before pulling. One shared branch makes this coordination frequent.

Claims such as “you must merge before almost every push” are too strong. You need to integrate remote work when the shared branch has moved beyond your local base; otherwise a normal push can succeed.

A better default for concurrent work is one branch per task. Keep main as the shared integration branch and do ordinary development elsewhere:

main ──────────────────────────────────────── stable integration
├── feature/pricing ── Ahmed's work
└── feature/navbar ── Sara's work

The branch name describes the change, not necessarily the person. A feature branch can hold unfinished code and be pushed for review without changing main. That does not make it automatically safe for production; it simply isolates its history until integration.

Start from an up-to-date base when possible:

Terminal window
git switch main
git pull --ff-only origin main
git switch -c feature/pricing

Work, test, and publish the branch when collaboration is needed:

Terminal window
git add src/PricingService.cs
git commit -m "feat: add pricing table"
git push -u origin feature/pricing

Pushing a feature branch leaves hosted main unchanged. A teammate can fetch it and create a local branch that tracks it:

Terminal window
git fetch origin
git switch --track origin/feature/pricing

If the local branch already exists, switch to it instead. The exact command can also depend on branch naming and existing local references. Sara can commit fixes on that shared feature branch if the team grants write access, then push it. Ahmed fetches or pulls its updates later. Their unrelated feature branches need not be merged into each other.

There are several governance levels, not several Git merge algorithms:

  1. Merge at will: a developer decides alone. Fine for a disposable or tiny project, risky when production stability matters.
  2. Agree, then merge locally: teammates review or discuss the change, and an authorized developer merges and pushes main.
  3. Pull Request: discussion, review, checks, and merge happen through the hosting platform. This is the focus of the next lesson.

The appropriate gate depends on team size, risk, repository permissions, and the importance of main. Even a Pull Request does not guarantee quality unless review and tests are meaningful.

Assume Ahmed’s feature is complete and his local feature branch includes the latest reviewed work:

Terminal window
git switch main
git pull --ff-only origin main
git merge feature/pricing
dotnet test
git push origin main

Git may fast-forward if main is an ancestor of the feature tip. If both lines moved, it may create a merge commit and possibly stop for conflict resolution. The merge lesson explains those mechanics in detail.

Sara later updates her local integration branch:

Terminal window
git switch main
git pull --ff-only origin main

Only after the merge reaches the hosted branch do other developers see it through fetch or pull. Local merge, push, teammate pull is the coordination sequence.

Deleting the completed feature branch is optional cleanup. First verify that the intended work is present on main, that tests pass, and that no one needs the branch for ongoing work.

Terminal window
git branch -d feature/pricing
git push origin --delete feature/pricing

The first command removes the local branch if Git considers it merged. The second deletes the hosted branch if permissions allow. Neither command is required for the merge itself. Be careful with a remotely shared branch: coordinate before removing a name collaborators still use.

What feature branches improve, and what they do not

Section titled “What feature branches improve, and what they do not”
  • They keep unfinished commits away from main until intentionally integrated.
  • They allow teammates to share and review a work-in-progress branch.
  • They reduce pressure to reconcile unrelated work directly on shared main for every commit.
  • They do not eliminate conflicts or guarantee fewer total merges. Long-lived branches may accumulate large integration problems.
  • A stable main also requires tests, review, and access policy, not just branch names.
  • Force-pushing after a rejected shared-branch push.
  • Saying Git forbids unfinished commits on main; it is usually a team rule.
  • Assuming different branch tips always cause a file conflict.
  • Creating a feature branch from an outdated base without checking remote work.
  • Pushing a feature branch and assuming main has changed.
  • Deleting a shared branch before confirming nobody still needs it.

One shared branch is simple but couples everyone’s incomplete work. A feature branch isolates each task, can be published for collaboration, and reaches main only after an intentional merge. Discussed local merges and Pull Requests are different review policies around the same branch integration.

  1. Why did Sara’s push fail after Omar pushed first?
  2. Does Git itself forbid an unfinished feature from being pushed to main?
  3. What changes on GitHub when you push only feature/pricing?
  4. What must happen after a local merge before teammates can receive it?
  5. Why might a long-lived feature branch still become difficult to integrate?