Skip to content

Commit d12e75f

Browse files
committed
repro: dir-copy x nested-entry footprint collision (case 5), flip fixed cases to regression guards
2026.8.10 landed jdx/mise#12190: cases 1 and 3 now compose, so they guard the fix instead of asserting the bug. Case 5 adds the new one: composition never checks a directory entry's source footprint, so a root that declares a file inside another root's copied directory applies silently in glob order, the loser drifts forever, and unapply --force removes both entries' files. Case 6 pins the documented exact-path conflict as the contrast.
1 parent 008cbd2 commit d12e75f

13 files changed

Lines changed: 103 additions & 42 deletions

File tree

README.md

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
1-
# mise `config_roots`: same-target `symlink-each` conflicts on disjoint leaves
1+
# mise `config_roots`: dotfiles composition cases
22

3-
Minimal repro for the `[bootstrap].config_roots` composition shipped in
4-
mise 2026.8.9 (jdx/mise#12105, jdx/mise#12132). Verified against 2026.8.9.
5-
6-
Run:
3+
Fixture trees for the `[bootstrap].config_roots` dotfiles composition,
4+
verified against mise **2026.8.10**. Run:
75

86
./run.sh # uses `mise` from PATH
97
MISE_BIN=/path/to/mise ./run.sh
108

119
Every case is a materialized tree under `case*/`. `run.sh` copies each into
1210
a throwaway sandbox (`mktemp -d`) and runs mise with an isolated HOME —
13-
nothing touches the real home. Exit code 0 means every case reproduced the
14-
behavior in the table below — including the two conflicts. When mise gains
15-
merge semantics for same-target `symlink-each`, cases 1 and 3 turn into
16-
successes and `run.sh` starts failing: it doubles as the fix detector.
11+
nothing touches the real home. Exit 0 means every case reproduced the
12+
behavior in the table below. Case 5 asserts the open bug: when mise starts
13+
rejecting that collision, case 5 fails — the script doubles as the fix
14+
detector.
1715

18-
| case | layout | result on 2026.8.9 |
16+
| case | layout | result on 2026.8.10 |
1917
|---|---|---|
20-
| 1 | two roots declare `"~/.config/mise/"` `symlink-each`, sources hold disjoint leaves (`a.toml` / `b.toml`) | error `conflicting dotfile declarations`, both origins named |
21-
| 2 | same bundles, distinct targets | composes; both leaves linked |
22-
| 3 | case 1 tree, `mise bootstrap` invoked from `bundles/a` | same conflict — the parent's `config_roots` is active in child invocations |
23-
| 4 | two roots write managed blocks with distinct ids into one file | merges — same-file composition exists for blocks, keyed by `(path, id)` |
24-
25-
## The ask
26-
27-
Compose same-target `symlink-each` by unioning the source trees and
28-
conflicting only when two roots claim the same leaf path — the leaf-keyed
29-
merge `symlink-each` already performs against a pre-existing real
30-
directory. Merging into a directory it does not own is that mode's
31-
documented purpose, so a shared directory across roots is its primary
32-
composition case, not an error.
33-
34-
## Real-world shape
35-
36-
A dotfiles repo structured exactly like the release-notes example
37-
(`config_roots = ["bundles/*"]`) has 31 `symlink-each` declarations for
38-
`"~/.config/mise/"` across the bundle tree — each bundle links its own
39-
`config-mise/conf.d/<bundle>.toml` fragment into the shared directory,
40-
disjoint by construction. Case 3 additionally means adoption cannot be
41-
staged root by root: the glob is live for every child `mise bootstrap`.
18+
| 1 | two roots declare `"~/.config/mise/"` `symlink-each`, sources hold disjoint leaves | composes — fixed by jdx/mise#12190 (conflicted on 2026.8.9) |
19+
| 2 | same bundles, distinct targets | composes |
20+
| 3 | case 1 tree, `mise bootstrap` invoked from `bundles/a` | composes — the parent's `config_roots` is live in child invocations |
21+
| 4 | two roots write managed blocks with distinct ids into one file | merges, keyed by `(path, id)` |
22+
| 5 | root a: directory `copy` whose source **contains** `collide.txt`; root b: whole-file `copy` at `<dir>/collide.txt` | **open bug** — no conflict; entries apply in glob order and b silently wins; a's `status` reports `differs` forever; `unapply --force` from a removes both entries' files |
23+
| 6 | two roots claim the same target with `copy` | error `conflicting dotfile declarations`, both origins named — documented on https://mise.jdx.dev/bootstrap.html |
24+
25+
## The ask (case 5)
26+
27+
The composed-apply conflict check is exact-path: it rejects two whole-path
28+
entries with identical targets (case 6) and composes same-target
29+
`symlink-each` by disjoint leaf (case 1), but it never considers a directory
30+
entry's *source footprint*. When one root's directory copy contains a file
31+
that another root declares exactly, composition applies both and resolves
32+
the collision silently by config order — against the documented
33+
"Independent roots never acquire precedence from their order in
34+
`config_roots`".
35+
36+
Reject this footprint collision before any mutation, the way jdx/mise#12190
37+
already does for same-target `symlink-each` leaves ("reject duplicate leaves
38+
and file/directory footprint collisions before any mutation") — or, if
39+
silent last-writer-wins is intended, document the nesting exception
40+
explicitly, including on https://mise.jdx.dev/dotfiles.html, whose modes
41+
table ("Directory copies are additive") currently reads as if such overlap
42+
is safe.
43+
44+
## History
45+
46+
This repo originally reproduced the 2026.8.9 behavior where cases 1 and 3
47+
(same-target `symlink-each`, disjoint leaves) hard-errored; jdx/mise#12190
48+
fixed that in 2026.8.10 and those cases now serve as regression guards. The
49+
discovering dotfiles repo migrated to per-bundle scoped `copy` targets
50+
(disjoint by construction); rendering one template next to a copied
51+
directory exposed case 5's shape.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dotfiles]
2+
"~/.config/demo" = { source = "srcA", mode = "copy" }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A version
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A own
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
B version
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dotfiles]
2+
"~/.config/demo/collide.txt" = { source = "f.txt", mode = "copy" }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[bootstrap]
2+
config_roots = ["bundles/*"]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dotfiles]
2+
"~/.config/demo" = { source = "srcA", mode = "copy" }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
A own
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[dotfiles]
2+
"~/.config/demo" = { source = "srcB", mode = "copy" }

0 commit comments

Comments
 (0)