Git, CI/CD & GitOpsVIII · BranchingBranching
Divergence and shared history — the commit graph when two branches split
What you'll learn
- Identify the common ancestor of two branches using the merge-base computation
- Distinguish a fast-forward situation from a true merge when both branches have moved forward
- Explain why two branches can share history without sharing tips, and what that means for reachability
- Read a divergence diagram and predict whether git merge will fast-forward or create a merge commit
- Recognise the operational difference between "ahead" and "behind" in a remote-tracking comparison
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Two branches in a Git repository are never independent objects. They are pointers into a single commit DAG, and the DAG has a topology that determines everything about how the branches relate. The vocabulary — common ancestor, divergence, fast-forward, merge base — is not jargon for its own sake. Each term names a property of the graph that Git uses to decide whether merging two branches is a no-op, a conflict, or a three-way merge. A production engineer who can read the DAG answers “what does merging these two branches actually do?” before running the merge. The engineer who cannot read the DAG finds out by running the merge.
Common ancestors and the merge base
When two branches share commits, those commits form a shared
history. The most recent shared commit is the merge base: the
youngest commit that is an ancestor of both branch tips. Git
computes the merge base with git merge-base:
git merge-base main feature/iam-rotation
# 6f4e5a6f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d
In a linear history — one branch created from the other with no commits in between — the merge base is the tip of the older branch. In a branched history — both branches moving forward from a common commit — the merge base is the commit at the fork point.
flowchart LR
A["commit 6f4e5a6\n(merge base)"] --> B["commit 8a3f9d2\n(main)"]
A --> C["commit 9f3c1d7\n(feature/x)"]
B --> D["commit 4d2c8e0\n(main)"]
C --> E["commit a1b2c3d\n(feature/x)"]
C --> F["commit 7e8f9a0\n(feature/x)"]
In the diagram, main is at 4d2c8e0 and feature/x is at
a1b2c3d. Both tips are descendants of 6f4e5a6 (the merge
base), but neither tip is a descendant of the other. The two
branches have diverged.
Fast-forward versus true merge
The merge base computation determines what git merge does. Two
cases:
- Fast-forward. If one branch’s tip is a direct descendant of the other branch’s tip — that is, the merge base is one of the two tips — then moving the slower branch forward to the faster tip requires no merge commit. Git simply moves the pointer.
- True merge. If neither tip is a descendant of the other, Git must create a merge commit with two parents. The merge commit records both branch tips as ancestors and the merge base as the common ground.
# Fast-forward case: feature is a descendant of main
git merge feature/iam-rotation
# Updating 8a3f9d2..a1b2c3d
# Fast-forward
# terraform/main.tf | 2 +-
# 1 file changed, 1 insertion(+), 1 deletion(-)
# True merge case: both branches have moved forward
git merge feature/iam-rotation
# Merge made by the 'recursive' strategy.
# terraform/main.tf | 4 ++--
# 1 file changed, 2 insertions(+), 2 deletions(-)
A fast-forward is invisible in the graph: the branch pointer moves, and no new commit appears. A true merge is visible: a new commit with two parents appears at the merge point.
flowchart LR
subgraph BEFORE["before merge"]
A1["6f4e5a6"] --> B1["8a3f9d2 main"]
A1 --> C1["9f3c1d7 feature"]
end
subgraph AFTER_FF["after fast-forward"]
A2["6f4e5a6"] --> B2["8a3f9d2"] --> C2["9f3c1d7 main = feature"]
end
The fast-forward diagram shows main simply moving to where
feature was. No merge commit, no new history, no topology
change.
flowchart LR
subgraph BEFORE2["before merge"]
A3["6f4e5a6 merge base"] --> B3["8a3f9d2 main"]
A3 --> C3["9f3c1d7 feature"]
B3 --> D3["4d2c8e0 main"]
end
subgraph AFTER_MERGE["after true merge"]
A4["6f4e5a6"] --> B4["8a3f9d2"] --> D4["4d2c8e0 main"]
A4 --> C4["9f3c1d7 feature"]
C4 --> E4["a1b2c3d feature"]
D4 --> F4["MERGE main"]
E4 --> F4
end
The true-merge diagram shows main and feature both extending
from the merge base, and a new merge commit with two parents
uniting them.
Why reachability matters
The merge base is a reachability question, not a time question. Two commits can be in chronological order and have neither be a descendant of the other — that is exactly what divergence is. Equally, two commits can be in time order and one be a descendant of the other (linear history).
The operational consequences:
- A branch that has not received new commits since it was
created from
mainis a descendant ofmainand can be fast-forwarded intomain. - A branch that has received new commits on
mainand has new commits of its own is not a descendant ofmain; it has diverged and requires a true merge.
The reachability property is computed by walking parent edges in the DAG. The merge base is the youngest commit that both branch tips can reach. The cost of the computation is linear in the depth of the graph in the worst case, but in practice the merge base is usually close to the branch tips.
Reading divergence in practice
git status and git branch -vv both report the
ahead/behind relationship between a branch and its upstream.
This is the same reachability question, expressed for a
remote-tracking comparison:
git status
# On branch feature/iam-rotation
# Your branch is ahead of 'origin/feature/iam-rotation' by 3 commits.
# (use "git push" to publish your local commits)
git branch -vv
# * feature/iam-rotation 9f3c1d7 [origin/feature/iam-rotation: ahead 3] rotate iam keys
# main 8a3f9d2 [origin/main: behind 2] bump terraform module
“Ahead” means local has commits that the upstream does not
(“local is not a descendant of upstream”). “Behind” means
upstream has commits that local does not (“local is not a
descendant of upstream”). When both are true — local has
commits the upstream does not, and the upstream has commits
local does not — the branch has diverged from its upstream
and a git pull will produce a merge commit rather than a
fast-forward.
# Diverged: ahead 3, behind 2
git pull
# Merge made by the 'recursive' strategy.
# (no-fast-forward is the default for diverged branches)
The pull output reports a true merge because neither side is a
descendant of the other. The reachability check that determines
this is the same merge-base computation that git merge runs.
The shared-history property
Two branches share history when their merge base is not one of their tips — that is, when they have a common ancestor older than the branch points. Sharing history does not mean sharing tips: the tips can be hundreds of commits apart, on completely different lines of work, and still share a common ancestor from months ago.
flowchart LR
A["6f4e5a6\n(months ago)"] --> B["main: 8a3f9d2"]
A --> C["feature/x: a1b2c3d"]
A --> D["hotfix/y: e7f8a9b"]
A --> E["experiment/z: 1c2d3e4"]
The four branches in this diagram have not diverged from each
other — they share the merge base 6f4e5a6 — but their tips
have no relationship to each other. Any pair of them can be
merged with a true merge commit; none of them is a descendant
of any other.
The shared-history property is what makes git log --graph
useful: the graph compresses the shared history into a single
backbone and the per-branch commits into individual lines,
which is the visual signature of a healthy branching workflow.
Production discipline
- Always read
git merge-basebefore merging. The merge base tells you what the merge is going to do: if it equals one of the two tips, you get a fast-forward; otherwise you get a true merge commit. - Use
--no-ffwhen the audit trail matters. A true merge commit records “this feature was integrated at this commit” in the graph; a fast-forward records only that the work landed. Choose based on whether the team reads the graph. - Treat diverged upstream branches as an alarm. A branch that is ahead and behind its upstream has been worked on while the upstream moved; the next pull will produce a merge commit, and any review should account for both sides of the merge.
- Document branch lifecycles with merge-base output. A release script that wants to know “which feature branches landed in this release?” should compute the merge base against the previous release tag and list branches whose tip is reachable from the new release.
Cross-course references
- Ansible for Production Sysadmins - Part XXXVIII (Review)
uses
git merge-baseto detect when a feature branch has diverged frommainand to scope the PR review to the diverged range. - GitOps with Argo CD - Part IV (AppSources) discusses divergence in the context of multi-source applications, where each source branch has its own upstream relationship.
- Terraform for Production Sysadmins - Part XII (State) draws the analogy between the merge base and the Terraform state version: both are reference points that determine what a “forward” operation actually does.
Quiz
Knowledge check · 4 questions
Q1. Branch `feature/x` was created from `main` at commit `8a3f9d2`. Since then, three commits have been added to `feature/x` and two to `main`. What happens when you run `git merge feature/x` from `main`?
Q2. When two branches share a common ancestor but neither is a descendant of the other, merging them always requires a merge commit and cannot be fast-forwarded.
Q3. What is the merge base, and how does it determine whether `git merge` fast-forwards or creates a merge commit?
Q4. Diagnose a release script that produces an unexpected commit count and recommend a fix.
A release script cuts a release branch from main, then runs `git merge --no-ff` for each of 14 feature branches to integrate them. The release is supposed to add 14 commits (one merge per feature). Instead, the release branch shows 28 commits: 14 merge commits plus 14 feature tips, and the team suspects every feature branch was force-pushed with a new tip. The release is now behind schedule and the on-call is investigating.
Passing score: 75%. Answers are checked in this browser.