|
1 | | -# mise `config_roots`: same-target `symlink-each` conflicts on disjoint leaves |
| 1 | +# mise `config_roots`: dotfiles composition cases |
2 | 2 |
|
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: |
7 | 5 |
|
8 | 6 | ./run.sh # uses `mise` from PATH |
9 | 7 | MISE_BIN=/path/to/mise ./run.sh |
10 | 8 |
|
11 | 9 | Every case is a materialized tree under `case*/`. `run.sh` copies each into |
12 | 10 | 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. |
17 | 15 |
|
18 | | -| case | layout | result on 2026.8.9 | |
| 16 | +| case | layout | result on 2026.8.10 | |
19 | 17 | |---|---|---| |
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. |
0 commit comments