惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Blog — PlanetScale
Blog — PlanetScale
Jina AI
Jina AI
C
Check Point Blog
V
V2EX
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
P
Proofpoint News Feed
A
About on SuperTechFans
D
DataBreaches.Net
腾讯CDC
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
IT之家
IT之家
WordPress大学
WordPress大学
人人都是产品经理
人人都是产品经理
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
MongoDB | Blog
MongoDB | Blog
J
Java Code Geeks
博客园_首页
T
Tailwind CSS Blog
M
MIT News - Artificial intelligence
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Stop Wrestling with Merge Conflicts: Automate the Whole W...
pponali · 2026-05-13 · via DEV Community

It's 4:47 PM on a Friday. You've been coding all week on a feature you're proud of. You open a PR, and GitHub greets you with the one message that turns your stomach:

"This branch has conflicts that must be resolved."

You already know what's waiting. The pom.xml has diverged again. Someone on main bumped a YAML config. There are three Markdown docs with ugly <<<<<<< HEAD markers scattered through them. You're not shipping today.

Every software developer has lived this moment. And most of us keep solving it the same slow, error-prone, manual way — every single time.

This post is about why that's a problem, and how the /resolve-conflicts Claude Cowork skill fixes it — a first-class Claude skill you simply invoke in natural language, and Claude handles the entire workflow end to end.

🧩 The /resolve-conflicts skill is an installable Claude Cowork skill that automates your entire merge conflict workflow. The source code, shell script, and full documentation are open source on GitHub: github.com/pponali/claude_skills

The Real Pain Points Behind Merge Conflicts

Merge conflicts feel like a Git problem, but they're really a team coordination problem made visible. Here's what actually makes them brutal:

They always hit at the worst time. You finish a feature after days of work, go to merge, and suddenly you're debugging someone else's schema migration inside your application.yml. Context-switching mid-sprint to resolve config conflicts is a massive cognitive tax.

The same files conflict over and over. In any non-trivial project, you'll find the same suspects in your conflict list week after week: pom.xml, docker-compose.yml, *.properties, localization files, OpenAPI specs. These aren't complex logical conflicts — they're mechanical, repetitive, and boring. Yet they still demand your full attention.

Manual resolution is error-prone under pressure. When you're staring at a wall of ======= markers, it's easy to accidentally keep the wrong version, drop a dependency update, or smash together two configs in a way that silently breaks something. There's no guardrail — just you, your eyes, and your mouse.

The feedback loop is slow. You resolve conflicts, push, wait for CI, find out you introduced a bug in the config you just "fixed," fix it again, push again. The cycle can eat hours.

It doesn't scale with team size. On a small team, conflicts are annoying. On a team of 20+ engineers all merging into main frequently, they're a daily tax on every developer's time.

The Traditional Approach (And Why It Falls Short)

The standard workflow most of us follow looks like this:

git fetch origin
git merge origin/main
# ... open every conflicted file in VS Code ...
# ... manually choose ours / theirs / both ...
git add .
git commit -m "Merge branch 'main' into feature/my-thing"
git push

This works, but it has serious problems:

  • No consistency. Different developers make different choices for the same file types. One dev always keeps pom.xml from main; another always keeps the branch version. The merge history becomes unpredictable.
  • No documentation. The merge commit rarely explains why you resolved conflicts the way you did. Future you (and your teammates) have no idea.
  • No scalability. Every conflict requires a human to sit down and make individual decisions, even for files where the right answer is always the same.

Enter /resolve-conflicts: A Claude Cowork Skill That Does It All

The /resolve-conflicts skill is a Claude Cowork skill — meaning you don't run commands yourself. You just tell Claude what you want in plain English:

  • "Merge main into my branch and resolve conflicts"
  • "My PR shows conflicts — fix them"
  • "Sync my feature branch with origin/main"

Claude takes it from there. Under the hood it drives .claude/scripts/resolve-conflicts.sh — a shell script with codified heuristics for every file type in your project. The skill + script together are fully open source:

📦 github.com/pponali/claude_skills

The core idea: most conflicts in most projects follow predictable patterns. Once those patterns are encoded as a Claude skill, you never have to think about them again — just describe the problem and let Claude handle it.

Here's what Claude does behind the scenes when you invoke the skill:

# Step 1 — Claude runs a dry-run first and summarises what it found
.claude/scripts/resolve-conflicts.sh origin/main true

# Step 2 — After your confirmation, Claude runs for real
.claude/scripts/resolve-conflicts.sh origin/main false

Claude handles everything: stashing uncommitted changes, merging, resolving each conflict using the right heuristic, staging, creating an auditable commit, pushing, and optionally checking PR status via the gh CLI. You just review and confirm.

How the Resolution Heuristics Work

The script doesn't guess — it applies explicit, per-file-type rules:

File pattern Strategy Rationale
pom.xml, */pom.xml Keep branch (--ours) Feature branch usually holds newer or WIP dependencies
*.properties Keep branch Branch has local/feature-specific config values
*.yaml / *.yml Keep branch Same reasoning as properties
docker-compose.* Keep branch Preserves the current service topology
*.md Keep both (concatenated) Docs from both sides are usually additive, not contradictory
*.java Keep branch Active feature work lives on the branch
*.js, *.ts, *.tsx Keep branch Same as Java
anything else Keep branch + warning Safe default; flagged for manual review

"Keep branch" means the HEAD side of the merge — your feature branch. The strategy is opinionated by design. If your project has different conventions, you update the script once, and every future merge follows the new rules automatically.

The Markdown strategy is particularly clever: instead of picking one side, it concatenates both. Documentation is almost always additive — two sections from different branches are usually both worth keeping, so the merge result includes both and a human can clean it up later without losing information.

The Dry-Run Is Your Best Friend

Before running for real, always do a dry run:

.claude/scripts/resolve-conflicts.sh origin/main true

The output tells you:

  • Which files have conflicts
  • Which rule the script will apply to each one
  • Any files in the "unknown" bucket (these get --ours by default and a warning logged)

This is the moment to catch surprises. If main bumped a critical version in pom.xml that your branch must absorb, you'll see it here. You can let the script handle everything else and manually fix just that one file afterwards.

Verify, Don't Assume

After the script runs, always check:

# Should show your merge commit
git log --oneline -3

# MUST be empty — any output here means unresolved conflicts
git diff --name-only --diff-filter=U

# Quick overview of what changed
git show HEAD --stat

If your project has a fast smoke test — mvn -q -DskipTests verify, npm run typecheck, docker compose config — run it now. Heuristic resolutions are fast but not infallible.

If you're using the gh CLI:

gh pr view <number> --json mergeable,mergeStateStatus

Note: GitHub takes 1–2 minutes to recompute mergeability after a push. Don't panic if it shows conflicts for a moment after you push.

When NOT to Use the Script

The script is great for mechanical conflicts, but there are cases where you need a human in the loop:

Core business logic in .java / .ts / .js files where both sides have unique, non-overlapping changes. The script will keep the branch version and silently drop main's changes. If both sides implemented different parts of the same feature, you need to merge them manually.

A critical dependency update in pom.xml that your branch must consume. The script keeps your branch version. After it runs, manually edit the file, git add, and git commit --amend --no-edit.

Files in the "unknown" bucket (*.sql, *.proto, *.kt, etc.). The script flags these but defaults to --ours. Open them, check if main's changes matter, resolve manually, then fold into the merge commit:

# After manually editing the file:
git add path/to/file.sql
git commit --amend --no-edit
git push --force-with-lease origin your-branch

The pattern that works best: let the script handle the 80% of mechanical conflicts, then manually patch the 20% that need actual reasoning. You get speed and correctness.

Real-World Payoff

Once your team adopts this workflow, a few things change:

Merge commits become consistent and auditable. The commit message documents which strategy was applied to which files. Six months from now, you can look at a merge commit and understand exactly what happened.

Conflict resolution stops being a senior-developer task. The rules are encoded in a script, not in someone's head. A junior developer can merge safely without needing to ask "should I keep ours or theirs for the YAML?".

Friday afternoon PRs stop being feared. When conflict resolution is one command instead of a half-hour of careful manual editing, you stop dreading the merge.

Getting Started: Install the Claude Skill

Everything — the skill definition, the shell script, and full documentation — is in the open source repo:

📦 github.com/pponali/claude_skills

# Add the script to your project
mkdir -p .claude/scripts
curl -o .claude/scripts/resolve-conflicts.sh \
  https://raw.githubusercontent.com/pponali/claude_skills/master/.claude/scripts/resolve-conflicts.sh
chmod +x .claude/scripts/resolve-conflicts.sh

Once installed, just open Claude in Cowork mode and describe the problem. Claude will:

  1. Run pre-flight checks (detects if you're already mid-merge)
  2. Do a dry run and summarise exactly which files it will touch and how
  3. Ask for your confirmation before committing anything
  4. Merge, resolve, commit, push — and report back

You're always in control. Claude handles the mechanical parts, you handle the judgment calls.

Running the script directly (without Claude) is also supported:

# Dry run first — always
.claude/scripts/resolve-conflicts.sh origin/main true

# Then for real
.claude/scripts/resolve-conflicts.sh origin/main false

# Merge a different branch
.claude/scripts/resolve-conflicts.sh origin/feat/payment false

The Full Script

Drop this into .claude/scripts/resolve-conflicts.sh in your repo and make it executable (chmod +x). It's self-contained — no external dependencies beyond bash, git, and optionally the gh CLI for PR status checks.

#!/usr/bin/env bash
# =============================================================================
# resolve-conflicts.sh
# Automated merge-conflict resolution with per-file-type heuristics.
#
# Usage:
#   .claude/scripts/resolve-conflicts.sh [target-branch] [dry-run]
#
# Arguments:
#   target-branch  Branch to merge in. Default: origin/main
#   dry-run        true|false. Default: false
#
# Examples:
#   .claude/scripts/resolve-conflicts.sh                          # merge origin/main, for real
#   .claude/scripts/resolve-conflicts.sh origin/main true        # dry-run first
#   .claude/scripts/resolve-conflicts.sh origin/feat/payment false
# =============================================================================

set -euo pipefail

TARGET_BRANCH="${1:-origin/main}"
DRY_RUN="${2:-false}"

# Colour helpers
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'; NC='\033[0m'
info()    { echo -e "${CYAN}[resolve-conflicts]${NC} $*"; }
success() { echo -e "${GREEN}[resolve-conflicts]${NC} $*"; }
warn()    { echo -e "${YELLOW}[resolve-conflicts] WARN:${NC} $*"; }
error()   { echo -e "${RED}[resolve-conflicts] ERROR:${NC} $*" >&2; }

# ---------------------------------------------------------------------------
# Pre-flight
# ---------------------------------------------------------------------------
if git rev-parse --verify MERGE_HEAD &>/dev/null; then
  error "Repo is already mid-merge (MERGE_HEAD exists)."
  error "Run 'git merge --abort' to cancel the previous merge, then retry."
  exit 1
fi

CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
info "Current branch : $CURRENT_BRANCH"
info "Target branch  : $TARGET_BRANCH"
info "Dry run        : $DRY_RUN"

info "Fetching origin..."
git fetch origin --quiet

# ---------------------------------------------------------------------------
# Stash any uncommitted changes
# ---------------------------------------------------------------------------
STASHED=false
if ! git diff --quiet || ! git diff --cached --quiet; then
  info "Stashing local changes..."
  git stash push -m "resolve-conflicts auto-stash $(date +%s)"
  STASHED=true
fi

# ---------------------------------------------------------------------------
# Attempt the merge
# ---------------------------------------------------------------------------
info "Merging $TARGET_BRANCH into $CURRENT_BRANCH..."
if git merge "$TARGET_BRANCH" --no-edit --no-commit 2>/dev/null; then
  success "Merge completed with no conflicts. Committing..."
  if [ "$DRY_RUN" = "false" ]; then
    git commit --no-edit -m "Merge $TARGET_BRANCH into $CURRENT_BRANCH (no conflicts)"
    git push origin "$CURRENT_BRANCH"
    success "Pushed."
  else
    git merge --abort 2>/dev/null || true
    info "[DRY RUN] No conflicts — merge would succeed cleanly."
  fi
  $STASHED && git stash pop || true
  exit 0
fi

CONFLICTED_FILES="$(git diff --name-only --diff-filter=U)"

if [ -z "$CONFLICTED_FILES" ]; then
  warn "Merge stopped but no conflict markers found. Check git status."
  git merge --abort 2>/dev/null || true
  $STASHED && git stash pop || true
  exit 1
fi

info "Conflicted files:"
echo "$CONFLICTED_FILES" | while read -r f; do echo "  - $f"; done

# ---------------------------------------------------------------------------
# Resolution heuristics
# ---------------------------------------------------------------------------
resolve_file() {
  local file="$1"
  local strategy="" reason=""

  case "$file" in
    pom.xml|*/pom.xml)
      strategy="ours";  reason="pom.xml — keep branch (feature deps)" ;;
    *.properties)
      strategy="ours";  reason="*.properties — keep branch (feature config)" ;;
    *.yaml|*.yml)
      strategy="ours";  reason="*.yaml/yml — keep branch (feature config)" ;;
    docker-compose*|*/docker-compose*)
      strategy="ours";  reason="docker-compose — keep branch (feature topology)" ;;
    *.md)
      strategy="union"; reason="*.md — keep both (documentation is additive)" ;;
    *.java)
      strategy="ours";  reason="*.java — keep branch (active feature work)" ;;
    *.js|*.ts|*.tsx|*.jsx)
      strategy="ours";  reason="*.js/ts — keep branch (active feature work)" ;;
    *)
      strategy="ours"
      reason="unknown type — defaulting to branch; MANUAL REVIEW RECOMMENDED"
      warn "No rule for: $file — keeping branch version. Please verify manually." ;;
  esac

  if [ "$DRY_RUN" = "true" ]; then
    info "[DRY RUN] $file$strategy  ($reason)"
    return
  fi

  info "Resolving: $file$strategy  ($reason)"

  if [ "$strategy" = "ours" ]; then
    git checkout --ours -- "$file"
    git add "$file"
  elif [ "$strategy" = "union" ]; then
    local ours theirs
    ours="$(git show :2:"$file" 2>/dev/null || true)"
    theirs="$(git show :3:"$file" 2>/dev/null || echo "")"
    { echo "$ours"; echo ""; echo "---"; echo "<!-- merged from $TARGET_BRANCH -->"; echo ""; echo "$theirs"; } > "$file"
    git add "$file"
  fi
}

while IFS= read -r file; do
  [ -n "$file" ] && resolve_file "$file"
done <<< "$CONFLICTED_FILES"

# ---------------------------------------------------------------------------
# Commit and push
# ---------------------------------------------------------------------------
if [ "$DRY_RUN" = "true" ]; then
  info "[DRY RUN] Complete. No files were changed."
  git merge --abort 2>/dev/null || true
  $STASHED && git stash pop || true
  exit 0
fi

STILL_CONFLICTED="$(git diff --name-only --diff-filter=U 2>/dev/null || true)"
if [ -n "$STILL_CONFLICTED" ]; then
  error "Some files are still unresolved — resolve manually, 'git add', then 'git commit'."
  echo "$STILL_CONFLICTED"
  $STASHED && git stash pop || true
  exit 1
fi

COMMIT_MSG="Merge $TARGET_BRANCH into $CURRENT_BRANCH (auto-resolved)

Heuristics applied:
$(echo "$CONFLICTED_FILES" | while read -r f; do echo "  - $f"; done)

Generated by resolve-conflicts.sh"

git commit -m "$COMMIT_MSG"
success "Merge commit created."

git push origin "$CURRENT_BRANCH"
success "Pushed to origin/$CURRENT_BRANCH."

if $STASHED; then
  info "Restoring stashed changes..."
  git stash pop || warn "Stash pop failed — run 'git stash list' to inspect."
fi

if command -v gh &>/dev/null; then
  info "Checking PR status (GitHub may take ~2 min to recompute)..."
  gh pr view --json mergeable,mergeStateStatus 2>/dev/null \
    | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'  mergeable={d[\"mergeable\"]}  state={d[\"mergeStateStatus\"]}')" \
    || true
fi

success "Done. Run 'git log --oneline -3' to verify the merge commit."

Save it, make it executable, and you're ready:

mkdir -p .claude/scripts
# paste the script above into .claude/scripts/resolve-conflicts.sh
chmod +x .claude/scripts/resolve-conflicts.sh


Merge conflicts aren't going away. As long as multiple people work on the same codebase, there will be divergence. But the amount of manual, repetitive work they require is entirely within your control. Codify your team's conventions, automate the mechanics, and save the human judgment for the conflicts that actually need it.

Your future Friday self will thank you.