Workflow
Branching strategies, conventional commits, and hotfixes under pressure.
Git Workflow
You know what a commit and a branch are. The next question is: how should you organise them? Workflow decisions compound over time — a bad branching strategy tolerated for six months is a major refactor.
Analogy
A git workflow is like the kitchen prep rules of a busy restaurant. Trunk-based development is a sushi counter: tiny dishes go out to the main pass continuously, every few minutes, and the pass is always servable. Git-flow is the multi-course tasting menu: cooks work long stretches on separate prep tables, then bring a whole course to the pass in a single big push. Merging conflicts are the argument that happens when two cooks reached for the same mise-en-place bowl and one of them shaved more garlic into it than the other expected — the sooner the head chef reconciles it, the smaller the mess. A long-running branch is a side table where ingredients are quietly going off; the longer it sits, the worse the smell when you finally haul it back to the main line.
Trunk-based development vs git-flow
Two strategies dominate the industry:
| Trunk-Based | Git-Flow | |
|---|---|---|
| Main branch | Always deployable | Often broken between releases |
| Feature branches | Short-lived (hours to 1–2 days) | Long-lived (days to weeks) |
| Releases | Continuous — deploy main | Tagged release branches |
| Merge frequency | Multiple times per day | Per-feature |
| Conflicts | Rare, small | Common, large |
| Best for | Products with CI/CD | Versioned software with fixed releases |
Most teams shipping web services today use trunk-based development. Git-flow survives in firmware, SDKs, and open-source libraries where multiple released versions need maintenance simultaneously.
Feature branches
A feature branch isolates work-in-progress from the rest of the team. The key discipline: keep them short. A branch open for more than two days is accumulating conflict debt. Every commit on main that you haven't rebased onto is a future conflict.
The mechanics:
git checkout -b feat/payment-webhooks # create from main
# ... work ...
git fetch origin
git rebase origin/main # pull in upstream changes daily
git push --force-with-lease # safe force-push after rebase
# open PR → review → merge
--force-with-lease is critical. Unlike --force, it refuses to overwrite if someone else has pushed to the branch since your last fetch. It protects collaborators.
Protected main
In production teams, the main branch has push rules:
- Direct push is disabled — all changes arrive via PR
- PRs require at least one approval and passing CI
- History can be set to require linear history (enforces rebase-before-merge)
You configure this in your git host (GitHub branch protection, GitLab protected branches, Bitbucket branch restrictions). Without it, anyone can accidentally push a broken commit directly.
Rebase-then-merge vs merge commits
Two options when a PR is ready:
Rebase-then-merge (linear history):
Before: main A──B──C feat D──E
After: main A──B──C──D'──E'
The PR commits are replayed on top of main. History is linear and bisect-friendly. The downside: commit SHAs change, so anyone who checked out the feature branch needs to reset.
Merge commit (preserves topology):
Before: main A──B──C feat D──E
After: main A──B──C──M (M has parents C and E)
The merge commit shows exactly when integration happened. History is non-linear but honest about what happened when.
Squash merge collapses the entire PR into a single commit on main. Useful when feature branch commits are noisy work-in-progress (wip, fix typo). The cost: you lose the individual commit history, which can hurt bisect precision.
Teams that use squash merge should also enforce good commit messages on the squash commit itself — not just "Merge PR #123".
Conventional commits
A lightweight standard for commit message format:
<type>(<scope>): <description>
[optional body]
[optional footer]
Types:
| Type | When to use |
|---|---|
feat |
A new capability visible to users or callers |
fix |
A bug fix |
chore |
Maintenance (deps, config, tooling) |
docs |
Documentation only |
refactor |
Code change with no behaviour change |
test |
Adding or updating tests |
ci |
CI/CD pipeline changes |
perf |
Performance improvement |
Examples:
feat(auth): add TOTP second factor
fix(api): return 422 instead of 500 on malformed JSON
chore(deps): bump next from 14.2.3 to 15.0.0
The payoff: automated changelogs, semantic versioning tools (like semantic-release), and consistency across a team that may not share a verbal style.
When to squash
Squash when:
- The feature branch contains many
wip,fix,typocommits that don't tell a useful story - You want main's log to read as a flat changelog of features and fixes, not implementation detail
Keep individual commits when:
- Each commit represents a meaningful, independently useful change
- You want precise bisect targets inside a PR
- You're contributing to open source and the maintainers prefer granular history
Hotfix under pressure
The hardest workflow moment: a bug is in production, you have an open feature branch, and you need to ship a fix fast.
git stash # or: push your WIP branch first
git checkout main
git checkout -b fix/payment-crash
# fix the bug
git add -p # stage only the fix
git commit -m "fix(payment): guard nil pointer on zero-amount orders"
git push
# open PR → review → merge to main
# deploy main
git checkout feat/payment-webhooks
git rebase origin/main # bring the fix into your feature branch
The playground below gives you a seeded repo with exactly this scenario.
Tools in the wild
6 tools- serviceGitHubfree tier
Most popular git host: PRs, code review, Actions CI, Issues — the dev OS for OSS.
- serviceGitLabfree tier
End-to-end DevOps platform: repos, MRs, pipelines, issue tracking, container registry.
- specConventional Commitsfree tier
Lightweight commit message convention powering changelogs + semantic releases.
- librarycommitlint + huskyfree tier
Pre-commit hook that rejects messages not matching your team's commit convention.
- cliGitButlerfree tier
Branch-juggling Git client that lets you split work across multiple virtual branches.
- service
Stacked-PR workflow on top of GitHub — ship many small PRs without context switching.