Skip to content

Three-way merge

dojang merge resolves files changed in both the repository and their destinations. It uses the intermediate snapshot as the common ancestor, runs a configured merge driver, and writes an accepted result to the source, destination, and intermediate snapshot.

Using the command

Run the command without paths to select every current three-way conflict:

$ dojang merge

Pass a source path, destination path, or containing directory to limit the selection. Multiple paths are allowed:

$ dojang merge HOME/.bashrc ~/.config/example/

--driver NAME selects a named driver instead of the configured default. --driver-file PATH reads a different configuration file. --dry-run validates the selected conflicts and driver configuration, then prints the planned merges without creating a workspace, starting the driver, or changing files and machine state.

Version 0.3 supports regular UTF-8 text files on copy routes that use the identity codec. The source, destination, and intermediate snapshot must all exist. Directory routes, deployment links, transformed codecs, binary files, invalid UTF-8, and conflicts without a common intermediate snapshot are rejected before any merge driver starts. If an unselected unsupported conflict blocks a command without paths, pass the paths of the supported conflicts explicitly.

Configuring drivers

Merge drivers are machine-local. The default configuration path is:

Platform Path
Windows %APPDATA%\dojang\merge-drivers.toml
macOS ~/Library/Application Support/dojang/merge-drivers.toml
Other platforms $XDG_CONFIG_HOME/dojang/merge-drivers.toml

Windows falls back to %USERPROFILE%\AppData\Roaming when APPDATA is missing or relative. Other platforms fall back to ~/.config when XDG_CONFIG_HOME is missing or relative.

The file names a default driver and defines one or more drivers:

default-driver = "example"

[merge-drivers.example]
command = [
  "my-merge-driver",
  "{result}",
  "{base}",
  "{source}",
]
inherit-environment = ["PATH"]
unresolved-exit-codes = [1]
canceled-exit-codes = [130]

[merge-drivers.example.environment]
LC_ALL = "C"

command is an argument array, not a shell command. {source}, {base}, and {result} must each appear exactly once as whole arguments. {destination} is optional and may appear once. The result file starts as a copy of the destination. A driver resolves the conflict by updating that file and returning zero.

The child process receives only variables named by inherit-environment plus the fixed values in environment. Fixed values override inherited values. Codes from 1 through 255 in unresolved-exit-codes mean the conflict remains unresolved; codes in canceled-exit-codes use the same range and mean the user canceled the merge. The two lists must not overlap.

Safety and recovery

Dojang validates every selected input before starting the first driver. Each driver receives private copies in an owner-only workspace and runs without a shell. If an input disappears or cannot be read during preliminary conflict detection or while it is being captured, Dojang reports a conflict instead of an internal error. Before each accepted result is written, Dojang checks that the authoritative files still match the bytes, identities, and modes observed during validation. It stages each result with its final mode, then replaces the replica only if its exact observed contents, identity, and mode still match. An edit made while staging therefore remains in place and reports a conflict. Because declared modes are applied before publication, a replacement symbolic link cannot redirect a later permission change. After the driver exits, Dojang also reloads the routing context and rejects the result if the selected route or its resolved paths, kind, mode, codec, or provenance changed. A changed repository, machine, or state generation identity also rejects the result. Invocation-workspace creation and final replica writes hold the repository-generation lock, so dojang forget cannot approve deletion between workspace cleanup and recreation or while a merge commit is in progress. Target publication checks the captured generation again under its state-update lock and rejects data from a forgotten and recreated generation. Post-merge hook setup reloads the current manifest, environment, and machine facts while reusing and validating the captured generation instead of preparing new machine state. Removed hooks therefore stay removed, and forgetting first prevents hook launch without recreating state.

Results are committed in source, destination, then intermediate order. This order prevents the intermediate snapshot from claiming convergence before both authoritative copies contain the result. Before the first write, Dojang journals the accepted result together with hashes of the original base and destination. If a later write fails, earlier writes remain. A retry uses that journal to finish a commit interrupted after the source write, but only while the source contains the accepted result and the other inputs still match their recorded hashes. It also recognizes the case where both authoritative copies already contain the result but the destination mode or intermediate content or mode is still stale. Rerun dojang merge or inspect the reported workspace to recover the operation. Conditional replacement briefly moves the old replica to a hidden .dojang-replaced-* sibling in the same directory. Before that move, Dojang binds the staged result to its prepared identity, contents, and final mode, then verifies the same entry after its rename. Ordinary failures restore the old replica or report the retained path. If the process or machine stops after that move but before the staged result is published, move the sibling back to the missing replica path before retrying. Failed, unresolved, and canceled workspaces are retained and printed in the error output. Successful workspaces are removed. dojang forget removes every retained merge workspace for the repository. Before recursively removing one, it verifies the workspace directory and its complete ancestor chain, and refuses cleanup if a symbolic link could redirect deletion outside machine-local state. Immediately before publishing machine state, Dojang re-observes all three replicas and rechecks the declared destination and intermediate modes. Lost content or mode convergence, or an input that can no longer be observed, reports a conflict and keeps the recovery journal. The same conflict status applies when authoritative inputs cannot be re-observed during the post-driver route-policy refresh. For regular files, it captures exact contents, identities, and modes around fingerprinting and immutable-baseline materialization, verifies that the fingerprint and baseline describe those same contents, then revalidates every replica. A change during any of those steps therefore aborts publication instead of combining observations from different versions. The rejected baseline is removed even when another item keeps the same multi-file snapshot transaction alive. Its ancestors are retained because an empty directory may itself be another successfully recorded baseline. The pending-publication marker also remains after a guarded commit abort and is removed only after target publication succeeds, so a later converged state can still repair its machine-state record. Pending-publication recovery scans only the known invocation and conflict directory levels. Each level is enumerated through a pinned directory entry, and pending-marker creation and removal remain bound to the captured workspace and ancestor identities. A concurrently replaced directory is retained instead of being modified. Recovery does not recurse into subdirectories created by a driver. Workspace setup removes partial private copies when possible. Each input copy is created with its owner-only mode relative to a pinned workspace directory, so replacing the workspace path cannot redirect those writes. Later filesystem failures retain the workspace and use exit status 2. A failure while removing a completed invocation workspace uses the same status and identifies the workspace root to inspect. Its empty invocation parent is removed only after a pinned empty-directory check and while the identity captured before workspace cleanup still matches. Sibling recovery workspaces therefore stay at their original paths. A driver result that cannot be read is invalid output instead: it is rejected before any replica write and uses exit status 4.

The command runs pre-merge hooks before loading the context used for conflict selection. Before selecting post-merge hooks after a successful command, it reloads the manifest, environment, and machine facts. See hooks for configuration.

Exit status

The most relevant exit codes are:

  • 1: invalid driver configuration or selection.
  • 2: filesystem write failure.
  • 4: the driver failed, could not start, or produced an invalid result.
  • 13: machine state could not be updated.
  • 30: a conflict is unsupported, unresolved, or changed concurrently.
  • 32: a selected path is not routed.
  • 36: the driver reported cancellation.
  • 40: a merge hook failed.

See exit codes for the complete list.