Repository navigation
file.mkdir ignores --conflict: anything that is not a directory at its path fails the deploy under every policy, while file.link and file.copy replace it #822
Description
Feature: #836 — file provider. (Moved from #463: every line of the fix is in pkg/op/provider/file.)
The contract
--conflict=<policy> answers one question: what does writ do when something already exists at a target it is
about to write?
| Policy | If the target exists |
|---|---|
stop |
refuse the deploy and list the targets |
skip |
leave it, write nothing there |
replace |
archive it, then write |
What the occupant is — a file, a directory, a live link, a dangling link — does not enter into it. "It exists" is the
whole test. That is what any developer, operator, or administrator reads into the flag, and it is how two of writ's
three writers behave.
The defect
file.mkdir: C:\Users\david-noble\.config\git exists, but is not a directory
file.linkreads the policy (provider.go:234) and underreplacearchives the occupant and writes the link.file.copydoes the same (provider.go:2112).file.mkdirnever reads the policy (provider.go:363,:380). Anything at its path that is not a directory is
an error, understop,skip, andreplacealike.
On the machine the occupant was a stale symlink from the previous dotfile manager. It could have been anything.
The run, invoked with --conflict=replace, stopped on a target the user had told it to replace.
The fix
file.mkdir applies the conflict policy exactly as file.link does:
stop: the target is reported by pre-flight with the others (see Pre-flight under --conflict=stop lists only file targets: a directory target that is occupied is missed, so the refusal is incomplete and the run fails later #831) and the deploy refuses before runningskip: the directory is not created; the units beneath it are skipped, as a skipped link's would bereplace: the occupant is archived to the recovery site, the directory is created, and the receipt records the
archive so compensation can restore it
Acceptance
-
file.mkdirover a regular file, a live symlink, and a dangling symlink: each policy produces the outcome in
the table, with a unit test per cell - the receipt of a replacing mkdir carries the archive id; compensation restores the occupant
-
writ deploy noblefactor --conflict=replaceon DANOBLE-WD11-3 gets pastfile.mkdir-6
Related
#831 — the stop refusal list omits directory targets for the same reason. #813, #814, #818, #819, #820, #821 —
the same machine, the same day.
Ticked 2026-09-21, all three. The third at 06:02 on DANOBLE-WD11-3, build ad2219ef with #900: writ deploy common noblefactor-ops --conflict=replace deployed 56 links, exit 0; C:\Users\david-noble\.config\git, the 2022 symlink, is archived in recovery (01a0c1e9-f911-…) and the path is a directory holding five links. Log: build/e2e/2026-09-21-windows-danoble-wd11-3-lane14-deploy-2.log in the worktree. In
pkg/op/provider/file/provider.go: Mkdir observes the leaf and lets the policy govern a non-directory occupant;
archiveMkdirOccupant discovers, archives, and marks the occupant gone before the claim; the receipt is create_dir
with the recovery id and digest; compensateMakeDir restores through restoreDisplacedOccupant. Tests in
provider_test.go: TestMkdir_Stop_RefusesAnOccupant, TestMkdir_Skip_LeavesAnOccupant,
TestMkdir_Replace_ArchivesAnOccupantAndCreates (a file, a live symlink, a dangling symlink each),
TestMkdir_Replace_CompensationRestoresTheOccupant, TestMkdir_ADirectoryIsNeverAConflict. On
danoble-ud24-1.local, writ deploy common noblefactor-ops --conflict=replace deployed 127 links where 2026-09-18's
run stopped at file.mkdir. Plan: docs/plans/fix/822-mkdir-obeys-conflict.md.
Activity
[-]A dangling symlink where a directory must be created aborts the deploy: file.mkdir refuses it and pre-flight never counted it as an occupant[/-][+]file.mkdir ignores --conflict: anything that is not a directory at its path fails the deploy under every policy, while file.link and file.copy replace it[/+]David-Noble-at-work commentedon Sep 8, 2026
Another instance, 2026-09-08, DANOBLE-WD11-3, build
919b952, the first deploy since the layer move:~/.config/gitwas a dangling stow-era symlink (its target underHome\all\…has not existed since August). Under--conflict=replace,file.linkandfile.copywould have archived and replaced it;file.mkdirfailed the whole scope instead. The pre-flight (#831) listed nothing, since the target is a directory's. Removing the dead link by hand let the deploy proceed.The end-to-end record says what actually ran
A document rendering, a terminal one, the renderings grouped, and pag…