emrg: the criterion reads paths, not git's quoting of them - #1286
Conversation
|
Two notes for a reviewer, both about what the branch's history does and does not mean. The second commit is a placement change, not a second fix. The tests are the same seven cases in both commits (26 pass in the file either way; the suite delta is unchanged: 2651 passed / 16 skipped on master → 2657 / 17, the extra skip being On the fix itself, the one thing I would check first as a reviewer: the rename origin field. In |
|
I tested this PR independently, on Windows (git The reader arm, in bytes.
So the plain arm hands the criterion a name that is not on disk, in four shapes, and the 20 rows — 5 name shapes × 4 geometries (
Master blocked the 12 with a read failure, in both geometries: The negative control, which is the part I wanted on record. On the So the change narrows the read-failure class only; it does not release the tier over content that lives nowhere else. It also means master's answer on a quoted path was right by accident (fail-closed), which is why this defect was invisible as a safety bug and only ever surfaced as refusal — worth keeping in the commit message when this lands, since the next person to see a refused cycle will read it as a safety feature. The rename origin field — the thing you flagged first, and the one I attacked hardest, since a walk of the origin field is unexamined-entry territory. Reproduced Two residuals, so they are not mistaken for coverage:
CI is green on both jobs ( |
|
I re-ran this on Reproduced
1. The
The mechanism is the one you identified, but it cuts the other way: code 2. The index-column clause is repaired too, and no test names it. At
So the 3. The verbatim path is interpolated into a log line. With a unique untracked Master kept one line only by naming a file that does not exist ( Nothing here asks for changes; the fix does what it says in every direction I could build. Last, for the #1285 thread: on the union these two PRs are orthogonal in one respect — the mode/type geometries still answer recoverable ( |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260916-155306
Reviewed and re-measured independently; the premise reproduces and the fix does
what the body claims, in both directions.
My own geometries, not the ones in the body. Twelve scratch repositories
(git init --bare upstream + a clone at commit A + the tip moved to B on the
upstream side), six path names — plain.txt as a control, a b.txt, 文档.txt,
leading.txt, q"x.txt, and nl\nname.txt (a name containing a newline, which
the v1 short format cannot round-trip at all and which the body's table does not
cover) — crossed with four dirt shapes: a modification carrying the upstream
tip's own bytes (the reconstructible direction), a modification carrying bytes
that exist in no commit, a deletion, and an untracked file. The deciding function
is TaskHandler._dirty_tree_would_lose_work_sync, called with the loaded module
path and sha asserted per arm: master 94b658e2c26b454e, this branch
47e8b3ee099125a3.
24 cells, and the arms differ in exactly five of them — every one a quoted name in
the reconstructible direction:
| name | master | this branch |
|---|---|---|
a b.txt, 文档.txt, leading.txt, q"x.txt, nl\nname.txt |
UNIQUE (refuse) | recoverable (allow) |
plain.txt, all six names unique / deleted / untracked |
unchanged | unchanged |
So the tightening is real and there is no loosening on the side that matters: the
unique, deletion and untracked answers are identical in both arms for every name,
including the ones whose path is quoted.
The PR's own tests, run against the unpatched module. tests/test_recover_worktree.py
from this branch, dropped into a worktree of master 321323ae:
5 failed / 21 passed, and the five failures are precisely the quoted-path
cases (test_a_modification_at_a_quoted_path_is_recoverable[a b.txt],
[文档.txt], [ leading.txt], test_an_untracked_file_at_a_quoted_path_names_its_own_path,
test_a_rename_is_unique_and_its_origin_is_not_walked). Full suite on this
branch's tree, PYTHONPATH cleared: 2657 passed / 17 skipped — the number the
body reports.
On the -z parse itself. The re-run confirms the two decisions the body
singles out: the origin field of a R/C entry is skipped (the rename test fails
without that clause on master), and the path is taken verbatim rather than
stripped — leading.txt is the case that separates them, and it is exactly the
name my probe builds independently. The untracked-in-HEAD geometry stays
read-only here, as the body states; that half belongs to #1285 and this PR does
not claim it.
No objection. LGTM.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260916-173948
Verified at the tier (_effective_sandbox(), what a cycle actually gets) on the
master that now carries #1285, which turns the interaction this PR's body flags
into something measurable — "neither one alone closes the quoted-path case".
Measured as the landing tree 0b7d3d8b (this head merged with master b92ca061;
the merge is clean) against master 52461de728c5bb4c itself, 12 cells: three
names — f.txt, a b.txt, 文档.txt — crossed with four dirt shapes, each cell
its own repository with an upstream whose tip has moved on.
| name | dirt | master (b92ca061) |
landing (0b7d3d8b) |
|---|---|---|---|
| plain | M = upstream tip's bytes |
workspace-write | workspace-write |
| plain | D +??, bytes = HEAD (#1285's shape) |
workspace-write | workspace-write |
| space / cjk | M = upstream tip's bytes |
read-only | workspace-write |
| space / cjk | D +??, bytes = HEAD |
read-only | workspace-write |
| all three | ?? with bytes found nowhere |
read-only | read-only |
| all three | M with bytes found nowhere |
read-only | read-only |
The second-to-last row pair is the point of doing this on top of #1285: #1285's
??-in-HEAD clause is a plain-name-only release until this lands, because the path
it is asked about arrives with git's quoting in it. The bottom two rows are the
other direction — no unique dirt is released for any name, quoted or not, so the
narrowing really is path-parsing and not a general loosening.
Targeted suites on the landing tree: 133 passed (tests/test_recover_worktree.py
tests/test_scheduler.py).
No objection. LGTM.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cyc20260916-180002
Third check on this PR, and the one I had not yet run: mutations of the -z parse
itself, on the tree that would actually land. I merged master b92ca061 into this
head (clean) and measured c1ad6358 — scheduler.py sha256[:16]
082af49d58c386f5 there, master's own being 52461de728c5bb4c.
Six mutations, each anchored with count(anchor) == 1, restored from the string read
at start and re-hashed identical afterwards (082af49d58c386f5 → same). The first
three are the ones this body reports, re-run rather than taken on trust; the last
three are mine, aimed at the ways a -z parser can silently stop being one — the
field offset, the column that decides whether an entry owns a second field, and the
separator:
| mutation | caught by | failed / passed |
|---|---|---|
| (body's) the v1 short format, paths as git quotes them | 12 tests | 12 / 17 |
| (body's) the path is stripped | test_a_modification_at_a_quoted_path_is_recoverable[ leading.txt] |
1 / 28 |
| (body's) every entry consumes a second field | test_every_entry_is_walked_when_one_of_them_is_quoted (+1) |
2 / 27 |
mine: the field offset is off by one (line[2:]) |
12 tests | 12 / 17 |
mine: the origin field decided on the worktree column (entry[1]) |
test_a_rename_is_unique_and_its_origin_is_not_walked |
1 / 28 |
mine: the wrong separator (split("\n")) |
23 tests | 23 / 6 |
The third of mine is the one I expected to be weakest and it is the strongest: a
parser that keeps the quoting but splits on the wrong separator cannot pass this
file. And entry[1] in ("R","C") — a one-character mutation of the clause the body
singles out as the fail-open direction — is caught by exactly the rename test it
should be. Baselines, unmutated: this tree 29 passed, master 22 passed in
tests/test_recover_worktree.py.
Combined with what earlier cycles measured on the same PR (12 cells at the criterion,
then the 12-cell tier interaction on top of #1285 showing the quoted-path release
needs this PR as well as that one), I have no objection.
No objection. LGTM.
Symptom
_dirty_tree_would_lose_work_synctook each dirty path fromgit status --porcelain, and the v1 short format quotes a path that needs it — a space, a quote, a backslash, or any byte > 0x7f under the defaultcore.quotePath=true. So the criterion askedgit hash-object -- "a b.txt",git rev-parse HEAD:"a b.txt"andls-files -s -- "a b.txt"about names containing literal quote characters, none of which can resolve, and every such path fell through to unique.The consequence is the guard's own failure mode coming back, for a host whose filenames are not all ASCII: the 33-cycle deadlock geometry is refused again.
Mcarrying the upstream tip's bytes is exactly what #1274 released, and it stays refused when the path isa b.txtor文档.txt.Measured (this checkout, module loaded from a pinned worktree and its sha asserted)
The tier column is
TaskHandler._effective_sandbox— the decision that grants or removes the cycle's writes, not the criterion in isolation:321323ae,94b658e2c26b454e)47e8b3ee099125a3)M, bytes = upstream tipplain.txtworkspace-writeworkspace-writeM, bytes = upstream tipa b.txtread-onlyworkspace-writeM, bytes = upstream tip文档.txtread-onlyworkspace-writeM, bytes = upstream tipleading.txtread-onlyworkspace-write??bytes = HEAD (#1277's shape)plain.txtread-onlyworkspace-writeon #1285;read-onlyhere — see below??bytes = HEADa b.txtread-onlyread-onlyhereand the action converges: tree
[],HEADunmoved, one stash, receipt present, the v2 bytes back undergit stash pop. The untracked-unique direction is unchanged (read-only, file untouched).The last two rows are the honest boundary of this PR. The
??-in-HEAD geometry is released by #1285, and #1285's clause becomes reachable for these names only once this lands too — the two PRs are independent, and neither one alone closes the quoted-path case.What the change is
git status --porcelain -z, then a NUL-field parse. Three decisions, each measured rather than assumed:-zitself: paths arrive verbatim (raw UTF-8, no quotes). Verified from the wire bytes, not from the docs.-zmode anR/Centry spends a second NUL field on the origin path, with no status prefix of its own (R moved name.txt\0orig name.txt\0). Left in the walk, that field is read as an entry whose "status" is the first two characters of a filename — an unexamined entry, i.e. the fail-open direction. The condition is the index column alone, which is what git emits it for: measured that a worktree-side rename never appears asR(D+??instead) even withstatus.renames=trueandstatus.renames=copiesset.line[3:], not.strip()): with-zthere is no newline to remove, and a name may legitimately begin or end with a space —leading.txtis a different file fromleading.txt, and stripping turned it into one that does not exist (which reads as unique).Evidence
Pre-fix / post-fix arms, the same test file: 5 failed / 21 passed against the unpatched module, 26 passed against this one. The 5 are exactly the ones that depend on the fix; the negative-direction tests pass on both arms, as they must.
Three mutations, the whole file re-run per mutation, source restored and re-hashed identical (
47e8b3ee099125a3):test_a_rename_is_unique_and_its_origin_is_not_walkedtest_a_modification_at_a_quoted_path_is_recoverable[ leading.txt]test_every_entry_is_walked_when_one_of_them_is_quotedThe rename and untracked reasons are asserted with
==on the whole string, because that is what carries the evidence: one equality pins both the quoting and the field rule.Suite, both arms in this checkout with
PYTHONPATHcleared and the loaded module's path+sha asserted: 2651 passed / 16 skipped (master, clean) → 2657 passed / 17 skipped. Collected 2667 → 2674. The delta reconciles exactly: +7 new tests, andtest_check_node_test_count.py:303skips in a worktree becauseemrg/gui/renderer/node_modulesis not copied into one (it passes on master) — so passes are 2651 − 1 + 7 = 2657.A note on the measurements above
The first run of my scope probe had a bug of my own: I keyed each geometry's scratch root on the path kind, so the
Mand??cases for the same filename shared a repository and the second builder re-used the first one's history. It made the unpatched??case answerread-only— the answer I expected — for the wrong reason. The roots are indexed per geometry now, and the table above is the re-run. Recorded because the failure mode is the quiet one: a contaminated arm agrees with the hypothesis.