Resolve merge conflicts (non-destructively)
Resolve git conflicts so that both sides' intent survives, verify the result,
and stop and ask a human the moment a safe resolution isn't obvious. The git
mechanics are the easy part; the danger is silently dropping someone's work — a
blind "take theirs," a clean-looking textual merge that's logically broken, or a
force-push over a teammate's commits.
Command recipes live in reference/playbook.md (load on
demand). This body is the decision rules.
First: rebase or merge?
You update a feature/PR branch against its base one of two ways. Either is fine —
pick by context, not dogma:
|
git rebase origin/main |
git merge origin/main |
| History |
linear (replays your commits) |
adds a merge commit |
| Conflicts |
may re-hit the same conflict per replayed commit (turn on rerere) |
resolve once |
| Push after |
--force-with-lease (rewrites your branch) |
normal push |
| Best for |
your own unshared PR branch |
a branch others also commit to |
In a squash-merge repo (see
git-trunk-branch-and-pr-automation)
the feature branch's history is flattened at merge anyway, so a merge commit on the
branch is harmless — when a rebase keeps re-conflicting, merge instead and
resolve once. Never rebase a branch other people are actively committing to.
The non-negotiable safety rules
- Never blind-resolve a whole side.
-X ours / -X theirs (strategy options)
and git checkout --ours/--theirs <file> take one side wholesale and throw the
other away silently. They're only correct for files you will regenerate (e.g.
lockfiles) or a file that genuinely should be one side entirely — never as a
"make the markers go away" shortcut.
- Resolve hunk-by-hunk, preserving both intents. A conflict means both sides
changed the same region. The correct result is usually neither side verbatim
but a combination. Turn on
diff3/zdiff3 so you can see the common ancestor
and reason about what each side changed from (recipe in the playbook).
- Know the rebase ours/theirs inversion. During
git merge, ours=your branch,
theirs=incoming. During git rebase, it's flipped: ours=the branch you're
replaying onto (e.g. main), theirs=your commits. Acting on the wrong one
discards exactly the work you meant to keep.
- Keep the escape hatch ready.
git merge --abort / git rebase --abort /
git cherry-pick --abort returns you to a clean pre-conflict state. If unsure,
abort and reassess rather than push a half-resolved tree. git reflog +
ORIG_HEAD recover from a bad finish.
- Force-push only
--force-with-lease, only your own PR branch. Plain
--force clobbers teammate commits pushed since your last fetch; --with-lease
refuses if the remote moved.
- Never commit conflict markers. Run
git diff --check and
git diff --cached --check (staged markers won't show in the unstaged diff after
git add), plus a grep for <<<<<<<, before --continue/commit.
The loop
1. UNDERSTAND → what's conflicting + why (mergeable status, conflict list, diff3)
2. CLASSIFY → per file: content · rename/delete · lockfile/generated · binary · semantic
3. RESOLVE → hunk-by-hunk, both intents; regenerate (don't hand-merge) lockfiles
4. VERIFY → no markers; build + tests; diff sanity (no unrelated lines vanished)
5. CONTINUE → git add; --continue / commit; push (--force-with-lease if rebased)
│
└─ at ANY step, if a safe resolution isn't clear → ABORT or stop → HAND OFF
Conflict types — and how each is resolved
- Content (same lines changed) → the default: merge both intents by hand.
- Rename/delete & add/add → decide whether the file should exist and where; don't
let one side's deletion silently win. (
git status labels these.)
- Lockfiles (
package-lock.json, pnpm-lock.yaml, yarn.lock, Cargo.lock) →
do not hand-merge. Take one side, then regenerate from the merged manifest
(npm install --package-lock-only / pnpm install --lockfile-only /
cargo generate-lockfile / etc.) so the lock matches its resolved manifest
(package.json, Cargo.toml, …). This is the legitimate use of "take one side."
- Generated/build output → regenerate from source, never merge by hand.
- Binary → you can't merge text; pick a side deliberately or escalate.
- Semantic → the file merges with zero markers but is logically broken (one
side renamed a function the other side calls). Textual success ≠ correctness —
only build + tests catch these.
Escalate to a human when…
Hand off (with context) rather than guess when:
- The conflict is a genuine design clash — both sides deliberately changed the
same logic in incompatible ways; only the authors can say which intent wins.
- It's in code whose intent you can't determine, or in migrations / schema /
data files where a wrong merge corrupts state.
- A lockfile regeneration pulls in unexpected dependency changes.
- The branch is from a fork / untrusted PR — you can't safely run its checked-out
code or push to it (consistent with the fork-safety rule in
autonomous-pr-driver). Validate via API; don't
resolve locally.
- A rebase keeps re-conflicting across many commits, or the conflict spans many files
beyond what you can verify.
How to hand off: state what files conflict, what you tried, and why you
stopped — then leave the branch in a clean (aborted) state, or push the partial
resolution to a clearly-labelled branch and ask. Same posture as the PR driver's
never-self-merge: when in doubt, a human decides.
Verify (before you call it done)
git diff --check → no leftover conflict markers / whitespace errors.
- Build + run the tests — the only thing that catches semantic conflicts.
- Diff sanity: compare against both parents; confirm no unrelated lines were
dropped during the resolution (the silent-data-loss failure mode).
- For a rebased PR branch, confirm the final range is what you expect before
--force-with-lease (recipe in the playbook).
See also
Sources
1---2name: resolve-merge-conflicts3description: Resolve git merge/rebase conflicts non-destructively — preserving the intent of BOTH sides — and hand off to a human when a conflict can't be resolved safely. Use when a branch or PR has conflicts with its base ('this branch has conflicts that must be resolved', GitHub mergeable=CONFLICTING / mergeStateStatus=DIRTY), when a rebase / merge / cherry-pick / stash / revert stops with conflict markers (<<<<<<< ======= >>>>>>>), when a PR is behind base and needs updating, when syncing a long-lived branch or back-merging a hotfix, or when deciding rebase vs merge. Covers the safe-resolution rules (no blind -X ours/theirs, hunk-by-hunk with diff3, abort/reflog escape hatches, verify-after), the rebase ours/theirs inversion, lockfile/rename/semantic conflicts, and the escalate-when-stuck criteria.4---56# Resolve merge conflicts (non-destructively)78Resolve git conflicts so that **both sides' intent survives**, verify the result,9and **stop and ask a human** the moment a safe resolution isn't obvious. The git10mechanics are the easy part; the danger is *silently dropping someone's work* — a11blind "take theirs," a clean-looking textual merge that's logically broken, or a12force-push over a teammate's commits.1314Command recipes live in [`reference/playbook.md`](./reference/playbook.md) (load on15demand). This body is the decision rules.1617## First: rebase or merge?1819You update a feature/PR branch against its base one of two ways. Either is fine —20pick by context, not dogma:2122| | `git rebase origin/main` | `git merge origin/main` |23| --- | --- | --- |24| History | linear (replays your commits) | adds a merge commit |25| Conflicts | may re-hit the **same** conflict per replayed commit (turn on `rerere`) | resolve **once** |26| Push after | `--force-with-lease` (rewrites your branch) | normal push |27| Best for | your **own** unshared PR branch | a branch **others** also commit to |2829In a **squash-merge** repo (see30[`git-trunk-branch-and-pr-automation`](../git-trunk-branch-and-pr-automation/SKILL.md))31the feature branch's history is flattened at merge anyway, so a merge commit on the32branch is harmless — when a rebase keeps re-conflicting, **merge instead** and33resolve once. **Never rebase a branch other people are actively committing to.**3435## The non-negotiable safety rules36371. **Never blind-resolve a whole side.** `-X ours` / `-X theirs` (strategy options)38 and `git checkout --ours/--theirs <file>` take one side *wholesale* and throw the39 other away silently. They're only correct for files you will **regenerate** (e.g.40 lockfiles) or a file that genuinely should be one side entirely — never as a41 "make the markers go away" shortcut.422. **Resolve hunk-by-hunk, preserving both intents.** A conflict means both sides43 changed the same region. The correct result is usually **neither** side verbatim44 but a combination. Turn on **`diff3`/`zdiff3`** so you can see the common ancestor45 and reason about what *each* side changed *from* (recipe in the playbook).463. **Know the rebase ours/theirs inversion.** During `git merge`, `ours`=your branch,47 `theirs`=incoming. During `git rebase`, it's **flipped**: `ours`=the branch you're48 replaying *onto* (e.g. `main`), `theirs`=*your* commits. Acting on the wrong one49 discards exactly the work you meant to keep.504. **Keep the escape hatch ready.** `git merge --abort` / `git rebase --abort` /51 `git cherry-pick --abort` returns you to a clean pre-conflict state. If unsure,52 **abort and reassess** rather than push a half-resolved tree. `git reflog` +53 `ORIG_HEAD` recover from a bad finish.545. **Force-push only `--force-with-lease`, only your own PR branch.** Plain55 `--force` clobbers teammate commits pushed since your last fetch; `--with-lease`56 refuses if the remote moved.576. **Never commit conflict markers.** Run `git diff --check` **and**58 `git diff --cached --check` (staged markers won't show in the unstaged diff after59 `git add`), plus a grep for `<<<<<<<`, before `--continue`/commit.6061## The loop6263```text641. UNDERSTAND → what's conflicting + why (mergeable status, conflict list, diff3)652. CLASSIFY → per file: content · rename/delete · lockfile/generated · binary · semantic663. RESOLVE → hunk-by-hunk, both intents; regenerate (don't hand-merge) lockfiles674. VERIFY → no markers; build + tests; diff sanity (no unrelated lines vanished)685. CONTINUE → git add; --continue / commit; push (--force-with-lease if rebased)69 │70 └─ at ANY step, if a safe resolution isn't clear → ABORT or stop → HAND OFF71```7273## Conflict types — and how each is resolved7475- **Content** (same lines changed) → the default: merge both intents by hand.76- **Rename/delete & add/add** → decide whether the file should exist and where; don't77 let one side's deletion silently win. (`git status` labels these.)78- **Lockfiles** (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `Cargo.lock`) →79 **do not hand-merge.** Take one side, then **regenerate** from the merged manifest80 (`npm install --package-lock-only` / `pnpm install --lockfile-only` /81 `cargo generate-lockfile` / etc.) so the lock matches its resolved manifest82 (`package.json`, `Cargo.toml`, …). This is the *legitimate* use of "take one side."83- **Generated/build output** → regenerate from source, never merge by hand.84- **Binary** → you can't merge text; pick a side deliberately or escalate.85- **Semantic** → the file merges with **zero markers** but is logically broken (one86 side renamed a function the other side calls). Textual success ≠ correctness —87 **only build + tests catch these.**8889## Escalate to a human when…9091Hand off (with context) rather than guess when:9293- The conflict is a **genuine design clash** — both sides deliberately changed the94 same logic in incompatible ways; only the authors can say which intent wins.95- It's in code whose **intent you can't determine**, or in **migrations / schema /96 data** files where a wrong merge corrupts state.97- A **lockfile regeneration** pulls in unexpected dependency changes.98- The branch is from a **fork / untrusted PR** — you can't safely run its checked-out99 code or push to it (consistent with the fork-safety rule in100 [`autonomous-pr-driver`](../autonomous-pr-driver/SKILL.md)). Validate via API; don't101 resolve locally.102- A rebase keeps re-conflicting across many commits, or the conflict spans many files103 beyond what you can verify.104105**How to hand off:** state *what* files conflict, *what* you tried, and *why* you106stopped — then leave the branch in a clean (aborted) state, or push the partial107resolution to a clearly-labelled branch and ask. Same posture as the PR driver's108**never-self-merge**: when in doubt, a human decides.109110## Verify (before you call it done)111112- `git diff --check` → no leftover conflict markers / whitespace errors.113- **Build + run the tests** — the only thing that catches semantic conflicts.114- **Diff sanity**: compare against both parents; confirm no *unrelated* lines were115 dropped during the resolution (the silent-data-loss failure mode).116- For a rebased PR branch, confirm the final range is what you expect before117 `--force-with-lease` (recipe in the playbook).118119## See also120121- [`autonomous-pr-driver`](../autonomous-pr-driver/SKILL.md) — drives a PR to122 merge-ready; delegates here when a PR is behind base / `mergeable=CONFLICTING`.123- [`git-trunk-branch-and-pr-automation`](../git-trunk-branch-and-pr-automation/SKILL.md)124 — owns the branch/rebase/squash-merge mechanics this works within.125- [`reference/playbook.md`](./reference/playbook.md) — the `git`/`gh` command recipes.126127## Sources128129- Git docs: `git-merge` / `git-rebase` (ours vs theirs, `--abort`), `git-rerere`,130 `merge.conflictStyle` (`diff3`/`zdiff3`), `git diff --check`.131 <https://git-scm.com/docs/git-merge>, <https://git-scm.com/docs/git-rerere>132- GitHub PR mergeability fields (`mergeable`, `mergeStateStatus` = `DIRTY` on133 conflict): <https://docs.github.com/en/graphql/reference/enums#mergestatestatus>