Skip to main content
RunBook Academy

Git, CI/CD & GitOpsX · Merge ConflictsConflicts

Resolving by hand — opening files, reading hunks, choosing sides

Intermediate⏱ ~22 mingit

What you'll learn

  • Open a conflicted file and read each marker block to understand both sides intent
  • Express four resolution shapes: pick ours, pick theirs, pick both, pick neither (new combination)
  • Use `git checkout --ours <file>` and `git checkout --theirs <file>` to express the simple cases
  • Verify a manual resolution with `git diff` and `git diff --check` before committing
  • Complete the merge with `git merge --continue` once every path is resolved

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.

Most conflicts in an infrastructure repository are resolved by hand. The merge tool is a convenience for visual layout, but the decision — what should the combined file say — is always the engineer’s. A manual resolution is four things: read the markers, decide which sides (or which new combination) the final file should contain, write that content without markers into the working tree, and verify the result. The four resolution shapes cover every case.

Step 1 — open the conflicted file

The first step is to find the conflicted paths and open them in the editor of choice. git status lists them; git diff --name-only --diff-filter=U gives a scriptable view:

git status
# Unmerged paths:
#         both modified:   terraform/iam/main.tf

git diff --name-only --diff-filter=U
# terraform/iam/main.tf

# Open the file in the editor
$EDITOR terraform/iam/main.tf

The file on disk contains the conflict markers and the interleaved ours/theirs content. The engineer’s job is to replace that with the intended combined content.

Step 2 — read each marker block

A Terraform IAM file mid-conflict typically looks like the following. Read each block as “what did ours change, what did theirs change, and what should the final file say”:

resource "aws_iam_role_policy" "deploy" {
  name = "deploy"
  role = aws_iam_role.deploy.id

  policy = jsonencode({
<<<<<<< HEAD
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = "arn:aws:s3:::prod-config/*"
    }]
  })
=======
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject", "s3:PutObject"]
      Resource = "arn:aws:s3:::prod-config/*"
    }]
  })
>>>>>>> feature/iam-rotation
}

Reading the block: ours (the current branch, HEAD) gave the role read-only access to the config bucket; theirs (the merged-in branch) gave it read-write access. The two statements are textually different — a single line differs — but the resolution question is not “which line wins”. The resolution question is “should the deploy role be able to write to the config bucket?” That question is answered only by understanding the system, not by reading the lines.

Step 3 — write the resolution

There are four shapes a resolution can take, and the engineer expresses each one differently in the file:

Pick ours. Replace the entire marker block (including the <<<<<<<, =======, and >>>>>>> lines and both sides’ content) with the ours side content alone:

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["s3:GetObject"]
      Resource = "arn:aws:s3:::prod-config/*"
    }]
  })

There is a shortcut: git checkout --ours &lt;file&gt; replaces the entire file’s contents with the ours side, resolving every hunk at once. But this shortcut is only safe when the entire file’s conflict is “ours” — for a file with multiple hunks where some should be theirs and some should be ours, hand- editing is mandatory.

Pick theirs. Same idea, but the theirs side. The shortcut is git checkout --theirs &lt;file&gt;. Same caveat: only safe when the entire file’s resolution is theirs.

Pick both. Combine the two sides’ content, preserving the context around them. This is the most common case in a configuration file where both branches added different entries to a list. The marker block is replaced with both halves:

<<<<<<< HEAD
    Action   = ["s3:GetObject"]
=======
    Action   = ["s3:GetObject", "s3:PutObject"]
>>>>>>> feature/iam-rotation

…is resolved as:

    Action   = ["s3:GetObject", "s3:PutObject"]

Pick neither / new combination. Replace the marker block with content that is not literally either side. This is the case when neither side is correct and the engineer is introducing a value that resolves the underlying question. In the IAM example, if both sides were wrong (the role should be read-write but only against a specific prefix), the resolution is a new string that neither side wrote.

Step 4 — verify and stage

Once the marker block has been replaced, the file should be verified. Three checks:

# 1. The file no longer contains conflict markers
git diff --check terraform/iam/main.tf

# 2. The diff against the index shows the intended resolution
git diff --cached terraform/iam/main.tf

# 3. The file is syntactically valid (e.g. terraform fmt -check)
terraform fmt -check terraform/iam/main.tf

# Stage the resolution
git add terraform/iam/main.tf

# Confirm `git status` no longer reports the file as unmerged
git status
# Changes to be committed:
#         modified:   terraform/iam/main.tf

The transition from Unmerged paths to Changes to be committed is the per-file resolution confirmation. Until that transition happens, the file is still in conflict.

Step 5 — complete the merge

Once every conflicted path is staged, the merge can be completed:

# Review the prepared merge commit message
git status
# All conflicts fixed but you are still merging.
#   (use "git commit" to conclude merge)

# Open the editor to confirm or edit the message
# (the default is "Merge branch 'feature/iam-rotation'")

git merge --continue
# This is equivalent to `git commit` with the merge state cleared

git merge --continue is shorthand for finishing the in- progress merge: it runs git commit with the prepared MERGE_MSG and then clears the merge state files (MERGE_HEAD, MERGE_MSG). It only works when there are no remaining unmerged paths; if any remain, it refuses to run.

Step 6 — verify the merge commit

The merge commit is a normal commit — but with two parents. Verify:

# Show the merge commit's parents
git log -1 --format="%H %P %s"
# <merge-oid> <parent1-oid> <parent2-oid> Merge branch 'feature/iam-rotation'

# Show the changed files
git show --stat HEAD
# terraform/iam/main.tf | 12 +++++++++---
#  1 file changed, 9 insertions(+), 3 deletions(-)

The two parents should be the pre-merge tip of the current branch and the tip of the merged-in branch, in that order. A single-parent commit during what was supposed to be a merge indicates the merge was lost (the engineer may have used git reset or git commit --amend by mistake).

Production discipline

Three rules for hand-resolving conflicts in a production workflow:

  1. Read the markers, then close the file and think. The markers show the lines; the resolution is the answer to a system question. Choose deliberately, not by line- counting.
  2. Verify with the project’s own tools. A Terraform resolution should pass terraform fmt -check and a terraform validate. An Ansible resolution should pass ansible-playbook --syntax-check. The conflict is resolved at the Git level; the project-level validity is a separate check that only the project’s tools can do.
  3. Run git diff --check before git add. The check catches markers left behind. It is a one-line addition to every conflict resolution and catches an entire class of “I forgot to remove the markers” production incidents.

Cross-course references

  • Linux for Production Sysadmins — Part XXVIII (ChangeMgmt) covers manual file conflict resolution in package management; the four-shape model is the same.
  • Ansible for Production Sysadmins — Part XXXVII (RepoArch) covers hand resolution of inventory file conflicts; the verification step includes an ansible-inventory --graph test.
  • Terraform for Production Sysadmins — Part XIX (PR) discusses hand resolution of .tf conflicts and the role of terraform plan output in verifying the resolution.

Quiz

Knowledge check · 4 questions

  1. Q1. An engineer wants to resolve a conflict in which the entire file's resolution is the theirs side. What is the right command?

  2. Q2. `git checkout --ours &lt;file&gt;` and `git checkout --theirs &lt;file&gt;` operate at file granularity, not at individual hunk granularity.

  3. Q3. Name the four resolution shapes and the command that completes an in-progress merge once every conflicted path has been staged.

  4. Q4. Walk through the hand resolution of a multi-hunk IAM conflict and the verification steps.

    An engineer merges `feature/iam-rotation` into `main`. The file `terraform/iam/main.tf` has two conflict blocks: hunk A is a `policy = jsonencode({...})` block where ours says read-only s3:GetObject and theirs says read-write s3:GetObject + s3:PutObject; hunk B is a `versioning { enabled = ... }` block where ours says `enabled = true` and theirs says `enabled = false`. The deploy pipeline requires write access; the team's policy is to keep versioning enabled.

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