C++ • Getting Started and Tooling

Resolving Merge Conflicts — Resolve simple merge conflicts in text files (intro)

Learn to identify, understand, and resolve the textual merge conflicts that arise when Git cannot automatically reconcile divergent changes.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 4 figures

SECTION 1

Historical Context & Motivation

Software development has always been a collaborative enterprise, and the challenge of merging work from multiple contributors dates back to the earliest shared computing environments. In the 1970s and 1980s, programmers working on large C and C++ codebases frequently encountered situations where two people modified the same file simultaneously, leading to lost work or subtle bugs that went undetected for weeks. Early version control systems (VCS) such as SCCS and RCS addressed this by implementing exclusive file locking — only one developer could edit a file at a time. While this prevented conflicts entirely, it severely limited parallel productivity on large projects.

The introduction of CVS in the late 1980s pioneered the optimistic concurrency model: multiple developers could edit the same file concurrently, and the system would attempt to merge changes automatically. When the automatic merge failed — typically because two developers modified the same lines — a merge conflict was reported, and the developer was responsible for resolving it manually. This model proved so successful that it carried forward through Subversion, Mercurial, and ultimately Git, which Linus Torvalds created in 2005 to manage the Linux kernel — a massive C codebase with thousands of contributors.

1972 • SCCS — File Locking Model

Marc Rochkind develops the Source Code Control System at Bell Labs, introducing exclusive file locks to prevent simultaneous edits — effective but restrictive for team workflows.

1986 • CVS — Optimistic Merging

Dick Grune releases CVS, which allows concurrent edits and attempts automatic three-way merging. When merging fails, conflict markers are inserted into the file for manual resolution.

2000 • Subversion Refines Conflict UX

Subversion (SVN) improves on CVS with atomic commits and clearer conflict reporting, but still relies on the same textual conflict marker format that developers resolve by hand.

2005 • Git — Distributed Branching

Linus Torvalds creates Git for Linux kernel development. Its lightweight branching model makes merging routine, but also makes merge conflicts a daily occurrence that every developer must understand.

2018+ • Modern Tooling & AI Assist

IDEs like VS Code, CLion, and JetBrains offer inline three-way merge editors. AI-assisted conflict resolution tools emerge, but understanding the underlying mechanics remains essential.

Today, merge conflicts are an unavoidable part of collaborative C++ development. Whether you are working on a class project with a teammate or contributing to an open-source library, the question is not if you will encounter a conflict, but when. Understanding what causes these conflicts and how to resolve them cleanly is a foundational skill in professional software engineering.

SECTION 2

Core Principles of Merge Conflicts

A merge conflict occurs when Git's automatic merge algorithm encounters competing changes to the same region of a file on two different branches. Git performs a three-way merge by comparing each branch's version of the file against the common ancestor — the version that existed before the branches diverged. When both branches modify the same lines relative to the ancestor, Git cannot determine which change the developer intends to keep, so it halts and marks the file as conflicted. Understanding this three-way comparison is the key to understanding why conflicts arise and how to resolve them.

1

Common Ancestor (Base)

The shared commit from which both branches diverged. Git uses this as the reference point to determine what each branch changed, rather than just comparing the two branches against each other.
2

Conflict Markers

When a conflict occurs, Git inserts three markers into the file: <<<<<<< (current branch), ======= (separator), and >>>>>>> (incoming branch). These delimit the conflicting regions.
3

Resolution Strategies

You can keep the current branch's version, accept the incoming version, manually combine both changes into a new version, or discard both and write something entirely new. The choice depends on the semantic intent of each change.
4

Staging the Resolution

After editing the file to remove all conflict markers, you stage it with git add to tell Git the conflict is resolved, then complete the merge with git commit.
✦

KEY TAKEAWAY

Think of a merge conflict like two editors simultaneously revising the same paragraph of a manuscript. If one editor rewrites the opening sentence while the other rewrites the closing sentence, the changes can be combined automatically — no conflict. But if both editors rewrite the same sentence, the publisher (Git) must ask the author (you) which revision to keep, or whether to blend both into a new sentence.

SECTION 3

Visualizing the Three-Way Merge

The following diagram illustrates the branching and merging process that leads to a conflict. Two branches, main and feature, diverge from a common ancestor commit. Both branches modify overlapping lines in the same file, which produces a conflict when the developer attempts to merge feature into main.

Three-Way Merge: Conflict ScenarioACommon Ancestorint x = 10;Bmainint x = 20;Cint x = 20;Dfeatureint x = 42;Eint x = 42;MERGE⚠ CONFLICTConflicted File<<<<<<< HEADint x = 20;=======int x = 42;>>>>>>> feature
Both branches modify the same line (int x = 10;) from the common ancestor. The main branch changes it to 20 while feature changes it to 42. Git cannot decide which is correct, so it inserts conflict markers into the file.

In the diagram above, commit A is the common ancestor containing int x = 10;. The cyan path represents the main branch (commits B and C), which changes the value to 20. The pink path represents the feature branch (commits D and E), which changes the value to 42. When Git attempts the merge, it detects that the same line was changed differently in both branches relative to A, producing the conflict shown in the red box. The three conflict markers — <<<<<<<, =======, and >>>>>>> — are the textual structures you must understand to resolve the conflict.

SECTION 4

How Git Detects and Marks Conflicts

Git's merge algorithm operates at the granularity of text hunks — contiguous groups of lines that differ between versions. When you run git merge feature while on main, Git identifies the merge base (common ancestor), then computes two diffs: base→main and base→feature. If a hunk is changed in only one diff, Git applies that change automatically. If the same hunk is changed in both diffs but in different ways, Git declares a conflict. This is fundamentally a line-based diff comparison, which is why conflicts are particularly common in C++ header files where multiple developers may add #include directives or modify class declarations in the same region.

Anatomy of a Conflict-Marked File

When a conflict is detected, Git writes a modified version of the file to your working directory. The conflicted region is delimited by three markers, each on its own line. The structure is precise: <<<<<<< HEAD marks the beginning of your current branch's version (the branch you are on when you issue the merge command). The ======= separator divides the two versions. Below it is the incoming branch's version, terminated by >>>>>>> feature (where "feature" is replaced by the actual branch name or commit hash).

💡

diff3 Mode — Seeing the Ancestor

You can configure Git to show the common ancestor's version as well by running git config merge.conflictstyle diff3. This inserts a fourth marker, ||||||| merged common ancestors, between the HEAD section and the separator. Seeing the ancestor makes it much easier to understand the intent behind each change — highly recommended for C++ projects where the context of a change is critical.

The Resolution Workflow

  1. Detect: Run git merge <branch>. Git reports CONFLICT (content): Merge conflict in <file> and sets the merge state to "in progress."
  2. Inspect: Open the conflicted file in your editor. Search for <<<<<<< to locate each conflict region. A single file may contain multiple conflict regions.
  3. Edit: Decide on the correct resolution — keep one side, combine both, or write a new version. Delete all three conflict markers entirely.
  4. Stage: Run git add <file> to mark the conflict as resolved for that file.
  5. Commit: Run git commit to finalize the merge commit. Git pre-populates the commit message with merge details.

SECTION 5

Anatomy of a C++ Merge Conflict

Merge conflicts in C++ files tend to occur in predictable locations. Header files with #include blocks are frequent conflict sites because multiple developers often add new includes in the same alphabetically-sorted region. Class declarations are another hotspot, especially when two developers add new member functions or modify constructor signatures. Understanding the typical conflict patterns helps you resolve them faster and with greater confidence.

Conflict Region Anatomy in a C++ Source Filemain (HEAD)1 #include <iostream>2 #include <string>3 4 int compute(int a) {5 return a * 2;6 }feature1 #include <iostream>2 #include <string>3 4 int compute(int a) {5 return a + 100;6 }Conflicted Working Copy1 #include <iostream>2 #include <string>3 4 int compute(int a) {5 <<<<<<< HEAD6 return a * 2;7 =======8 return a + 100;9 >>>>>>> feature10 }✓ Resolved Version (developer chooses to combine both)1 #include <iostream>2 #include <string>3 4 int compute(int a) {5 return (a * 2) + 100; // combined logic6 }
Left: each branch's version of the function. Center-right: the conflicted working copy with markers. Bottom: the resolved version where the developer chose to combine both changes into a single expression. Note that all three conflict markers have been removed in the resolved version.

The diagram highlights the critical detail: lines outside the conflict markers are not in dispute. Lines 1–4 and the closing brace are identical in both branches relative to the ancestor, so Git merges them without issue. Only line 5 — the return statement — differs between the two branches. In the resolved version at the bottom, the developer decided that the correct behavior requires both operations, so they wrote a new expression that incorporates the logic from both branches. This is a common pattern: the resolution is often neither "ours" nor "theirs" but a thoughtful synthesis.

📝

Common C++ Conflict Sites

#include blocks, namespace declarations, class member lists, constructor initialization lists, and CMakeLists.txt target_link_libraries calls are the most frequent conflict locations in C++ projects. Keeping these sections well-organized and consistently formatted reduces conflict frequency significantly.

SECTION 6

Worked Example: Resolving a Conflict in a C++ File

Suppose you and a partner are building a C++ project. You are on the main branch, and your partner's work is on a branch called add-logging. Both of you have modified the greet function in utils.cpp. You run git merge add-logging and Git reports a conflict.

Resolving a Merge Conflict in utils.cpp

  1. 1

    Step 1 — Trigger the Merge

    From the main branch, run git merge add-logging. Git outputs: Auto-merging utils.cpp followed by CONFLICT (content): Merge conflict in utils.cpp. The merge is paused. Running git status shows utils.cpp under "Unmerged paths."
    Git identifies one conflicted file: utils.cpp
  2. 2

    Step 2 — Inspect the Conflict

    Open utils.cpp in your editor. Search for <<<<<<<. You find: <<<<<<< HEAD std::string greet(const std::string& name) { return "Hello, " + name + "!"; } ======= std::string greet(const std::string& name) { std::cout << "[LOG] greet called for: " << name << std::endl; return "Hi, " + name + "!"; } >>>>>>> add-logging The HEAD version changed the greeting text while the incoming branch added a logging statement and used a different greeting.
    Two differences: greeting text ("Hello" vs. "Hi") and the added logging line.
  3. 3

    Step 3 — Decide on the Resolution

    After discussing with your partner, you agree: the logging statement should be kept (that was the purpose of their branch), and the greeting text should be "Hello" (your version is the project standard). This means you need to combine elements from both versions — neither side alone is the correct resolution.
  4. 4

    Step 4 — Edit the File

    Replace the entire conflict block (from <<<<<<< HEAD through >>>>>>> add-logging) with the combined version: std::string greet(const std::string& name) { std::cout << "[LOG] greet called for: " << name << std::endl; return "Hello, " + name + "!"; } Ensure no conflict markers remain. Save the file.
    File is clean — no <<<<<<<, =======, or >>>>>>> markers remain.
  5. 5

    Step 5 — Stage and Commit

    Run git add utils.cpp to mark the conflict as resolved. Then run git commit. Git opens your editor with a pre-populated merge commit message (e.g., "Merge branch 'add-logging'"). Save and close to finalize. Verify with git log --oneline --graph that the merge commit joins both branches.
    Merge complete. The merge commit has two parents — one from main and one from add-logging.

SECTION 7

Resolution Strategies & Trade-offs

Not all merge conflicts require the same resolution approach. Git provides several strategies and tools, each suited to different situations. The table below compares the most common approaches for resolving conflicts in C++ projects, along with their strengths and limitations.

StrategyWhen to UseLimitations
Accept Ours (git checkout --ours)When the incoming change is known to be obsolete, experimental, or incorrect. Quick for bulk-discarding.Silently discards the incoming change. Risk of losing valid work if applied carelessly.
Accept Theirs (git checkout --theirs)When the incoming branch's version is clearly superior or your changes should be superseded.Discards your local changes completely. You must verify your work isn't needed.
Manual EditWhen both sides contribute valid changes and you need to synthesize them into a correct combined version.Requires understanding the semantics of both changes. Time-consuming but most precise.
IDE Merge Tool (VS Code, CLion)For complex conflicts with multiple hunks. Three-pane views show base, ours, theirs side by side.Requires familiarity with the tool. Can be overwhelming for beginners on large diffs.
Abort Merge (git merge --abort)When you're unprepared to resolve, need to consult with teammates, or want to rethink the merge strategy.Does not resolve the conflict — merely postpones it. The conflict will recur on the next merge attempt.

Comparison of common merge conflict resolution strategies

✦

KEY TAKEAWAY

In professional C++ development, the manual edit strategy is by far the most common because both changes usually carry semantic value. Blindly accepting one side — the equivalent of tearing out a page from a co-authored paper — risks introducing bugs that the compiler won't catch. Always read both versions, understand the intent behind each change, and construct a resolution that preserves correctness.

SECTION 8

Connection to Advanced Git Workflows

The simple text-file conflicts covered in this lesson form the foundation for more sophisticated merge scenarios you will encounter in professional C++ projects. As codebases scale — think the Linux kernel with its 30+ million lines of C/C++ — developers employ advanced strategies like rebasing, cherry-picking, and interactive staging that all build on the same conflict resolution mechanics you have just learned.

This Lesson (Intro)Advanced Topic
Two-branch merge with text conflict markersRebase conflicts — same markers, but applied commit-by-commit rather than all at once
Single file, single conflict regionMulti-file, multi-hunk conflicts across an entire project tree
Conflict in source code (text files)Binary file conflicts (object files, images) — cannot be merged textually
Manual resolution in a text editorThree-way merge tools (kdiff3, meld, Beyond Compare) with automatic conflict highlighting
Resolving after the conflict occursConflict prevention — CI pipelines, code ownership (CODEOWNERS), trunk-based development

Introductory concepts mapped to their advanced counterparts

An important nuance to keep in mind is that git rebase replays your commits on top of the target branch one at a time, so you may encounter sequential conflicts — one for each commit that touches the same lines. The resolution process for each individual conflict is identical to what you have practiced here, but the overall workflow requires additional commands (git rebase --continue, git rebase --abort). Mastering simple merge conflicts now will make these advanced scenarios straightforward when you encounter them.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

Explain why Git uses a three-way merge (comparing both branches against a common ancestor) rather than a simple two-way diff between the two branch tips. What information would be lost if Git only compared the two branch tips?
Show answer
A two-way diff between the branch tips would show all lines that differ between the two files, but it cannot determine which branch introduced which change. By including the common ancestor (the base), Git can compute two separate diffs — base→ours and base→theirs — and determine which branch modified any given line. If only one branch changed a line, Git applies that change automatically. If both branches changed the same line, Git knows a conflict exists. Without the ancestor, Git would flag every difference between the two tips as a potential conflict, even if one side never changed the line at all. This would result in far more false-positive conflicts and a much worse developer experience.

Question 2: BASIC CALCULATION

Given the following conflicted file content, write the resolved version assuming you want to keep the feature branch's version: #include <vector> <<<<<<< HEAD int max_val = 100; ======= int max_val = 255; >>>>>>> feature std::vector<int> data; What Git commands would you run after editing the file?
Show answer
The resolved file should read: #include <vector> int max_val = 255; std::vector<int> data; All three conflict markers (<<<<<<<, =======, >>>>>>>) must be removed. After saving the file, run git add <filename> to stage the resolved file, then git commit to complete the merge. Alternatively, you could have used git checkout --theirs <filename> to accept the incoming version without manual editing, since the entire conflict region should take the feature branch's value.

Question 3: INTERMEDIATE

Consider this conflicted C++ header file with two separate conflict regions: #pragma once <<<<<<< HEAD #include <string> #include <map> ======= #include <string> #include <unordered_map> >>>>>>> optimize-lookups class Cache { public: <<<<<<< HEAD std::map<std::string, int> store; ======= std::unordered_map<std::string, int> store; >>>>>>> optimize-lookups }; The optimize-lookups branch intentionally replaces std::map with std::unordered_map for performance. Write the correct resolved file. Explain why both conflict regions must be resolved consistently.
Show answer
The resolved file should be: #pragma once #include <string> #include <unordered_map> class Cache { public: std::unordered_map<std::string, int> store; }; Both conflict regions must be resolved consistently because the #include directive and the type declaration are semantically coupled. If you accept the std::unordered_map type for the member variable but keep #include <map> instead of #include <unordered_map>, the code will fail to compile because the std::unordered_map type is declared in the <unordered_map> header. This demonstrates why merge conflict resolution is not purely a textual task — you must consider the semantic relationships between different parts of the code.

Question 4: APPLIED

You are working on a team C++ project managed with Git. Your teammate merged their branch first, and now your branch has 5 conflicts across 3 files (main.cpp, utils.h, CMakeLists.txt). Describe a systematic workflow for resolving all conflicts. What commands would you use to check your progress? How would you verify correctness after resolution?
Show answer
A systematic workflow: (1) Run git status to see all unmerged files listed under "Unmerged paths." (2) Tackle one file at a time. Open the first file, search for all <<<<<<< markers to identify conflict regions. Resolve each region, ensuring no markers remain. (3) After resolving a file, run git add <file> and then git status again to confirm it moved from "Unmerged" to "Changes to be committed." (4) Repeat for all three files. (5) Before committing, build the project (cmake --build build) to catch compilation errors from inconsistent resolutions. Run the test suite if available. (6) Use grep -rn '<<<<<<' . to verify no conflict markers remain anywhere in the project. (7) Finally, run git commit to finalize the merge.

Question 5: CRITICAL THINKING

Some teams adopt a "rebase before merge" policy where developers rebase their feature branches onto the latest main before creating a pull request. Others use a "merge commit" workflow. Analyze how each approach affects the frequency and difficulty of merge conflicts. Under what circumstances might rebasing actually increase the total number of conflict resolutions a developer must perform?
Show answer
In a merge commit workflow, conflicts are resolved once during the final merge. In a rebase workflow, Git replays each commit from the feature branch onto the target, so if multiple commits touch the same conflicting lines, the developer may need to resolve the same conflict multiple times — once per conflicting commit. This is particularly likely in long-lived feature branches with many small commits that incrementally modify the same function or file. However, rebasing produces a linear history that is easier to read and bisect. The trade-off is workflow complexity versus history clarity. Teams working on large C++ projects (e.g., game engines, compilers) often prefer rebase for individual feature work but use merge commits for integration branches. A practical mitigation is to squash related commits before rebasing with git rebase -i, reducing the number of replay steps and thus the number of potential conflicts. Ultimately, the best approach depends on branch lifetime, commit granularity, and team conventions.

SUMMARY

Lesson Summary

Summary

A merge conflict occurs when Git's three-way merge algorithm detects that two branches have modified the same lines of a file relative to their common ancestor. Git marks conflicted regions with conflict markers — <<<<<<<, =======, and >>>>>>> — that delimit the current branch's version (HEAD) and the incoming branch's version. Your responsibility is to edit the file to produce the correct combined version, remove all markers, then stage with git add and finalize with git commit.

The four primary resolution strategies are: accept ours, accept theirs, manual edit (the most common in practice), and IDE-assisted three-way merge. In C++ projects, conflicts frequently appear in #include blocks, class declarations, and build configuration files. Always resolve semantically related conflict regions consistently, build the project after resolution to verify correctness, and search for leftover conflict markers before committing. These foundational skills transfer directly to advanced workflows like rebasing and cherry-picking, where the same conflict markers and resolution process apply.

Varsity Tutors • C++ • Resolving Merge Conflicts — Resolve simple merge conflicts in text files (intro)