Git is the tool most developers learn just enough of to survive. That usually means add, commit, push, and a quiet panic whenever something goes wrong. Once you work with two or three other people, that is no longer sufficient. This guide covers a practical branching workflow for a small team — the kind with two to six developers, one product, and no dedicated release engineer.
Why branching matters in a small team
When everyone commits straight to main, three predictable problems appear. Half-finished work blocks a release because it is already on the shared branch. Two people edit the same file and resolve conflicts under pressure. And nobody reviews anything, because there is no natural moment at which review happens.
A branch is simply a movable pointer to a commit. Creating one costs nothing — no file copying, no server round trip. That cheapness is what makes the workflow below practical rather than bureaucratic.
The workflow in one paragraph
Keep main always deployable. For every piece of work, branch off main, commit as you go, push the branch, open a pull request, get one review, merge, delete the branch. That is the whole thing. It is often called trunk-based development or GitHub Flow, and for a small team it beats heavier models like Git Flow, which was designed for versioned software with long release cycles and parallel maintenance branches.
Branch naming that actually helps
Agree on a prefix convention and stick to it. It costs nothing and makes the branch list readable at a glance:
feature/checkout-coupon-fieldfix/login-redirect-loopchore/upgrade-eslintrefactor/extract-payment-servicehotfix/payment-timeout
Include the ticket number if you use an issue tracker: feature/PROJ-142-coupon-field. Avoid personal branches named after yourself — sara-work tells the rest of the team nothing about what it contains or whether it is safe to delete.
Starting a branch correctly
The most common branching mistake is branching off stale code:
git checkout main
git pull origin main
git checkout -b feature/coupon-field
That pull in the middle is not optional. Branching from a main that is three days old guarantees a bigger merge later. Newer Git versions let you write git switch -c feature/coupon-field, which does the same thing with clearer intent.
Commit habits that make review possible
A commit should be one logical change. Not one day of work, not one file — one change that could be described in a single sentence without the word “and”.
A workable message format:
fix: prevent redirect loop when session expires
The auth guard redirected to /login while /login itself
was guarded, producing an infinite loop for expired tokens.
Guard now skips public routes.
Closes PROJ-88
Subject line in the imperative mood, under about seventy characters, no full stop. Blank line. Then the body explaining why, because the diff already shows what. If you adopt the conventional prefixes (feat, fix, chore, docs, refactor, test), tooling can generate changelogs from them later.
Use git add -p to stage selected chunks rather than whole files. It takes a few seconds and stops unrelated changes from riding along in a commit.
Keeping your branch current: merge or rebase
While you work, main moves. You have two ways to catch up, and small teams argue about this more than it deserves.
Merge brings changes in and records a merge commit:
git checkout feature/coupon-field
git merge main
Rebase replays your commits on top of the new main, producing a linear history:
git fetch origin
git rebase origin/main
A reasonable team rule: rebase your own unpushed or unshared branches to keep history clean, but never rebase a branch someone else is working on. Rebasing rewrites commit hashes, so a colleague who has pulled that branch will find their copy has diverged from yours in a genuinely confusing way.
If you do need to update a rebased branch on the remote, use git push --force-with-lease rather than --force. The lease version refuses to overwrite commits you have not seen, which protects you from destroying a teammate’s push.
Pull requests in a team of four
A pull request is not paperwork. It is the only moment where a second person looks at the code before it becomes everyone’s problem.
Keep them small. A 200-line PR gets a real review; a 2,000-line PR gets “looks good to me”. If a piece of work is genuinely large, split it into a sequence of PRs that each leave main in a working state — often by merging the plumbing first behind a feature flag, then the UI.
Write the description for someone who was not in the conversation: what changed, why, how to test it, and anything the reviewer should look at closely. Screenshots for UI changes save an enormous amount of back-and-forth.
One approval is usually enough at this team size. Two becomes a bottleneck.
Choosing a merge strategy
Most platforms offer three buttons, and the choice shapes your history:
- Merge commit — preserves every commit plus a merge node. Full detail, noisier graph.
- Squash and merge — collapses the branch into one commit on
main. Clean history, one commit per feature, and messy work-in-progress commits disappear. This is the pragmatic default for small teams. - Rebase and merge — replays each commit individually with no merge node. Linear and detailed, but only sensible if every commit is genuinely clean.
Pick one, enable branch protection on main requiring a passing CI run and one approval, and turn on automatic branch deletion after merge. Stale branches accumulate faster than anyone expects.
Handling conflicts without fear
A conflict means Git cannot decide between two changes to the same region. It is not an error, and nothing is lost.
<<<<<<< HEAD
const timeout = 5000;
=======
const timeout = 10000;
>>>>>>> main
The top block is your current branch, the bottom is what you are merging in. Edit the file into the state you actually want, delete all three marker lines, then git add the file and continue with git merge --continue or git rebase --continue.
Two things help enormously: resolve conflicts in small, frequent updates rather than one giant one at the end, and talk to the person whose code you are conflicting with instead of guessing their intent. If a resolution goes badly, git merge --abort or git rebase --abort returns you to where you started.
Hotfixes
Production breaks. The fix should not wait behind three in-progress features:
git checkout main
git pull
git checkout -b hotfix/payment-timeout
# fix, commit
git push -u origin hotfix/payment-timeout
Open the PR, get a fast review, merge, deploy. Because main was always deployable, this branch contains only the fix and nothing half-finished. That property is the entire reason for the discipline around main.
Commands worth knowing before you need them
git stashandgit stash pop— park uncommitted work to switch branches quickly.git log --oneline --graph --all— see the branch topology in the terminal.git reflog— a log of everywhere HEAD has been. This is how you recover commits you thought you destroyed.git revert <hash>— undo a commit by creating a new one. Safe on shared branches, unlikereset.git cherry-pick <hash>— copy one commit onto the current branch.git blame <file>— find which commit last changed each line, usually as a prelude to reading its message.
Agreeing the rules once, in writing
Most Git friction in small teams comes from unspoken assumptions rather than technical difficulty. One person squashes, another does not. One rebases shared branches, another discovers their history rewritten. Nobody is wrong; nobody agreed.
Write a short file in the repository — CONTRIBUTING.md is the conventional name — covering five decisions. Which branch is the trunk. The branch naming prefixes. The commit message format. The merge strategy. Whether rebasing shared branches is allowed. Half a page is enough, and it settles every future argument by pointing at the file rather than relitigating.
Pair it with tooling so the rules enforce themselves. Branch protection on the trunk requiring a passing CI run and one approval. A pull request template prompting for what changed and how to test it. A commit-message linter if you have adopted conventional prefixes. Automatic branch deletion after merge.
The point is not process for its own sake. It is that a team of four cannot afford to spend review time on mechanics, and every decision moved into tooling is attention returned to the actual code.
Frequently asked questions
Is Git Flow better than this for a small team?
Usually not. Git Flow’s develop, release and support branches solve problems that come with shipping versioned software to customers who upgrade on their own schedule. A team deploying a web app continuously pays the overhead without getting the benefit.
How long should a feature branch live?
Ideally one to three days. Beyond about a week, the merge cost starts growing faster than the feature does. If a feature genuinely takes longer, merge it incrementally behind a feature flag.
What do I do if I committed to main by accident?
If you have not pushed, move the commit to a branch: create the branch at the current commit, then reset main back one commit. If you have already pushed and others have pulled, use git revert instead of rewriting history.
Should we protect the main branch?
Yes, from the first week. Requiring a pull request and a passing CI check costs nothing when everyone is already following the workflow, and it prevents the one tired evening where someone pushes directly.
Conclusion
A small team does not need an elaborate branching model. It needs a main branch that is always safe to deploy, short-lived branches with clear names, small commits with honest messages, and pull requests small enough that review is real. Add branch protection and automatic cleanup, agree once on squash versus merge, and the rest of Git stops feeling like a minefield.