Git Workflows That Don't Make You Want to Quit
At Home Depot, we used GitFlow. Feature branches, develop branches, release branches, hotfix branches. Our branch graph looked like a subway map. Merging a feature required a PhD in conflict resolution.
Now I use trunk-based development. One branch. Ship from main. My deploy frequency went from weekly to daily.
Why Most Git Workflows Are Over-Complicated
GitFlow was designed for software that ships quarterly on physical media. If your deployment process involves burning a CD, you need release branches.
If you deploy by merging to main and Vercel/GitHub Actions handles the rest, you don't need 90% of GitFlow.
What I Actually Do
```bash
1. Create a branch for the change
git checkout -b fix/stripe-webhook-idempotency
2. Make small, focused commits
git commit -m "add webhook event log table" git commit -m "check for duplicate events before processing" git commit -m "add test for duplicate webhook delivery"
3. Push and create a PR (even solo — for the diff view)
git push -u origin fix/stripe-webhook-idempotency gh pr create --title "Fix: Stripe webhook idempotency" --body "..."
4. Self-review the PR diff (24 hours later)
5. Merge to main
6. Auto-deploy to production
Delete the branch — it served its purpose
git branch -d fix/stripe-webhook-idempotency ```
That's it. No develop branch. No staging branch. No release branches.
The Commit Message Convention
I use conventional commits, but keep it simple:
``` feat: add Stripe webhook idempotency check fix: prevent duplicate payment processing docs: add ADR for database choice chore: update dependencies refactor: extract billing logic to lib/billing.ts test: add regression test for double-charge bug ```
The prefix tells you what KIND of change without reading the diff. `feat` is new functionality. `fix` is a bug fix. `chore` is maintenance. That's all you need.
When I Use Feature Flags Instead of Branches
Long-lived branches are where productivity goes to die. If a feature takes more than 3 days, I don't keep it in a branch — I merge it to main behind a feature flag:
```typescript function DashboardPage() { const showNewAnalytics = process.env.NEXT_PUBLIC_FF_ANALYTICS === 'true';
return (
This means:
- Main is always deployable
- Incomplete features don't block other work
- I can demo the feature by flipping a flag
- No merge conflicts from a 2-week-old branch
The Solo Developer Advantage
Most Git workflow advice is for teams of 5-50 people coordinating parallel work streams. If you're solo, you don't have coordination problems. Your workflow should be as simple as possible:
- Branch for every change (even small ones — for the PR record)
- Keep branches short-lived (1-3 days max)
- Merge to main = deploy to production
- Feature flags for anything that takes more than 3 days
No develop branch. No staging branch. No release manager. Just main, branches, and deploys.
The Non-Negotiable: Never Push Directly to Main
Even solo, I never push directly to main. Every change goes through a branch and a PR. Why?
- PR history is searchable. "When did we add Stripe?" → search PRs for "Stripe"
- The diff view catches mistakes. I've caught bugs in my own PR diffs that I missed in the editor
- It's a forcing function for small changes. If a PR has 40 files, it's too big. Break it up.
The 30 seconds it takes to create a branch pays for itself in clarity, traceability, and quality.