fix: git gets plain Windows paths; the FIFO test is Unix-only (P11c)
check / check (push) Canceled after 0s
portable-macos / portable (macos-latest) (push) Canceled after 0s
portable-windows / portable (windows-latest) (push) Canceled after 0s
two-user / two-user (push) Canceled after 0s

std::fs::canonicalize on Windows returns \\?\C:\..., which git refuses
("not a git repository"): every argument handed to git now has the
extended-length prefix taken off (root::without_extended_prefix, with a
unit test that runs everywhere; \\?\UNC\ becomes \\). The access-record
test's fixture is a FIFO in the log's place, which Windows has no
counterpart for; the refusal path is the same code on every system, so
the test and its helper are Unix-only. main (8f0e5f08) merged in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
swaitsandClaude Fable 5.1 committed 2026-10-30 03:13:47 -06:00
1 parent fa7c2805a6
commit 9f57269def
3 files changed
+45 -3

No files matched your search

+6 -2
View File
@@ -1159,7 +1159,9 @@ fn logged(tag: &str) -> (BooksServer, TestDir) {
} }
/// Books whose access log can't be written: its file is a FIFO no one /// Books whose access log can't be written: its file is a FIFO no one
/// reads, so every record fails (as on a full disk). /// reads, so every record fails (as on a full disk). Unix only: there is
/// no FIFO in a file's place on Windows.
#[cfg(unix)]
fn unwritable(tag: &str) -> (BooksServer, TestDir) { fn unwritable(tag: &str) -> (BooksServer, TestDir) {
let books = unit_dir(tag); let books = unit_dir(tag);
let store = store(&books); let store = store(&books);
@@ -1430,7 +1432,9 @@ fn a_call_the_books_close_or_stop_under_is_refused_as_not_run() {
/// access.rs INVARIANT 3: a record that can't be written stops the /// access.rs INVARIANT 3: a record that can't be written stops the
/// books, and the answer says so; a call or read refused as not run /// books, and the answer says so; a call or read refused as not run
/// (closing) keeps its not-run code, so the bridge still sends it /// (closing) keeps its not-run code, so the bridge still sends it
/// again; anything else is an error in words. /// again; anything else is an error in words. Unix only: its fixture is a
/// FIFO (the refusal path itself is the same on every system).
#[cfg(unix)]
#[test] #[test]
fn an_access_record_that_cant_be_written_stops_the_books_and_says_so() { fn an_access_record_that_cant_be_written_stops_the_books_and_says_so() {
rt().block_on(async { rt().block_on(async {
+7 -1
View File
@@ -223,7 +223,13 @@ impl Git {
fn run(&self, what: &str, args: &[&std::ffi::OsStr]) -> Result<String, BackupError> { fn run(&self, what: &str, args: &[&std::ffi::OsStr]) -> Result<String, BackupError> {
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
let mut c = self.command(); let mut c = self.command();
c.args(args); // A path made canonical on Windows carries `\\?\`, which git refuses.
c.args(args.iter().map(|a| match a.to_str() {
Some(text) if text.contains(r"\\?\") => {
std::ffi::OsString::from(crate::root::without_extended_prefix(text))
}
_ => (*a).to_os_string(),
}));
#[cfg(unix)] #[cfg(unix)]
{ {
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
+32
View File
@@ -314,6 +314,23 @@ pub fn private_book_folder(book_dir: &Path, name: &str) -> Result<PathBuf, RootE
Ok(dir) Ok(dir)
} }
/// `text` with Windows' extended-length prefix taken off the paths in it:
/// `\\?\C:\dir` becomes `C:\dir` and `\\?\UNC\server\share` becomes
/// `\\server\share`. `std::fs::canonicalize` returns that form on Windows,
/// and programs such as git refuse it. Text without the prefix is
/// returned as it is, on every system (so the rule is tested everywhere).
pub fn without_extended_prefix(text: &str) -> String {
text.replace(r"\\?\UNC\", r"\\").replace(r"\\?\", "")
}
/// [`without_extended_prefix`] for a path (the same path on Unix).
pub fn plain_path(path: &Path) -> PathBuf {
match path.to_str() {
Some(text) if text.contains(r"\\?\") => PathBuf::from(without_extended_prefix(text)),
_ => path.to_path_buf(),
}
}
/// The longest socket path used as it is: a Unix socket's address holds /// The longest socket path used as it is: a Unix socket's address holds
/// 104 bytes on macOS and 108 on Linux, the terminating zero included. /// 104 bytes on macOS and 108 on Linux, the terminating zero included.
const SOCKET_PATH_MAX: usize = 100; const SOCKET_PATH_MAX: usize = 100;
@@ -2264,6 +2281,21 @@ fn copy_flat_private(from: &Path, to: &Path) -> Result<u64, RootError> {
mod unit { mod unit {
use super::*; use super::*;
#[test]
fn the_extended_length_prefix_is_taken_off_paths() {
assert_eq!(without_extended_prefix(r"\\?\C:\a\b"), r"C:\a\b");
assert_eq!(
without_extended_prefix(r"--git-dir=\\?\C:\a\archive.git"),
r"--git-dir=C:\a\archive.git"
);
assert_eq!(
without_extended_prefix(r"\\?\UNC\server\share\x"),
r"\\server\share\x"
);
assert_eq!(without_extended_prefix("/usr/bin/git"), "/usr/bin/git");
assert_eq!(plain_path(Path::new("/a/b")), Path::new("/a/b"));
}
#[test] #[test]
fn a_buffer_grows_while_the_lookup_says_too_small() { fn a_buffer_grows_while_the_lookup_says_too_small() {
// Too small twice, then found: 16, 32, 64 KiB tried, in order. // Too small twice, then found: 16, 32, 64 KiB tried, in order.