Git Workflow Best Practices for Small Teams

Git Workflow Best Practices for Small Teams


Disclosure: This article may contain affiliate links. We only recommend products we believe in. See how we make money.

Small teams do not need a novel-length branching model. They need main to stay shippable, reviews that actually happen, and a history they can bisect. That is trunk-based development with short-lived branches — the workflow GitHub, Conventional Commits, and most CI templates assume.

Why not GitFlow

GitFlow (a develop line, release branches, hotfix branches) was designed for versioned desktop software with a QA freeze. A site that deploys on merge to the default branch pays for that graph without getting the freeze. Two people end up merging develop into main “when we remember.” Skip it.

Use a release branch only when you truly support an old line (a mobile app in the store, a library with semver). That is a product constraint, not a default.

The loop

  1. main is always deployable.
  2. Branch from main for one concern.
  3. Open a pull request when you want review, not when the feature is “done” in your head.
  4. CI is green. One human has approved. Then merge.
  5. The pipeline deploys main. Delete the branch.
feature/add-user-auth
fix/login-timeout-error
chore/update-dependencies
docs/api-endpoint-reference

Prefix plus kebab-case is enough. Do not encode ticket numbers in five places (branch, commit, PR title, Slack). One link in the PR body is enough.

Conventional Commits — what they are for

feat: add user authentication endpoint
fix: resolve timeout on login page
chore: update axios to 1.6.0
docs: add API reference for /users endpoint
refactor: extract validation logic to middleware
test: add integration tests for auth flow

The Conventional Commits spec is a changelog and semver contract, not a personality test. feat: and fix: are the ones tooling cares about. feat!: (or a BREAKING CHANGE: footer) is how you mark a break.

If nobody generates a changelog, the prefixes are still useful: they make git log --oneline skimmable and they keep an AI commit message from writing a novel.

Pull requests that get reviewed

One concern. A bugfix plus a refactor plus a dependency bump is three reviews pretending to be one. Split them.

Size. Review quality falls off as the diff grows — that is a consistent finding in the code-review literature, not a vibe. Aim for a diff a reviewer can finish before context-switch. Mechanical moves (mv a directory) should be their own PR so reviewers are not hunting a behavior change inside a rename.

A body that answers four questions:

## What
Add `Retry-After` handling to the API client.

## Why
The upstream started returning 429s during deploys.

## How tested
`npm test -- retry-after`. Manual: forced a 429 in staging.

## Risk
Clients will wait instead of immediately retrying. No schema change.

Put that in .github/pull_request_template.md so the empty PR is slightly embarrassing.

Drafts are allowed. A draft PR is how you get design feedback before CI is sacred. Do not require approval on a draft and then wonder why nobody looks.

An AI review comment can catch the null-check. It cannot tell you the feature is the wrong feature.

Protect main like you mean it

On GitHub: Settings → Branches → Branch protection (or a ruleset):

  • Require a pull request (no direct pushes).
  • Require 1 approval from someone who is not the last pusher, if the product allows it. Solo maintainers skip this; two-person teams should not.
  • Require the CI status checks you actually trust — lint + test + build, not a flaky e2e you will start ignoring.
  • Require branches to be up to date if the team is large enough that main moves during review.
  • Delete the head branch on merge.

Squash merge keeps main readable. Merge commits keep the original commits. Rebase-and-merge keeps a linear history without squashing. Pick one in the repo settings so the team does not argue per PR.

Hooks that stay out of the way

# .husky/pre-commit  (or a plain .git/hooks/pre-commit)
npx lint-staged

lint-staged runs the linter on staged files. A pre-commit that runs the entire unit suite is how people start --no-verify. Typecheck and tests belong in CI, which cannot be skipped with a flag the same way.

commitlint against Conventional Commits is optional. If you add it, allow fixup! during rebase or you will hate your own tool.

When this workflow is the wrong fit

  • Open source with drive-by contributors. You want main protected and a CODEOWNERS file more than you want trunk-based slogans.
  • Regulated review. Two humans, a ticket id, and an audit log beat a squash-merge aesthetic.
  • Long-running forks (a custom deploy for one customer). That is a product branch. Treat it as a release line, not as a feature branch that lived too long.

If a feature branch is more than a few days old, the process already failed — split the work or merge a dark-flagged slice to main so you stop rebasing a novel.