Git Merge Conflicts

Quote

“You can disagree with me as much as you want, but during this talk, by definition, anybody who disagrees is stupid and ugly.”

Linus Torvalds, Git mailing list

Conceptual Model

Before working with conflicts, understand the three-layer structure Git uses when merging two branches.


flowchart TD
    A["Two branches diverge from<br/>common ancestor"] --> B{"Both modified<br/>same lines?"}
    B -->|NO| C["Auto-merge succeeds<br/>No conflict"]
    B -->|YES| D["CONFLICT<br/>Git writes markers to file"]
    D --> E["Index stores 3 stages:<br/>ancestor / ours / theirs"]
    E --> F["Developer edits file<br/>removes markers"]
    F --> G["git add marks resolution<br/>collapses to stage 0"]
    G --> H{"All files<br/>resolved?"}
    H -->|NO| F
    H -->|YES| I["git commit / rebase --continue<br/>completes the operation"]

    style C fill:#1f3b2d,stroke:#73d13d,color:#c0caf5
    style D fill:#4a1f24,stroke:#db4b4b,color:#c0caf5

Git performs a three-way merge by comparing each file across three versions: the common ancestor (the last commit shared by both branches), ours (HEAD), and theirs (the incoming branch). If only one side changed a region, Git takes that change automatically. If both sides changed the same region differently, Git declares a conflict: it writes both versions into the file separated by conflict markers, stores all three versions in the index as stages 1–3, and pauses the operation. The developer must edit the file to produce the correct final version, run git add to collapse the three stages back to stage 0, and then complete the merge or rebase.

Why conflicts happen

Git’s three-way merge algorithm compares each line of a file across three versions. The comparison logic:

AncestorOursTheirsResult
AAANo change — keep A
ABAOnly ours changed — take B
AACOnly theirs changed — take C
ABCBoth changed differently — CONFLICT
ABBBoth changed the same way — take B (no conflict)

A conflict occurs only in the fourth case: both branches modified the same region, and the modifications differ. If both branches made identical changes to the same line, Git accepts them without conflict.

Conflict types

Not all conflicts involve text content. Git detects several conflict categories:

Conflict typeCauseMarker behavior
ContentBoth branches edit the same lines of the same fileStandard <<<<<<< / ======= / >>>>>>> markers inserted
BinaryBoth branches modify a binary file (images, Parquet, compiled assets)No markers — Git reports CONFLICT (binary) and you must choose one entire version
Rename/deleteOne branch renames a file, the other deletes itGit reports CONFLICT (rename/delete) and stages the renamed version for you to keep or remove
Rename/renameBoth branches rename the same file to different namesGit reports CONFLICT (rename/rename) and you choose which name to keep
ModeOne branch changes file permissions (e.g., adds execute bit) while the other modifies contentUsually auto-resolved, but can conflict if both change mode differently
Add/addBoth branches create a new file with the same name but different contentStandard content markers inserted into the file

Anatomy of a Conflict

A conflict arises when two branches diverge from a common ancestor and each modifies the same region of a file. Git detects the overlap at merge time and cannot decide which version to keep without your input.


gitGraph TB:
  commit id: "A"
  commit id: "B"
  branch feature
  commit id: "C" type: REVERSE
  checkout main
  commit id: "D" type: REVERSE

Main and feature both descend from commit B (the common ancestor). Commit C on feature changed CACHE_TTL to 60. Commit D on main changed CACHE_TTL to 300. Both modified the same line of config.py with different values (marked red), so Git cannot auto-merge — a content conflict is declared when the branches are merged.

Conflict markers | reading HEAD vs incoming

When Git encounters a content conflict, it edits the affected file and inserts conflict markers that divide the region into two competing versions:

<<<<<<< HEAD
CACHE_TTL = 300
=======
CACHE_TTL = 60
>>>>>>> demo/conflict-merge
MarkerMeaning
<<<<<<< HEADStart of the current branch’s version (the branch you are on)
=======Divider between the two versions
>>>>>>> demo/conflict-mergeEnd of the incoming branch’s version (the branch being merged in)

Everything between <<<<<<< HEAD and ======= is what your current branch (HEAD) has. Everything between ======= and >>>>>>> demo/conflict-merge is what the incoming branch has. Git is saying: “These two versions disagree. You decide which one to keep.”

Conflict Marker Labels Vary by Operation

The marker labels change depending on which Git operation triggered the conflict:

  • git merge: HEAD = your branch, >>>>>>> branch-name = the branch being merged in
  • git rebase: HEAD = the target branch (the base), >>>>>>> commit-sha (message) = your commit being replayed
  • git cherry-pick: HEAD = your current branch, >>>>>>> commit-sha (message) = the commit being cherry-picked
  • git stash pop: Updated upstream = the current branch state, Stashed changes = your stashed work

Index stages | three-way merge internals

During a conflict, Git’s index (the staging area) stores three versions of each conflicted file instead of the normal single version. These are called stages:

Inspect the three index stages during a conflict

After Git reports a conflict and before you begin editing. It is typically triggered by you want to understand exactly what each side contributed before deciding how to resolve. Runs in any Git shell. Read-only — inspects the index without modifying anything. See the exact blob SHA and stage number for each version of the conflicted file.

List the unmerged index entries showing all three stages for each conflicted file.

git ls-files -u
100644 dea4f7f403ce202a3dd9e76b591b94bf1154db77 1	config.py
100644 da8be79cdf68e858e1678348f3674e82fe04b149 2	config.py
100644 9e9763108f59917010c22648528b01c150e463db 3	config.py
StageLabelContent (this example)
1Common ancestorCACHE_TTL = 120 — the value before either branch changed it
2Ours (HEAD)CACHE_TTL = 300 — what the current branch set it to
3Theirs (incoming)CACHE_TTL = 60 — what the incoming branch set it to

You can view the content at any stage using git show :N:filename:

Show the content of config.py at each of the three conflict stages.

git show :1:config.py    # stage 1 — common ancestor
git show :2:config.py    # stage 2 — ours (HEAD)
git show :3:config.py    # stage 3 — theirs (incoming)
# Stage 1 (common ancestor):
CACHE_TTL = 120
 
# Stage 2 (ours / HEAD):
CACHE_TTL = 300
 
# Stage 3 (theirs / incoming):
CACHE_TTL = 60

When you run git add on a resolved file, Git removes stages 1–3 and writes the file to stage 0 (the normal staging state). The conflict is resolved for that file.

Merge Conflict Resolution

This section covers the complete workflow for resolving conflicts during a standard git merge operation.

git merge | detect and resolve content conflicts

The most common conflict scenario: merging a feature branch into main when both branches modified the same file.

Trigger the merge and observe the conflict

When integrating a feature branch into the target branch. It is typically triggered by running git merge when both branches have diverged and modified the same file regions. Local operation. The merge pauses without creating a commit. Working directory and index enter the “merging” state. Combine the changes from two branches into a single branch.

Merge the demo/conflict-merge branch into main, triggering a content conflict in config.py.

git merge demo/conflict-merge
Auto-merging config.py
CONFLICT (content): Merge conflict in config.py
Automatic merge failed; fix conflicts and then commit the result.

Git tried to auto-merge config.py but found that both branches changed the CACHE_TTL line differently. The merge is now paused — the working directory contains conflict markers, and the index holds all three stages.

Identify conflicted files

Immediately after Git reports a conflict. It is typically triggered by the “Automatic merge failed” message. Read-only status check. See exactly which files need resolution and confirm the repository is in a merge state.

List all files with unresolved conflicts.

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:
  (use "git add <file>..." to mark resolution)
	both modified:   config.py
 
no changes added to commit (use "git add" and/or "git commit -a")

Files listed under “Unmerged paths” with both modified are the ones that need manual resolution. The status also shows the two available escape routes: fix and commit, or abort.

Examine the conflict markers

Open the conflicted file to see the two competing versions.

cat config.py
<<<<<<< HEAD
CACHE_TTL = 300
=======
CACHE_TTL = 60
>>>>>>> demo/conflict-merge
MAX_CONNECTIONS = 10
RETRY_COUNT = 3
TIMEOUT = 30
LOG_LEVEL = "INFO"

The conflict is isolated to a single line: HEAD (main) has CACHE_TTL = 300 and the incoming branch has CACHE_TTL = 60. The remaining lines are identical on both branches and appear cleanly below the conflict region.

View the combined diff during a conflict

Show the combined diff that Git computed, with dual-column change indicators.

git diff
diff --cc config.py
index da8be79,9e97631..0000000
--- a/config.py
+++ b/config.py
@@@ -1,4 -1,4 +1,8 @@@
++<<<<<<< HEAD
 +CACHE_TTL = 300
++=======
+ CACHE_TTL = 60
++>>>>>>> demo/conflict-merge
  MAX_CONNECTIONS = 10
  RETRY_COUNT = 3
  TIMEOUT = 30

The diff --cc format uses two columns of +/- markers (one per parent). Lines prefixed with ++ are conflict markers that Git added. Lines with a single + in one column show the content from that parent.

Resolve the conflict and complete the merge

After editing the file to contain the correct final version with all markers removed. It is typically triggered by you have decided which version (or combination) to keep. git add moves the file from stages 1–3 to stage 0. git commit creates the merge commit. Finalize the merge by recording the resolved state.

Edit the file to remove all conflict markers and keep the desired value:

Stage the resolved file and create the merge commit.

git add config.py
git commit -m "merge: resolve config.py conflict — keep production TTL of 300s"
[main 4bc7c52] merge: resolve config.py conflict — keep production TTL of 300s

Remove All Conflict Markers Before Staging

If you stage a file that still contains <<<<<<< markers, Git will accept the commit — but the file will be broken. Always verify the file is clean before running git add. Search for remaining markers with: grep -rn "<<<<<<" .

Verify Clean Resolution

After staging, confirm no markers remain:

grep -rn "<<<<<<" .

If the command returns no output, all markers have been removed.

git merge | resolve with —ours or —theirs

When you know in advance that one entire version of a file is correct and the other should be discarded, you can bypass manual editing by checking out one side directly.

Accept the current branch version for a specific file

During an active merge conflict when you want to keep your branch’s version of a file wholesale. It is typically triggered by the incoming branch’s changes to this file are not wanted or are superseded by yours. Replaces the conflicted working-tree file with the stage-2 (ours) version. You still need to git add afterward. Quickly resolve a single file by choosing one side without manual editing.

Replace the conflicted file with the current branch (HEAD) version.

git checkout --ours pipeline_config.py

Verify the file contains the HEAD version.

cat pipeline_config.py
CACHE_TTL = 300
MAX_CONNECTIONS = 50
RETRY_COUNT = 3
TIMEOUT = 60
LOG_LEVEL = "WARNING"

Accept the incoming branch version for a specific file

Replace the conflicted file with the incoming branch version.

git checkout --theirs pipeline_config.py

Verify the file contains the incoming version.

cat pipeline_config.py
CACHE_TTL = 300
MAX_CONNECTIONS = 50
RETRY_COUNT = 3
TIMEOUT = 45
LOG_LEVEL = "DEBUG"

After choosing one side, stage and commit:

git add pipeline_config.py
git commit -m "merge: keep main's log level and timeout"

--ours and --theirs Swap Meaning During Rebase

During git merge: --ours = your branch (HEAD), --theirs = the branch being merged in. During git rebase: --ours = the branch you are rebasing onto (the base), --theirs = your commits being replayed.

This reversal is the single most common source of confusion when resolving rebase conflicts. The reason: during a rebase, Git checks out the target base first (making it HEAD/ours), then replays your commits on top (making them theirs).

Mnemonic for Rebase --ours/--theirs

During rebase, think from Git’s perspective: Git is sitting on the base branch and applying your commits as patches. So “ours” = the base it is sitting on, and “theirs” = the patches being applied.

FlagSyntaxDescription
--oursgit checkout --ours <file>Replace the conflicted file with the stage-2 (current branch / HEAD) version
--theirsgit checkout --theirs <file>Replace the conflicted file with the stage-3 (incoming branch) version

git merge | rename/delete conflicts

A rename/delete conflict occurs when one branch renames a file while the other branch deletes it. Git cannot determine whether to keep the renamed version or honor the deletion.

Detect and resolve a rename/delete conflict

When merging a branch that renamed a file against a branch that deleted it. It is typically triggered by git reports CONFLICT (rename/delete) during merge. The renamed file appears in the working directory. The index marks it as unmerged with deleted by us or deleted by them. Decide whether the file should exist (under its new name) or be removed.

Merge a branch that renamed config.py to pipeline_config.py into main, where config.py was deleted.

git merge demo/conflict-rename
CONFLICT (rename/delete): config.py renamed to pipeline_config.py in demo/conflict-rename, but deleted in HEAD.
Automatic merge failed; fix conflicts and then commit the result.

Check the conflict status.

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:
  (use "git add/rm <file>..." as appropriate to mark resolution)
	deleted by us:   pipeline_config.py
 
no changes added to commit (use "git add" and/or "git commit-a")

To keep the renamed file:

git add pipeline_config.py
git commit -m "merge: keep renamed pipeline_config.py from feature branch"

To honor the deletion instead:

git rm pipeline_config.py
git commit -m "merge: remove pipeline_config.py per main branch deletion"

Binary Conflicts Have No Markers

Merge conflicts in binary files (images, Parquet, compiled assets, Excel files) show as CONFLICT (binary) with no conflict markers to edit. You cannot manually merge binary content — you must pick one entire version using git checkout --ours <file> or git checkout --theirs <file>, then git add <file>.

Resolve Binary Conflicts

Pick the correct version explicitly, then stage it:

git checkout --ours path/to/file.parquet
git add path/to/file.parquet

Aborting a Merge

When conflicts are too complex to resolve immediately, or when you realize your merge strategy was wrong, cancel the entire operation and return to a clean state.

git merge —abort | cancel and restore pre-merge state

Abort an in-progress merge

When you have started a merge that produced conflicts and want to cancel it entirely. It is typically triggered by conflicts are too numerous or complex, or you want to rebase instead of merge, or you need to coordinate with the teammate who made the conflicting changes first. Restores the working directory and index to the exact state before git merge was run. Completely safe — no data is lost. Return to a clean pre-merge state so you can choose a different approach.

Abort the in-progress merge and restore the pre-merge state.

git merge --abort

Verify the repository is clean.

git status
On branch main
nothing added to commit but untracked files present (use "git add" to track)

The working directory is exactly as it was before the merge started. No conflict markers, no staged changes, no merge state.

Abort Is Always Safe

git merge --abort is completely safe. It restores your working directory exactly to where it was before you ran git merge. No work is lost. The incoming branch is untouched. You can attempt the merge again at any time.

FlagSyntaxDescription
--abortgit merge --abortCancel the in-progress merge and restore the pre-merge state
--continuegit merge --continueResume a paused merge after all conflicts are resolved (equivalent to git commit)
--quitgit merge --quitAbandon the merge but leave the working directory as-is (partial resolution preserved)

Visual Merge Tools

Text-based conflict marker editing works for simple conflicts. For complex multi-file conflicts, a visual merge tool presents three panes — common ancestor, ours, theirs — and lets you build the resolution interactively.

git mergetool | open configured GUI for conflict resolution

git mergetool opens the merge editor configured in your Git config for each conflicted file in sequence. Without explicit configuration, Git attempts to find any available GUI diff tool on your system.

Open the visual merge tool for all conflicted files

During an active merge or rebase conflict when you prefer a GUI over editing markers manually. It is typically triggered by multiple files with complex conflicts, or conflicts involving rearranged code blocks where markers are hard to read. Opens an external application. Git prompts for each conflicted file in sequence. Resolve conflicts visually with side-by-side comparison of all three versions.

Launch the configured merge tool for every conflicted file.

git mergetool

Configure VS Code as the default merge tool

Once, as part of initial Git configuration. It is typically triggered by you want VS Code’s built-in three-way merge editor as your default conflict resolution tool. Writes to ~/.gitconfig (global). Applies to all repositories for the current user. Ensure git mergetool always opens VS Code.

Set VS Code as the global merge tool.

git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'

After this configuration, running git mergetool during any conflict opens VS Code with its built-in merge editor. VS Code displays inline action buttons above each conflict block:

  • Accept Current Change — keep the HEAD (your branch) version
  • Accept Incoming Change — keep the incoming branch’s version
  • Accept Both Changes — append both versions sequentially
  • Compare Changes — open a diff view showing both sides

VS Code Three-Way Merge Editor

VS Code 1.70+ includes a dedicated three-way merge editor (not just inline buttons). Open it via the command palette: “Merge Editor: Open Merge Editor”. It shows the common ancestor, ours, and theirs in three separate panes with a result pane at the bottom — identical to what professional merge tools like Beyond Compare or P4Merge provide.

FlagSyntaxDescription
--tool=<tool>git mergetool --tool=vimdiffOverride the configured tool for this invocation
-ygit mergetool -yDo not prompt before launching each file’s merge session
--no-promptgit mergetool --no-promptSame as -y — skip the per-file prompt

Rebase Conflict Resolution

Conflicts during git rebase work the same way mechanically, but the workflow differs from merge in two critical ways: (1) Git replays each commit individually, so conflicts can occur multiple times — once per conflicting commit; (2) the meaning of “ours” and “theirs” is reversed compared to merge.

git rebase | resolve conflicts during replay

Never Rebase Published Branches

Rebasing rewrites commit SHAs. If you rebase a branch that teammates have already pulled, their local history will diverge from the force-pushed remote — causing confusion and potential data loss.

Safe Rebase Pattern

Only rebase commits that have not been pushed to a shared remote, or on personal feature branches where you are the sole contributor. For shared branches, use git merge to incorporate upstream changes without rewriting history.

Before rebase:


gitGraph TB:
  commit id: "A"
  commit id: "B"
  branch feature
  commit id: "C" type: REVERSE
  checkout main
  commit id: "D"

Main and feature diverged at commit B. Commit C on feature changed retry_count to 5. Commit D on main changed timeout_seconds to 900. Both modified the same file (config.yaml) in the same region — when the feature branch is rebased onto main, Git replays commit C on top of D and encounters a conflict (C marked red).

After rebase:


gitGraph TB:
  commit id: "A"
  commit id: "B"
  commit id: "D"
  branch feature
  commit id: "C'" type: HIGHLIGHT

After rebase: Git first applied D (the new base), then replayed C as C’ (marked green) with a new SHA. The conflict was resolved by keeping both changes — retry_count = 5 from the feature branch and timeout_seconds = 900 from main. The result is a linear history where the feature work appears to have started after D.

Trigger a rebase conflict

When you need to replay your branch’s commits on top of an updated base branch. It is typically triggered by your branch has diverged from main and you want a linear history before merging. Rebase rewrites commit history — every replayed commit gets a new SHA. If your branch was already pushed, you will need --force-with-lease afterward. Create a clean linear history by replaying your commits on top of the latest base.

Rebase the feature branch onto the updated main.

git rebase main
Rebasing (1/1)
Auto-merging config.yaml
CONFLICT (content): Merge conflict in config.yaml
error: could not apply c90e250... ops: increase retry count to 5 for transient API failures
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply c90e250...

Check the rebase state.

git status
interactive rebase in progress; onto 04d20b1
Last command done (1 command done):
   pick c90e250 # ops: increase retry count to 5 for transient API failures
No commands remaining.
You are currently rebasing branch 'demo/conflict-rebase' on '04d20b1'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)
 
Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
	both modified:   config.yaml
 
no changes added to commit (use "git add" and/or "git commit -a")

The status shows the rebase progress (Rebasing 1/1), the exact commit being replayed (c90e250), and all three escape routes.

View the conflict markers in the file.

cat config.yaml
pipeline:
  name: stock-index-pipeline
  schedule: "0 18 * * 1-5"
<<<<<<< HEAD
  retry_count: 3
  timeout_seconds: 900
=======
  retry_count: 5
  timeout_seconds: 600
>>>>>>> c90e250 (ops: increase retry count to 5 for transient API failures)
  market_close_utc: "16:30"
 
sources:
  - name: euronext
    api: rest
    rate_limit: 100
  - name: yahoo_finance
    api: rest
    rate_limit: 50

During rebase, HEAD points to the base branch (main) — not your feature branch. The <<<<<<< HEAD section shows main’s values (retry_count: 3, timeout_seconds: 900). The >>>>>>> c90e250 section shows your commit’s values (retry_count: 5, timeout_seconds: 600).

The correct resolution keeps both changes — the higher retry count from the feature branch and the longer timeout from main:

  retry_count: 5
  timeout_seconds: 900

Resolve and continue the rebase

After editing the conflicted file to contain the correct final version. It is typically triggered by all conflict markers removed, file saved. git add marks resolution, git rebase --continue applies the resolution and moves to the next commit. Complete the current commit’s replay and proceed to the next one (if any).

Stage the resolved file and continue the rebase.

git add config.yaml
git rebase --continue
Successfully rebased and updated refs/heads/demo/conflict-rebase.

git rebase | escape hatches

When a rebase cannot proceed as planned, use these commands to cancel the entire operation or skip the current conflicting commit.

Abort the entire rebase

When the rebase conflicts are too complex, or you realize you should merge instead of rebase. It is typically triggered by you want to abandon the rebase entirely and return to the pre-rebase state. Completely safe. Restores the branch to its exact state before git rebase was run. Cancel the rebase without any side effects.

Cancel the rebase and restore the original branch state.

git rebase --abort

Skip the current commit during rebase

When the commit being replayed is entirely redundant — its changes were already incorporated into the base branch. It is typically triggered by the conflict exists only because the same change was already applied upstream. Permanently discards the current commit’s changes. The remaining commits continue to replay. Drop a redundant commit from the rebased history.

Skip the current conflicting commit and continue with the rest.

git rebase --skip

--skip Permanently Discards Changes

git rebase --skip permanently drops the current commit. Only use it when you are certain the commit’s changes are fully redundant with what already exists on the base branch. If in doubt, resolve the conflict manually instead.

Check Before Skipping

Before running --skip, verify the commit is truly redundant by comparing the diff:

git diff HEAD

If the diff shows only the content that is already on the base branch, the commit is safe to skip.

FlagSyntaxDescription
--continuegit rebase --continueResume the rebase after resolving conflicts in the current commit
--abortgit rebase --abortCancel the entire rebase and restore the pre-rebase branch state
--skipgit rebase --skipDiscard the current commit and continue replaying the rest
--quitgit rebase --quitAbandon the rebase but leave HEAD where it is (partial rebase preserved)

Cherry-Pick Conflict Resolution

Conflicts during git cherry-pick follow the same resolution mechanics as merge, but the three-way merge uses the cherry-picked commit’s parent as the common ancestor — not the merge base of the two branches.

git cherry-pick | resolve conflicts from single-commit application

Trigger a cherry-pick conflict

When applying a single commit from another branch and the target file has diverged. It is typically triggered by the cherry-picked commit modifies lines that were also changed on the current branch since the commit’s parent. Local operation. The cherry-pick pauses at the conflicting commit. Port a specific change from one branch to another.

Cherry-pick a version bump commit that conflicts with a hotfix already on main.

git cherry-pick 63fdcfa
Auto-merging src/__init__.py
CONFLICT (content): Merge conflict in src/__init__.py
error: could not apply 63fdcfa... feat: bump version to 2.1.0 for Q2 release
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".

View the conflict markers.

cat src/__init__.py
"""Stock index analytics pipeline."""
 
<<<<<<< HEAD
__version__ = "2.0.1"
=======
__version__ = "2.1.0"
>>>>>>> 63fdcfa (feat: bump version to 2.1.0 for Q2 release)
__author__ = "Data Engineering Team"

HEAD has 2.0.1 (the hotfix) and the cherry-picked commit has 2.1.0 (the Q2 release bump). The correct resolution depends on context — if the Q2 release supersedes the hotfix, keep 2.1.0.

Resolve and continue the cherry-pick

Stage the resolved file and complete the cherry-pick.

git add src/__init__.py
git cherry-pick --continue --no-edit
[main 421a7f5] feat: bump version to 2.1.0 for Q2 release
 1 file changed, 1 insertion(+), 1 deletion(-)
FlagSyntaxDescription
--continuegit cherry-pick --continueResume after resolving conflicts
--abortgit cherry-pick --abortCancel the cherry-pick and restore the pre-operation state
--skipgit cherry-pick --skipDiscard the current commit and continue (when cherry-picking a range)
--no-editgit cherry-pick --no-editAccept the original commit message without opening the editor
-xgit cherry-pick -x <sha>Append a “(cherry picked from commit …)” note to the message for traceability

Stash Pop Conflict Resolution

git stash pop applies stashed changes to the working directory using a three-way merge. If the file has changed since you stashed (because you switched branches or committed new work), the merge can conflict.

git stash pop | resolve conflicts from re-applied stash

Trigger a stash pop conflict

When re-applying stashed changes to a branch that has diverged since the stash was created. It is typically triggered by running git stash pop when the stashed changes touch lines that have been modified since the stash was saved. The stash entry is preserved (not dropped) when conflicts occur. Your work is safe until you explicitly git stash drop. Re-apply shelved work onto a branch that has moved forward.

Pop a stash that conflicts with committed changes.

git stash pop
Auto-merging requirements.txt
CONFLICT (content): Merge conflict in requirements.txt
The stash entry is kept in case you need it again.

View the conflict markers — stash labels differ from merge.

cat requirements.txt
pandas==2.2.0
numpy==1.26.4
sqlalchemy==2.0.25
dbt-core==1.7.4
apache-airflow==2.8.1
<<<<<<< Updated upstream
boto3==1.34.25
=======
requests==2.31.0
>>>>>>> Stashed changes

Updated upstream marks the current branch state (equivalent to “ours” in a merge). Stashed changes marks the stashed work (equivalent to “theirs”).

Resolve and clean up the stash

After editing the conflicted file to include both dependencies. It is typically triggered by conflict markers removed, file saved. Because the pop conflicted, the stash entry was not auto-dropped — you must drop it manually. Complete the stash re-application and remove the stash entry.

The correct resolution keeps both additions (they are independent packages):

Stage the resolved file and drop the preserved stash.

git add requirements.txt
git stash drop
Dropped refs/stash@{0} (397873e26cb34672a4f0f887a685670137f129ae)

Stash Pop Preserves the Stash on Conflict

When git stash pop encounters a conflict, the stash entry is not auto-dropped. This is a safety feature — your original stashed work remains recoverable via git stash list. But it also means you must git stash drop manually after resolving, or you will accumulate stale stash entries.

Always Drop After Resolving Stash Conflicts

After resolving stash pop conflicts and staging the files:

git stash drop

Verify with git stash list that the entry is gone.

Post-Resolution Verification

Resolving conflict markers is only half the job. Incorrectly combined code can pass Git’s merge checks but fail at runtime. Every conflict resolution must be followed by verification.

Verification checklist | confirm correct resolution

Post-Conflict Verification Checklist

After resolving all conflicts and before committing or pushing:

  1. Search for leftover markersgrep -rn "<<<<<<" . must return empty
  2. Run testspytest, dbt test, or your project’s test suite. Merged code that compiles but fails tests is a silent regression.
  3. Run lintersruff check ., flake8, eslint. Conflicts often produce indentation errors or missing imports after manual editing.
  4. Validate migrations — if SQL or Alembic migration files were conflicted, run the migration against a test database. A syntactically valid merge can produce invalid SQL.
  5. Parse DAGs — if Airflow DAG files were conflicted, run python -c "import dags.my_dag" to verify the DAG parses. Broken imports from a bad merge will only surface at scheduler time.
  6. Review the diffgit diff --staged to see exactly what will be committed. Read every hunk, not just the ones you edited.
  7. Build the project — if the project has a build step (docker build, npm run build, dbt compile), run it. Dependency conflicts often surface only at build time.

Preventing Merge Conflicts

Prevention is cheaper than resolution. These practices reduce conflict frequency in active data-engineering repositories.

Practices | reduce conflict frequency

  • Pull from main before starting any new work:
  git checkout main && git pull && git checkout -b feat/my-feature
  • Keep PRs small and focused — one feature or fix per PR. Large PRs take longer to review, increasing the chance that main moves ahead.
  • Merge or rebase frequently — if your branch lives for more than a day or two, periodically rebase onto the latest main to stay current.
  • Coordinate on shared files — if two people are editing the same file for unrelated reasons, communicate and consider sequencing the PRs.
  • Use GitHub’s branch update button — enable “Always suggest updating pull request branches” in repo Settings → General → Pull Requests. This adds a one-click “Update branch” button on the PR page.
  • Enable branch protection rules — “Require branches to be up to date before merging” forces every PR to be rebased before it can merge.
  • Enable rereregit config --global rerere.enabled true. Git records how you resolved each conflict and automatically applies the same resolution if the identical conflict recurs (common during repeated rebases).

Rebase Early, Rebase Often

Running git rebase origin/main on your feature branch every morning takes 30 seconds when there are no conflicts. It saves hours when you wait until the PR is blocked at merge time.

SQL migration conflicts | data-engineering gotcha

Migration File Ordering Conflicts

When two engineers create SQL migration files simultaneously (e.g., V003_add_column.sql and V003_create_table.sql), Git sees a file-level conflict only if both modified the same file. But migration tools (Flyway, Alembic, dbt) process files by version number — two files with the same version number will fail at runtime, not at merge time.

Use Timestamp-Based Migration Names

Use timestamp-based migration names (20260330_001_add_column.sql) instead of sequential version numbers. Timestamps never collide. If using sequential versioning, coordinate via a shared “next version” tracker or rebase and renumber before merging.

Notebook conflicts | avoiding JSON merge noise

Jupyter Notebook Conflicts Are Unreadable

.ipynb files are JSON under the hood. A conflict in a notebook file produces conflict markers inside deeply nested JSON structures that are nearly impossible to resolve manually. Cell outputs, execution counts, and metadata all contribute to false conflicts.

Strip Outputs Before Committing Notebooks

Use nbstripout to remove cell outputs and execution counts before committing:

pip install nbstripout
nbstripout --install     # installs as a Git filter

This dramatically reduces false conflicts. For remaining structural conflicts, use nbdime — a tool specifically designed for diffing and merging Jupyter notebooks:

pip install nbdime
nbdime config-git --enable --global

Lockfile conflicts | dependency resolution files

Lockfile Conflicts Should Not Be Manually Resolved

Files like poetry.lock, package-lock.json, yarn.lock, and Pipfile.lock are machine-generated. Manual conflict resolution almost always produces an invalid lockfile with inconsistent hashes or missing transitive dependencies.

Regenerate Lockfiles After Conflict

Accept one side’s version, then regenerate:

git checkout --theirs poetry.lock
poetry lock --no-update
git add poetry.lock

This ensures the lockfile is internally consistent with the merged pyproject.toml.

Git Merge Conflicts Troubleshooting

ProblemCauseFix
fatal: You have not concluded your merge (MERGE_HEAD exists)A previous merge was not completed or abortedRun git merge --abort to cancel, or resolve remaining conflicts and commit
Conflict markers appear in committed fileFile was staged with markers still presentAmend the commit: edit the file, git add, git commit --amend
git rebase --continue says “No changes”The resolved file is identical to the base after your editsRun git rebase --skip — the commit is now empty and redundant
Stash entry not dropped after git stash popPop encountered a conflict, so Git preserved the stashAfter resolving: git stash drop
--ours and --theirs produce the wrong version during rebaseThe meaning is reversed during rebase vs mergeDuring rebase: --ours = base branch, --theirs = your commits
Conflict in binary file with no markersBinary files cannot be text-mergedUse git checkout --ours or --theirs to pick one version, then git add
error: could not apply <sha> during rebaseThe commit being replayed conflicts with the new baseResolve conflict markers, git add, git rebase --continue
Rebase produces many conflicts across commitsEach commit is replayed individually, hitting the same region repeatedlyConsider squashing related commits first: git rebase -i to combine, then rebase onto main
Merge succeeds but tests failAuto-merge combined code that is syntactically valid but logically wrongAlways run tests after merge. Git merges text, not semantics
rerere applies wrong resolutionA previously recorded resolution is no longer correctClear the cache: git rerere forget <file>

Operating Guidance

  1. Read markers carefully. Know which side is HEAD and which is incoming — especially during rebase where the meaning is reversed.
  2. Never leave markers in committed code. Always grep -rn "<<<<<<" . before committing.
  3. Prefer --force-with-lease over --force. After rebase, this prevents overwriting teammates’ work.
  4. Abort is always safe. git merge --abort and git rebase --abort restore the pre-operation state with zero data loss.
  5. Verify after every resolution. Run tests, linters, and build steps. Git merges text, not logic.
  6. Use rerere for repeated rebases. Enable rerere.enabled = true to avoid resolving the same conflict repeatedly.
  7. Regenerate, don’t edit lockfiles. Accept one side, then run the package manager’s lock command.
  8. Strip notebook outputs. Use nbstripout to prevent JSON-level false conflicts in .ipynb files.
  9. Coordinate on shared files. When two people edit the same file, sequence the PRs or split the file.
  10. Use timestamp-based migration names. Sequential version numbers collide silently when two engineers create migrations simultaneously.

Quick Reference

GoalCommand
See which files have conflictsgit status
List index stages for conflicted filesgit ls-files -u
View content at a specific stagegit show :N:filename (N = 1, 2, or 3)
Stage a resolved filegit add <file>
Accept current branch versiongit checkout --ours <file>
Accept incoming branch versiongit checkout --theirs <file>
Complete the mergegit commit
Abort the mergegit merge --abort
Quit merge, keep partial stategit merge --quit
Open visual merge toolgit mergetool
Continue after rebase conflictgit rebase --continue
Abort the entire rebasegit rebase --abort
Skip a redundant commit during rebasegit rebase --skip
Continue after cherry-pick conflictgit cherry-pick --continue
Abort a cherry-pickgit cherry-pick --abort
Drop preserved stash after pop conflictgit stash drop
Search for leftover conflict markersgrep -rn "<<<<<<" .
Enable rerere for auto-resolution recordinggit config --global rerere.enabled true
Clear a wrong rerere recordinggit rerere forget <file>