Semantic Merging vs Git Merge: Why Line-Based Diffs Fail for AI Code
Git Merge Was Designed for Humans
When Linus Torvalds wrote Git in 2005, code changes looked like this: a developer opens a file, changes a few lines, saves, commits. The diff is small, localized, and sequential. Git's merge algorithm — comparing lines of text, detecting insertions and deletions, flagging overlapping regions — was a perfect fit.
Twenty years later, the primary authors of code are no longer humans typing line by line. AI agents rewrite entire functions at once. They refactor across files in a single pass. They generate 200 lines of new code where a human would have written 20. And Git's line-based merge, designed for the small sequential edits of human developers, breaks in ways that are both predictable and painful.
How Git Merge Actually Works
Git's merge strategy (the default "recursive" strategy) operates on text:
- Find the common ancestor of two branches
- Compute a line-level diff between the ancestor and each branch
- If the diffs touch different lines, combine them automatically
- If the diffs touch the same lines (or lines within a few lines of each other), declare a conflict
This is elegant and fast. It's also completely ignorant of what the lines mean. Git doesn't know that line 47 is the start of a function. It doesn't know that line 82 is a type definition. It doesn't know that a change on line 30 in file A breaks a call on line 15 in file B. It just compares text.
Where Line-Based Diffs Fail
False Conflicts: Different Functions, Same File
Agent A adds input validation to create_user(). Agent B adds rate limiting to delete_user(). Both functions are in user_handler.rs. Git sees two changes to the same file, and if the functions are close together (or if either agent reformatted nearby code), Git reports a conflict.
This is the single most common merge failure in multi-agent workflows. Agents frequently touch the same files because related functionality lives together. But they're rarely editing the same functions.
Missed Conflicts: Different Files, Same Symbol
Agent A renames a function from get_user to fetch_user in user.rs. Agent B adds a new call to get_user in api.rs. Git merges these cleanly — different files, no overlapping lines. The code compiles. The old function name doesn't exist anymore. The call fails at runtime.
Line-based diffing can't detect cross-file semantic dependencies. It doesn't know that get_user and fetch_user are related, or that a call site in another file is now broken.
False Clean Merges: Reformatted Code
Agent A reformats auth.rs to comply with a new style guide — adjusting indentation, reordering imports, adding blank lines. Agent B makes a substantive change to a function in auth.rs. Git sees massive text-level differences from Agent A and tries to merge Agent B's changes into the reformatted file. Sometimes it succeeds (producing corrupted code). Sometimes it flags a conflict (even though the substantive changes don't overlap).
AI agents frequently reformat code as a side effect of their edits. This creates enormous noise in line-based diffs.
How Semantic Merging Works
Semantic merging operates on the Abstract Syntax Tree (AST) — the structural representation of code that compilers use internally. Instead of comparing lines of text, it compares meaningful code elements: functions, types, constants, imports, and their relationships.
The process:
- Parse both versions into ASTs using language-aware parsers (tree-sitter for speed, full compiler frontends for precision)
- Diff at the symbol level — identify which functions were added, modified, deleted, or renamed
- Check for structural conflicts — two agents modifying the same function is a conflict; two agents modifying different functions is safe
- Check for dependency conflicts — an agent deleting a function that another agent calls is a conflict, even if the changes are in different files
- Merge compatible changes by combining the AST modifications and regenerating the source
The Comparison
Here's how Git merge and semantic merge behave in the same scenarios:
| Scenario | Git Merge | Semantic Merge |
|---|---|---|
| Two agents modify different functions in the same file | CONFLICT — overlapping line regions | Auto-merge — different symbols, no dependency |
| Two agents add different fields to the same struct | CONFLICT — adjacent line changes | Auto-merge — non-overlapping additions |
| Two agents modify different parts of the same function | CONFLICT — same line region | Auto-merge — if AST nodes don't overlap |
| Agent A deletes a function, Agent B calls it | Clean merge — different files | CONFLICT — dependency violation caught |
| Two agents add the same import statement | CONFLICT — same line | Deduplicate — identical additions collapsed |
| Agent reformats file, other agent edits function | CONFLICT or corrupted merge | Auto-merge — formatting is cosmetic, not structural |
| Two agents rewrite the same function entirely | CONFLICT | CONFLICT — true structural conflict |
The pattern: semantic merging produces fewer false conflicts and catches more true conflicts. It's strictly more accurate than line-based diffing for determining whether two changes are compatible.
Why This Matters More for AI Agents
Human developers write small, incremental changes. A typical human commit touches 5-20 lines across 1-3 files. Git's line-based merge handles this well because the changes are small enough that line proximity is a reasonable proxy for semantic overlap.
AI agents don't work this way. They:
- Rewrite entire functions — an agent asked to "add error handling to login()" will rewrite the whole function, not add a few lines
- Touch many files at once — a refactoring agent might modify 30 files in a single changeset
- Generate boilerplate — agents add imports, type definitions, and utility functions that cluster at the top of files, creating false conflicts with other agents doing the same
- Reformat incidentally — agents often normalize formatting as they edit, creating huge text-level diffs for small semantic changes
Every one of these patterns amplifies the weaknesses of line-based merging. False conflicts multiply. True conflicts hide in clean merges. The merge step becomes the bottleneck of the entire multi-agent workflow.
The Semantic Code Graph
Semantic merging doesn't just compare two ASTs in isolation. It operates on a continuously maintained code graph that tracks:
- Symbol table — every function, class, type, constant, with its signature, visibility, and location
- Call graph — which functions call which, enabling detection of broken call sites across files
- Dependency graph — external packages, which symbols use them, version pins
- Invariant registry — rules that must hold after every merge (e.g., "all public APIs have auth middleware")
When a merge happens, the engine doesn't just check for AST conflicts — it validates the merged result against the full code graph. If Agent A's change breaks an invariant that Agent B's code depends on, the merge is flagged even if the AST diff looks clean.
Performance
Semantic merging sounds expensive — parsing ASTs, building graphs, checking invariants. In practice, it's fast because it's incremental:
- Incremental parsing: only changed files are re-parsed. The AST for unchanged files is cached.
- Targeted graph updates: only affected edges in the call graph and dependency graph are recomputed.
- Merge time: < 50ms for two compatible changesets. That's faster than many Git merges on large repositories.
- Conflict detection: < 10ms. The symbol-level comparison is simpler than line-level diffing because there are fewer symbols than lines.
The engine is implemented in Rust with tree-sitter for incremental parsing, giving it both correctness and speed.
What Semantic Merging Doesn't Solve
Semantic merging handles structural conflicts — two changes to the same symbol, broken call sites, violated invariants. It does not handle:
- Logic conflicts — Agent A assumes users are authenticated before reaching a handler; Agent B removes the auth middleware. Both changes are structurally valid. The conflict is in the logic, not the code structure. This requires verification (running tests) to catch.
- Performance conflicts — Agent A adds a cache. Agent B adds a different cache for the same data. Both are structurally fine. The redundancy is a quality issue, not a merge issue.
- Style conflicts — Agent A uses early returns, Agent B uses nested if/else. The code is functionally identical but structurally different. This is a linting concern, not a merge concern.
These are real problems, but they belong to the verification pipeline, not the merge engine. The key insight is that each layer handles what it's good at: semantic merge handles structural conflicts, verification handles behavioral conflicts, and human review handles design conflicts.
This is the third post in our series on agent-native development. Previously: Session Isolation. Next: Introducing dkod: The Agent-Native Code Platform.
Join the community
Discuss this post, ask questions, and connect with other developers building with AI agents.
Join Discord