Git Merge Conflict Resolution Case Study | Merging a 3-Month Refactor Branch
이 글의 핵심
Large-scale Git merge: conflict minimization, phased resolution, and integration testing.
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
- Context: three-month refactor branch
- First merge attempt fails
- Strategy: phased integration
- Step 1: merge main into the refactor branch
- Step 2: classify conflicts
- Step 3: auto-resolvable conflicts
- Step 4: manual conflicts
- Step 5: test and verify
- Step 6: merge to main
- Lessons: conflict minimization
- Closing thoughts
1. Context
Refactor scope
- Branch:
refactor/new-architecture - Duration: Dec 2025 – Mar 2026 (~3 months)
- Changes:
- Directory layout (
src/→app/) - Renames (
UserManager→UserService) - Dependency injection
- Tests rewritten
- Directory layout (
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
- Merge main into the refactor branch first (not the reverse on a dirty main)
- Categorize conflicts
- Trivial / mechanical first
- Logic conflicts one by one
- 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
- Sync often—merge main weekly (or rebase if policy allows)
- Split work—multiple smaller PRs when possible
- Classify conflicts (trivial vs logic)
- 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
.gitattributesfor lockfiles/generated assetsgit diff --checkandgrep '<<<<<<< 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
- Merge main into the feature branch to protect main
- Classify for throughput
- Resolve in phases to reduce mistakes
- 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.
Related posts
- 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 diffsanity check
Keywords
Git, merge conflict, refactoring, large merge, branch strategy, rebase, collaboration, case study, code review