Skip to main content

Git Workflow

Branch Strategy

Pivot uses a linear promotion workflow:

development → main → production

development → A → B → C → D → E → F → G  (most commits)
main → A → B → C → D → E (staging, tested subset)
production → A → B → C (live)

Feature Branch Workflow

Working on Features

When developing new features, developers create feature branches off development:

  • development: A → B → C → D
  • feature-branch: A → B → C → Feature work → More work

This creates divergence - both branches have different commits. This is normal and expected for feature branches!

Updating Your Feature Branch (IMPORTANT)

When development has moved forward and you need to incorporate those changes into your feature branch, you MUST use rebase, not merge.

# ✅ Always use rebase
git checkout feature-branch
git rebase development
git push --force-with-lease

GitHub branch protection enforces linear history - PRs with merge commits will be blocked.

For detailed explanations, visual diagrams, common mistakes and how to fix them, see the Git Rebase Guide.

Merging Feature Branches

When merging a feature branch into development via Pull Request, you have options:

All feature commits get squashed into a single commit on development:

Result: ✅ Clean, linear history. Feature commits become one new commit with a new hash.

Option 2: Merge Commit

Creates a merge commit joining both histories:

Result: ⚠️ More complex history with merge commits.

Option 3: Rebase + Fast-Forward

Rebase feature commits onto development, then fast-forward:

Result: ✅ Linear history, but feature commits get new hashes (F1*, F2*) because they were replayed on top of D.

Key Point: Feature Branches Are Different

  • Feature branches SHOULD diverge from development - that's normal!
  • Merging feature branches creates new commits or changes hashes - that's expected!
  • Environment branches (main, production) should NEVER diverge - only fast-forward!

Environment Promotion (main, production)

For promoting between environments, we use a different strategy:

Fast-Forward vs Rebase vs Merge

Scenario 1: No Divergence (Normal Workflow)

When all commits happen in development first, and main just follows:

Before merge:

  • development: A → B → C → D → E → F (all work happens here)
  • main: A → B → C

After fast-forward main to development:

  • development: A → B → C → D → E → F
  • main: A → B → C → D → E → F (same commits, same hashes)

Result: ✅ Commits D, E, F keep their exact same Git hashes

Fast-forward and rebase produce identical results - they just move the pointer forward.

# These do the same thing when no divergence:
git merge --ff-only development # Fast-forward
git rebase development # Rebase (no-op, then fast-forward)

This is what should always happen in our workflow since we never commit directly to main or production.

Scenario 2: Divergence (What to Avoid)

When you accidentally commit directly to main (or have commits on both branches), they have different histories - this is called divergence:

Before rebase (branches diverged):

  • development: A → B → C → README
  • main: A → B → C → Fix (hash: 5aea767) ← committed directly to main!

After rebasing main onto development:

  • development: A → B → C → README → Fix
  • main: A → B → C → README → Fix (hash changed!)

Result: ⚠️ Commit "Fix" gets a new hash (5aea767 → b577685)

The hash changes because the commit was replayed with a new parent (README), creating a new commit object.

Merge commits create complicated Git history and should be avoided.

Best Practice

Always use fast-forward or rebase (they're the same when no divergence)

  • Keeps linear history
  • Simple, clean Git log
  • Easy to understand

Merge commits only needed when history has diverged

  • Creates complicated branching structure
  • Should be avoided in our linear promotion workflow
  • Indicates someone committed directly to main/production (which shouldn't happen)

Why No PRs for Environment Promotion

We don't use Pull Requests when promoting code between environment branches (development → main → production). Here's why:

PRs Add No Value for Fast-Forward

What PRs provideWhy it doesn't apply here
Code reviewCode was already reviewed in the feature PR
DiscussionNothing to discuss - same commits, same hashes
Approval gateWhat would you approve? "Yes, move the pointer"?
CI checksConfigure CI to trigger on push, not PR

The Real Gate is Development

The meaningful review happens when feature branches merge to development. Once code is in development:

  • It's been reviewed
  • It's been tested
  • It's "on the train" to production

Adding a PR for promotion is like checking your ID twice at the same door.

What About Audit Trail?

Git log already shows exactly when main or production moved forward:

git log --oneline main
# Shows every commit with timestamps

What About Compliance?

Some organizations require PRs for compliance reasons. If that's a hard requirement, do it - but recognize it's checkbox theater, not adding real safety.

The Clean Approach

Instead of creating PRs, just push:

# Option 1: Merge locally and push
git checkout main
git merge --ff-only development
git push origin main

# Option 2: Push directly (even simpler)
git push origin development:main

This is faster, cleaner, and the git history stays linear without merge commits.

Commands

Merging development → main

git checkout main
git merge --ff-only development # Fails if diverged (good safety check)

If fast-forward fails, investigate why main diverged (normally this shouldn't happen).

Merging main → production

git checkout production
git merge --ff-only main # Fails if diverged (good safety check)

Summary

As long as we follow the rule:

  • Never commit directly to main or production
  • All work happens in development first
  • Promote by moving the head pointer forward

Then:

  • Fast-forward and rebase produce identical results
  • All commit hashes stay the same
  • History stays clean and linear

In Pivot's workflow:

  • Fast-forward = Rebase (same result when no divergence)
  • Merge commits = Complicated history (only when diverged)
  • Changes only flow one direction: development → main → production
  • Branches diverge when they have different histories (commits on both branches)

Hotfix Process

Sometimes bugs need to be fixed faster than the normal train allows. We have three levels of hotfix urgency:

Use when: Bug is important but can wait for normal testing flow.

development → (PR) → main → (cherry-pick after testing) → production

Steps:

  1. Create a feature branch from development
  2. Fix the bug and open a PR to development
  3. After PR is merged, cherry-pick the commit to main
  4. Test the fix in staging
  5. Cherry-pick the commit to production
# After PR merges to development
git checkout main
git cherry-pick <commit-hash>
git push origin main

# After testing in staging
git checkout production
git cherry-pick <commit-hash>
git push origin production

Urgent Hotfix

Use when: Bug is critical and needs to go to production quickly, but you still have time to test in staging first.

main → (PR) → production → (back-merge to development)

Steps:

  1. Create a feature branch from main (staging)
  2. Fix the bug and open a PR directly to main
  3. Test the fix in staging
  4. Cherry-pick the commit to production
  5. Back-merge: merge main into development to sync
# After testing fix in staging
git checkout production
git cherry-pick <commit-hash>
git push origin production

# Sync development with main
git checkout development
git merge main
git push origin development

Emergency Hotfix

Use when: Production is on fire. No time to test in staging. Fix it NOW.

production → (PR) → back-merge to main → back-merge to development

Steps:

  1. Create a feature branch from production
  2. Fix the bug and open a PR directly to production
  3. After fix is live and verified, back-merge to sync all branches
# After fix is live in production
git checkout main
git merge production
git push origin main

git checkout development
git merge main
git push origin development
warning

Emergency hotfixes bypass staging testing. Only use this when production is critically broken and every minute counts.

Hotfix Summary

LevelSpeedTestingTarget BranchRisk
StandardNormalFull flowdevelopmentLowest
UrgentFastStaging onlymainMedium
EmergencyImmediateNoneproductionHighest

After Any Hotfix

Always ensure branches are synchronized after hotfixes:

production ⊆ main ⊆ development

All commits in production should exist in main, and all commits in main should exist in development. Run these checks:

# Check if production is subset of main
git log production --not main --oneline # Should be empty

# Check if main is subset of development
git log main --not development --oneline # Should be empty

If either shows commits, you need to back-merge.