dam (diff-and-merge) #
Syncs line-oriented text files between a local directory and a remote host
over rsync, with a plan/apply workflow: --plan (the default) previews the
changes, --pull applies them locally, --push applies them to the remote.
Merge strategy: when a file exists on both sides, the result is the de-duplicated union of its local and remote lines: the local lines in their original order, with the lines only the remote has appended. When a file exists on only one side it is propagated verbatim. The merged result is the same in every mode; the mode only chooses which side gets written.
Order is never changed — pushing a reordered file (say, a playlist)
propagates the local order rather than fighting it. dam is intended for
list-like data (allow-lists, hosts files, playlists, word lists). Blank lines
and duplicate lines are not preserved by the merge.
--local and --remote may be directories or single files. When
--local is an existing directory, dam syncs the whole tree (recursing into
subdirectories); otherwise it treats both paths as a single file. To mirror a
directory into a new local location, create that directory first.
dam never deletes. Because the merge is a union, a line removed on one side reappears from the other. Removing data is a manual edit on both sides (or a one-shot
rsync), not somethingdamdoes.
Local files are written atomically (staged in a temp file and renamed), so an
interrupted run can't leave a half-written file. rsync runs with
ssh -o BatchMode=yes and an I/O --timeout so it fails fast in automation
rather than blocking on a prompt.
Building #
cargo build --release
Usage #
dam --host <host> --local <dir> --remote <dir> [--plan | --pull | --push] [--sudo | --doas] [--exit-code] [--timeout <secs>]
Modes #
--plan, --pull, and --push are mutually exclusive; --plan is the default.
| Flag | Description |
|---|---|
--plan |
Show the changes each direction would make. Writes nothing. (default) |
--pull |
Apply the merge to the local directory (remote → local) |
--push |
Apply the merge to the remote host (local → remote) |
Options #
| Flag | Description |
|---|---|
--host |
SSH host alias (from ~/.ssh/config) to sync with |
--local |
Local directory containing the text files |
--remote |
Remote directory to sync from/to |
--sudo |
Elevate the remote rsync with sudo (via --rsync-path). Mutually exclusive with --doas |
--doas |
Elevate the remote rsync with doas (via --rsync-path). Mutually exclusive with --sudo |
--exit-code |
Exit 1 if --plan finds pending changes. Useful in CI |
--timeout |
rsync I/O timeout in seconds (default: 30; 0 disables) |
Run dam --help for full details.
--sudo/--doas elevate only the rsync process on the remote host (e.g. when
the remote path is root-owned) by passing --rsync-path="sudo rsync". The
local rsync always runs unprivileged, so your own SSH config and keys are used.
Exit status #
| Code | Meaning |
|---|---|
0 |
Success; nothing pending (or changes were applied with --pull/--push) |
1 |
--exit-code was set with --plan and pending changes were found |
2 |
One or more files failed to process |
Progress and the summary go to stderr; the plan diff goes to stdout,
so dam --plan … > changes.diff captures only the differences.
Examples #
# See what would change on either side
dam --host myserver --local ~/lists --remote /srv/lists
# Bring the local directory up to date
dam --host myserver --local ~/lists --remote /srv/lists --pull
# Bring the remote up to date
dam --host myserver --local ~/lists --remote /srv/lists --push
# Sync a single file
dam --host myserver --local ~/hosts.allow --remote /etc/hosts.allow --pull
# Fail if the two sides have drifted
dam --host myserver --local ~/lists --remote /srv/lists --plan --exit-code