Learning Objectives
By the end of this module, you will be able to:
- Explain the core philosophy behind Git Flow, GitHub Flow, GitLab Flow, and trunk-based development
- Design a branching strategy that matches your team's size, release cadence, and deployment model
- Implement Git Flow's multi-branch model with feature, develop, release, hotfix, and main branches
- Apply GitHub Flow's lightweight pull-request-driven workflow
- Use environment branches and release branches in GitLab Flow
- Manage releases with branches and tags across different strategies
- Evaluate trade-offs between long-lived branches and short-lived feature branches
1. Why Branching Strategies Matter
A branching strategy is the contract your team follows about when branches are created, how they're named, where they merge, and who approves them. Without one, teams quickly devolve into merge conflict hell, unclear release states, and "works on my branch" syndrome.
The Core Tension
Every branching strategy navigates a fundamental tension:
Isolation ◄──────────────────────► Integration
Long-lived branches Short-lived branches
More divergence Less divergence
Bigger merges Smaller merges
More control More speed
More isolation means developers work undisturbed but drift apart. More integration means developers stay in sync but face more frequent coordination.
Key Factors in Choosing a Strategy
| Factor | Favors Long-Lived Branches | Favors Short-Lived Branches |
|---|---|---|
| Team size | Large (10+) | Small (2–8) |
| Release cadence | Scheduled (monthly/quarterly) | Continuous (daily/hourly) |
| Deployment model | On-premise / versioned software | SaaS / web apps |
| Regulatory requirements | High (audit trails required) | Low (move fast) |
| QA process | Manual / multi-stage | Automated CI/CD |
| Customer support | Multiple versions in production | Single production version |
2. Git Flow
Git Flow was introduced by Vincent Driessen in 2010 and became the dominant branching model for years. It's designed for projects with scheduled releases and multiple versions in production.
Branch Structure
┌─────────────────────────────────────────────┐
│ main │
│ (production-ready, tagged releases only) │
└──────┬──────────────┬───────────────────────┘
│ │
tag v1.0 tag v1.1
│ │
┌──────────────────────────────┴──────────────┴───────────────────────┐
│ develop │
│ (integration branch — always the latest delivered development) │
└───┬──────────┬──────────┬───────────┬──────────┬────────────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
feature/ feature/ release/ hotfix/ feature/
login search 1.1 1.0.1 dashboard
The Five Branch Types
1. main (or master)
- Contains only production-ready code
- Every commit on
mainis a release, tagged with a version number - Never commit directly to
main
2. develop
- The integration branch where features come together
- Always reflects the latest completed development work
- Nightly builds or staging deployments typically run from
develop
3. feature/* branches
- Branch from:
develop - Merge back into:
develop - Naming:
feature/user-authentication,feature/JIRA-1234-search - Lifetime: days to weeks (not months!)
# Start a feature
git checkout develop
git checkout -b feature/user-authentication
# Work on the feature...
git add .
git commit -m "feat: add user authentication flow"
# Finish the feature
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authenticationThe --no-ff flag is critical in Git Flow — it ensures a merge commit is always created, preserving the fact that a feature existed as a logical unit:
Without --no-ff (fast-forward): With --no-ff:
A ── B ── C ── D ── E A ── B ──────── M ── ...
╱
C ── D ──
(feature visible in history)
4. release/* branches
- Branch from:
develop - Merge back into:
developANDmain - Naming:
release/1.1.0 - Purpose: freeze features, allow only bug fixes, update version numbers, finalize docs
# Start a release
git checkout develop
git checkout -b release/1.1.0
# Bump version number
# Fix last-minute bugs only — NO new features
# Finish the release
git checkout main
git merge --no-ff release/1.1.0
git tag -a v1.1.0 -m "Release 1.1.0"
# Merge back into develop (to capture release bug fixes)
git checkout develop
git merge --no-ff release/1.1.0
git branch -d release/1.1.05. hotfix/* branches
- Branch from:
main - Merge back into:
developANDmain - Naming:
hotfix/1.0.1 - Purpose: emergency production fixes that can't wait for the next release
# Start a hotfix
git checkout main
git checkout -b hotfix/1.0.1
# Fix the critical bug
git commit -m "fix: patch SQL injection vulnerability"
# Finish the hotfix
git checkout main
git merge --no-ff hotfix/1.0.1
git tag -a v1.0.1 -m "Hotfix 1.0.1"
git checkout develop
git merge --no-ff hotfix/1.0.1
git branch -d hotfix/1.0.1Git Flow with the git-flow Extension
The git-flow CLI tool automates these branch operations:
# Install
brew install git-flow-avh # macOS
apt install git-flow # Ubuntu/Debian
# Initialize in a repo
git flow init
# Feature workflow
git flow feature start user-authentication
# ... work ...
git flow feature finish user-authentication
# Release workflow
git flow release start 1.1.0
# ... finalize ...
git flow release finish 1.1.0
# Hotfix workflow
git flow hotfix start 1.0.1
# ... fix ...
git flow hotfix finish 1.0.1When Git Flow Works
- Packaged software with version numbers (libraries, frameworks, mobile apps)
- Teams with a dedicated QA phase before each release
- Products with multiple versions supported simultaneously
- Regulated environments requiring clear release audit trails
When Git Flow Doesn't Work
- Web apps with continuous deployment (too much ceremony)
- Small teams (1–3 developers — too much overhead)
- Rapid iteration environments where
develop≈mainmost of the time
3. GitHub Flow
GitHub Flow was created by Scott Chacon at GitHub in 2011 as a reaction to Git Flow's complexity. It's designed for teams that deploy continuously.
The Rules
mainis always deployable- Branch off
mainfor any change (features, fixes, experiments) - Commit to your branch locally, push regularly to the remote
- Open a pull request when you want feedback or are ready to merge
- After PR review and CI passes, merge to
main - Deploy immediately after merging
Branch Structure
main ── A ── B ──────── M1 ──────── M2 ── C ──────── M3 ── ...
╱ ╱ ╱
feature-1 fix-bug feature-2
That's it. One long-lived branch (main) and short-lived topic branches.
The Workflow
# 1. Create a branch from main
git checkout main
git pull origin main
git checkout -b add-user-search
# 2. Make commits
git add .
git commit -m "feat: add user search endpoint"
git commit -m "feat: add search UI component"
git commit -m "test: add search integration tests"
# 3. Push and open a PR
git push -u origin add-user-search
gh pr create --title "Add user search" --body "Implements user search..."
# 4. After review and CI, merge via GitHub UI or CLI
gh pr merge --squash
# 5. Deploy (triggered automatically via CI/CD)Feature Flags Instead of Feature Branches
In GitHub Flow, long-running features aren't kept on long-lived branches. Instead, use feature flags:
# Feature flag pattern
if feature_flags.is_enabled("new_search", user):
return new_search_results(query)
else:
return legacy_search(query)This lets you merge incomplete features to main without exposing them to users. When ready, flip the flag — no branch merge required.
When GitHub Flow Works
- Web applications with continuous deployment
- Small to medium teams (2–15 developers)
- SaaS products with a single production version
- Teams with strong CI/CD and automated testing
When GitHub Flow Doesn't Work
- Multiple release versions in production simultaneously
- Environments requiring a formal release/QA gate
- Projects where "always deployable main" is impractical
4. GitLab Flow
GitLab Flow, proposed by GitLab, sits between Git Flow's complexity and GitHub Flow's simplicity. It adds environment branches or release branches on top of GitHub Flow.
Variant 1: Environment Branches
For teams deploying to multiple environments (staging, pre-production, production):
main ── A ── B ── C ── D ── E ── F ── ...
│ │
▼ ▼
pre-production ── C ── E ── ...
│
▼
production ── C ── ...
Changes flow downstream only: main → pre-production → production. You cherry-pick or merge from upstream to downstream — never the reverse.
# Feature developed and merged to main via MR
# When ready for staging:
git checkout pre-production
git merge main
# After staging validation, promote to production:
git checkout production
git merge pre-productionVariant 2: Release Branches
For projects that ship versioned releases while continuing development:
main ── A ── B ── C ── D ── E ── F ── G ── ...
│ │
▼ ▼
release/1.0 release/2.0
│ │ │
v1.0.0 v1.0.1 v2.0.0
- Development continues on
main - When ready for a release, create a
release/X.Ybranch - Bug fixes go to
mainfirst, then cherry-pick to the release branch (upstream first policy)
# Bug found in release/1.0
# Fix on main first
git checkout main
git commit -m "fix: correct date parsing for leap years"
# Then cherry-pick to release branch
git checkout release/1.0
git cherry-pick <commit-hash>
git tag v1.0.1The "Upstream First" Rule
GitLab Flow mandates that fixes always land on main first, then get cherry-picked to release or environment branches. This prevents the common problem of fixes being made on a release branch but never making it back to main.
When GitLab Flow Works
- Teams with multiple deployment environments
- Projects needing both continuous development and versioned releases
- Organizations that want more structure than GitHub Flow but less than Git Flow
5. Trunk-Based Development
Trunk-based development (TBD) is the most integration-heavy strategy. Developers commit directly to main (the "trunk") or use extremely short-lived branches (< 1 day).
Core Principles
- One branch:
main(the trunk) is the single source of truth - Small commits: multiple times per day
- No long-lived branches: feature branches live hours, not days
- Feature flags: hide incomplete work behind flags
- CI is mandatory: every commit to trunk must pass all tests
How It Works
Trunk (main):
A ── B ── C ── D ── E ── F ── G ── H ── I ── J ── ...
╲ ╱ ╲ ╱
b1 ── b2 e1
(< 1 day branch) (< 1 day)
Branches, if used at all, are merged within hours. Some teams skip branches entirely:
# Direct-to-trunk workflow
git checkout main
git pull --rebase origin main
# Make a small, focused change
git add .
git commit -m "feat: add email validation"
git push origin mainScaled Trunk-Based Development
For larger teams, short-lived feature branches are used with these constraints:
# Branch lives for hours, not days
git checkout -b feat/validate-email
git commit -m "feat: add email validation to signup"
git push -u origin feat/validate-email
# Open PR, get quick review, merge same day
gh pr create --title "Add email validation"
# After approval:
gh pr merge --rebaseRelease from Trunk
Releases are cut from the trunk using tags or short-lived release branches:
main ── A ── B ── C ── D ── E ── F ── G ── ...
│ │
tag v1.0 tag v1.1
OR
main ── A ── B ── C ── D ── E ── F ── G ── ...
│
release/1.0 ── cherry-pick ── tag v1.0.1
(created at release time, only for hotfixes)
When Trunk-Based Works
- Teams with mature CI/CD and comprehensive test suites
- Google, Facebook, Microsoft (Windows) — proven at massive scale
- Products with continuous deployment
- Teams that prioritize integration speed over isolation
When Trunk-Based Doesn't Work
- Open-source projects with external contributors (need PR review)
- Teams without strong automated testing
- Regulated environments requiring pre-release approval gates
- Junior-heavy teams that need the safety net of code review before merge
6. Strategy Comparison
Side-by-Side Overview
| Aspect | Git Flow | GitHub Flow | GitLab Flow | Trunk-Based |
|---|---|---|---|---|
| Long-lived branches | main + develop | main only | main + env/release | main only |
| Feature branches | feature/* | topic branches | topic branches | very short / none |
| Release process | release/* branch | tag from main | release/* or env promotion | tag from trunk |
| Hotfix process | hotfix/* branch | branch + PR | fix on main, cherry-pick | fix on trunk |
| Merge strategy | --no-ff always | squash or merge | merge | rebase or direct push |
| Best for | Versioned software | SaaS / web apps | Multi-env deployments | High-velocity teams |
| Complexity | High | Low | Medium | Low (process), High (discipline) |
| CI/CD dependency | Low | Medium | Medium | Critical |
Decision Flowchart
Do you support multiple release versions simultaneously?
├── YES → Git Flow or GitLab Flow (release branches)
└── NO
│
Do you have multiple deployment environments (staging, prod)?
├── YES → GitLab Flow (environment branches)
└── NO
│
How often do you deploy?
├── Multiple times per day → Trunk-Based Development
└── Daily to weekly → GitHub Flow
Merging Strategies Within Branching Models
Different branching models pair naturally with different merge strategies:
Git Flow: merge --no-ff (preserve feature history)
GitHub Flow: squash merge (clean main history)
GitLab Flow: merge commit (traceability across environments)
Trunk-Based: rebase (linear trunk history)
7. Release Management Across Strategies
Tagging Releases
Regardless of strategy, tags mark release points:
# Annotated tag (recommended for releases)
git tag -a v2.1.0 -m "Release 2.1.0: search feature, bug fixes"
# Push tags
git push origin v2.1.0
# or push all tags
git push --follow-tagsVersion Branches vs. Tags
Tags (lightweight releases):
main ── A ── B ── C ── D ── E ── F ── ...
v1.0 v1.1 v2.0
Version branches (when you need to patch old releases):
main ── A ── B ── C ── D ── E ── F ── ...
│ │
release/1.x release/2.x
│ │ │
v1.0 v1.0.1 v2.0
Use tags alone when you only support the latest version. Use release branches when you need to backport fixes to older versions.
Backporting Fixes
When a bug is found in an older release:
# Fix on main first (upstream first)
git checkout main
git commit -m "fix: handle null user in permissions check"
# Cherry-pick to the release branch
git checkout release/1.x
git cherry-pick <commit-hash>
git tag -a v1.0.1 -m "Patch: null user fix"
git push origin release/1.x --follow-tags8. Branch Naming Conventions
Consistent naming makes branch purpose immediately clear:
Common Patterns
| Pattern | Example | Use Case |
|---|---|---|
feature/<description> | feature/user-search | New functionality |
fix/<description> | fix/login-redirect | Bug fixes |
hotfix/<description> | hotfix/security-patch | Urgent production fixes |
release/<version> | release/2.1.0 | Release preparation |
chore/<description> | chore/update-deps | Maintenance tasks |
docs/<description> | docs/api-reference | Documentation changes |
refactor/<description> | refactor/auth-module | Code restructuring |
Including Ticket Numbers
# Ticket prefix helps trace branches to issues
git checkout -b feature/PROJ-1234-user-search
git checkout -b fix/GH-567-login-redirectEnforcing Conventions
Use a pre-push hook or CI check to validate branch names:
#!/bin/bash
# .githooks/pre-push
branch=$(git rev-parse --abbrev-ref HEAD)
pattern="^(feature|fix|hotfix|release|chore|docs|refactor)/"
if [[ ! "$branch" =~ $pattern ]] && [[ "$branch" != "main" ]] && [[ "$branch" != "develop" ]]; then
echo "ERROR: Branch name '$branch' doesn't follow naming convention."
echo "Use: feature/, fix/, hotfix/, release/, chore/, docs/, or refactor/"
exit 1
fiCommand Reference
| Command | Description |
|---|---|
git checkout -b <branch> | Create and switch to a new branch |
git merge --no-ff <branch> | Merge with a merge commit (no fast-forward) |
git merge --squash <branch> | Squash all branch commits into one, stage result |
git cherry-pick <hash> | Apply a specific commit to the current branch |
git tag -a <tag> -m "<msg>" | Create an annotated tag |
git push --follow-tags | Push commits and annotated tags together |
git branch -d <branch> | Delete a merged branch |
git branch -D <branch> | Force-delete an unmerged branch |
git flow init | Initialize git-flow in a repository |
git flow feature start <name> | Start a git-flow feature branch |
git flow feature finish <name> | Finish a git-flow feature branch |
git flow release start <ver> | Start a git-flow release branch |
git flow release finish <ver> | Finish a git-flow release (merge + tag) |
git flow hotfix start <ver> | Start a git-flow hotfix branch |
git flow hotfix finish <ver> | Finish a git-flow hotfix branch |
Hands-On Lab: Practicing Branching Strategies
Setup
mkdir branching-lab && cd branching-lab
git init
echo "# My Project" > README.md
echo "version=1.0.0" > version.txt
git add .
git commit -m "initial commit"Part 1: Git Flow Simulation
Goal: Walk through a complete Git Flow cycle — feature, release, and hotfix.
# 1. Create develop branch
git checkout -b develop
# 2. Start a feature
git checkout -b feature/add-login
echo "function login() { return true; }" > auth.js
git add auth.js
git commit -m "feat: add login function"
echo "function logout() { return true; }" >> auth.js
git add auth.js
git commit -m "feat: add logout function"
# 3. Finish the feature (merge into develop with --no-ff)
git checkout develop
git merge --no-ff feature/add-login -m "Merge feature/add-login into develop"
git branch -d feature/add-login
# 4. Start a release
git checkout -b release/1.1.0
echo "version=1.1.0" > version.txt
git add version.txt
git commit -m "chore: bump version to 1.1.0"
# 5. Finish the release
git checkout main
git merge --no-ff release/1.1.0 -m "Merge release/1.1.0"
git tag -a v1.1.0 -m "Release 1.1.0"
git checkout develop
git merge --no-ff release/1.1.0 -m "Merge release/1.1.0 back into develop"
git branch -d release/1.1.0
# 6. Simulate a production bug → hotfix
git checkout main
git checkout -b hotfix/1.1.1
echo "function login() { /* fixed */ return validate(); }" > auth.js
git add auth.js
git commit -m "fix: validate credentials in login"
echo "version=1.1.1" > version.txt
git add version.txt
git commit -m "chore: bump version to 1.1.1"
# 7. Finish the hotfix
git checkout main
git merge --no-ff hotfix/1.1.1 -m "Merge hotfix/1.1.1"
git tag -a v1.1.1 -m "Hotfix 1.1.1"
git checkout develop
git merge --no-ff hotfix/1.1.1 -m "Merge hotfix/1.1.1 into develop"
git branch -d hotfix/1.1.1Checkpoint: Run git log --all --oneline --graph --decorate and verify you see:
- The merge commits on
mainfor the release and hotfix - Tags
v1.1.0andv1.1.1onmain - Feature history preserved via
--no-ffondevelop
Part 2: GitHub Flow Simulation
Goal: Practice the lightweight branch-and-PR workflow.
# Start fresh
mkdir github-flow-lab && cd github-flow-lab
git init
echo "# App" > README.md
git add . && git commit -m "initial commit"
# 1. Feature branch from main
git checkout -b add-search
echo "function search(q) { return results; }" > search.js
git add search.js
git commit -m "feat: add search function"
echo "test('search works', () => expect(search('hi')).toBeDefined());" > search.test.js
git add search.test.js
git commit -m "test: add search tests"
# 2. Simulate squash merge (like GitHub's "Squash and merge" button)
git checkout main
git merge --squash add-search
git commit -m "feat: add search feature (#42)"
git branch -d add-search
# 3. Tag a release
git tag -a v1.0.0 -m "Release 1.0.0"Checkpoint: Run git log --oneline. You should see a clean, linear history with a single squashed commit for the entire feature.
Part 3: GitLab Flow with Environment Branches
Goal: Simulate promotion of changes through environments.
# Start fresh
mkdir gitlab-flow-lab && cd gitlab-flow-lab
git init
echo "# App" > README.md
git add . && git commit -m "initial commit"
# 1. Create environment branches
git branch pre-production
git branch production
# 2. Develop a feature on main
git checkout -b add-api
echo "app.get('/api/users', handler);" > api.js
git add api.js
git commit -m "feat: add users API endpoint"
git checkout main
git merge --no-ff add-api -m "Merge add-api"
git branch -d add-api
# 3. Promote to pre-production
git checkout pre-production
git merge main -m "Promote to pre-production"
# Verify pre-production looks good...
# 4. Promote to production
git checkout production
git merge pre-production -m "Promote to production"
git tag -a v1.0.0 -m "Release 1.0.0"
# 5. Return to main for next development
git checkout mainCheckpoint: Run git log --all --oneline --graph --decorate and verify the same commit appears on all three branches, with the production branch tagged.
Part 4: Trunk-Based Development
Goal: Practice the commit-to-trunk workflow with short-lived branches.
# Start fresh
mkdir tbd-lab && cd tbd-lab
git init
echo "# App" > README.md
git add . && git commit -m "initial commit"
# 1. Direct commit to trunk (for small changes)
echo "const config = { debug: false };" > config.js
git add config.js
git commit -m "chore: add production config"
# 2. Short-lived branch (for slightly larger changes)
git checkout -b feat/add-logger
echo "function log(msg) { console.log('[LOG]', msg); }" > logger.js
git add logger.js
git commit -m "feat: add logging utility"
# Merge back immediately (within hours)
git checkout main
git rebase feat/add-logger # or git merge --ff-only feat/add-logger
git branch -d feat/add-logger
# 3. Another direct commit with a feature flag
cat > features.js << 'EOF'
const FEATURES = {
NEW_DASHBOARD: false, // flip to true when ready
DARK_MODE: true,
};
module.exports = FEATURES;
EOF
git add features.js
git commit -m "feat: add feature flag for new dashboard"
# 4. Tag a release from trunk
git tag -a v1.0.0 -m "Release 1.0.0"Checkpoint: Run git log --oneline --graph. The history should be completely linear — no merge commits.
Part 5: Comparing Histories
Goal: Visually compare the histories produced by each strategy.
# Go to each lab directory and run:
echo "=== Git Flow ==="
cd ../branching-lab
git log --all --oneline --graph --decorate | head -20
echo ""
echo "=== GitHub Flow ==="
cd ../github-flow-lab
git log --all --oneline --graph --decorate | head -20
echo ""
echo "=== GitLab Flow ==="
cd ../gitlab-flow-lab
git log --all --oneline --graph --decorate | head -20
echo ""
echo "=== Trunk-Based ==="
cd ../tbd-lab
git log --all --oneline --graph --decorate | head -20Notice the differences:
- Git Flow has the most complex graph with multiple merge points
- GitHub Flow has a clean linear history (due to squash merge)
- GitLab Flow shows the promotion path through environments
- Trunk-Based is completely linear
Challenge: Design Your Strategy
Create a branching strategy for this scenario:
You're leading a team of 6 developers building a SaaS application. You deploy to staging automatically on every merge to
main, and promote to production manually once per week. You occasionally need to hotfix production between weekly releases. Design your branching model, document the branch naming convention, and implement it in a test repository.
Hint: This scenario calls for a blend — GitHub Flow's simplicity with GitLab Flow's environment promotion.
Common Pitfalls
| Pitfall | Why It Happens | How to Avoid It |
|---|---|---|
| Long-lived feature branches | Features grow beyond original scope | Time-box branches (< 1 week), split large features |
| Forgetting to merge hotfix to develop | Rush to fix production | Use checklists or git-flow tool which automates this |
Direct commits to main/develop | Bypassing the workflow under pressure | Branch protection rules on the remote |
Divergent develop and main | Infrequent releases | Release regularly, even small batches |
| Merge conflicts from stale branches | Branch existed too long | Pull/rebase from target branch daily |
| No branch protection | Trust-based workflow fails at scale | Enable required reviews + CI checks on main |
| Over-engineering the strategy | Applying Git Flow to a 2-person project | Match strategy complexity to team size |
| Inconsistent branch naming | No documented convention | Enforce naming via hooks or CI |
| Not deleting merged branches | Clutter accumulates | git fetch --prune, auto-delete on merge in GitHub/GitLab |
| Cherry-pick without upstream-first | Fix lands on release but not main | Always fix on main first, then cherry-pick down |
Pro Tips
-
Start simple, add complexity when needed. Most teams should begin with GitHub Flow and only graduate to Git Flow or GitLab Flow when they hit a real limitation — not a hypothetical one.
-
Branch protection is non-negotiable. Regardless of strategy, protect
mainwith required PR reviews and CI checks. This is the single most impactful guardrail. -
Automate branch cleanup. Enable "delete branch after merge" in GitHub/GitLab settings, and periodically run
git fetch --prunelocally to remove stale tracking branches. -
Document your strategy. Put your branching model in a
CONTRIBUTING.mdfile. Include a diagram, naming conventions, and merge policies. New team members should be able to understand the workflow from this document alone. -
Measure your branch lifetime. Track how long feature branches live. If the average exceeds a week, your branches are too long-lived. This is a leading indicator of painful merges.
-
The best strategy is one your team actually follows. A perfectly designed workflow that nobody understands is worse than a simple one that everyone follows consistently.
Quiz / Self-Assessment
1. In Git Flow, which branch do hotfix branches branch from?
Show Answer
main (not develop). Hotfixes address production issues, so they branch from the production-ready branch. They merge back into both main and develop.
2. What does the --no-ff flag do in Git Flow, and why is it important?
Show Answer
--no-ff (no fast-forward) forces Git to create a merge commit even when a fast-forward is possible. This is important in Git Flow because it preserves the history of a feature branch as a logical unit — you can see where the feature started and ended in the graph, making it easy to revert an entire feature if needed.
3. How does GitHub Flow differ from Git Flow?
Show Answer
GitHub Flow uses only one long-lived branch (main) while Git Flow uses two (main and develop) plus formalized branch types. GitHub Flow has no release branches, no hotfix branches, and no develop branch. Features branch from main and merge back to main via pull request. It's simpler but assumes continuous deployment where main is always deployable.
4. What is the "upstream first" rule in GitLab Flow?
Show Answer
The upstream first rule states that bug fixes must always be applied to main first, then cherry-picked to downstream branches (release branches or environment branches like pre-production and production). This prevents the common problem of fixes being made on a release branch but never making it back to main.
5. In trunk-based development, how are incomplete features handled if everything merges to main?
Show Answer
Incomplete features are hidden behind feature flags (also called feature toggles). The code exists on main but is disabled for users via a flag. When the feature is complete and tested, the flag is flipped to enable it. This allows continuous integration without exposing unfinished work.
6. A team of 3 developers is building a SaaS web app with daily deployments. Which strategy would you recommend and why?
Show Answer
GitHub Flow or trunk-based development. Both are well-suited for small teams with frequent deployments. Git Flow would be overkill — the ceremony of develop, release, and hotfix branches adds overhead without proportional benefit for a small, fast-moving team. Between the two, GitHub Flow is more accessible if the team wants PR-based code review; trunk-based is better if the team has comprehensive automated testing and wants maximum velocity.
7. Why shouldn't feature branches live for weeks or months?
Show Answer
Long-lived feature branches diverge significantly from main, leading to:
- Large, painful merge conflicts that are hard to resolve correctly
- Integration bugs discovered late when the branch finally merges
- Stale dependencies as the rest of the codebase evolves
- Delayed feedback from CI, code review, and other developers
The general rule is to keep branches under one week. If a feature takes longer, break it into smaller, independently mergeable pieces, potentially using feature flags to hide incomplete functionality.
8. What's the difference between tagging a release from main versus using a release branch?
Show Answer
Tagging from main is simpler — you mark a commit on main as a release point. This works when you only support one version at a time and deploy continuously.
Release branches (release/1.x) are needed when you must support multiple versions in production. The release branch allows you to apply bug fixes (cherry-picked from main) to older versions without pulling in new features. Each patch is tagged on the release branch (v1.0.1, v1.0.2, etc.).
9. How does git merge --squash help in GitHub Flow?
Show Answer
git merge --squash takes all the commits from a feature branch and compresses them into a single commit on main. This creates a clean, linear history where each commit on main represents a complete feature or fix. It's ideal for GitHub Flow because:
- The
mainbranch history is easy to read and bisect - Individual "WIP" and "fix typo" commits on the feature branch don't clutter
main - Each squashed commit maps to one pull request
The trade-off is that detailed branch history is lost (though it remains visible in the PR on GitHub).
10. Your team currently uses Git Flow but is migrating to continuous deployment. What changes would you make to the branching strategy?
Show Answer
Transition from Git Flow to GitHub Flow or trunk-based development:
- Eliminate the
developbranch — merge features directly tomain - Remove release branches — deploy from
mainusing tags or automated pipelines - Remove the hotfix branch type — hotfixes are just regular branches (they merge to
mainand deploy immediately like any other change) - Add feature flags to replace long-lived feature branches for features that span multiple days
- Strengthen CI/CD to ensure
mainis always deployable (automated tests, linting, security scans) - Enable branch protection on
mainwith required reviews and passing CI
The key shift is from "branches manage release state" to "CI/CD manages release state."