Git, CI/CD & GitOpsX · Merge ConflictsConflicts
Resolving by hand — opening files, reading hunks, choosing sides
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
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 <file> 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 <file>. 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:
- 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.
- Verify with the project’s own tools. A Terraform
resolution should pass
terraform fmt -checkand aterraform validate. An Ansible resolution should passansible-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. - Run
git diff --checkbeforegit 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 --graphtest. - Terraform for Production Sysadmins — Part XIX (PR)
discusses hand resolution of
.tfconflicts and the role ofterraform planoutput in verifying the resolution.
Quiz
Knowledge check · 4 questions
Q1. An engineer wants to resolve a conflict in which the entire file's resolution is the theirs side. What is the right command?
Q2. `git checkout --ours <file>` and `git checkout --theirs <file>` operate at file granularity, not at individual hunk granularity.
Q3. Name the four resolution shapes and the command that completes an in-progress merge once every conflicted path has been staged.
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.