fix: copy-ignored handles non-ASCII filenames (git quotePath)#3487
fix: copy-ignored handles non-ASCII filenames (git quotePath)#3487worktrunk-bot wants to merge 3 commits into
Conversation
git ls-files quotes non-ASCII paths by default (core.quotePath=true), returning e.g. "data/Report \342\200\224 Q1.txt" — surrounding quotes and octal escapes included — which was then treated as a literal filesystem path and never found, so the entry was silently dropped and the metadata read failed with 'No such file or directory', exiting 1. Pass -z to git ls-files and split on NUL, which disables quoting and also sidesteps the newline-in-filename edge case. Closes #3486
worktrunk-bot
left a comment
There was a problem hiding this comment.
The -z switch and NUL-split are the right fix for the reported case, and the new test pins it well — git ls-files --directory -z still trails directory entries with /, and the !entry.is_empty() filter correctly drops the trailing NUL terminator, so parsing is equivalent to the old .lines() path minus the quoting bug. Verified locally: list_ignored_entries_handles_non_ascii_names fails on the old code and passes here.
One residual worth a look (not a regression — it was equally broken before, and this diff strictly improves the common case): -z disables git's quoting, but list_ignored_entries then does String::from_utf8_lossy(&output.stdout) and builds the real PathBuf used for the copy from that lossy string. For a filename with genuinely invalid UTF-8 bytes (arbitrary bytes are legal on Linux), from_utf8_lossy substitutes U+FFFD, so the constructed path no longer exists on disk — copy-ignored silently skips the file, and wt step promote's stage_ignored/distribute_staged (guarded by symlink_metadata(...).is_ok()) silently leave it un-exchanged. So the fix resolves valid-UTF-8 non-ASCII names (the reporter's em dash), not the whole "verbatim regardless of core.quotePath" set the PR body describes.
The sibling -z reader in untracked_diff_stats (src/git/repository/working_tree.rs) sidesteps this by splitting the raw bytes on the NUL byte (output.stdout.split(|&b| b == 0)) and feeding those raw bytes to the git operation, keeping the lossy String display-only. If closing the invalid-UTF-8 gap is in scope here, the same shape — split output.stdout on the 0 byte and, on Unix, build each PathBuf via std::os::unix::ffi::OsStrExt::from_bytes rather than through from_utf8_lossy — would make discovery byte-exact. It adds #[cfg(unix)] handling, so it's a judgment call whether it belongs in this PR or a follow-up; flagging since correct non-ASCII handling is precisely this change's subject.
Use .expect() instead of unwrap_or_else(|| panic!(...)) so the assertion has no failure-only line that codecov/patch flags as uncovered.
Problem
wt step copy-ignoredfailed to copy any ignored file whose path contains non-ASCII characters, and exited 1 — which, because the docs recommend wiring it into a blockingpre-starthook, madewt switch --createfail in any repo containing such a file.Root cause:
git ls-filesquotes non-ASCII paths by default (core.quotePath = true), solist_ignored_entriesinsrc/commands/step/shared.rsreceived e.g."data/Report \342\200\224 Q1.txt"— surrounding double quotes and octal escapes included. That literal string was joined onto the worktree path and treated as a real filesystem path, so the entry was never matched and the subsequent metadata read failed withNo such file or directory (os error 2).Solution
Pass
-ztogit ls-filesand split the output on NUL.-zNUL-terminates entries and disables path quoting, so paths come through verbatim regardless ofcore.quotePath. It also sidesteps the newline-in-filename edge case that line splitting can't represent. This matches the reporter's suggested fix.Testing
Added
list_ignored_entries_handles_non_ascii_namesto the existing test module insrc/commands/step/shared.rs. It creates an ignored file with an em dash (U+2014) in its name and asserts discovery returns a path that exists on disk. The test fails on the old code (the discovered path is the quoted/escaped string, which doesn't exist) and passes with the fix. The fullstep_copy_ignoredintegration suite passes (one unrelated pre-existing failure,test_copy_ignored_preserves_file_executable_permissions, is a sandbox-umask artifact — 0664 vs 0644 — and fails identically without this change).Closes #3486 — automated triage