git · level 1

Workflow

Branching strategies, conventional commits, and hotfixes under pressure.

200 XP

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, typo commits 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
  • GitHubfree tier

    Most popular git host: PRs, code review, Actions CI, Issues — the dev OS for OSS.

    service
  • GitLabfree tier

    End-to-end DevOps platform: repos, MRs, pipelines, issue tracking, container registry.

    service
  • Lightweight commit message convention powering changelogs + semantic releases.

    spec
  • Pre-commit hook that rejects messages not matching your team's commit convention.

    library
  • GitButlerfree tier

    Branch-juggling Git client that lets you split work across multiple virtual branches.

    cli
  • Stacked-PR workflow on top of GitHub — ship many small PRs without context switching.

    service