Published: September 7, 2025

Git Disasters and Process Debt

This is a cautionary tale mixed with post-mortem solutions. When we grew the team, what started as a clean dev → staging → main flow became a nightmare as we scaled from 2 to 6 developers.

Merge conflicts became my main job for an entire week. It did my head in. PRs ballooned to thousands of lines. Deployment velocity died as the team grew — the opposite of what you'd expect. We spent more time fighting Git than building features. Without regular production merges, changes accumulated until PRs became unreadable.

If merge conflicts are multiplying as your team grows, you need better patterns. This is how we broke Git—and the exact commands and workflows that fixed it.

What Went Wrong

Looking back, the problem is obvious. When you're building fast, these issues compound quietly until they explode.

The Spider Web Effect

Our Git graph showed the mess:

  • Bidirectional merges: dev → staging, then staging → main, then main → staging
  • Out-of-sequence hotfixes: Emergency PRs bypassing the normal flow
  • Long-lived branches: Feature branches living for weeks, accumulating conflicts
  • Mixed merge strategies: Both merge and squash commits, preventing Git from consolidating history

We created a tangled web where branches diverged completely. Cherry-picks failed. Merge conflicts cascaded. Everything got worse.

Why This Mattered

We couldn't cherry-pick features to production because branches had no shared history.

Merge conflicts became impossible due to squash commits destroying shared history.

PRs became massive because fixing conflicts took so long that more changes accumulated, creating a vicious cycle.

Recovery Commands

After a week of untangling our Git mess, these are the commands that actually work.

See the Damage

# Get the full picture of where all branches stand
git fetch --all --prune
git branch -avv
git log --oneline --graph --decorate --all
 
# See the differences between branches
git diff main..staging --name-status
git diff staging..dev --name-status

Reset When Broken

# Reset to origin even during a rebase
git fetch origin
git checkout main
git reset --hard origin/main
 
# Find the last common ancestor
git merge-base main staging
 
# Hard reset to a specific commit (requires force-push)
git reset --hard <COMMIT_SHA>
git push --force-with-lease origin <branch>

Fix Conflicts

# Abort problematic operations
git merge --abort
git rebase --abort
 
# Cherry-pick with provenance tracking
git cherry-pick -x <COMMIT_SHA>
 
# Create throwaway branches for complex merges
git checkout -b merge-test main
git merge --no-ff origin/staging

Safe Conflict Resolution

The single most important pattern we learned: never resolve conflicts on protected branches.

  1. Create a throwaway branch from your target
  2. Merge the problematic branch into it
  3. Resolve conflicts safely in this sandbox
  4. PR the clean result back to target
  5. Delete the throwaway branch

Your protected branches stay clean. Conflicts become visible in PRs. No more force-pushing disasters.

One-Way Flow

The fix that eliminated 90% of our problems: never merge upstream.

feature/* → dev → main

The Rules

  1. Single directional promotion - No more main → staging or staging → dev merges. Just dev and main
  2. Short-lived feature branches - Maximum 3 days, branch off dev, merge back via PR, then delete
  3. Squash and merge into dev only - Preserve clean history by avoiding squash merges on long-lived branches
  4. Merge commits from dev to main - Use merge commits (not squash) when promoting between long-lived branches

Hotfix Pattern

When production is on fire:

# Create hotfix off main
git checkout -b hotfix/1.2.3 main
 
# After merging hotfix → main, propagate back:
git checkout staging
git merge --no-ff main
git push origin staging
 
git checkout dev
git merge --no-ff main
git push origin dev

Branch Protection

GitHub settings that actually prevent disasters:

  • Require PRs with up-to-date branches and passing CI
  • Require linear history to prevent messy merge commits
  • Auto-delete feature branches after merge
  • Code owners for approval oversight
  • Squash and merge into dev only - disable rebase merge entirely

Critical detail: use different merge strategies for different branches. Squash features into dev, but use merge commits when promoting to main. This preserves history where it matters.

Faster Reviews

We eliminated context switching with the GitHub Pull Requests and Issues VS Code extension:

  • Review PRs without leaving your editor
  • Test branches locally with one click
  • Comment inline while coding
  • Merge directly from VS Code

Review time dropped by 40%.

The Workflow

What actually works in practice:

  1. Create feature branch off dev with descriptive name
  2. Work in small increments - keep branches under 3 days
  3. Open PR to dev - squash and merge, auto-delete branch
  4. Nightly dev → main promotion - fast, regular deployments via merge commits
  5. Feature flags for incomplete work - deploy safely without exposing unfinished features

Merge Standards

Code gets merged when:

  • Functionality must be testable and non-detrimental to existing features
  • Backwards compatibility maintained
  • Modular architecture - new features in separate folders/routes
  • Focus reviews on logic and architecture - ignore formatting nits

Process Debt Compounds

Our Git disaster wasn't just about bad merge strategies. It was process debt accumulating interest:

The signals we missed:

  • PR size exploded from ~200 lines to 2000+ lines
  • Deployment frequency dropped from daily to weekly
  • New developers took a lot of time to understand our "workflow"
  • Code review became rubber-stamping because PRs were unreadable

Each shortcut compounded. Skip documentation? New devs create their own workflows. Allow any merge strategy? Git history becomes unsearchable. Delay conflict resolution? PRs become unreviewable.

The real cost: That week I spent untangling Git? Multiply by 6 developers. Add the bugs that slipped through massive PRs. Add the features we didn't ship. Process debt doesn't just slow you down — it stops you cold.

The Lessons

  • Constraints beat freedom. Complete Git autonomy creates chaos. Clear rules prevent problems.
  • Merge strategy matters. Squash merges on long-lived branches destroy history and create conflicts.
  • Protected branches need special care. You can't force-push out of problems. Use proper workflows.
  • Promote regularly. Daily or weekly promotion prevents massive, unreviewable PRs.
  • Document everything. Without clear processes, everyone invents their own workflow.
  • Watch the metrics. PR size and deployment frequency are your canaries in the coal mine.

The Takeaway

Scaling from 2 to 6 developers shouldn't break your Git workflow. But it will if you don't evolve your processes.

The fix wasn't complex—it was ruthlessly simple. One-way flow. Protected branches. Clear merge strategies. Most importantly: treat process improvements as real work, not something you'll "get to later."

Process debt compounds faster than technical debt. Fix it before it fixes you.