Skip to main content
RunBook Academy

Git, CI/CD & GitOpsIX · MergingMerging

The merge process — what Git actually does during a merge

Intermediate⏱ ~24 mingit

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

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

Not yet marked complete on this device.

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_HEAD exists, a previous merge is in progress; running git merge again 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-ff or 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 '&lt;name&gt;', 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:

  1. Read git status from 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.
  2. Inspect the three-stage index with git ls-files -u during 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.
  3. Use --no-commit for CI verification. A CI pipeline that wants to lint or test the merged tree before the merge is final runs git merge --no-commit, runs the checks, and either git commit (success) or git 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_HEAD is 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

  1. Q1. What three files in `.git/` describe an in-progress merge state?

  2. Q2. After a clean merge completes, the merge commit has two parents: the previous HEAD and the merged-in branch tip.

  3. Q3. Name the Git plumbing command that lists the three-stage index entries for a conflicted file, and describe what each stage contains.

  4. 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 &lt;file&gt;`, 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.