aura merge

AST-level three-way merge that resolves conflicts at the function boundary, not the line.

Synopsis

aura merge <branch> [--strategy=semantic|prefer-ours|prefer-theirs] [--dry-run] [--no-commit] [--continue] [--abort]

Description

aura merge integrates changes from another branch into the current branch, operating on the logic graph rather than on raw text lines. Where git merge would produce a line-level conflict marker any time two developers touched the same hunk of a file, aura merge asks a finer-grained question: did we touch the same logic node? If two developers modified different functions in the same file, the merge completes cleanly. If they modified the same function, the conflict is reported on the function — with semantic before/after bodies — rather than as an interleaved line diff.

The merge takes a three-way view. The base is the nearest common ancestor in the Aura semantic index (not the Git merge-base; these usually agree but can differ after rewinds). Ours is the current branch's logic graph; theirs is the graph at the tip of the named branch. For each logic node, Aura classifies the change as one of: both unchanged, one side modified, both sides modified identically, both sides modified differently (the only true conflict), deleted by one side and modified by the other (a delete-modify conflict), or renamed on one side (follow the rename and re-classify).

When there are no true conflicts, aura merge produces a single merge commit with two parents, identical to a Git merge from the outside, but carrying an aura.merge.graph trailer describing which logic nodes came from which side. When there are conflicts, the command stops and writes a MERGE_AURA state file that aura status will surface.

Flags

| Flag | Description | | --- | --- | | --strategy=semantic | Default. Conflict only when both sides modified the same logic node. | | --strategy=prefer-ours | Auto-resolve every function-level conflict in favor of the current branch. | | --strategy=prefer-theirs | Auto-resolve every function-level conflict in favor of the incoming branch. | | --dry-run | Print the merge plan without modifying the working tree. | | --no-commit | Apply the merge to the working tree and index but do not create the merge commit. | | --continue | Resume a paused merge after all conflicts have been resolved. | | --abort | Discard the in-progress merge and restore the pre-merge HEAD. | | --into <branch> | Merge into a branch other than the current one (checks it out first). |

Conflict Output

A function-level conflict looks like this:

CONFLICT (semantic): fn PaymentGateway::charge
  base:   src/payments/gateway.rs @ a17f3c1
  ours:   modified on main
          - added: retry_on_network_error
          - added: log_audit_trail
  theirs: modified on feat/3ds
          - added: run_3ds_challenge
          - removed: legacy_cvv_check

Resolution options:
  [o] keep ours     [t] keep theirs     [b] keep both (both-sides merge)
  [e] open $EDITOR on a 3-way AST template
  [s] suggest       — ask Aura to synthesize a merge (experimental)

>

Choosing [b] is the common case: Aura will emit the function twice with distinct suffixes (charge_ours, charge_theirs) and leave callsite rewiring to you. Choosing [e] opens an editor with a scaffolded function showing both sides' bodies as annotated regions.

Differences from git merge

  • Line moves do not conflict. If you moved a function to a new file on one side and modified it on the other, git merge usually conflicts; aura merge follows the logic-node identity and merges cleanly.
  • Renames do not conflict. A rename on one side is treated as metadata; the modification on the other side is applied to the renamed node.
  • Import reorderings do not conflict. Import/use statements are canonicalized before diffing.
  • Whitespace-only changes are invisible. The AST ignores them entirely.
  • Deleted-and-modified is surfaced loudly. Git's default is to keep the modified version with a warning; Aura stops the merge and asks.

Examples

1. A clean semantic merge

aura merge feat/webhooks
Merging feat/webhooks into main
  common ancestor:  c4a19e0
  logic nodes:      ours 412, theirs 418, base 408
  changes:          +12 theirs, +6 ours, 0 overlapping
  result:           418 nodes, no conflicts
Created merge commit 9b7a221.

2. Seeing the plan first

aura merge feat/billing --dry-run

Prints a classification table for every affected logic node without touching the working tree. Useful before a large merge to see whether a conflict storm is coming.

3. Resolving with a strategy

aura merge hotfix/rate-limit --strategy=prefer-theirs

Any function that both sides modified is resolved in favor of hotfix/rate-limit. Functions only changed on the current branch are kept. This is much safer than git merge -X theirs because non-overlapping changes are preserved on both sides — the strategy only fires on true semantic conflicts.

4. Aborting a bad merge

aura merge feat/storage
# ... 14 conflicts, you decide this is the wrong time ...
aura merge --abort

Restores HEAD, the index, and the working tree exactly as they were before the merge started. Any snapshots taken during conflict resolution are preserved and visible via aura snapshot list.

5. Continuing after manual resolution

aura merge feat/search
# ... 2 conflicts ...
$EDITOR src/search/index.rs       # resolve fn build_index
aura save --paths src/search/index.rs --intent "Merge feat/search: keep ours for build_index, take theirs for query"
aura merge --continue

Note that you commit the resolution with aura save, not git commit. This preserves the semantic record of which side each function came from.

Exit Codes

| Code | Meaning | | --- | --- | | 0 | Merge completed, commit created (or would be, with --no-commit). | | 1 | Generic failure. | | 2 | Conflicts remain. Resolve them and run aura merge --continue. | | 3 | --continue invoked with no merge in progress. | | 4 | --abort succeeded (returned as 0 in some shells; treat as informational). | | 5 | Target branch does not exist. |

The three-way algorithm in more detail

For each logic node that exists in any of base, ours, or theirs, Aura determines a state tuple (in_base, in_ours, in_theirs) and a hash tuple for the bodies. The state tuple has eight possible values; combined with the hash comparisons, it produces the following classification matrix.

| State | Body relation | Classification | Resolution | | --- | --- | --- | --- | | in all three | all equal | unchanged | take either | | in all three | ours differs from base | one-side modified (ours) | take ours | | in all three | theirs differs from base | one-side modified (theirs) | take theirs | | in all three | both differ from base, equal to each other | converged | take either | | in all three | both differ from base and from each other | conflict | prompt | | in base and ours only | body equal to base | deleted by theirs, unchanged in ours | take the delete | | in base and ours only | body differs from base | delete-modify conflict | prompt | | in base and theirs only | symmetric to above | symmetric | symmetric | | in ours and theirs only | bodies equal | added independently, converged | take either | | in ours and theirs only | bodies differ | add/add conflict | prompt | | in ours only | — | added on our side | take ours | | in theirs only | — | added on their side | take theirs |

Renames are applied before classification: if a node was renamed on one side, the renamed node is reunited with its pre-rename identity so the classification uses identity, not name.

Configuration

aura.toml can tune the merge under [merge]:

[merge]
default_strategy    = "semantic"
auto_resolve_docs   = true   # doc-comment-only conflicts auto-resolve by combining both
auto_resolve_imports= true   # import/use reordering conflicts auto-resolve
prompt_on_delete_modify = true
editor_template     = "three-way"

With auto_resolve_docs, a conflict where both sides only changed the doc comment on the same function is resolved by concatenating both comments with a divider. This is a pragmatic convenience: documentation conflicts are common and rarely important.

Merge commits and the Git graph

The commit that aura merge produces is a standard Git merge commit — two parents, a merge message, no surprises for tools like git log or gh pr view. The semantic metadata lives in the commit trailer and in .aura/merges/<sha>.json, which records the per-node resolution decisions. This file is checked in alongside the commit so later reviewers can see why a particular side was chosen for each conflicting node.

If you rebase over an Aura merge commit, the metadata file is preserved and re-parented automatically. If you squash a merge, the metadata is aggregated into the resulting single commit's trailer.

Post-merge checks

Once the merge commit exists, Aura runs the same hooks as aura save against the merge's effective diff. Layer violations in the merged graph that did not exist on either parent are flagged — this is how refactors on two branches that are each valid locally can silently introduce cross-layer leaks when combined. Fix them with a follow-up save.

See Also

  • aura save — used to commit merge resolutions
  • aura pull — pull + merge in one step
  • aura diff — inspect a pending merge's logic-node changes
  • aura status — shows MERGE_AURA in-progress state