libra merge
Command reference for `libra merge`
Merge one or more targets into the current branch.
Synopsis
libra merge [--ff | --ff-only | --no-ff] [-s <strategy>...] [-X <option>...] [--allow-unrelated-histories] [--log[=<n>] | --no-log] [--squash | --no-commit] [-m <msg>] [--no-verify] [--no-edit] [--stat | -n | --no-stat] [--verify-signatures | --no-verify-signatures] [--no-rerere-autoupdate] [--no-gpg-sign] [--dry-run] [--autostash | --no-autostash] <branch>...
libra merge --continue
libra merge --abort
libra merge --restart
Description
libra merge <branch>... resolves one or more local branches, commit hashes, or remote-tracking refs such as refs/remotes/origin/main. One target uses the single-head path; two or more targets select an octopus merge.
Before starting any merge, Libra checks merge state and the index. Existing
merge state directs you to merge --continue or --abort. If no merge state
exists but unresolved index stages remain—for example after a conflicted
squash—even an already-up-to-date merge or --dry-run fails with
LBR-CONFLICT-002 while preserving HEAD, the index, and working tree.
Resolve and stage the paths, then run a plain libra commit before merging
again.
If the current branch can be fast-forwarded, Libra moves the branch pointer to the target commit and restores the index and working tree. If the branches have diverged, Libra performs a single-head three-way merge using the merge base, or a recursive virtual ancestor when the history has multiple merge bases.
The default single-head strategy is ort; two or more targets still select the automatic octopus path. Human success output names the selected backend. JSON adds selected_strategy while retaining the historical strategy outcome categories.
Strategy selection
| Strategy | Behavior |
|---|---|
ort |
Default single-head three-way backend; recursively folds all merge bases when needed and handles renames. |
recursive |
Behavior-equivalent compatibility alias for ort; output retains the requested name. |
resolve |
Conservative single-head backend: uses the first object-id-ordered merge base, builds no recursive virtual ancestor, and does not handle renames. A rename degrades to delete/add and human output prints a notice naming the paths. |
ours |
Records HEAD and all surviving targets as parents while retaining the current HEAD tree; distinct from -X ours. |
For one target, -s is repeatable. Libra probes strategies in command-line order without repository writes, prints Trying merge strategy <name>... in human mode, and uses the first clean result. If all conflict, it replays the lowest Git evaluate_result score (worktree/index differences plus unmerged index entries); a later strategy wins a tie. The backend is persisted so --restart reproduces it.
For several targets, omit -s for automatic octopus, or use one -s ours. Explicit -s octopus, single-head strategies for several targets, external strategies, and subtree are not implemented. -s cannot be combined with -X.
Clean three-way merges create a two-parent merge commit, update HEAD, rebuild the index, restore the working tree, and write a merge reflog entry. Conflicting three-way merges write line-level conflict markers to the working tree (matching Git — only the diverging hunks are enclosed between <<<<<<< HEAD / ======= / >>>>>>>; the renderer re-diffs both postimages so common edges and sufficiently long common runs stay outside; binary or modify/delete paths fall back to whole-file markers), write unmerged index stages, and return LBR-CONFLICT-002. A normal conflict saves merge state and offers libra merge --continue / --abort. A squash conflict deliberately saves no merge state: HEAD stays unchanged, and after resolving and staging the paths you finish with a plain libra commit, which creates a single-parent commit. Merge control actions report no merge in progress for that squash result.
Octopus merges (several targets)
With two or more targets, Libra removes duplicates and tips reachable from another supplied tip while retaining the command-line order of the independent targets. If every surviving target is reachable from HEAD, the result is already up to date. Otherwise the octopus path never fast-forwards: a successful commit has HEAD first, followed by the surviving targets in command-line order, and --ff-only refuses it.
Before the strategy starts, Libra requires one common history across HEAD and every target unless --allow-unrelated-histories is present, matching Git's all-tip octopus entry gate. Each target tree is then merged in memory. A round computes the bases between that target and a hypothetical merge of the already-incorporated merge-reference commits, including every criss-cross LCA; this is intentionally not the all-tip intersection. No merge object, index entry, tracked working-tree path, merge state, ref, or reflog is written until every round is clean. A conflict fails with LBR-CONFLICT-002 and “Should not be doing an octopus”, leaving HEAD, index, and working tree unchanged. No conflict state is created; merge the targets one at a time to resolve conflicts.
Clean octopus merges support --no-commit: merge state retains every target as a GC root, --continue creates the same ordered multi-parent commit, and --abort restores the pre-merge state. --squash, --dry-run, --autostash, -s ours, -X options, unrelated-history permission, hooks, shortlogs, and signature verification also apply; --verify-signatures verifies every target before mutation. Trusted unsandboxed external merge drivers retain their documented exception: direct repository mutations made by a driver are outside Libra's atomic rollback boundary.
Conflict style (merge.conflictStyle)
The marker format follows the Git-compatible merge.conflictStyle config key (config-only — matching Git, merge has no CLI style flag):
libra config merge.conflictStyle diff3
merge(default, or unset) — the two-marker style above. Conflicting postimages are re-diffed: equal prefixes and suffixes move outside the markers, equal runs longer than three lines split neighboring blocks, and runs of up to three lines stay inside one block to avoid fragmentation.diff3— additionally emits the common-ancestor content between a||||||| basemarker and the=======separator, so you can see what both sides started from.zdiff3— keeps the complete ancestor block while moving equal postimage prefixes and suffixes outside the markers; it does not apply themergestyle's internal equal-run splitting.- Any other value is a hard error when a content merge must be rendered (exit 128), never a silent fall-back to the default style.
The config is honored by libra merge, libra cherry-pick, and libra revert for text conflicts. NUL-containing content handled by the text fallback and modify/delete conflicts keep their two-part whole-file presentation; an explicitly selected binary driver instead keeps ours without markers. Marker lines use CRLF when every detected input line ending is CRLF; otherwise they use LF. Refinement changes presentation only: a genuine conflict remains a conflict, except that byte-identical postimages become clean. libra rebase currently renders whole-file markers without a base block regardless of this setting.
Per-path merge drivers (gitattributes)
For content changed on both sides, Libra reads the existing .gitattributes /
.libra_attributes cascade. Bare merge uses the normal text merge and
-merge keeps the complete ours version on an unresolved conflict without
markers. A named value first selects a matching external command; without one,
merge=text, merge=binary, and merge=union select the corresponding
built-ins (union retains ours followed by theirs in each overlap, but falls
back to binary handling if xdiff classifies an input as binary). Other names
fall back to text. Only a path with no
merge attribute consults merge.default=<name>. External commands are read
through the strict local → global → system cascade and override same-named
built-ins; boolean merge and -merge attributes always select the built-in
text and binary drivers.
The configured value runs through sh -c (Windows also needs a compatible
sh on PATH) and is trusted, unsandboxed configuration. %O/%A/%B identify
private unpredictable temporary base/ours/theirs files; shell-safe paths keep
Git's raw expansion, while an unsafe worktree directory is single-quoted as an
injection hardening. Native path bytes are preserved without lossy UTF-8
conversion. %L is the marker length, and %P/%S/%X/%Y are the
Git-single-quoted path and labels. %A is read back even when empty. Status 0
is clean, 1 through 128 is a conflict that keeps %A, and a higher status or
shell startup failure is fatal, after which Libra performs no merge-driven HEAD,
index, or tracked working-tree update. The driver is unsandboxed, so direct
repository mutations made by the configured command are outside Libra's
rollback boundary. Driver output and the configured command are not echoed.
Temporary inputs live in a mode-0700 worktree-local directory and are cleaned
after normal completion or child interruption; if Libra itself cannot unwind,
any residual remains private under mode 0700. -X ours/theirs does not replace an
external result; recursive folds reuse the driver with temporary-branch
labels. merge.<name>.recursive, sandboxing, and timeouts are not implemented.
merge-file, cherry-pick, and revert share the built-in dispatch but do not
execute external merge drivers yet. merge-file outside a Libra repository has
no attributes/config source and always uses text.
Input normalization
The default three-way strategy accepts -X ignore-space-change,
ignore-all-space, ignore-space-at-eol, and ignore-cr-at-eol. Built-in
text/union merge compares lines with the same normalizers as libra diff, then
backfills original text and ours-side line endings. Binary input is unchanged,
and these whitespace flags do not alter external-driver inputs.
merge.renormalize=true or -X renormalize canonicalizes all three inputs
according to text / eol attributes before built-in or external driver
dispatch. -text disables conversion, text=auto converts non-binary input,
and set/value text or eol=lf|crlf enables it. Arbitrary clean/smudge filter
programs remain unsupported. -X no-renormalize disables the configured
default. The merged result preserves ours-side line endings.
-X is repeatable: favor and renormalize toggles are independently
last-one-wins; multiple whitespace modes select the strongest comparison. An
explicit renormalize toggle overrides config. Otherwise the strict
merge.renormalize value is consumed only for a real three-way merge, before
repository mutation; fast-forward and already-up-to-date merges ignore it.
libra pull inherits the configured default on its merge path but exposes no
-X override.
Libra still does not implement external strategies, subtree, explicit
-s octopus, strategy options outside those listed above, or interactive message editing
(--edit/launching an editor). Signature verification
(--verify-signatures) is supported but limited to the local vault PGP key
(no external GPG keyring).
Path-level rename conflicts
Both sides renaming the same file to the same destination moves the merge base
there and performs a normal three-way merge. Different destinations produce
CONFLICT (rename/rename) and retain both destination paths. Libra
intentionally resolves the old source name by removal; Git retains a legacy
stage-1 source entry. A rename/delete keeps the renamed path beside its merge
base and reports CONFLICT (rename/delete). Rename/add and two renames landing
on the same destination preserve both contributions as a base-less add/add;
when the rename's own content merge is unclean, Libra also emits Git's
CONFLICT (rename involved in collision) line.
Rename-related conflict markers are one character longer than ordinary markers
and use <branch>:<path> labels. With merge.conflictStyle=diff3, Libra labels
the ancestor base:<source-path>; Git uses an abbreviated ancestor commit ID.
Directory renames
When every tracked path has left an old directory, the unique destination with
the most file renames is inferred as its new name. Additions from the other
side follow that move. merge.directoryRenames=true moves them automatically;
false disables inference; the default conflict suggests the new location
but leaves it unmerged for confirmation. A tied destination stops the merge
while leaving affected additions at stage 0, so the layout can be accepted
directly with merge --continue or discarded with merge --abort.
Libra accepts conflict plus Git-compatible boolean spellings for true and
false (including yes/no, on/off, and numeric booleans). Values outside
those three logical modes fail closed before merge writes with
LBR-REPO-003—an intentional safety difference from Git's current
ignore-and-default behavior. JSON/machine output suppresses the human directory
messages; dry-run reports directory-rename as the conflict kind. A target
occupied by an independent add remains a normal add/add conflict with both
sides preserved.
Options
| Option | Description |
|---|---|
<branch>... |
One or more target branches, commits, or remote-tracking refs. Two or more select the atomic octopus path. |
-m, --message <MSG> |
Override the merge commit message (default Merge <branch> into <head>). |
--ff |
Allow fast-forwarding when possible, overriding `merge.ff=false |
--ff-only |
Refuse unless a single-head merge can fast-forward. A non-up-to-date octopus never fast-forwards. |
--no-ff |
Always create a merge commit when a single-head fast-forward is possible. Octopus already always creates one. |
-s, --strategy <strategy> |
Select ort (single-head default), its recursive alias, conservative resolve, or ours. Repeatable for one target: first clean result wins; otherwise the lowest Git evaluate_result score is replayed (later tie wins). Several targets allow automatic octopus or one -s ours. Cannot be combined with -X. |
-X, --strategy-option <option> |
Accepts ours, theirs, ignore-space-change, ignore-all-space, ignore-space-at-eol, ignore-cr-at-eol, renormalize, or no-renormalize. Repeatable; favor and renormalize toggles are independently last-one-wins, while whitespace modes select the strongest comparison. Cannot be combined with -s. |
--allow-unrelated-histories |
Permit histories without a common ancestor by using a virtual empty merge base, including on the octopus path. |
--log[=<N>] / --no-log |
Include up to N one-line merged-side subjects in the auto-generated merge message, or disable them. |
--squash |
Produce the merged index/working tree but create no commit, do not move HEAD, and record no merge state, including on conflict. Resolve and stage conflicts, then finish with a plain single-parent libra commit. |
--no-commit |
Stop before committing. A clean octopus state retains every target for --continue/--abort; an octopus conflict is atomic and creates no state. |
--no-edit |
Accept the auto-generated merge message without launching an editor. Libra never opens an editor for merge, so this is a no-op accepted for Git parity. |
--stat |
Show a diffstat of the merge result (the changes between the pre-merge HEAD and the new commit) after the merge completes. Git shows this by default; Libra defaults to no diffstat, so --stat opts in. Last-one-wins toggle with --no-stat/-n. Human output only. |
-n, --no-stat |
Do not show a diffstat at the end of the merge (Libra's default). Last-one-wins toggle with --stat. |
--no-progress |
Do not show a progress meter. No-op accepted for Git parity: Libra's merge never renders a progress meter. |
--verify-signatures |
Verify every target tip and abort before mutation if any is unsigned or bad. Like tag -v, only signatures made by this repository's vault PGP key can be validated (no external GPG keyring). |
--no-verify-signatures |
Do not verify the merged commit's signature (the default). The inverse of --verify-signatures; the last one wins. |
--no-verify |
Bypass the pre-merge-commit and commit-msg hooks used when a merge commit is created. |
--no-rerere-autoupdate |
Accepted for Git parity, but the per-invocation override is not implemented. When rerere is enabled, replay staging follows rerere.autoUpdate. (Git's positive --rerere-autoupdate is not exposed.) |
--no-gpg-sign |
Do not GPG-sign the merge commit. No-op accepted for Git parity: Libra's merge never signs. (Git's -S/--gpg-sign is not implemented.) |
--continue |
Finish an in-progress non-squash merge after conflicts have been resolved and staged. |
--abort |
Restore the pre-merge HEAD, index, and working tree for an in-progress non-squash merge. A squash has no merge state to abort. |
--autostash / --no-autostash |
Stash local tracked changes before the merge. A non-squash conflict holds the stash outside stash list until --continue/--abort. A squash conflict saves it directly in stash list without applying it, preserving the conflict stages; resolve and commit first, then run libra stash pop. Config: merge.autostash; untracked files are not stashed. --json adds autostash: applied|stashed|kept. |
--dry-run |
Libra extension: preview the merge outcome writing nothing — reports fast-forward / already-up-to-date / clean selected strategy / would-conflict (with the paths). Exits 0 for a clean preview, 1 when the merge would conflict. Mutually exclusive with --continue/--abort/--restart/--squash/--no-commit. |
--restart |
Libra extension (ports Lore's branch merge restart): abort the in-progress conflicted merge — discarding any resolution work, exactly like --abort — then immediately re-run the same merge against the recorded target commit and selected strategy, regenerating fresh conflict markers and state. Takes no branch and no merge options (the original -m/--no-ff/… are not replayed). Requires a conflicted merge: a staged --no-commit merge is refused (finish it with --continue or discard with --abort). |
--json |
Emit a structured success envelope. selected_strategy names the backend when one ran; strategy remains the outcome category. |
--machine |
Emit the same structured envelope as one compact JSON line. |
--quiet |
Suppress human success output. |
Common Commands
libra merge feature-x
libra merge topic-a topic-b topic-c
libra merge -s resolve -s ort feature-x
libra merge refs/remotes/origin/main
libra merge --continue
libra merge --abort
libra merge --dry-run feature-x
libra merge --restart
libra merge --json feature-x
Conflict Lifecycle
When a non-squash merge conflicts:
- Edit files containing conflict markers.
- Stage each resolved path with
libra add <path>. - Run
libra merge --continueto create the merge commit.
Run libra merge --abort before continuing to restore the branch, index, and working tree to the pre-merge commit. libra status shows the in-progress merge target and the continue/abort commands while merge state exists.
This resolution flow applies to single-head conflicts. An octopus conflict is rejected atomically and creates no merge state; merge those targets one at a time. A clean octopus stopped by --no-commit does create state and may be continued or aborted normally.
For a squash conflict, edit and stage each resolution, then run a plain
libra commit. There is no merge state, so --continue, --abort, and
--restart are unavailable. If autostash was enabled, restore the saved local
changes with libra stash pop after committing the squash result.
To throw away a botched resolution attempt and start over in one step, run libra merge --restart: it restores the pre-merge state exactly like --abort (any edits to conflicted files are discarded) and immediately re-runs the same merge against the recorded target commit and selected strategy — deterministic even if the branch has moved since — leaving fresh conflict markers and a fresh merge state. Other options use their defaults.
Dry Run
libra merge --dry-run <branch>... (a Libra extension — Git has no true merge dry-run) reports what the merge would do without writing anything: no HEAD, index, working-tree, reflog, merge-state, or object-store mutation (auto-merged blobs are computed in memory only). It works on a dirty working tree, but still refuses an existing merge state or unresolved index stages; otherwise it does not validate cleanliness, so a real merge may still refuse where the preview succeeded.
Outcomes and exit codes:
| Preview outcome | Human output | Exit |
|---|---|---|
| Fast-forward possible | Would fast-forward |
0 |
| Already up to date | Already up to date. |
0 |
| Clean three-way/ours/octopus merge | Would merge cleanly by the '<strategy>' strategy. |
0 |
| Would conflict | Would conflict in: <paths> |
1 |
The would-conflict exit of 1 is an outcome signal (like merge-file and diff --exit-code), deliberately distinct from the 128 a real conflicting merge exits with — the preview itself succeeded. With --json/--machine the summary carries "dry_run": true and, when conflicting, "would_conflict": true plus conflicted_paths; both keys are absent from every real merge's output (frozen schema).
Human Output
Fast-forward:
Fast-forward
Clean default single-head merge:
Merge made by the 'ort' strategy.
Clean octopus merge:
Merge made by the 'octopus' strategy.
Already up to date:
Already up to date.
After --continue:
Merge completed.
After --abort:
Merge aborted.
Conflict errors are printed through Libra's standard structured error envelope on stderr and include recovery hints.
JSON / Machine Output
Success output keeps the historical files_changed numeric field and adds merge-lifecycle fields only when relevant.
{
"ok": true,
"command": "merge",
"data": {
"strategy": "three-way",
"selected_strategy": "ort",
"old_commit": "abc1234...",
"commit": "def5678...",
"files_changed": 2,
"up_to_date": false,
"parents": ["abc1234...", "fedcba9..."]
}
}
selected_strategy is additive: existing consumers can keep using the unchanged strategy categories. Single-head three-way outcomes name ort, recursive, or resolve; ours and automatic multi-target merges name ours and octopus. It is omitted when no backend runs (fast-forward, already-up-to-date, or abort). parents contains HEAD followed by every non-redundant target in command-line order. Already-up-to-date merges use strategy: "already-up-to-date", commit: null, files_changed: 0, and up_to_date: true.
--abort sets aborted: true; --continue sets continued: true. Conflict failures return an error envelope on stderr with LBR-CONFLICT-002.
Parameter Comparison: Libra vs Git vs jj
| Parameter | Libra | Git | jj |
|---|---|---|---|
| Branch target | <branch>... (one or more) |
<commit>... (one or more) |
N/A (use jj new) |
| Fast-forward | Supported | Supported | N/A |
| Single-head three-way | Supported | Supported | N/A |
| Continue / abort | --continue, --abort |
--continue, --abort |
N/A |
| Octopus merge | Supported; atomic conflict refusal, ordered reduced parents | Supported | N/A |
| Fast-forward only | --ff-only |
--ff-only |
N/A |
| Force merge commit | --no-ff |
--no-ff |
N/A |
| Squash | --squash |
--squash |
N/A |
| No-commit | --no-commit |
--no-commit |
N/A |
| Commit message | -m <msg> |
-m <msg> |
N/A |
| No editor | --no-edit (no-op; never edits) |
--no-edit |
N/A |
| Post-merge diffstat | --stat (prints it); -n / --no-stat (default: omit) |
--stat (default) / -n / --no-stat |
N/A |
| No progress meter | --no-progress (no-op; never renders one) |
--no-progress |
N/A |
| Disable signature verification | --no-verify-signatures (default; disables --verify-signatures) |
--no-verify-signatures |
N/A |
| No rerere autoupdate | --no-rerere-autoupdate (no-op; no rerere) |
--no-rerere-autoupdate |
N/A |
| No GPG sign | --no-gpg-sign (no-op; never signs) |
--no-gpg-sign |
N/A |
| Default / recursive strategy | -s ort (default), -s recursive alias |
-s ort (default), -s recursive |
N/A |
| Resolve strategy | -s resolve (one base; no virtual ancestor or rename handling) |
-s resolve |
N/A |
| Repeated strategy fallback | Supported for one target | Supported | N/A |
| External/custom strategy | Not supported | --strategy |
N/A |
| Verify signatures | --verify-signatures (vault-key PGP only) |
--verify-signatures |
N/A |
| JSON output | --json / --machine |
Not supported | N/A |
Error Handling
| Scenario | StableErrorCode | Exit |
|---|---|---|
| Missing branch / action | LBR-CLI-001 |
129 |
| Target ref cannot be resolved | LBR-CLI-003 |
129 |
| Failed to load merge target/current commit/tree | LBR-REPO-002 |
128 |
| Unrelated histories | LBR-REPO-003 |
128 |
Unknown -s / -X value or mutually exclusive strategy options |
LBR-CLI-002 |
129 |
| A single-head strategy (or repeated strategies) is requested for several targets | LBR-UNSUPPORTED-001 |
128 |
--verify-signatures: any target tip unsigned, signature invalid, or vault unavailable |
LBR-REPO-003 |
128 |
| Merge conflicts | LBR-CONFLICT-002 |
128 |
| Octopus conflict (atomic; no merge state) | LBR-CONFLICT-002 |
128 |
| Dirty worktree or staged changes | LBR-CONFLICT-002 |
128 |
| Untracked file would be overwritten | LBR-CONFLICT-002 |
128 |
| Merge already in progress | LBR-CONFLICT-002 |
128 |
No merge in progress for --continue / --abort |
LBR-REPO-003 |
128 |
Unknown merge.conflictStyle value when rendering a content merge |
LBR-REPO-003 |
128 |
Unresolved conflict stages remain for --continue |
LBR-CONFLICT-002 |
128 |
| Failed to read merge state or index | LBR-IO-001 |
128 |
| Failed to save state, index, tree, commit, HEAD, or worktree | LBR-IO-002 |
128 |