From cd324678fe86b090c45fa3743b90d9f635b24bef Mon Sep 17 00:00:00 2001 From: Surma Date: Fri, 6 Nov 2026 20:04:06 +0000 Subject: [PATCH] Preserve indexed executable modes in WASI git --- userland/git/src/changes.rs | 18 +++++++++++++++++- userland/git/src/staging.rs | 4 ++-- userland/git/test.nu | 18 ++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/userland/git/src/changes.rs b/userland/git/src/changes.rs index 5c26ca3..b91fdfe 100644 --- a/userland/git/src/changes.rs +++ b/userland/git/src/changes.rs @@ -238,6 +238,22 @@ pub fn status(repo: &gix::Repository) -> Result { .map_err(|e| error("status", e))?; for item in iter { let item = item.map_err(|e| error("status", e))?; + #[cfg(target_os = "wasi")] + if matches!( + &item, + Item::Modification { + status: gix::status::plumbing::index_as_worktree::EntryStatus::Change( + gix::status::plumbing::index_as_worktree::Change::Modification { + executable_bit_changed: true, + content_change: None, + .. + } + ), + .. + } + ) { + continue; + } let path = item.rela_path().to_str_lossy().into_owned(); let Some(summary) = item.summary() else { continue; @@ -332,7 +348,7 @@ fn worktree_file( } else { entry.mode }; - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "wasi")))] let mode = if mode != 0o120000 && caps.executable_bit { use std::os::unix::fs::PermissionsExt; if meta.permissions().mode() & 0o111 != 0 { diff --git a/userland/git/src/staging.rs b/userland/git/src/staging.rs index d5d873d..a0e59a3 100644 --- a/userland/git/src/staging.rs +++ b/userland/git/src/staging.rs @@ -229,7 +229,7 @@ fn work_entry( Mode::SYMLINK, ) } else if m.is_file() { - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "wasi")))] let mode = { use std::os::unix::fs::PermissionsExt; if m.permissions().mode() & 0o111 != 0 { @@ -238,7 +238,7 @@ fn work_entry( Mode::FILE } }; - #[cfg(not(unix))] + #[cfg(any(not(unix), target_os = "wasi"))] let mode = if previous == Some(Mode::FILE_EXECUTABLE) { Mode::FILE_EXECUTABLE } else { diff --git a/userland/git/test.nu b/userland/git/test.nu index 5b57829..eb0c78b 100644 --- a/userland/git/test.nu +++ b/userland/git/test.nu @@ -268,6 +268,24 @@ def main [component: path] { print $'($args | str join " "): (if $equal {"PASS documented usage"} else {"FAIL documented usage"}) wasi=($result.exit_code)' if not $equal { $failures += 1; print ($result | to json) } } + # WASI checkout and staging retain an existing executable file's indexed mode. + let executable = ($root | path join executable) + mkdir $executable + ^git -C $executable init -q -b main + "#!/bin/sh\necho executable\n" | save ($executable | path join run.sh) + ^chmod +x ($executable | path join run.sh) + ^git -C $executable add run.sh + ^git -C $executable commit --no-gpg-sign -q -m executable + rm ($executable | path join run.sh) + mut executable_ok = true + for args in [[checkout -q HEAD -- run.sh] [status --short] [diff] [add run.sh] [diff --cached] [status --short]] { + let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($executable)::/mode' $component -C /mode ...$args | complete) + if $result.exit_code != 0 or $result.stdout != '' { $executable_ok = false; print ($result | to json) } + } + let indexed = (^git -C $executable ls-files --stage run.sh | complete) + $executable_ok = ($executable_ok and $indexed.exit_code == 0 and ($indexed.stdout | str starts-with '100755 ')) + print $'executable checkout stays clean: (if $executable_ok {"PASS"} else {"FAIL"})' + if not $executable_ok { $failures += 1 } # A merge tree that differs from both parents requires a combined diff. 'combined resolution\n' | save --force ($root | path join native/repo/file.txt) let native = ($root | path join native/repo)