Git Merge Conflict Resolution Guide | Strategies, Tools & Case Study

Git Merge Conflict Resolution Guide | Strategies, Tools & Case Study

이 글의 핵심

Step-by-step Git merge conflict resolution: minimize surprises, fix markers safely, and validate with tests—includes a long-running refactor case study.

Introduction

After three months on refactor/new-architecture, merging into main caused conflicts in hundreds of files. This post explains how we resolved them systematically.

What you will learn

  • Strategies for large branch merges
  • How to reduce conflict volume
  • Phased resolution techniques
  • Testing strategy after integration

Table of contents

  1. Context: three-month refactor branch
  2. First merge attempt fails
  3. Strategy: phased integration
  4. Step 1: merge main into the refactor branch
  5. Step 2: classify conflicts
  6. Step 3: auto-resolvable conflicts
  7. Step 4: manual conflicts
  8. Step 5: test and verify
  9. Step 6: merge to main
  10. Lessons: conflict minimization
  11. Closing thoughts

1. Context

Refactor scope

  • Branch: refactor/new-architecture
  • Duration: Dec 2025 – Mar 2026 (~3 months)
  • Changes:
    • Directory layout (src/app/)
    • Renames (UserManagerUserService)
    • Dependency injection
    • Tests rewritten

main kept moving

  • ~20 new features
  • ~50 bug fixes
  • ~10 dependency bumps

2. First merge attempt

$ git checkout refactor/new-architecture
$ git merge main

CONFLICT (content): Merge conflict in src/user_manager.cpp
...
Automatic merge failed; fix conflicts and then commit the result.
$ git status | grep "both modified" | wc -l
247

Problem: resolving 247 files in one sitting is not realistic.


3. Strategy

  1. Merge main into the refactor branch first (not the reverse on a dirty main)
  2. Categorize conflicts
  3. Trivial / mechanical first
  4. Logic conflicts one by one
  5. Green tests before merging to main

Why merge into the feature branch?

# Preferred: on refactor branch
$ git checkout refactor/new-architecture
$ git merge main
# Fix here; main stays healthy until the end

# Risky: merge huge branch straight into main first
$ git checkout main
$ git merge refactor/new-architecture
# main can be broken for a long time during resolution

4. Step 1: merge main in

$ git checkout refactor/new-architecture
$ git merge main --no-commit --no-ff

$ git status > conflicts.txt

Counts

$ grep "both modified" conflicts.txt | wc -l
189

$ grep "deleted by us" conflicts.txt | wc -l
34

$ grep "added by them" conflicts.txt | wc -l
24

5. Step 2: prioritize

Example classifier:

import subprocess

conflicts = subprocess.check_output(
    ['git', 'diff', '--name-only', '--diff-filter=U']
).decode().splitlines()

categories = {
    'rename': [],
    'trivial': [],
    'logic': [],
    'delete': [],
}

for file in conflicts:
    if 'test' in file:
        categories['trivial'].append(file)
    elif file.endswith('.h') or file.endswith('.hpp'):
        categories['rename'].append(file)
    else:
        categories['logic'].append(file)

Rough outcome: renames, trivial (imports/tests), logic, delete/modify.


6. Step 3: mechanical fixes

Import path conflicts

Prefer the new layout from the refactor:

$ git checkout --ours src/some_file.cpp

Tests fully rewritten on refactor

$ git checkout --ours tests/*.cpp

Batch

$ for file in $(cat trivial_conflicts.txt); do
    git checkout --ours "$file"
    git add "$file"
done

7. Step 4: manual merges

Both sides changed behavior

Merge refactor structure + main features (e.g. caching):

class UserService {
    std::shared_ptr<Database> db_;
    std::unordered_map<int, User> cache_;
    
public:
    User getUser(int id) {
        if (auto it = cache_.find(id); it != cache_.end()) {
            return it->second;
        }
        auto user = db_->query("SELECT * FROM users WHERE id = ?", id);
        cache_[id] = user;
        return user;
    }
};

8. Step 5: verify

$ cmake --build build
$ cd build && ctest
$ ./integration_tests.sh
$ ./benchmark.sh

Commit the integration

$ git add .
$ git commit -m "Merge branch 'main' into refactor/new-architecture

Resolved conflicts; preserved main features; tests green."

9. Step 6: merge to main

Open PR, review conflict resolutions, then:

$ git checkout main
$ git merge refactor/new-architecture --no-ff

$ git push origin main

10. Lessons

Takeaways

  1. Sync often—merge main weekly (or rebase if policy allows)
  2. Split work—multiple smaller PRs when possible
  3. Classify conflicts (trivial vs logic)
  4. Never skip tests after resolution

Long-running branches

$ git checkout refactor/new-architecture
$ git merge main
# small, frequent integrations beat rare huge ones

Tips

  • Separate rename-only commits from logic commits
  • .gitattributes for lockfiles/generated assets
  • git diff --check and grep '<<<<<<< HEAD' before commit

11. Conflict patterns

Same function, both edited

Combine validation from main with new names/types from refactor.

File moved on refactor, edited on main

$ git show main:src/user_manager.cpp > /tmp/main_version.cpp
# Port new methods into app/user_service.cpp
$ git rm src/user_manager.cpp
$ git add app/user_service.cpp

Include paths

Unify on new paths; re-home any new headers from main.


12. Tools

VS Code merge editor, vimdiff, merge.conflictstyle diff3.


Closing thoughts

  1. Merge main into the feature branch to protect main
  2. Classify for throughput
  3. Resolve in phases to reduce mistakes
  4. Test to prevent regressions

Don’t try to resolve everything in one undifferentiated batch.


FAQ

Q1. merge vs rebase on long branches?

Often merge for shared long branches; rebase rewrites history.

Q2. Too many conflicts?

Split the branch: structure first, renames next, behavior last—multiple PRs.

Q3. New files on main?

Port them into the new layout on the refactor branch.


  • Git branch and merge
  • Git rebase
  • Git remote collaboration

Checklists

Large merge

  • Backup branch
  • Count conflicts
  • Classify
  • Trivial first
  • Manual one by one
  • Build
  • Tests
  • Review
  • Merge
  • Monitor

Per-file resolution

  • No conflict markers left
  • Both sides’ intent preserved where needed
  • Build + targeted tests
  • Final git diff sanity check

Keywords

Git, merge conflict, refactoring, large merge, branch strategy, rebase, collaboration, case study, code review