git checkout -b feature/name — create and switch to a new branch
git merge feature/x — integrate branch changes into current branch
git rebase main — replay your commits on top of main's latest state
--no-ff — force a merge commit even when fast-forward is possible
Plain-English First
Think of a Git repository as a book being written by multiple authors. A branch is like each author working on their own photocopy of the latest chapter. They can write freely without interfering with each other. When they're done, they bring their pages back to the original book — that's a merge.
Rebase is different: instead of stapling your pages to the end, you take your pages and rewrite them as if you started writing after the latest page in the original book. The content is the same, but the page numbers change. If someone already wrote down your old page numbers, their references break.
Branching enables parallel development by isolating changes into independent lines of history. Each branch is a pointer to a commit — creating one costs almost nothing in Git because no files are copied.
The merge vs rebase decision affects how your repository's history reads during code review and debugging. Merge preserves the exact timeline of how work happened. Rebase creates a clean linear narrative. Both are used in production teams — the choice depends on your team's workflow and tooling.
Common misconceptions: that rebase is always better because it produces cleaner history, that fast-forward merges are always preferable, and that branches are expensive to create. Understanding the trade-offs prevents the force-push incidents and history corruption that plague teams adopting Git without training.
Creating and Managing Branches
A Git branch is a pointer to a commit. Creating a branch does not copy files — it creates a 41-byte reference file in .git/refs/heads/. This is why branches are cheap: you can create hundreds without meaningful storage or performance cost.
The pointer moves forward automatically when you make new commits on the branch. When you switch branches (git checkout or git switch), Git updates your working directory to match the commit the branch points to.
Branch naming conventions matter for tooling. Most CI systems, PR platforms, and branch protection rules use pattern matching (feature/, release/, hotfix/*). Consistent naming enables automated workflows — branch protection rules, auto-deletion after merge, and CI trigger filters all depend on naming patterns.
io/thecodeforge/git/BranchManagement.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# io.thecodeforge — BranchManagement
# ─────────────────────────────────────────────────────────────
# CREATEANDSWITCH
# ─────────────────────────────────────────────────────────────
# Modernsyntax (Git2.23+)
git switch -c feature/payment-retry
# Creates branch and switches to it.
# Legacysyntax (still widely used)
git checkout -b feature/payment-retry
# Create branch from a specific commit
git switch -c hotfix/critical-bug abc1234
# Branch starts from commit abc1234, not HEAD.
# ─────────────────────────────────────────────────────────────
# LISTANDINSPECT
# ─────────────────────────────────────────────────────────────
git branch # local branches (current branch marked with *)
git branch -r # remote-tracking branches
git branch -a # all branches (local + remote)
git branch -v # branches with last commit info
git branch --merged main # branches already merged into main
git branch --no-merged main # branches not yet merged into main
# Find branches containing a specific commit
git branch --contains abc1234
# Useful when you need to know which branch introduced a bug.
# ─────────────────────────────────────────────────────────────
# PUSHANDTRACK
# ─────────────────────────────────────────────────────────────
# First push: set upstream tracking
git push -u origin feature/payment-retry
# -u sets upstream so future 'git push' and 'git pull' work without arguments.
# Subsequentpushes (upstream already set)
git push
# Automatically pushes to origin/feature/payment-retry.
# Push all local branches
git push origin --all
# ─────────────────────────────────────────────────────────────
# DELETEANDCLEANUP
# ─────────────────────────────────────────────────────────────
# Delete local branch (safe — refuses if unmerged changes exist)
git branch -d feature/payment-retry
# Force delete local branch (discards unmerged changes)
git branch -D feature/payment-retry
# Delete remote branch
git push origin --delete feature/payment-retry
# Prune stale remote-tracking references
git remote prune origin
# Removes references to remote branches that have been deleted.
# Auto-delete after merge (GitHub/GitLab setting)
# Most platforms delete the branch automatically after PR merge.
# This keeps the remote clean without manual intervention.
# ─────────────────────────────────────────────────────────────
# RENAME
# ─────────────────────────────────────────────────────────────
# Rename current branch
git branch -m new-name
# Rename a different branch
git branch -m old-name new-name
Branch naming conventions are not cosmetic — they drive automation. CI systems use glob patterns to trigger builds (feature/ triggers PR validation, release/ triggers staging deploy). Branch protection rules use patterns to enforce policies (main requires 2 reviewers, develop requires 1). Inconsistent naming (feat/login vs feature/login vs login-feature) breaks these automations silently. Teams should enforce naming conventions via pre-push hooks or CI linting, not documentation.
Key Takeaway
Branches are cheap pointers to commits — create them freely. Branch naming conventions drive CI/CD automation and branch protection rules. Use git branch --no-merged main to find branches that haven't been integrated yet. Auto-delete merged branches to keep the remote clean.
Merge vs Rebase
Merge and rebase both integrate changes from one branch into another. They produce different histories, and the choice affects code review, debugging, and collaboration.
Merge creates a merge commit — a special commit with two parents. It preserves the exact history of how work happened: when the branch was created, what commits were made on it, and when it was integrated. The history is a DAG (directed acyclic graph), not a straight line.
Rebase replays your commits on top of another branch. Each commit is cherry-picked onto the new base, creating new commits with new SHAs. The result is a linear history — no merge commits, no branching visible in git log. But the original commit SHAs are gone, which breaks any reference to them.
The golden rule: never rebase commits that have been pushed to a shared branch. Rebasing rewrites commit SHAs. Every teammate who pulled the old SHAs now has references to commits that no longer exist on the remote.
io/thecodeforge/git/MergeVsRebase.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# io.thecodeforge — Merge vs Rebase
# ─────────────────────────────────────────────────────────────
# MERGE: Preserves exact history
# ─────────────────────────────────────────────────────────────
# Standardmerge (fast-forward if possible, merge commit if diverged)
git checkout main
git merge feature/payment-retry
# Force merge commit even when fast-forward is possible
git merge --no-ff feature/payment-retry
# Creates a merge commit that groups all feature branch commits.
# Usefulfor traceability: git log --first-parent main shows
# only merge commits, giving a high-level integration timeline.
# Squash merge: collapse all feature commits into one
git merge --squash feature/payment-retry
git commit -m "feat(payment): Add PaymentRetryService"
# All changes from the feature branch are staged.
# You create a single commit on main with the combined changes.
# The feature branch's individual commit history is lost on main.
# ─────────────────────────────────────────────────────────────
# REBASE: Linearises history
# ─────────────────────────────────────────────────────────────
# Rebase feature branch onto latest main
git checkout feature/payment-retry
git rebase main
# Your commits are replayed on top of main's latest commit.
# Each commit gets a newSHA.
# Then fast-forward merge:
git checkout main
git merge feature/payment-retry # fast-forward, linear history
# Interactive rebase: squash, reorder, edit commits before merging
git rebase -i main
# Editor opens with:
# pick a1b2c3d AddPaymentRetryService
# pick e4f5g6h Add retry config validation
# pick i7j8k9l Fix typo in RetryConfig
#
# Change to:
# pick a1b2c3d AddPaymentRetryService
# squash e4f5g6h Add retry config validation
# fixup i7j8k9l Fix typo in RetryConfig
#
# Result: one clean commit combining all three.
# ─────────────────────────────────────────────────────────────
# THEGOLDENRULE
# ─────────────────────────────────────────────────────────────
# SAFE: rebase local-only branches (never pushed)
git rebase main # on a branch only you use
# DANGEROUS: rebase pushed shared branches
git rebase main # on develop, which 12 engineers pull from
# Then: git push --force ← THISBREAKSEVERYONE
# IFYOUMUST rewrite shared history:
# 1. Coordinate with the team first
# 2. Use --force-with-lease (never --force)
# 3. Have everyone fetch and reset after your push
The golden rule: never rebase commits that others have pulled.
Production Insight
The merge vs rebase choice affects git bisect effectiveness. With merge commits, git log --first-parent main shows a clean integration timeline — each merge commit represents a feature. With rebase, all commits appear linearly, mixing feature work with bug fixes. This makes git bisect more powerful (finer granularity) but git log harder to read. Teams using rebase should enforce squash-before-rebase to keep the linear history readable. Teams using merge should use --no-ff to preserve branch grouping in the history.
Key Takeaway
Merge preserves commit SHAs and creates a DAG history showing how work was integrated. Rebase creates linear history by replaying commits with new SHAs. Never rebase pushed shared branches — it breaks every teammate's local history. Use squash merge for simple integration, --no-ff merge for traceability, and rebase for local branch cleanup before PR.
Resolving Merge Conflicts
Merge conflicts occur when both branches modify the same lines of a file, or when one branch deletes a file the other modifies. Git cannot automatically decide which version to keep — it marks the file with conflict markers and pauses the merge.
Conflict markers show three versions: the current branch's version (<<<<<<< HEAD), a separator (=======), and the incoming branch's version (>>>>>>> feature/branch). Your job is to edit the file to produce the correct result, removing the markers.
Resolving conflicts is not just about picking one version over the other. Often the correct resolution combines both changes — for example, if both branches added different imports to the same file, you need both imports in the resolved version.
After resolving all conflicts, stage the files and commit. Git creates the merge commit automatically if all conflicts are resolved.
io/thecodeforge/git/MergeConflicts.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# io.thecodeforge — ResolvingMergeConflicts
# ─────────────────────────────────────────────────────────────
# CONFLICTDURINGMERGE
# ─────────────────────────────────────────────────────────────
git merge feature/payment-retry
# CONFLICT (content): Merge conflict in src/main/java/io/thecodeforge/payment/PaymentService.java
# Automatic merge failed; fix conflicts and then commit the result.
# Check which files have conflicts
git status
# Both modified: src/main/java/io/thecodeforge/payment/PaymentService.java
git diff --name-only --diff-filter=U
# Shows only files with unresolved conflicts.
# ─────────────────────────────────────────────────────────────
# CONFLICTMARKERSINTHEFILE
# ─────────────────────────────────────────────────────────────
# <<<<<<< HEAD (your current branch)
# publicPaymentResultprocessPayment(Order order) {
# return paymentClient.charge(order.getAmount(), order.getCurrency());
# =======
# publicPaymentResultprocessPayment(Order order, boolean retryEnabled) {
# if (retryEnabled) {
# return retryService.chargeWithRetry(order);
# }
# return paymentClient.charge(order.getAmount(), order.getCurrency());
# }
# >>>>>>> feature/payment-retry
# Edit the file to produce the correct combined result:
# publicPaymentResultprocessPayment(Order order, boolean retryEnabled) {
# if (retryEnabled) {
# return retryService.chargeWithRetry(order);
# }
# return paymentClient.charge(order.getAmount(), order.getCurrency());
# }
# ─────────────────────────────────────────────────────────────
# RESOLVEANDCOMPLETE
# ─────────────────────────────────────────────────────────────
# Stage the resolved file
git add src/main/java/io/thecodeforge/payment/PaymentService.java
# Complete the merge
git commit
# Git auto-generates a merge commit message.
# Or provide your own:
git commit -m "Merge featuren
# ─────────────────────────/payment-retry: resolve PaymentService conflict"\────────────────────────────────────
# ABORT: If conflicts are too complex
# ─────────────────────────────────────────────────────────────
git merge --abort
# Cancels the merge. Working directory restored to pre-merge state.
# Safe to use at any point during conflict resolution.
# ─────────────────────────────────────────────────────────────
# CONFLICTDURINGREBASE
# ─────────────────────────────────────────────────────────────
# If conflicts occur during rebase:
git rebase main
# CONFLICT (content): Merge conflict in PaymentService.java
# Resolve the conflict, then:
git add src/main/java/io/thecodeforge/payment/PaymentService.java
git rebase --continue
# Git moves to the next commit in the rebase sequence.
# More conflicts may occur in subsequent commits.
# Skip a commit during rebase (if it's not important):
git rebase --skip
# Discards the current commit's changes entirely.
# Abort the entire rebase:
git rebase --abort
# Returns to the state before the rebase started.
# ─────────────────────────────────────────────────────────────
# MERGETOOLS
# ─────────────────────────────────────────────────────────────
# Configure a merge tool
git config --global merge.tool vimdiff
# Or: vscode, meld, p4merge, kdiff3, beyondcompare
# Launch merge tool for all conflicted files
git mergetool
# Opens the configured tool with three panes:
# LOCAL (your version), REMOTE (incoming version), BASE (common ancestor)
Output
# git status during conflict
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
#
# Unmerged paths:
# (use "git add <file>..." to mark resolution)
#
# both modified: src/main/java/io/thecodeforge/payment/PaymentService.java
#
# no changes added to commit
# (use "git add" and/or "git commit -a")
Conflicts Are Communication Failures, Not Git Failures
Conflict markers show three versions: yours, theirs, and the common ancestor
Resolving often requires combining both changes, not picking one
After resolving: always compile and test before committing the merge
Frequent conflicts in the same files indicate the code needs refactoring into separate modules
Production Insight
Conflict resolution is where bugs are introduced. The person resolving the conflict often picks one version and discards the other without understanding both changes. This creates regressions: a fix from one branch is silently dropped during conflict resolution. Best practice: after resolving conflicts, run the full test suite before committing the merge. Better practice: have the conflict resolution reviewed as part of the PR — some teams add a 'conflict resolution' section to PR descriptions showing what was changed and why.
Key Takeaway
Merge conflicts occur when both branches modify the same code. Edit the file to produce the correct result, removing conflict markers. Stage and commit to complete the merge. Use git merge --abort to cancel. After resolving, always compile and test — conflict resolution is a common source of regression bugs.
Fast-Forward vs 3-Way Merge
A fast-forward merge happens when the target branch's HEAD is a direct ancestor of the source branch. Git simply moves the target pointer forward — no merge commit is created. The history remains linear.
A 3-way merge happens when both branches have diverged — each has commits the other doesn't. Git creates a merge commit with two parents, combining the changes from both branches. This requires finding the common ancestor (merge base) and computing the diff from that point.
The --no-ff flag forces a merge commit even when fast-forward is possible. This is useful for traceability: git log --first-parent main shows only merge commits, giving a clean integration timeline. Without --no-ff, feature branch commits appear directly in main's history with no grouping.
io/thecodeforge/git/FastForwardVs3Way.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# io.thecodeforge — Fast-Forward vs 3-WayMerge
# ─────────────────────────────────────────────────────────────
# FAST-FORWARD: No divergence, pointer just moves forward
# ─────────────────────────────────────────────────────────────
# Scenario: main is at commit A. You branched feature from A.
# You made commits B and C on feature. main is still at A.
# No commits were added to main since you branched.
git checkout main
git merge feature/payment-retry
# Fast-forward
# Updating 8d1e3f5..a1b2c3d
# Fast-forward
# src/main/java/io/thecodeforge/payment/PaymentRetryService.java | 87 +++
# Result: main now points to commit C. No merge commit created.
# git log shows: C → B → A (linear)
# Force a merge commit even with fast-forward
git merge --no-ff feature/payment-retry
# Result: Merge commit M with parents C and A.
# git log shows: M → C → B → A (with branch visible)
# ─────────────────────────────────────────────────────────────
# 3-WAYMERGE: Both branches diverged
# ─────────────────────────────────────────────────────────────
# Scenario: main is at commit A. You branched feature from A.
# You made B and C on feature. Meanwhile, someone added D on main.
# Both branches diverged from A.
git checkout main
git merge feature/payment-retry
# Merge made by the 'ort' strategy.
# src/main/java/io/thecodeforge/payment/PaymentRetryService.java | 87 +++
# 1 file changed, 87insertions(+)
# Result: Merge commit M with parents C (feature) and D (main).
# The merge base is A. Git computes: diff(A→C) + diff(A→D) and combines them.
# ─────────────────────────────────────────────────────────────
# VIEWINGTHEMERGEBASE
# ─────────────────────────────────────────────────────────────
git merge-base main feature/payment-retry
# Output: <sha-of-commit-A>
# This is the common ancestor used for the 3-way merge.
# ─────────────────────────────────────────────────────────────
# FIRST-PARENTLOG: See only merge commits on main
# ─────────────────────────────────────────────────────────────
git log --first-parent main --oneline
# Shows only commits on main's direct lineage.
# Merge commits appear, but individual feature commits don't.
# This gives a high-level integration timeline.
# Only works well if you use --no-ff merges.
Output
# Fast-forward: pointer moves from A to C. No merge commit.
# 3-way merge: merge commit M created with parents C and D.
# --no-ff: forces merge commit even when fast-forward is possible.
Fast-forward: no merge commit, linear history, pointer just moves forward
3-way merge: merge commit with two parents, DAG history, content combined
--no-ff forces a merge commit for traceability even when fast-forward is possible
git log --first-parent main shows only the integration timeline (merge commits)
Production Insight
The --no-ff flag is a team policy decision, not a technical one. Teams that use --no-ff get a clean git log --first-parent main that shows exactly when features were integrated. Teams that allow fast-forward get a linear history that's easier to read with git log but harder to trace which commits belong to which feature. For release management and changelog generation, --no-ff is superior because each merge commit represents a complete feature. For simple projects with few contributors, fast-forward is simpler and sufficient.
Key Takeaway
Fast-forward merges move the pointer forward when there's no divergence — no merge commit created. 3-way merges combine changes from diverged branches using a common ancestor. Use --no-ff to force merge commits for traceability. git log --first-parent main shows only the integration timeline when using --no-ff merges.
Branch Strategies: GitFlow, GitHub Flow, and Trunk-Based Development
Branch strategy defines how your team uses branches for development, releases, and hotfixes. The three dominant strategies are GitFlow, GitHub Flow, and trunk-based development. Each has different trade-offs for release cadence, code stability, and operational complexity.
GitFlow uses long-lived branches (main, develop) and short-lived branches (feature, release, hotfix). It's designed for scheduled releases with strict version management. The overhead is significant — multiple branch types, merge ordering rules, and release branch management.
GitHub Flow is simpler: one long-lived branch (main), short-lived feature branches, and deploy-from-main. Every merge strong CI/CD and feature flags for incomplete work.
Trunk-based development uses very short-lived branches (hours, not days) or direct commits to main. Feature flags control incomplete work. It maximizes integration frequency and minimizes merge conflicts but requires mature CI/CD and feature flag infrastructure.
io/thecodeforge/git/BranchStrategies.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# io.thecodeforge — BranchStrategies
# ─────────────────────────────────────────────────────────────
# GITFLOW
# ─────────────────────────────────────────────────────────────
# Branches: main, develop, feature/*, release/*, hotfix/*
# Start a feature
git checkout develop
git checkout -b feature/payment-retry
# Finish a feature: merge into develop
git checkout develop
git merge --no-ff feature/payment-retry
git branch -d feature/payment-retry
# Start a release
git checkout develop
git checkout -b release/v2.5.0
# Bug fixes only on release branch
# Finish a release: merge into main works well for continuous to main triggers a deploy. It deployment but requires AND develop
git checkout main
git merge --no-ff release/v2.5.0
git tag -a v2.5.0 -m "Release v2.5.0"
git checkout develop
git merge --no-ff release/v2.5.0
git branch -d release/v2.5.0
# Hotfix: branch from main, merge back to main AND develop
git checkout main
git checkout -b hotfix/critical-bug
# Fix the bug
git checkout main
git merge --no-ff hotfix/critical-bug
git tag -a v2.5.1 -m "Hotfix v2.5.1"
git checkout develop
git merge --no-ff hotfix/critical-bug
# ─────────────────────────────────────────────────────────────
# GITHUBFLOW
# ─────────────────────────────────────────────────────────────
# Branches: main, feature/* (short-lived)
# Create feature branch from main
git checkout main
git checkout -b feature/payment-retry
# Push and open PR
git push -u origin feature/payment-retry
# OpenPR on GitHub. CI runs automatically.
# AfterPR approval: merge into main (or squash merge)
# GitHub merges the PR. Main is deployed automatically.
# Delete feature branch after merge
git branch -d feature/payment-retry
# ─────────────────────────────────────────────────────────────
# TRUNK-BASEDDEVELOPMENT
# ─────────────────────────────────────────────────────────────
# Branches: main (trunk), very short-lived branches (hours)
# Createshort-lived branch
git checkout -b feature/payment-retry
# Commitfrequently (multiple times per day)
git add src/main/java/io/thecodeforge/payment/PaymentRetryService.java
git commit -m "WIP: PaymentRetryService skeleton"
# Rebase onto main before merging (to stay current)
git fetch origin
git rebase origin/main
# Merge into main via PR (or direct push with CI gates)
git checkout main
git merge --ff-only feature/payment-retry
# --ff-only ensures no merge commit. Linear history.
# Feature flags for incomplete work
# In application code:
# if (featureFlags.isEnabled("payment-retry")) {
# return retryService.chargeWithRetry(order);
# }
# The feature is merged to main but hidden behind a flag.
Output
# GitFlow: structured, versioned, high overhead
# GitHub Flow: simple, deploy-from-main, requires strong CI
# Trunk-based: fast integration, requires feature flags and mature CI
Branch Strategy = Release Cadence Decision
GitFlow: main + develop + feature/release/hotfix branches. Scheduled releases. High overhead.
GitHub Flow: main + short-lived feature branches. Deploy on merge. Simple.
Trunk-based: main + very short-lived branches (hours). Feature flags. Maximum integration frequency.
Most teams at scale use trunk-based development with feature flags.
Production Insight
Branch strategy affects merge conflict frequency. GitFlow's long-lived develop branch accumulates changes between releases, creating large merge conflicts when release branches are created. GitHub Flow's short-lived branches merge frequently, keeping conflicts small. Trunk-based development's branches last hours, making conflicts nearly impossible. The trade-off: shorter branches require stronger CI/CD (every merge must be tested) and feature flag infrastructure (for incomplete work). Teams without these capabilities should not attempt trunk-based development — it will produce broken main branches.
Key Takeaway
Branch strategy should match your release cadence: GitFlow for scheduled releases, GitHub Flow for continuous deployment, trunk-based for high-frequency integration. Shorter branches mean fewer merge conflicts but require stronger CI/CD and feature flag infrastructure. Most large-scale teams use trunk-based development.
● Production incidentPOST-MORTEMseverity: high
Rebase on Shared Branch: 12 Engineers Lose 3 Hours of Work
Symptom
Multiple engineers reported 'Your branch and 'origin/develop' have diverged' on their next pull. Three engineers' feature branches showed merge conflicts against develop that didn't exist before. CI reported 'commit not found' for a webhook payload referencing a SHA from before the rebase.
Assumption
The senior engineer assumed rebasing develop was safe because 'nobody had pushed since the last merge.' They did not realize that 12 engineers had already pulled the pre-rebase develop and based their work on those commit SHAs.
Root cause
1. The engineer ran git rebase main on the develop branch to linearize 8 merge commits.
2. This rewrote all 8 commit SHAs. The new commits had the same content but different hashes.
3. git push origin develop was rejected (non-fast-forward). The engineer used git push --force.
4. The remote develop now pointed to the rebased commits. The old 8 commits were orphaned on the remote.
5. Twelve engineers had the old commits in their local develop. Their next git pull showed divergence.
6. Three engineers had feature branches with parents pointing to old develop SHAs. Those parent references were now dangling — their branches were effectively orphaned from develop's history.
7. CI webhook payloads referenced the old SHA of the last pre-rebase commit, which no longer existed on the remote.
Fix
1. Identified the last pre-rebase commit SHA from CI webhook logs.
2. Force-pushed the old commit chain back to develop: git reset --hard <pre-rebase-sha> && git push origin develop --force-with-lease.
3. For the 12 engineers with diverged local develop: git fetch origin && git reset --hard origin/develop.
4. For the 3 engineers with orphaned feature branches: used git rebase --onto origin/develop <old-parent> <branch-tip> to replay their feature commits on top of the restored develop.
5. Re-triggered CI manually.
6. Added branch protection rules requiring PR reviews for develop — preventing direct pushes entirely.
Key lesson
Never rebase a branch that other people pull from. The golden rule exists because rewritten SHAs break every clone that pulled the old history.
--force is never acceptable on shared branches. --force-with-lease would have been irrelevant here (nobody else pushed), but normalizing --force creates dangerous habits.
Branch protection rules that prevent direct pushes to shared branches would have prevented this entirely.
Feature branches with parent pointers to specific SHAs are fragile. Always rebase feature branches onto the latest develop before merging, rather than relying on fixed parent SHAs.
Production debug guideSystematic recovery paths for history corruption, merge conflicts, and divergent branches.5 entries
Symptom · 01
'Your branch and origin/<branch> have diverged' after a teammate force-pushed
→
Fix
1. Run git fetch origin to see what the remote has.
2. Run git log --oneline origin/<branch> to inspect the remote's history.
3. If the remote history looks correct: git reset --hard origin/<branch> to align your local branch.
4. If you had local commits on top of the old history: find them via git reflog, then cherry-pick or rebase them onto the new branch tip.
5. Prevention: never force-push to branches others pull from without coordinating first.
Symptom · 02
Merge conflict in a file you didn't modify
→
Fix
1. This happens when both branches modified adjacent lines or when one branch renamed/moved a file the other modified.
2. Run git diff to see the full conflict context.
3. Check if the conflict is from whitespace changes: git diff --ignore-all-space.
4. If the conflict is from a rebase: your commits are being replayed — resolve and continue with git rebase --continue.
5. If too many conflicts: abort with git merge --abort or git rebase --abort and consider a different integration strategy.
Symptom · 03
Feature branch has merge conflicts with develop that seem unrelated to your changes
→
Fix
1. Your feature branch diverged from an old point on develop. Other changes on develop modified the same files.
2. Run git merge-base HEAD develop to see where your branch diverged.
3. Rebase your branch onto the latest develop: git rebase develop.
4. Resolve conflicts one commit at a time during the rebase.
5. If the rebase has too many conflicts: consider squashing your commits first (git rebase -i --autosquash develop) to reduce the number of conflict resolution points.
Symptom · 04
git merge says 'Already up to date' but you know there are changes to merge
→
Fix
1. The branch you're merging is already fully contained in your current branch. This happens when you previously merged or rebased.
2. Run git log --oneline --graph --all to see the actual branch topology.
3. Check if you're on the correct branch: git branch --show-current.
4. If the changes are on a different branch than expected: switch to the correct branch and merge from there.
5. If the changes were squash-merged: the commits are different but the content is the same. Git sees the content as already merged.
Symptom · 05
After rebase, git log shows duplicate commits or unexpected commit order
→
Fix
1. You may have rebased onto the wrong base, or rebased twice (creating duplicate commits).
2. Run git reflog to see the sequence of operations.
3. Identify the commit SHA before the bad rebase started.
4. git reset --hard <pre-rebase-sha> to undo the rebase.
5. Re-run the rebase with the correct base: git rebase <correct-base-branch>.
★ Git Branch and Merge Triage Cheat SheetFast recovery for branch-related production incidents.
'Your branch and origin/<branch> have diverged' after teammate's force-push−
Immediate action
Reset local branch to match remote.
Commands
git fetch origin
git reset --hard origin/<branch>
Fix now
If you had local commits: git reflog to find them, then cherry-pick onto the reset branch.
Merge conflict — too many files to resolve manually+
Immediate action
Abort the merge and consider a different strategy.
Commands
git merge --abort (or git rebase --abort)
git log --oneline --graph HEAD <branch> to understand the divergence
Fix now
If rebasing: squash your commits first to reduce conflict points. If merging: consider merging develop into your branch more frequently.
Feature branch parent points to a SHA that no longer exists on develop+
Immediate action
Rebase the feature branch onto the current develop tip.
git rebase --onto origin/develop <old-parent-sha> <feature-branch-tip>
Fix now
If the old parent SHA is unknown: git merge-base HEAD origin/develop to find the closest common ancestor.
git merge says 'Already up to date' but changes exist on the source branch+
Immediate action
Verify you're on the correct branch and merging from the correct source.
Commands
git branch --show-current && git log --oneline -3
git log --oneline --graph --all to see the full branch topology
Fix now
If changes were squash-merged: Git considers the content already merged. Create a new branch from develop if you need those changes.
After rebase, commits appear duplicated or in wrong order+
Immediate action
Undo the rebase using reflog.
Commands
git reflog | head -20
git reset --hard HEAD@{1} (or the pre-rebase entry)
Fix now
Re-run rebase with the correct base branch. Verify with git log --oneline --graph before pushing.
Merge vs Rebase vs Squash Merge
Aspect
git merge
git rebase
git merge --squash
History shape
DAG (merge commits show branching)
Linear (no merge commits)
Linear (single commit per feature)
Commit SHAs preserved
Yes — original commits keep their SHAs
No — all replayed commits get new SHAs
N/A — only one new commit created
Safe on shared branches
Yes — does not rewrite history
No — rewrites history, breaks references
Yes — creates new commit, no rewrite
Traceability
High — merge commits group feature work
Medium — linear but commits are interleaved
Low — individual commits lost on main
git bisect effectiveness
Lower — merge commits are opaque
Higher — each commit is a bisect point
Lowest — only one commit per feature
Conflict resolution
Once at merge time
Once per commit during rebase
Once at merge time
Best for
Team integration, release branches
Local branch cleanup before PR
Simple integration, clean main history
Used by
GitFlow, release-managed teams
Individual developers before PR
Many open-source projects, simple teams
Key takeaways
1
Branches are cheap pointers to commits
create them freely. They cost nothing in Git.
2
Merge preserves commit SHAs and creates a DAG history. Rebase creates linear history but rewrites all commit SHAs.
3
Never rebase commits that have been pushed to a shared branch. It breaks every teammate's local history.
4
--no-ff forces a merge commit even when fast-forward is possible
useful for traceability and git log --first-parent.
5
git merge --abort cancels an in-progress merge and restores the pre-merge state. Use it when conflicts are too complex.
6
After resolving merge conflicts, always compile and test before committing. Conflict resolution is a common source of regression bugs.
7
Branch strategy should match release cadence
GitFlow for scheduled releases, GitHub Flow for continuous deployment, trunk-based for high-frequency integration.
8
Short-lived branches (hours, not days) minimize merge conflicts. Long-lived branches accumulate divergence and create integration pain.
9
Feature flags enable merging incomplete work to main safely
the feature is deployed but hidden until enabled.
10
git log --first-parent main shows only the integration timeline when using --no-ff merges
useful for release management and changelog generation.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
FAQ · 6 QUESTIONS
Frequently Asked Questions
01
When should I use merge vs rebase?
Use rebase to clean up your local feature branch before opening a PR — it keeps the PR diff clean and up-to-date with main. Use merge to integrate the PR into main — it preserves the fact that work happened in a branch. Many teams use squash merge to collapse a feature branch into one commit on main.
Was this helpful?
02
What is a fast-forward merge?
A fast-forward merge happens when the branch being merged has no diverging commits from the target — target's HEAD is an ancestor of the branch. Git just moves the target pointer forward rather than creating a merge commit. Use --no-ff to force a merge commit if you want to preserve the branch structure in history.
Was this helpful?
03
What is a squash merge and when should I use it?
Squash merge (git merge --squash) takes all commits from the source branch and stages them as a single change. You then create one commit on the target branch. This produces the cleanest main history — one commit per feature — but loses the individual commit history of the feature branch. Use it for simple features where the intermediate commits aren't valuable for debugging.
Was this helpful?
04
How do I prevent merge conflicts?
Merge conflicts are minimized by keeping branches short-lived and integrating frequently. Rebase your feature branch onto main daily (git rebase main). If a branch must live for weeks, merge main into it regularly (git merge main) to resolve conflicts incrementally. Good modular architecture also helps — if two branches rarely touch the same files, conflicts are rare.
Was this helpful?
05
What is trunk-based development?
Trunk-based development is a branching strategy where developers integrate to main (the trunk) very frequently — multiple times per day. Branches live for hours, not days. Incomplete work is hidden behind feature flags. This maximizes integration frequency, minimizes merge conflicts, and enables continuous deployment. It requires strong CI/CD pipelines and feature flag infrastructure.
Was this helpful?
06
What happens if I rebase a shared branch?
Rebasing rewrites all commit SHAs from the rebase point onward. If teammates have pulled the old SHAs, their local branches now reference commits that don't exist on the remote. Their next pull shows divergence. Feature branches based on old SHAs become orphaned. The recovery requires every teammate to fetch, inspect, and reset their local branches. This is why the golden rule exists: never rebase public commits.