Skip to main content
RunBook Academy

Git, CI/CD & GitOpsX · Merge ConflictsConflicts

The conflict markers — reading <<<<<<<, =======, and >>>>>>>

Intermediate⏱ ~18 mingit

What you'll learn

  • Recognise the three conflict markers and state what each one delimits
  • Explain how a conflicted file is staged in the three-stage index (stages 1, 2, 3)
  • Use `git diff`, `git diff --cached`, and `git diff --base/--ours/--theirs` to inspect a conflict from four angles
  • Predict the output of `git status` before and after `git add &lt;file&gt;` on a conflicted file
  • Distinguish "unmerged" paths from "resolved" paths in `git status` vocabulary

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.

When Git stops a merge, it writes the conflicting file into the working tree with literal conflict markers in it, and it stages three blobs at the same path in the index. The working tree is what the engineer reads; the index is what git diff queries. Understanding both — what each marker means in the file, and which stage each of the three blobs occupies in the index — is what makes a conflict legible. Every subsequent action (git add to resolve, git diff --ours to inspect, git checkout --theirs to discard a side) operates on the same three-stage structure.

The three markers

A conflict hunk in a file is delimited by three fences:

<<<<<<< HEAD
retention_days = 90
encrypt_at_rest = true
=======
retention_days = 7
encrypt_at_rest = false
>>>>>>> feature/iam-rotation

The meaning of each line:

  • <<<<<<< followed by a label — opens the “ours” side. The label is most commonly HEAD (the current branch tip) but can also be the branch name, a tag, or any ref name Git has a reason to print. Everything between <<<<<<< and ======= is exactly the content of the file as it appears on the current branch.
  • ======= — the separator. Everything above this line is ours; everything below is theirs.
  • >>>>>>> followed by a label — closes the “theirs” side. The label is usually the branch being merged in. Everything between ======= and >>>>>>> is exactly the content of the file as it appears on the merged-in branch.

For a conflict at the start of a file, the <<<<<<< line is the first line of the file. For a conflict at the end, the >>>>>>> line is the last line. For multiple conflicts in the same file, the marker pattern repeats — every conflict hunk has its own <<<<<<< and >>>>>>> pair, with ======= inside.

What the file looks like for a two-hunk conflict

Most conflicts in an infrastructure file touch one hunk at a time, but a multi-hunk conflict is a repeated pattern inside the same file:

resource "aws_s3_bucket" "logs" {
  bucket = "prod-logs"
<<<<<<< HEAD
  acl    = "private"
=======
  acl    = "log-delivery"
>>>>>>> feature/iam-rotation

  versioning {
    enabled = true
  }

  lifecycle_rule {
<<<<<<< HEAD
    expiration_days = 365
=======
    expiration_days = 30
>>>>>>> feature/iam-rotation
  }
}

Each hunk is independent: the engineer resolves one, deletes its markers, leaves the other hunk alone, and continues. Git will not consider the file resolved until every hunk’s markers are removed and git add has staged the result.

How the file is staged in the index

A conflicted file is staged in three slots at the same path. This is the three-stage index, and it is what makes git diff --base, git diff --ours, and git diff --theirs possible:

flowchart LR
    P["Path: terraform/iam/main.tf"] --> S1["Stage 1\nbase blob"]
    P --> S2["Stage 2\nours blob (HEAD)"]
    P --> S3["Stage 3\ntheirs blob (merged-in)"]
    S1 --> R["git diff --base"]
    S2 --> R2["git diff --ours\ngit diff --cached"]
    S3 --> R3["git diff --theirs"]
    R2 --> W["Working tree\ncontains markers"]

Stage 1 holds the merge base — the state of the file at the common ancestor. Stage 2 holds ours (the current branch’s version). Stage 3 holds theirs (the merged-in branch’s version). The four git diff queries against a conflicted file all use the same three-stage structure to produce different views of the same conflict:

# Diff between ours and base: what did the current branch change?
git diff --base terraform/iam/main.tf

# Diff between ours and ours (== 0), but it's actually the
# diff between the index/stage-2 and the working tree —
# i.e. what is different in the working tree right now
git diff terraform/iam/main.tf

# Diff between ours (stage 2) and the index — this is what
# has been staged already, i.e. nothing until `git add` runs
git diff --cached terraform/iam/main.tf

# Diff between ours and theirs
git diff --ours terraform/iam/main.tf

# Diff between base and theirs
git diff --theirs terraform/iam/main.tf

The full set of git diff outputs during a conflict reads as four complementary views:

  • git diff &lt;file&gt; — what is different in the working tree relative to the index (i.e. what the markers look like vs the staged ours version).
  • git diff --cached &lt;file&gt; — what is different between ours and the index’s currently-staged blob (empty until git add stages the resolution).
  • git diff --base &lt;file&gt; — what did each side change relative to the merge base.
  • git diff --ours &lt;file&gt; / git diff --theirs &lt;file&gt; — one side or the other vs the merge base.

What git status reports before and after git add

The state transitions are observable in git status:

# Before `git add` — the path is "unmerged"
git status
# Unmerged paths:
#   (use "git add <file>..." to mark resolution)
#         both modified:   terraform/iam/main.tf

# After the engineer edits the file, removes markers,
# and runs `git add` — the path is staged as resolved
git add terraform/iam/main.tf
git status
# Changes to be committed:
#         modified:   terraform/iam/main.tf

The phrase changes from “both modified” (under Unmerged paths) to “modified” (under Changes to be committed). The “Unmerged paths” header disappears entirely once all conflicts are staged. The merge commit can be created with git merge --continue (which is just git commit with the in-progress merge’s prepared message), and the conflict state — including the three-stage entries, the MERGE_HEAD file, and the MERGE_MSG — is cleared.

Production discipline

Three rules for working with conflict markers in a production workflow:

  1. Read the markers in the file, not in git status. The status output classifies the case (both modified, etc.); the markers themselves name the two branches involved and show both sides’ content. The combination is what the engineer needs to make the resolution decision.
  2. Always run git diff --check after editing, before git add. The check catches markers left behind by an oversight and ensures the resolved file is in the shape intended.
  3. Trust the three-stage index, not your editor. If a merge tool claims to have resolved the file but git status still shows the path as “unmerged”, the resolution has not been staged — run git add &lt;file&gt; to commit the resolution into the index, then git status will report the path under “Changes to be committed”.

Cross-course references

  • Linux for Production Sysadmins — Part XXVIII (ChangeMgmt) covers manual file conflict resolution in package management; the marker pattern in Git is the same shape as dpkg’s conflict markers.
  • Ansible for Production Sysadmins — Part XXXVII (RepoArch) covers YAML conflict markers in inventory merges; the markers are identical to Git’s.
  • Terraform for Production Sysadmins — Part XIX (PR) discusses how PR review tools render conflict markers, and why a Terraform plan output that mentions a resource twice is the same signal as a conflict marker in the text.

Quiz

Knowledge check · 4 questions

  1. Q1. What does the `=======` line in a conflict marker block separate?

  2. Q2. A conflicted file occupies three slots at the same path in the index, holding the merge base, the ours version, and the theirs version. When `git add` runs on the file, the three slots collapse into one and the path is no longer reported as unmerged.

  3. Q3. Name the three commands — `git diff`, `git diff --cached`, `git diff --base` — and state what each shows during an unresolved conflict.

  4. Q4. A junior engineer resolves a conflict by deleting one side, saves the file, and runs `git add`. The team discovers that the file in production contains conflict markers. Diagnose the missed signal.

    After merging `feature/iam-rotation` into `main`, a junior engineer opens `terraform/iam/main.tf`, sees two conflict blocks, deletes the theirs block from each, leaves the ours block as the resolution, saves, and runs `git add terraform/iam/main.tf` followed by `git commit`. CI passes. Two weeks later, a `terraform plan` in production fails with a parse error mentioning `<<<<<<< HEAD`. The team has to figure out how the markers survived.

Passing score: 75%. Answers are checked in this browser.