Git, CI/CD & GitOpsIX · MergingMerging
The merge process — what Git actually does during a merge
What you'll learn
- List the six phases of a true merge: precondition checks, ref resolution, merge-base computation, strategy execution, index staging, and commit writing
- Explain what MERGE_HEAD, MERGE_MSG, and MERGE_MODE contain during an in-progress merge
- Read `git status` output during a merge and identify whether the merge is in progress, conflicted, or complete
- Use `git diff --cached` during a merge to inspect what has been staged in the index
- Recognise the difference between a clean merge and a conflicted merge from the working tree state
Prerequisites
- Branch creation and switching — git switch, git checkout, and the three-tree update
- Graph topology and merges — what merge commits actually encode
- Fast-forward merges — when a merge is just a pointer move
- Three-way merges — when fast-forward is not possible
- Merge commits and --no-ff — forcing topology for audit
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
A git merge that produces a true merge commit runs through
six phases, each of which leaves an inspectable state. The
phases are not abstractions — they are implemented in
builtin/merge.c and merge-recursive.c in the Git source —
and knowing what state each phase produces is what makes a
mid-merge git status legible instead of alarming. The same
six-phase model applies whether the merge is clean (no
conflicts), conflicted (resolution required), or staged for
later (--no-commit).
Phase 1 — precondition checks
Before any tree is read, Git asserts that the repository is in a state where a merge can begin:
git merge feature/iam-rotation
# error: Your local changes to the following files would be overwritten by merge:
# terraform/main.tf
# Please commit your changes or stash them before you merge.
# Aborting
The preconditions are:
- Working tree clean. Uncommitted changes to tracked files are not overwritten. Stashing or committing is required.
- Not already merging. If
.git/MERGE_HEADexists, a previous merge is in progress; runninggit mergeagain is refused until the in-progress merge is committed or aborted. - No detached HEAD without specifying a branch.
git merge <commit>(not a branch name) requires an explicit--no-ffor it is treated as a fast-forward-only case.
The error messages are explicit about which precondition failed.
The “local changes would be overwritten” message tells you the
working tree is dirty; the “merge in progress” message tells you
.git/MERGE_HEAD exists. Both are recoverable; neither leaves
the repository in a corrupted state.
Phase 2 — ref resolution
The branch names passed on the command line are resolved to commit OIDs:
git rev-parse feature/iam-rotation
# a1b2c3d4e5f6...
git rev-parse HEAD
# 4d2c8e0...
If a name does not resolve (typo, missing remote-tracking
branch, deleted branch), git merge exits before any state is
modified. The error is unambiguous — fatal: '<name>' - not something we can merge — and the working tree and index are
untouched.
For a fast-forward merge, this phase ends the algorithm: the two OIDs are checked for the ancestor relationship, and the ref is updated. For a true merge, the algorithm continues.
Phase 3 — merge-base computation
The merge base is found by walking parent edges from both tips and selecting the youngest commit reachable from both. The plumbing command exposes this directly:
git merge-base HEAD feature/iam-rotation
# 6f4e5a6...
For a simple forked graph, the result is the commit at the
fork point. For a criss-crossed graph, the recursive strategy
calls itself recursively to merge multiple bases into a
virtual base (see IX-06). The output of git merge-base is
the third tree input to the strategy; the algorithm does not
read it again after this point.
flowchart LR
A["merge base\n6f4e5a6"] --> B["HEAD\n4d2c8e0"]
A --> C["feature/iam-rotation\na1b2c3d"]
B --> D["ours tree\n(4d2c8e0)"]
C --> E["theirs tree\n(a1b2c3d)"]
A --> F["base tree\n(6f4e5a6)"]
D --> G["recursive strategy"]
E --> G
F --> G
G --> H["merged tree\n(written to index)"]
The three trees are read from the object store by OID, not by path. The strategy then iterates over every path present in any of the three trees and runs the per-path decision algorithm described in IX-02.
Phase 4 — strategy execution
The strategy (default: recursive) runs the per-path
algorithm. For each path, it reads the blob OIDs from the
three trees and decides:
- Unchanged in both: the blob from either side (they are identical) is the merged blob.
- Changed in one side: the changed side’s blob is the merged blob.
- Changed identically in both: the common new blob is the merged blob.
- Changed differently in both: conflict; both blobs are
written into the index at stage 2 (
ours) and stage 3 (theirs), with stage 1 holding the base blob. The working tree gets a file with conflict markers; the merge is in a conflicted state.
The strategy does not write the merge commit yet. It stages
the result in the index and updates the working tree (for
clean resolutions) or leaves the working tree with conflict
markers (for conflicts). The state at the end of this phase
is inspectable with git status and git diff --cached.
# During a clean merge, after the strategy has staged:
git status
# On branch main
# All conflicts fixed but you are still merging.
# (use "git commit" to conclude merge)
#
# Changes to be committed:
# modified: terraform/main.tf
# During a conflicted merge:
git status
# On branch main
# You have unmerged paths.
# (fix conflicts and run "git commit")
# (use "git merge --abort" to abort the merge)
#
# Unmerged paths:
# both modified: terraform/main.tf
Phase 5 — index staging
The strategy writes the result into the index. For clean resolutions, the path is staged at stage 0 with the merged blob OID. For conflicts, the path is staged three times:
git diff --cached --diff-filter=U
# :100644 100644 6f4e5a6... 4d2c8e0... M terraform/main.tf
git ls-files -u terraform/main.tf
# 100644 6f4e5a6... 1 terraform/main.tf <-- stage 1: base
# 100644 4d2c8e0... 2 terraform/main.tf <-- stage 2: ours
# 100644 a1b2c3d... 3 terraform/main.tf <-- stage 3: theirs
The three-stage index is the data structure that makes
mid-merge inspection possible. git diff --cached at this
point shows the difference between ours and theirs for
each conflicted path. Tools that resolve conflicts (git mergetool, git checkout --conflict, git add to collapse
the stages) operate on these three entries.
The --no-commit flag stops the merge after this phase: the
index is staged, MERGE_HEAD is written, but the merge commit
is not created. This is useful for CI pipelines that want to
inspect the staged result before committing (e.g. to run a
linter against the merged tree) or for engineers who want
to amend the merge message.
Phase 6 — commit writing
If the merge is clean (no conflicts) and --no-commit was
not passed, Git writes the merge commit. The new commit
object has:
- The merged tree OID (the tree staged in phase 5).
- Two parents: the current HEAD and the merged-in branch tip.
- An author and committer (the engineer who ran the merge).
- A message (default:
Merge branch '<name>', or whatever was passed to-m).
git cat-file -p HEAD
# tree <merged-tree-oid>
# parent 4d2c8e0... <-- was HEAD before the merge
# parent a1b2c3d... <-- was feature/iam-rotation
# author Ops <ops@example.com> 1730000000 +0000
# committer Ops <ops@example.com> 1730000000 +0000
#
# Merge branch 'feature/iam-rotation'
The current branch’s ref is then updated to point at the new
merge commit. .git/MERGE_HEAD is removed. The working tree
is unchanged from the staged state. The repository is now
in the post-merge state.
For --no-commit, phase 6 is skipped: the index is staged,
MERGE_HEAD is written, MERGE_MSG is populated, and the user
runs git commit to finalise. The ref is not updated until
the commit is written.
Production discipline
Three rules for understanding the merge process in a production-grade workflow:
- Read
git statusfrom the top. “All conflicts fixed but you are still merging” is not the same as “You have unmerged paths”, and both are different from “Changes to be committed”. The first line tells you which phase the merge is in. - Inspect the three-stage index with
git ls-files -uduring conflicts. The three entries per path are the data your merge tool operates on. Knowing what stage 1, 2, and 3 mean lets you script conflict resolution. - Use
--no-commitfor CI verification. A CI pipeline that wants to lint or test the merged tree before the merge is final runsgit merge --no-commit, runs the checks, and eithergit commit(success) orgit merge --abort(failure). The merge commit is created only if the checks pass.
Cross-course references
- Linux for Production Sysadmins - Parts XII (RepoSecurity)
covers package-state files;
.git/MERGE_HEADis the Git-level analogue of an apt/dpkg lock file. - Ansible for Production Sysadmins - Part XXXVII (RepoArch) covers merge hooks; the index-stage model is the same shape as the staged-file model used in CI artefact staging.
- Terraform for Production Sysadmins - Parts IX-XII (State) cover Terraform state files; a merge commit that lands a state format change is visible in the same way a Terraform apply is visible in the plan log.
Quiz
Knowledge check · 4 questions
Q1. What three files in `.git/` describe an in-progress merge state?
Q2. After a clean merge completes, the merge commit has two parents: the previous HEAD and the merged-in branch tip.
Q3. Name the Git plumbing command that lists the three-stage index entries for a conflicted file, and describe what each stage contains.
Q4. Diagnose the state of an in-progress merge from the output of `git status`.
An engineer runs `git merge feature/iam-rotation` on main. The merge produces conflicts in two files. The engineer edits one of them, runs `git add <file>`, and runs `git status`. The output shows 'All conflicts fixed but you are still merging.' for one file and 'Unmerged paths: both modified' for the other.
Passing score: 75%. Answers are checked in this browser.