gwz merge¶
Merge one source ref into the current branch of each selected workspace repository.
gwz merge <source> [--dry-run] [--ff-only] [--no-ff] [--filesystem-strict] [-m <message>]
gwz merge --remote <name> [<ref>] [--wait <secs>]
gwz merge --status [<merge-id>]
gwz merge --continue
gwz merge --abort [--preserve]
gwz merge --gc [<merge-id>]
With no selection, the workspace root and all active members participate.
Explicit member selection remains partial. For the previous member-only default,
use --target @all --no-target @root.
Quick start¶
Preview the merge without changing any repository:
The preview performs an in-memory merge for every participant that would need a true merge. It reports whether each such merge is predicted to be clean and lists predicted conflict paths. The result is advisory: starting the merge repeats preflight under the workspace mutation lock.
Git paths that are not safely representable as ordinary UTF-8 are shown in a
quoted, byte-safe form such as "config-\xFF.toml". That spelling is a stable
diagnostic, not a path value to copy back into a filesystem command.
Start it:
Supply a custom body for any merge commits created by the operation:
GWZ normalizes CRLF and bare CR line endings to LF, removes trailing newlines,
and appends its mandatory merge and operation identity lines. Empty,
whitespace-only, and NUL-containing messages are rejected before any
repository is changed. The exact final message is recorded once and reused by
restart and conflict resolution. -m does not change the separate root
composition-publication message.
If a repository conflicts, inspect the coordinated operation:
Resolve and stage the reported files, then continue:
Or safely restore every participant to its pre-merge state:
If successful participants have accumulated safe post-merge work, preserve it before rollback:
Do not substitute raw git merge --abort. It knows about only one repository
and cannot restore a coordinated workspace operation.
Choosing participants¶
The source ref is resolved independently in every selected repository.
# All active members; root excluded
gwz merge feature/refactor
# Two members only
gwz --target mem_app --target mem_docs merge feature/refactor
# Workspace root only
gwz --target @root merge feature/refactor
# Members plus the workspace root
gwz --target mem_app --target @root merge feature/refactor
Selection is frozen from the pre-merge manifest. Members remain in manifest order and a selected root is appended last. A root merge cannot add, remove, reorder, or rename participants in the operation already under way.
@all and bare --all include root and members. Use --target @root for a
root-only merge.
The coordinated state machine¶
Most successful merges pass through Executing and Finalizing too quickly to
notice. The other states exist so interruption and partial progress remain
recoverable.
| State | Meaning | What to do |
|---|---|---|
Idle |
No merge is open. | Start a merge. |
Executing |
GWZ is applying the frozen plan in order. | If interrupted, run gwz merge --status. |
AwaitingResolution |
At least one repository has an expected Git conflict. Other independent participants may already have completed. | Resolve and stage conflicts, then continue or abort. |
Halted |
An unexpected Git or host failure stopped later participants. | Inspect status, repair the reported cause, then continue or abort. |
Finalizing |
Participant results are verified and workspace composition evidence is being published. | Continue after interruption; GWZ resumes the recorded publication step. |
Preserving |
A preserve-abort is recording and verifying backup refs or coordinated stashes before rollback. | If interrupted, rerun gwz merge --abort --preserve. |
RecoveryRequired |
Live Git state no longer exactly matches a safe recovery point. | Follow the drift report and restore the exact expected state before retrying. |
RollingBack |
A coordinated abort is being persisted and applied in reverse order. | Retry gwz merge --abort after interruption. |
Completed |
Results and composition evidence were verified and the operation was closed. | No recovery action is needed. |
Aborted |
Every affected repository was restored and the operation was closed. | No recovery action is needed. |
Each participant also has its own state, such as UpToDate,
FastForwarded, Merged, Conflicted, Continued, Failed,
Unattempted, RolledBack, or Aborted. This is why one workspace operation
can be awaiting resolution while another repository has already merged
successfully.
A merge left open by GWZ 0.13 or earlier¶
GWZ 0.14 has one merge implementation. A merge record written by 0.13.x or earlier is not a merge this build can act on, and it is not nothing either: it occupies the merge slot, so every merge verb and every command that a merge blocks refuses with one sentence.
gwz: OpenOperation: this is a pre-0.14 merge; use gwz 0.13.0 (the last release before 0.14) to continue or abort
--status, --continue and --abort all refuse alike, so the usual
open-merge advice does not apply here and is deliberately not printed. The
whole remedy is the release named in the sentence: install a 0.13.x build,
finish or abort the merge with it, then return to 0.14.
Before upgrading, close any merge you have open. Run gwz merge --status
on 0.13.x and either gwz merge --continue or gwz merge --abort it. There is
no conversion step and no upgrade path for an open record.
Merges that were already closed are unaffected. Archived records under the
retention directory still project read-only through gwz merge --status <id>,
and gwz merge --gc never deletes an archive it cannot read.
What happens when a merge starts¶
GWZ first preflights every selected repository before mutating any of them. It requires an attached, born target branch, a clean index and worktree, no unrelated Git sequencer state, a resolvable source ref, and shared source/target history.
It then:
- persists the frozen participant plan and exact pre-merge state;
- executes members in manifest order;
- continues past expected conflicts so independent later members can run;
- stops after an unexpected backend or host failure and marks later
participants
Unattempted; - executes an explicitly selected workspace root last;
- verifies every recorded result; and
- publishes the updated lock and merge marker in one checked root composition commit.
The accepted workspace lock remains at its exact pre-merge baseline while the operation is open. Durable participant results, rather than a partially advanced lock, are the source of truth for status and recovery.
True merge commits use:
GWZ rejects unrelated histories; it does not implicitly enable Git's
--allow-unrelated-histories.
Requiring fast-forwards everywhere¶
Use --ff-only when the entire selected workspace must advance without
creating a merge commit:
This is a selection-wide guarantee. GWZ completes preflight for every participant and rejects the whole operation before local mutation if any changing repository would need a true merge. Up-to-date repositories remain valid no-ops.
Resolving conflicts¶
An expected conflict remains in that participant's ordinary Git merge state.
Edit the conflicted files normally, but stage them with gwz add. While a merge
is open, gwz add accepts only repositories recorded as conflicted and rejects
the entire add if the selection includes a clean or unrelated repository.
Before each Git mutation, GWZ records the exact pending action. After a crash or lost connection, status classifies that action as not started, at the expected conflict, completed exactly, or ambiguous. Only exact states are adopted automatically.
When the workspace root participates¶
Root participation is opt-in because the root contains the manifest, lock, and composition history used to coordinate the workspace.
The root must already have a commit and must pass the same attached-branch and clean-state checks as a member. Its pre-merge manifest and lock are read from the recorded root commit, so recovery does not depend on the current files remaining parseable.
Consequently:
gwz merge --status,gwz add,gwz merge --continue, andgwz merge --abortstill work when a root conflict has left the live manifest or lock with conflict markers;- finalization reloads merged root metadata only after the root merge succeeds;
- selected member identities, paths, and source identities must still match the frozen operation; and
- the root participant keeps its merge-result commit while GWZ verifies the later composition evidence internally during publication and recovery.
If root metadata attempts to redefine an in-flight member, finalization fails closed. The operation remains open and can be inspected or aborted using its durable pre-merge record.
Status and drift¶
gwz merge --status is strictly read-only. It reports:
- the operation and publication states;
- recorded before, source, result, and live commits;
- conflict paths and pending-action state;
- participant and operation drift;
- continue and abort eligibility; and
- the root merge result and the current publication step. Composition evidence is verified internally but is not currently a separate response field.
Post-merge work is never silently discarded. A branch switch, new commit, modified index, changed worktree, foreign Git operation, missing object, or changed publication artifact blocks unsafe recovery. Preserve or remove that work and restore the exact state named by status before retrying.
With no id, status inspects the open merge or reports Idle. Closed records
can be inspected while they are retained:
An id-qualified closed response is historical: it has open: false and does
not inspect or reopen the repositories.
Coordinated abort¶
Abort preflights every participant and all root publication evidence before its first mutation. If any repository is unsafe to roll back, nothing is changed.
Rollback follows the reverse of execution:
- remove an incomplete composition-evidence commit, if present;
- restore the explicitly merged root to its pre-merge commit;
- restore members in reverse execution order;
- verify the exact baseline manifest and lock bytes; and
- close the operation as
Aborted.
If abort itself is interrupted, rerun the same command. Durable rollback progress makes the operation restart-safe.
If abort refuses, if it reports success and the result still looks wrong, or if neither continue nor abort can close the operation, see the Merge Recovery Runbook.
Preserving work before abort¶
gwz merge --abort --preserve is an explicit, conservative escape hatch for
work created after a participant merged successfully. It accepts only an
attached recorded target branch, no unresolved entries, no active or foreign
Git operation, and a live commit equal to or descended from the recorded merge
result. A rewound or divergent branch, partial conflict resolution, branch
switch, or ambiguous state rejects the whole preserve attempt before rollback.
GWZ verifies that every still-conflicted repository retains its original
conflict index and conflict-marker file contents; edit or stage that work only
after deciding to continue, or preserve it manually before aborting.
If Git created a conflict but GWZ was interrupted before recording the original
marker snapshot, --continue may still reconcile and resolve the conflict, but
preserve-abort refuses to infer the original from later live bytes.
The root is also eligible when a member-only merge has already recorded its composition commit. Root work created before that composition evidence exists is rejected with manual-preservation guidance so candidate metadata cannot be mistaken for user work. Automatic root worktree preservation is likewise conservative when the composition commit created the root's first commit; committed descendants can still be retained through the reported backup ref.
Depending on what changed, GWZ creates:
refs/gwz/merge/<merge-id>/<member-id>/headfor committed member work;refs/gwz/merge/<merge-id>/root/headfor committed root work; and- one coordinated
stash_<merge-id>bundle for staged, unstaged, and untracked work. Ignored files are not included.
The operation enters Preserving before artifact creation. Every required
artifact is verified and recorded before the existing reverse-order abort path
can begin. If creation is interrupted, rerun the same preserve command; GWZ
checks the recorded ref targets and stash object ids rather than duplicating
them. Plain gwz merge --abort rejects an operation in Preserving, because
it must not bypass artifact reconciliation or verification.
A mutating GWZ command holds the workspace mutation lock for its complete
service call. Do not edit a selected repository or run a separate mutating Git
command while that call is executing. The lock serializes cooperating GWZ
commands, but it cannot freeze an editor or a raw filesystem/Git writer during
native stash creation; such concurrent writes have the same unsupported race
as running git stash while another process edits the checkout.
Preserved work is not reapplied automatically because it was created against the post-merge tree. The human, JSON, and JSONL responses report every ref, commit, stash id, and native stash object id. Inspect or branch from a backup ref with Git. Restore the coordinated stash deliberately after abort:
This restore works for preservation rows owned by members and by @root.
After applying and checking the recovered work, the same deterministic bundle
can be dropped, including an explicit root selection:
Retention and cleanup¶
GWZ keeps the latest 20 ordinary closed merge records for local diagnostics. Records that own preservation evidence are exempt from automatic retention.
# Apply ordinary retention only
gwz merge --gc
# Remove one retained record and its verified private backup refs
gwz merge --gc merge_20260725_1234
Explicit GC preflights every recorded ref before deleting any of them and refuses to run while a coordinated merge is open. A missing ref is accepted on retry, but a ref pointing at a different commit fails closed. After its refs are deleted, the archived merge record is removed. Successful GC output lists only preservation artifacts that remain, such as native stash object ids; it does not present deleted backup refs as recoverable.
GC never deletes native stashes or coordinated stash bundles. After recovering or intentionally abandoning the preserved changes, remove those separately:
Machine output¶
Use --json for one structured response or --jsonl for lifecycle events
followed by the terminal response:
gwz --target mem_app --target @root --jsonl merge feature/refactor
gwz --json merge --status
gwz --json merge --status merge_20260725_1234
gwz --json merge --gc merge_20260725_1234
Machine results identify root rows with target_id: "@root", path: ".", and
target_kind: "Root". Errors retain the same structured target fields, so
consumers do not need to extract repository identity from human text.
Record-version failures likewise include record_context with the merge id,
readable schema/version pair, required semantic wave when known, and legacy
mode when applicable.
--no-ff¶
--no-ff always creates a merge commit, even where a fast-forward is
possible. It is the counterpart to --ff-only; supplying both together
is rejected. A --no-ff start publishes a two-parent integration commit.
Every start writes the same coordinated merge record — ordinary, --ff-only,
custom-message and --no-ff alike — so status, continue, abort and recovery
behave identically whichever way the merge was started. A start on a filesystem
that cannot support crash recovery still runs; see
Crash recovery and filesystems.
Merge also rejects unrelated operation policies supplied explicitly:
--sync, --jobs, --max-per-host, --progress-interval, --partial, and
--force. Diagnostics name the option that must be removed. --remote is the
exception: on merge it is not an operation policy at all, but the local clone
family selector described below.
gwz branch --merge <source> remains a deprecated compatibility spelling. It
constructs the same first-class merge request.
Merging from a local clone (--remote <name>)¶
gwz merge --remote <name> integrates work from another working copy of this
workspace on the same machine — a member of the local clone
family — instead of resolving a Git ref:
With no ref, the source is the named member's HEAD commit, resolved independently for every paired participant. With a ref, that ref is resolved inside the named member, again per participant. Either way the receiving participant's current branch is the merge target, and everything after that — preflight, the coordinated record, conflicts, continue, abort, retention — is the ordinary merge lifecycle described above.
Before the engine runs, the source commit is fetched into each receiving
repository under one fresh, retained ref, refs/gwz/local-imports/<transfer-id>,
and that ref is what the engine merges; status output names it as the source.
Import refs are kept after completion and after abort — see
Local Clones. As with any
merge, the root participates only when selected, so
gwz --target @root merge --remote A is how the clone's root commits come
across; without it they stay unpreserved, and gwz local dispose A says so.
On merge the name is family-only. A name that is not a ready family member is
refused with unknown_local, whose message says which case it is — a name the
index does not hold (or a reserved one), or a row that is creating or
disposing rather than ready. It never falls back to a Git remote, so
gwz merge --remote origin is a family miss rather than a fetch. The mirror
image also holds: a bare gwz merge A resolves the Git ref A in each
receiving repository and never means the family member A.
The selector is accepted only when starting a merge — not with --continue,
--abort, --status or --gc — and family bindings are resolved from the
index at operation time. They are never written into gwz.conf/gwz.yml and
never become Git remotes.
Waiting for the family lock (--wait <secs>)¶
A family merge takes the family lock before it imports anything, and holds it
across the import and the merge itself. While another family operation holds
it — a gwz local clone, a gwz local dispose, a gwz local disband, or
another family merge — the merge refuses immediately:
That is the right answer for a person at a terminal and the wrong one for two
unattended invocations fired for one request. --wait <secs> keeps retrying
instead:
GWZ retries the try-lock at a short fixed interval until the deadline; there
is no blocking acquisition, so the wait stays portable and is bounded by the
number you give. Only a busy lock is retried — a malformed index, an
unsupported platform lock or an I/O failure is the answer, immediately. A wait
that wins the lock rereads the family index before it resolves <name>, so a
merge that waited behind a create or a dispose is answered by the family that
operation left, not by the view it saw before the wait.
Omitting --wait, or passing --wait 0, is the unchanged behaviour: one
attempt, then the refusal above.
--wait is accepted only together with --remote <name>. An ordinary merge
takes no family lock, so gwz merge feature/x --wait 30 is refused rather
than accepted and ignored. The same spelling and the same semantics are on
gwz local clone, gwz local dispose and gwz local disband.
Crash recovery and filesystems¶
A merge runs on every filesystem. Crash recovery — GWZ's ability to prove on restart which artifacts an interrupted start had created — is a capability of the volume, decided once when the merge starts, not a gate in front of the command.
Where the volume can prove a durable filesystem identity, the merge records its artifacts as before and nothing is printed. Where it cannot, GWZ prints one warning on stderr and the merge continues without that recording:
warning: crash recovery is unsupported on btrfs (no durable filesystem identity). Merge will continue. Use --filesystem-strict to refuse.
The filesystem is named, or unknown when it cannot be named, and the
parenthetical is exactly one of no durable filesystem identity,
remote filesystem, or volatile filesystem. The merge itself is unchanged:
same participants, same order, same verification, same published composition
evidence. --continue and --abort of an open merge, and Ctrl-C of a running
one, never depended on crash recovery and behave identically.
--filesystem-strict turns that warning back into a refusal, before any lease,
record, or Git mutation. Use it where an unrecoverable interrupted merge is
unacceptable. It is a start-only flag: supplying it with --continue,
--abort, --status or --gc is rejected. There is no environment variable
and no configuration key.
The bar is identity, not a filesystem name. On Linux a volume is above it when
FS_IOC_GETFSUUID answers with a volume UUID and name_to_handle_at returns a
persistent handle, which admits ext4, xfs and f2fs alike; btrfs is below it
because it never publishes its UUID to the kernel's VFS, kernels before 6.9
have no such call at all, tmpfs and ramfs are refused as volatile because their
contents do not survive power loss, and network mounts (NFS, SMB/CIFS, SSHFS
and other FUSE mounts) are named as remote. On macOS the bar is a local APFS or
HFS+ volume; on Windows, open-by-file-ID capability, a successful nonzero
128-bit identity query, a local volume GUID and required case/handle probes.
The Windows filesystem name is diagnostic only.
Volumes without persistent file handles¶
Some volumes are below the bar and additionally cannot answer for a file across
a rename — overlayfs mounted without nfs_export, and some FUSE mounts. The
merge still runs there. Its record is written directly rather than through the
checked artifact boundary: still staged, renamed, flushed and read back to
verify its bytes, and still refusing to overwrite an existing record; what it
cannot carry is the catalog, which such a volume cannot host at all.
There is no second warning. The one above gains a clause instead:
warning: crash recovery is unsupported on overlay (no durable filesystem identity). Merge will continue. Use --filesystem-strict to refuse. Selected-root and --preserve abort may refuse until the workspace is on a handle-capable volume.
That clause is the remaining limit, and it falls on abort rather than on start.
A plain abort still clears the record — it touches no checked artifact. An abort
that must re-verify one — a selected root's manifest and lock, a
preservation bundle under --preserve, or the merge's published evidence —
needs handles this volume does not have, and refuses. One escape works: copy the
whole workspace onto a volume that proves handles (a local APFS or HFS+ volume
on macOS; ext4, xfs or f2fs on Linux; a Windows volume proving persistent file IDs) and run
gwz merge --abort there, adding --preserve if that was the door that
refused.
A power loss mid-merge on such a volume is operator cleanup rather than something GWZ recovers: nothing on that volume can prove after a reboot which objects the interrupted attempt created.
--json, porcelain and --jsonl consumers read the decision from the
response's crash_recovery object rather than from stderr. Its handles_ok
field is false on such a volume, true on a below-bar volume whose handles
work, and null above the bar; see Machine Output.