Skip to main content

Merging & Conflicts

Atomic merges by inserting changes between views, not by replaying diffs. Because every change is a set of graph operations with explicit context (see Dual-Layer Diff & Semantic Merge), most merges are automatic and order-independent. When two changes are genuinely incompatible, Atomic records a conflict — and it never hides one.

This page covers what actually happens during a merge, the guarantees Atomic makes, what a conflict looks like, how to see it, and how to resolve it.

How merging works

In Git you merge branches by computing and replaying diffs, producing merge commits and, often, conflicts caused by shifted line numbers.

In Atomic, all changes already live in a single canonical graph. A view is a filter over that graph — an ordered set of change references. Merging is atomic insert: it adds a change (and its dependency closure) to another view's filter.

AspectGit merge / cherry-pickAtomic insert
Data movedDiffs replayedNone — edges already in the graph
CostO(diff size)O(1) per change (metadata)
DependenciesManualAutomatic transitive closure
Source branchCherry-pick leaves it aloneSource view is never modified
ConflictsCommon (line-based)Only when changes truly overlap

Because merging is metadata, it is fast and the source view is untouched. A conflict arises only when two changes modify the same token to different values (or touch the same structural position in incompatible ways) — not merely because they edited nearby lines.

The four guarantees

Every merge in Atomic upholds four invariants. These are enforced by the engine and continuously exercised by a scenario test matrix.

  1. No silent duplication — every logical line appears exactly once. (Inside a conflict, each side appears exactly once.)
  2. No false conflict — changes that commute never produce conflict markers.
  3. No lost edit — both sides' content is always present, either merged into the file or preserved inside a conflict.
  4. Honest exit state — if a file contains conflict markers, the repository knows it. atomic status reports it, atomic conflicts lists it, and atomic record refuses to bake the markers into history. A conflict is never reported as "clean."

The fourth guarantee is the important one: a conflict you can't see is worse than a conflict. Atomic keeps three signals in lock-step — markers on disk ⇔ status says Conflicted ⇔ conflicts lists the file — and atomic doctor check audits that they always agree.

What a conflict looks like

When a real conflict occurs, the file is written with conflict markers. Atomic's markers are inverted relative to Git:

>>>>>>> 1
first version of the region
======= 1 [7K3XSCZ3]
second version of the region
<<<<<<< 1
  • >>>>>>> opens the block (the first side).
  • ======= separates the sides. With three or more concurrent sides, the separators nest — one opener, one separator between each side, one closer.
  • <<<<<<< closes the block (the last side).

Shared, unconflicted lines around the block are written normally, exactly once.

Seeing conflicts

atomic status

A conflicted file is reported with the C code, and supersedes any Modified report for the same path:

atomic status --short
C  src/config.rs
M src/main.rs

atomic conflicts

Lists every conflicted file on the current view, with the line where each conflict begins:

atomic conflicts
1 conflicted file:

src/config.rs
order conflict at line 12

Machine-readable form — one line per conflict as <path>:<line>:<kind>:

atomic conflicts --short
src/config.rs:12:order

atomic record refuses to bury a conflict

If a file still contains conflict markers, atomic record will not record it — otherwise the markers would become permanent content:

src/config.rs still contains conflict markers at line 12
(resolve the conflict, or pass --allow-conflict-markers to override)

Resolving a conflict

Resolution is the same working-copy edit you already know:

  1. Open the conflicted file and edit it to the intended result, deleting the markers.
  2. Record the resolution:
atomic record -m "resolve config conflict"

Once the markers are gone, status and conflicts drop the file automatically, and the recorded resolution clears the persisted conflict state. The resolution sticks — switching views away and back does not resurrect the markers.

Verifying integrity

atomic doctor check is a read-only audit of the working copy against the graph. It catches two classes of problem and exits non-zero if any are found:

atomic doctor check
  • Materialization drift — a file status considers clean whose on-disk bytes differ from what the graph would produce.
  • Conflict honesty — the invariant above: on-disk markers, status Conflicted, and conflicts must all agree.

Run it any time you want reassurance that the working copy and the graph tell the same story.

Scenario reference

What Atomic does when two changes converge on the same file:

ScenarioOutcome
Edits in different regions / functionsClean merge, both edits kept
Same line, different tokensClean merge (token-level)
Identical edit made on both sidesDeduplicated — appears once
Same position, different contentConflict, both sides in markers
Three or more concurrent edits at one spotConflict with correctly nested markers
One side deletes a region, the other edits a different lineDeletion applies; the edited line survives
Both sides delete the same fileFile is removed once, cleanly
Same change arriving via two paths (a diamond)Deduplicated by change identity — applied once
Two views independently create the same pathName conflict surfaced (both versions preserved in markers)
Binary / non-text file edited at the same spot on both sidesWhole-file conflict (no token merge)

Renames & moves

Atomic tracks a file by a stable inode, not its path, so renames and moves preserve identity and history. Use atomic move (alias atomic mv) — or just rename on disk and record — and Atomic records the change as a move that reuses the file's inode rather than a delete-plus-add.

The payoff shows up in merges:

  • Rename vs edit — one view renames auth.rsauthentication.rs while another edits a function inside it. Because both operate on the same inode, inserting the rename carries the concurrent edit along to the new path. Clean, no conflict.
  • Cross-view rename — inserting a rename into another view applies it: the file appears at the new path with its content, the old path is gone, and the inode (and its blame/history) is preserved.

Known limitations

Atomic favors an honest, tracked gap over a silent one. The current limitation in this area:

  • Rename vs rename (different targets) — if two views rename the same file to different names concurrently, the merge currently resolves to a single name (last writer wins) rather than surfacing a name conflict. Content is never lost (the file exists under one of the names), but the competing name is dropped without a conflict marker. Surfacing this as a first-class name conflict is in progress.

See also