Version Control

Conflict Resolution Patterns and Strategies

A practical guide to resolving Git merge conflicts effectively, covering command-line and visual tools, package-lock.json handling, rerere, conflict prevention, and automation strategies.

Conflict Resolution Patterns and Strategies

Merge conflicts are not bugs. They are Git telling you that two humans made decisions about the same code and it needs a human to reconcile them. Understanding conflict resolution deeply — not just clicking "Accept Incoming" — separates engineers who use Git from engineers who fight it.

This guide covers the full spectrum: reading conflict markers, choosing the right tools, handling generated files like package-lock.json, automating what you can, and preventing conflicts before they happen.

Prerequisites

  • Git 2.30+ installed
  • Comfortable with branching and merging basics
  • A Node.js project with at least a few collaborators
  • Optional: VS Code, Beyond Compare, or Meld installed

Understanding Conflict Markers

When Git cannot automatically merge two changes, it writes conflict markers directly into the file. Every developer has seen these, but not every developer reads them carefully.

<<<<<<< HEAD
var port = process.env.PORT || 3000;
=======
var port = process.env.PORT || 8080;
>>>>>>> feature/update-port

Three markers define the conflict:

  • <<<<<<< HEAD — Start of your current branch's version (what you have checked out)
  • ======= — Divider between the two versions
  • >>>>>>> feature/update-port — End marker showing the incoming branch name

The key insight: HEAD is always your side. Whether you are merging or rebasing, the content between <<<<<<< and ======= is what is currently in your working tree. The content between ======= and >>>>>>> is what is being brought in.

Multi-way Conflicts with diff3

The default two-way conflict view hides critical information: what the code looked like before either side changed it. Enable diff3 style to see the common ancestor:

git config --global merge.conflictstyle diff3

Now conflicts show three sections:

<<<<<<< HEAD
var port = process.env.PORT || 3000;
||||||| merged common ancestors
var port = 8080;
=======
var port = process.env.PORT || 8080;
>>>>>>> feature/update-port

The ||||||| section shows the original. Now you can see that HEAD added process.env.PORT || and the feature branch kept the same pattern but changed the default. The correct resolution is obvious: keep both changes.

var port = process.env.PORT || 8080;

I recommend diff3 for every developer. Set it globally and never look back.

Merge Conflicts vs Rebase Conflicts

Both operations produce conflicts, but they behave differently and require different mental models.

Merge Conflicts

A merge creates a single merge commit. All conflicts appear at once. You resolve everything in one pass.

git checkout main
git merge feature/auth-system
# Conflicts appear — resolve all at once
git add .
git commit

If you have 5 conflicting files, you see all 5 immediately. This is straightforward but can feel overwhelming on large branches.

Rebase Conflicts

A rebase replays your commits one at a time on top of the target branch. Conflicts can appear at every commit in your branch.

git checkout feature/auth-system
git rebase main
# Conflict at commit 1 of 12
# Resolve, then:
git add .
git rebase --continue
# Conflict at commit 4 of 12
# Resolve again...

Rebase conflicts are smaller individually but can be repetitive. If commit 3 changes a function signature and commit 7 modifies the same function, you may resolve the same area twice.

When to prefer merge: Large branches with many conflicts. Team branches where multiple people have committed. When you want to preserve the exact history.

When to prefer rebase: Small feature branches. Solo work that you want to linearize. When the branch is behind main by many commits and you want a clean history.

Aborting Safely

If a merge or rebase goes sideways, bail out cleanly:

# Abort a merge in progress
git merge --abort

# Abort a rebase in progress
git rebase --abort

Both commands return your working tree to the state before you started. No damage done.

Resolving Conflicts with Command-Line Tools

Manual Resolution

The most fundamental approach. Open the file, read the markers, edit the code, remove the markers.

# See which files have conflicts
git status

# Output:
# Unmerged paths:
#   both modified:   src/server.js
#   both modified:   src/config.js
#   both modified:   package-lock.json

Open each file, find the markers, decide what the correct code should be, and remove the markers entirely. Then stage the resolved files:

git add src/server.js src/config.js

Using git checkout for Wholesale Resolution

When you know one side is entirely correct, skip the manual editing:

# Accept YOUR version (current branch) for a file
git checkout --ours src/config.js

# Accept THEIR version (incoming branch) for a file
git checkout --theirs src/config.js

# Stage the resolved file
git add src/config.js

Warning during rebase: The meaning of --ours and --theirs flips during rebase. In a rebase, --ours refers to the branch you are rebasing onto (usually main), and --theirs refers to your feature branch commits. This trips up almost everyone at least once.

Using git mergetool

Git ships with a built-in command that launches your configured merge tool for each conflicting file:

git mergetool

Configure your preferred tool:

# Set VS Code as the merge tool
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

# Set Beyond Compare
git config --global merge.tool bc
git config --global mergetool.bc.path "C:/Program Files/Beyond Compare 4/bcomp.exe"

# Set Meld
git config --global merge.tool meld

After resolving, git mergetool creates .orig backup files. Disable them:

git config --global mergetool.keepBackup false

Visual Merge Tools

VS Code

VS Code has the best built-in merge editor in the business for a free tool. When you open a conflicting file, it shows inline annotations:

  • Accept Current Change — Keep your version
  • Accept Incoming Change — Keep their version
  • Accept Both Changes — Concatenate both (rarely what you actually want)
  • Compare Changes — Side-by-side diff

For complex conflicts, use the three-way merge editor (available since VS Code 1.70+):

  1. Open the conflicting file
  2. Click "Resolve in Merge Editor" in the notification bar
  3. You get three panes: Incoming (left), Current (right), Result (bottom)
  4. Check boxes to accept individual changes or edit the result directly

The three-way editor is the right tool when a conflict spans many lines or involves structural changes.

Beyond Compare

Beyond Compare is the gold standard for paid merge tools. Its three-way merge view is exceptional for complex conflicts. Configure it in Git:

git config --global merge.tool bc
git config --global mergetool.bc.path "/usr/local/bin/bcomp"
git config --global diff.tool bc
git config --global difftool.bc.path "/usr/local/bin/bcomp"

Beyond Compare handles binary files, images, and even structured data comparisons. Worth the license fee if you work on projects with frequent conflicts.

Meld

Meld is the best free GUI merge tool on Linux. It provides a clean three-pane view and handles Unicode well. On macOS, install via Homebrew:

brew install meld

Handling package-lock.json and yarn.lock Conflicts

This is where most developers waste the most time. Lock file conflicts look terrifying — thousands of lines of JSON diffs. The solution is simple: never manually resolve lock file conflicts.

package-lock.json Strategy

# When you hit a conflict in package-lock.json:

# 1. Accept either version (doesn't matter which)
git checkout --theirs package-lock.json

# 2. Regenerate the lock file from the merged package.json
npm install

# 3. Stage both files
git add package.json package-lock.json

The lock file is a derived artifact of package.json. Once package.json is correctly merged, regenerating the lock file produces the correct result.

yarn.lock Strategy

Same principle, different command:

git checkout --theirs yarn.lock
yarn install
git add package.json yarn.lock

Automating Lock File Resolution with Git Attributes

You can tell Git how to handle lock files automatically using a custom merge driver:

# .gitattributes
package-lock.json merge=npm-merge
yarn.lock merge=yarn-merge
# Configure the custom merge drivers
git config merge.npm-merge.name "Regenerate package-lock.json"
git config merge.npm-merge.driver "cp %A %A.bak && git checkout --theirs %A && npm install && git add package-lock.json"

# Simpler approach: just accept theirs and regenerate
git config merge.npm-merge.driver "npm install --package-lock-only"

In practice, I find the manual three-step approach more reliable than automated drivers. Lock file merge drivers can behave unpredictably in CI environments.

Resolving Conflicts in Generated Files

Generated files — bundled assets, compiled output, auto-generated API clients — should never have their conflicts manually resolved.

The Rule

If a file is generated by a build step, resolve the conflict by re-running the build step.

# Example: Prisma client conflicts
git checkout --theirs prisma/schema.prisma  # or resolve manually
npx prisma generate
git add prisma/

# Example: OpenAPI generated client
git checkout --theirs openapi.yaml  # or resolve manually
npm run generate-api-client
git add src/generated/

Should Generated Files Be in Git?

Honestly, most should not be. Add them to .gitignore and generate them during CI/CD. But if your team has decided to track them (which is common for lock files and Prisma schemas), the regenerate-after-resolve pattern is the way.

Rerere: Reuse Recorded Resolution

rerere stands for "reuse recorded resolution." It is one of Git's most underused features. When enabled, Git remembers how you resolved a conflict and automatically applies the same resolution if it encounters the same conflict again.

Enabling Rerere

git config --global rerere.enabled true

How It Works

  1. You encounter a conflict and resolve it manually.
  2. Git records the pre-image (the conflict) and post-image (your resolution).
  3. Next time Git sees the exact same conflict, it applies your recorded resolution automatically.

This is invaluable during long-running feature branches where you rebase frequently. Without rerere, you resolve the same conflicts every time you rebase. With rerere, you resolve once and Git handles it going forward.

Managing Recorded Resolutions

# See recorded resolutions
git rerere status

# See the diff of a recorded resolution
git rerere diff

# Clear all recorded resolutions (nuclear option)
git rerere forget --all

# Clear the resolution for a specific file
git rerere forget src/server.js

Rerere and Rebase Workflows

Rerere shines in teams that use rebase-heavy workflows. Consider this scenario:

  1. You rebase your feature branch onto main, resolving 3 conflicts.
  2. You push for code review.
  3. Reviewer requests changes. You make new commits.
  4. You rebase again onto updated main.
  5. Without rerere: you resolve the same 3 conflicts again.
  6. With rerere: Git auto-applies the previous resolutions. You only handle new conflicts.

Enable it. There is no downside.

Conflict Prevention Strategies

The best conflict is one that never happens. These strategies reduce conflict frequency dramatically.

Small, Focused Pull Requests

A PR that touches 3 files over 2 days will rarely conflict. A PR that touches 40 files over 3 weeks will almost certainly conflict. Keep branches short-lived and tightly scoped.

# Good: focused branch
git checkout -b fix/validate-email-input
# Touch: src/validators/email.js, tests/validators/email.test.js
# Lifespan: 1 day

# Bad: sprawling branch
git checkout -b feature/redesign-user-system
# Touch: 47 files across 6 directories
# Lifespan: 3 weeks

Rebase Before Merging

Always rebase your feature branch onto the target before opening a PR:

git fetch origin
git rebase origin/main
# Resolve any conflicts now, before the PR
git push --force-with-lease

--force-with-lease is safer than --force — it refuses to overwrite remote changes that you have not pulled.

Communication and File Ownership

If two developers are working on the same file simultaneously, talk to each other. Formalize this with a CODEOWNERS file:

# .github/CODEOWNERS
src/auth/          @alice
src/payments/      @bob
src/shared/utils/  @alice @bob
package.json       @alice @bob

CODEOWNERS does not prevent conflicts, but it ensures the right people review changes to shared code, which reduces conflicting approaches.

Consistent Formatting

Adopt a formatter (Prettier, ESLint with --fix) and enforce it in CI. Formatting conflicts are the most wasteful kind — they carry zero semantic value.

{
  "scripts": {
    "format": "prettier --write \"src/**/*.js\"",
    "format:check": "prettier --check \"src/**/*.js\""
  }
}

Run the formatter before every commit. Use a pre-commit hook:

npx husky add .husky/pre-commit "npx pretty-quick --staged"

Merge Strategies

Git supports several merge strategies that control how conflicts are handled at a high level.

Recursive (Default)

The standard strategy for merging two branches. It finds a common ancestor and performs a three-way merge.

git merge feature/auth
# Equivalent to:
git merge -s recursive feature/auth

Ours

Completely ignores the incoming branch's changes. The merge commit exists, but only your side's content survives. This is useful for "we considered this branch and decided not to take it" situations.

git merge -s ours feature/abandoned-experiment

Do not confuse -s ours (strategy) with -X ours (strategy option). The -X ours option uses the recursive strategy but automatically resolves conflicts by picking your side:

# Strategy: ignore their branch entirely
git merge -s ours feature/branch

# Strategy option: merge normally, but when conflicts occur, pick our side
git merge -X ours feature/branch

Theirs (Strategy Option)

There is no -s theirs strategy, but the -X theirs option resolves all conflicts in favor of the incoming branch:

git merge -X theirs feature/branch

This is useful when you know the other branch is authoritative and any conflicts should be resolved in its favor.

Subtree

Used for merging repositories or subdirectory merges. Rarely needed in day-to-day work.

git merge -s subtree --prefix=libs/external feature/vendor-update

Handling Complex Conflicts

Renamed Files

When one branch renames a file and another modifies it, Git usually detects this and merges correctly. But sometimes it fails:

# Branch A renames: src/utils.js -> src/helpers/utils.js
# Branch B modifies: src/utils.js

git merge branch-a
# CONFLICT (modify/delete): src/utils.js deleted in branch-a
# and modified in HEAD.

Git sees a delete + create instead of a rename. Resolve manually:

# Check what branch-a did
git diff main...branch-a -- src/utils.js src/helpers/utils.js

# Apply your modifications to the new location
git show HEAD:src/utils.js > src/helpers/utils.js
# Edit src/helpers/utils.js to incorporate branch-a's changes
git rm src/utils.js
git add src/helpers/utils.js

Moved Code Blocks

When two branches move the same code to different locations within a file, Git produces a conflict that looks like duplication. The resolution requires understanding what both sides intended:

<<<<<<< HEAD
function validateInput(data) {
    // moved to top of file for visibility
    if (!data.email) throw new Error("Email required");
    if (!data.name) throw new Error("Name required");
}

module.exports = { validateInput, processForm };
=======
module.exports = { processForm, validateInput };

// validation logic grouped at bottom
function validateInput(data) {
    if (!data.email) throw new Error("Email required");
    if (!data.name) throw new Error("Name required");
    if (!data.password) throw new Error("Password required");
}
>>>>>>> feature/add-password

Here, HEAD moved the function to the top and the feature branch added password validation. The correct resolution combines both: the function at its new location with the added validation.

Conflict Resolution in Monorepos

Monorepos amplify conflict frequency because more developers touch overlapping files. Specific patterns help.

Root-Level Configuration Conflicts

Files like package.json, tsconfig.json, and CI configs at the repo root are conflict magnets. Mitigate this:

# Use workspace-level configs that extend root
# packages/api/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist"
  }
}

Each package extends the base config. Developers modify their package config, not the shared root.

Shared Dependency Version Conflicts

In monorepos with shared package.json files, version bumps cause frequent conflicts:

<<<<<<< HEAD
    "express": "^4.18.2",
=======
    "express": "^4.19.0",
>>>>>>> feature/upgrade-deps

Resolution: always take the higher version (unless there is a specific reason not to). Automate this with a merge driver:

# .gitattributes
package.json merge=union

The union merge driver keeps both sides' additions. It works for simple cases but can produce invalid JSON on complex conflicts. Use with caution.

CI Configuration Conflicts

When multiple developers add CI jobs simultaneously:

# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
<<<<<<< HEAD
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: npm test
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint
=======
    steps:
      - uses: actions/checkout@v4
      - run: npm test
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit
>>>>>>> feature/add-security

Resolution: accept both new jobs and the timeout addition. YAML conflicts require careful attention to indentation.

Automating Conflict Resolution for Specific File Types

Custom Merge Drivers

Git lets you define custom merge drivers in .gitattributes for any file pattern:

# .gitattributes
*.generated.js merge=regenerate
db/schema.sql merge=migrate
package-lock.json merge=npm-lock
# Configure drivers in .git/config or global config
git config merge.regenerate.driver "npm run generate -- %A"
git config merge.npm-lock.driver "npm install --package-lock-only"

Script-Based Resolution

For teams with predictable conflict patterns, a resolution script can save hours:

// scripts/resolve-conflicts.js
var fs = require("fs");
var path = require("path");
var execSync = require("child_process").execSync;

function getConflictedFiles() {
    var output = execSync("git diff --name-only --diff-filter=U").toString();
    return output.trim().split("\n").filter(Boolean);
}

function resolveFile(filePath) {
    var ext = path.extname(filePath);
    var basename = path.basename(filePath);

    // Lock files: regenerate
    if (basename === "package-lock.json") {
        console.log("Regenerating package-lock.json...");
        execSync("git checkout --theirs package-lock.json");
        execSync("npm install --package-lock-only");
        execSync("git add package-lock.json");
        return true;
    }

    if (basename === "yarn.lock") {
        console.log("Regenerating yarn.lock...");
        execSync("git checkout --theirs yarn.lock");
        execSync("yarn install --mode update-lockfile");
        execSync("git add yarn.lock");
        return true;
    }

    // Generated files: regenerate
    if (filePath.match(/\.generated\.(js|ts|json)$/)) {
        console.log("Skipping generated file (will regenerate): " + filePath);
        execSync("git checkout --theirs \"" + filePath + "\"");
        execSync("git add \"" + filePath + "\"");
        return true;
    }

    return false;
}

var files = getConflictedFiles();
var resolved = 0;
var manual = [];

files.forEach(function(file) {
    if (resolveFile(file)) {
        resolved++;
    } else {
        manual.push(file);
    }
});

console.log("\nResolved automatically: " + resolved);
if (manual.length > 0) {
    console.log("Requires manual resolution:");
    manual.forEach(function(f) { console.log("  - " + f); });
}
# Run after a merge with conflicts
node scripts/resolve-conflicts.js
# Then manually resolve remaining files

Complete Working Example

Let us walk through resolving realistic conflicts in a Node.js project. We will create a scenario with three types of conflicts: code, package-lock.json, and configuration.

Setup

mkdir conflict-demo && cd conflict-demo
git init

Create the initial project:

npm init -y
npm install express dotenv
// server.js
var express = require("express");
var app = express();
var port = process.env.PORT || 3000;

app.get("/", function(req, res) {
    res.json({ status: "ok", version: "1.0.0" });
});

app.get("/health", function(req, res) {
    res.json({ healthy: true });
});

app.listen(port, function() {
    console.log("Server running on port " + port);
});
// config.js
var config = {
    appName: "conflict-demo",
    logLevel: "info",
    maxRetries: 3
};

module.exports = config;
git add -A
git commit -m "Initial commit"

Create Diverging Branches

Branch A: Add authentication middleware

git checkout -b feature/auth
// server.js (modified on feature/auth)
var express = require("express");
var helmet = require("helmet");
var app = express();
var port = process.env.PORT || 3000;

app.use(helmet());

app.get("/", function(req, res) {
    res.json({ status: "ok", version: "1.1.0" });
});

app.get("/health", function(req, res) {
    res.json({ healthy: true, uptime: process.uptime() });
});

app.get("/auth/login", function(req, res) {
    res.json({ message: "Login endpoint" });
});

app.listen(port, function() {
    console.log("Server running on port " + port);
});
// config.js (modified on feature/auth)
var config = {
    appName: "conflict-demo",
    logLevel: "info",
    maxRetries: 3,
    sessionSecret: process.env.SESSION_SECRET || "dev-secret",
    tokenExpiry: 3600
};

module.exports = config;
npm install helmet
git add -A
git commit -m "Add auth middleware and helmet"

Branch B: Add monitoring and update config

git checkout main
git checkout -b feature/monitoring
// server.js (modified on feature/monitoring)
var express = require("express");
var morgan = require("morgan");
var app = express();
var port = process.env.PORT || 8080;

app.use(morgan("combined"));

app.get("/", function(req, res) {
    res.json({ status: "ok", version: "1.2.0" });
});

app.get("/health", function(req, res) {
    res.json({ healthy: true, timestamp: Date.now() });
});

app.get("/metrics", function(req, res) {
    res.json({ requests: 0, errors: 0 });
});

app.listen(port, function() {
    console.log("Server running on port " + port);
});
// config.js (modified on feature/monitoring)
var config = {
    appName: "conflict-demo",
    logLevel: "debug",
    maxRetries: 5,
    metricsInterval: 30000
};

module.exports = config;
npm install morgan
git add -A
git commit -m "Add monitoring with morgan and metrics"

Merge and Resolve

# Merge auth into main first (no conflicts)
git checkout main
git merge feature/auth
# Fast-forward, clean merge

# Now merge monitoring (conflicts!)
git merge feature/monitoring

Expected output:

Auto-merging server.js
CONFLICT (content): Merge conflict in server.js
Auto-merging package.json
CONFLICT (content): Merge conflict in package.json
Auto-merging package-lock.json
CONFLICT (content): Merge conflict in package-lock.json
Auto-merging config.js
CONFLICT (content): Merge conflict in config.js
Automatic merge failed; fix conflicts and then commit the result.

Step 1: Resolve package-lock.json (30 seconds)

Do not read the lock file diff. Just regenerate.

git checkout --theirs package-lock.json
# Ensure package.json is resolved first (accept both dependencies)
# Then regenerate:
npm install
git add package.json package-lock.json

Step 2: Resolve config.js (CLI approach)

Open config.js and see:

var config = {
    appName: "conflict-demo",
<<<<<<< HEAD
    logLevel: "info",
    maxRetries: 3,
    sessionSecret: process.env.SESSION_SECRET || "dev-secret",
    tokenExpiry: 3600
||||||| merged common ancestors
    logLevel: "info",
    maxRetries: 3
=======
    logLevel: "debug",
    maxRetries: 5,
    metricsInterval: 30000
>>>>>>> feature/monitoring
};

module.exports = config;

With diff3, the common ancestor shows us: both sides changed logLevel and maxRetries, and each added new fields. The correct resolution keeps monitoring's logLevel and maxRetries (they are the later intent) and preserves both additions:

var config = {
    appName: "conflict-demo",
    logLevel: "debug",
    maxRetries: 5,
    sessionSecret: process.env.SESSION_SECRET || "dev-secret",
    tokenExpiry: 3600,
    metricsInterval: 30000
};

module.exports = config;
git add config.js

Step 3: Resolve server.js (VS Code approach)

Open server.js in VS Code. Use the three-way merge editor:

  1. Click "Resolve in Merge Editor"
  2. Accept helmet middleware from Current (auth branch)
  3. Accept morgan middleware from Incoming (monitoring branch)
  4. For the version number, manually type "1.3.0" (both features combined)
  5. Accept the health endpoint from Incoming (it has the timestamp)
  6. Accept both new routes: /auth/login from Current, /metrics from Incoming
  7. For the port, decide: use process.env.PORT || 8080 (monitoring's default)

Final resolved server.js:

var express = require("express");
var helmet = require("helmet");
var morgan = require("morgan");
var app = express();
var port = process.env.PORT || 8080;

app.use(helmet());
app.use(morgan("combined"));

app.get("/", function(req, res) {
    res.json({ status: "ok", version: "1.3.0" });
});

app.get("/health", function(req, res) {
    res.json({ healthy: true, uptime: process.uptime(), timestamp: Date.now() });
});

app.get("/auth/login", function(req, res) {
    res.json({ message: "Login endpoint" });
});

app.get("/metrics", function(req, res) {
    res.json({ requests: 0, errors: 0 });
});

app.listen(port, function() {
    console.log("Server running on port " + port);
});
git add server.js

Step 4: Complete the Merge

git commit -m "Merge feature/monitoring with auth changes"

Verify everything works:

node server.js
# Server running on port 8080

curl http://localhost:8080/health
# {"healthy":true,"uptime":0.234,"timestamp":1706900000000}

Total resolution time for an experienced developer: 3-5 minutes.

Common Issues and Troubleshooting

1. "fatal: Not possible to fast-forward, aborting"

fatal: Not possible to fast-forward, aborting.

This happens when you have pull.ff only set in your Git config and the remote has diverged. Fix:

# Pull with rebase instead
git pull --rebase origin main

# Or allow merge commits
git pull --no-ff origin main

# Check your config
git config pull.rebase
git config pull.ff

2. "CONFLICT (modify/delete)"

CONFLICT (modify/delete): src/old-utils.js deleted in feature/cleanup
and modified in HEAD. Version HEAD of src/old-utils.js left in tree.

One branch deleted a file that another branch modified. Decide:

# If the delete was intentional and modifications should be discarded:
git rm src/old-utils.js
git add .

# If the modifications are needed, move them to the new location:
git show HEAD:src/old-utils.js > src/new-utils.js
git rm src/old-utils.js
git add src/new-utils.js

3. "error: Your local changes to the following files would be overwritten by merge"

error: Your local changes to the following files would be overwritten by merge:
        src/server.js
Please commit your changes or stash them before you merge.

You have uncommitted changes that conflict with the merge. Stash or commit first:

# Stash changes
git stash
git merge feature/branch
git stash pop
# Now resolve any conflicts between your stash and the merge

# Or commit your work-in-progress
git add -A
git commit -m "WIP: save work before merge"
git merge feature/branch

4. "Resolved but unfinished — you must edit all merge conflicts and then mark them as resolved"

error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.

You tried to commit before staging all resolved files. Check what is still unresolved:

git status
# Look for "Unmerged paths" section
# Resolve those files, then:
git add <resolved-files>
git commit

5. Merge Conflict Markers Left in File

Sometimes you accidentally commit files with conflict markers still in them. Catch this early:

# Search for conflict markers in your codebase
git diff --check

# Pre-commit hook to prevent committing conflict markers
# .husky/pre-commit
git diff --cached --check

Add this to CI as well:

# In your CI pipeline
git diff --check HEAD~1 || (echo "Conflict markers found!" && exit 1)

Best Practices

  • Enable diff3 globally. The three-way conflict view with the common ancestor makes resolution dramatically easier. Run git config --global merge.conflictstyle diff3 right now.

  • Enable rerere globally. There is no downside and it saves real time on rebase-heavy workflows. Run git config --global rerere.enabled true.

  • Never manually resolve lock files. Accept either version, regenerate with npm install or yarn install, and move on. This alone will save you hours over the life of a project.

  • Rebase onto the target branch before opening a PR. Resolve conflicts in your branch, not during the merge to main. This keeps main's history clean and puts the resolution burden on the feature author who best understands the changes.

  • Use --force-with-lease instead of --force. After rebasing and needing to push, --force-with-lease will refuse to overwrite changes that someone else pushed to your branch. It is a safety net with no downside.

  • Keep branches short-lived. A branch that lives for 2 days will have fewer conflicts than one that lives for 2 weeks. Break large features into smaller, independently mergeable pieces.

  • Enforce consistent formatting with automated tools. Prettier, ESLint, and similar tools eliminate an entire class of conflicts. If everyone's code is formatted identically, formatting differences can never cause conflicts.

  • Communicate when touching shared files. A quick message to the team — "I'm refactoring config.js today" — prevents parallel conflicting changes. Use CODEOWNERS to formalize file ownership.

  • Run tests after resolving conflicts. A conflict resolution that produces syntactically valid but semantically wrong code is worse than a conflict. Always run your test suite after resolving:

git merge feature/branch
# Resolve conflicts
npm test
# Only commit if tests pass
git commit
  • Learn your merge tool deeply. Whether it is VS Code, Beyond Compare, or vimdiff, invest time in learning the keyboard shortcuts and workflows. The speed difference between a developer who knows their merge tool and one who does not is an order of magnitude.

References

Powered by Contentful