Git Collaboration Workflows

Feature branch + pull request

graph LR main["main"] --> pr["Open a PR:<br/>feature -> main"] feature["feature branch"] --> pr pr --> review["Code review + CI checks"] review --> merge["Merge into main<br/>(or rebase, per team convention)"]

The dominant workflow in most teams: branch off main for a unit of work, push it, open a pull request, get review + automated checks, merge. The entire value of this workflow depends on branches staying short-lived — a feature branch open for weeks accumulates drift from main that makes the eventual merge painful, which is exactly the problem trunk-based development (below) is designed around.

Trunk-based development vs. Git Flow

Trunk-based development: everyone works off main (the "trunk") with short-lived branches (hours to a couple of days), merged frequently. Incomplete work that would break main if deployed is hidden behind a feature flag rather than kept on a long-lived branch — the code is merged and deployed, just not activated, decoupling "is this code in main" from "is this feature live for users."

Git Flow: a heavier convention with dedicated long-lived branches (develop, release/*, hotfix/*) and structured rules for when each merges into what. It was designed for projects with infrequent, versioned releases (desktop software with release cycles) — for a continuously-deployed web service, the extra branches mostly add process overhead without a matching benefit, which is why trunk-based development has become the more common default for that kind of project specifically, not a universal replacement for Git Flow's original use case.

graph TD subgraph trunk["Trunk-based"] t1["main"] --> t2["short branch (hours)"] --> t1 end subgraph flow["Git Flow"] f1["develop"] --> f2["release/1.2"] --> f3["main"] f4["feature/x"] --> f1 f5["hotfix/1.2.1"] --> f3 end

Rebase-before-merge vs. merge commits, as a team convention

This connects directly to Branching and Merging: some teams require rebasing a feature branch onto main before merging (keeping main's history linear, one commit per logical change), others allow merge commits (preserving the exact shape of how work happened). Neither is objectively correct — it's a real trade between "easy to read/bisect later" (favors rebase) and "preserves exactly what happened, including exactly when branches diverged" (favors merge commits) — but a team should pick one and apply it consistently, since a history that mixes both conventions inconsistently is harder to read than either applied uniformly.

Common pitfall

Letting a feature branch live for weeks "because the feature isn't done yet" and treating that as the reason to avoid merging early is backwards — it's precisely the long branch lifetime that makes the eventual merge risky (more conflicting changes accumulated on both sides), not a reason to delay merging further. Trunk-based development's answer — merge early and often, gate the incomplete feature behind a flag instead of behind a long-lived branch — exists specifically to break this trap, by decoupling "is the code merged" from "is the feature exposed to users," which a long-lived branch conflates into one all-or-nothing event.