/// Process-wide `git` configuration for the child processes this service spawns. /// /// The forge does not build the argv for those: `gitbaby` does, and it parses /// path names out of `git ls-tree` **text** output. See [`init`] for why one of /// those settings has to be forced from the environment. use std::sync::Once; static INIT: Once = Once::new(); /// Force `core.quotepath=off` for every `git` child process of this service. /// /// With the default `core.quotePath=true`, git prints any path holding a byte it /// considers non-printable as a quoted, octal-escaped C string: `说明.md` comes /// back over the pipe as the literal `"\350\257\264\346\230\216.md"`, **quotes /// included**. `gitbaby`'s `ls-tree` parser takes everything after the tab /// verbatim as the entry name, so that quoting becomes the name the whole forge /// then works with — the directory listing, the breadcrumb, the blob and history /// lookups, and the JSON tree API. It is visible most clearly on markdown: an /// opened `说明.md` no longer ends in `.md`, so the viewer stops treating it as a /// document and shows it as source. /// /// `gitbaby` already passes `-c core.quotepath=off` for the two commands that /// report paths (`log` for last-commit and path history); `ls-tree` is not one of /// them and its argv is not ours to change. Environment config is the supported /// way to reach a subprocess we do not build arguments for: git reads /// `GIT_CONFIG_KEY_` / `GIT_CONFIG_VALUE_` with the same precedence as `-c` /// (later entries win), and `gitbaby`'s `build_env` forwards the whole process /// environment to the child. It also survives `GIT_CONFIG_NOSYSTEM`, which only /// suppresses the system config *file* — verified against both. /// /// Operator-supplied `GIT_CONFIG_*` slots are preserved: this appends one more /// rather than resetting the count, so ours simply takes precedence. If the /// count is already present but unreadable the environment is not ours to guess /// at, and nothing is changed. /// /// # Safety /// /// `std::env::set_var` races with concurrent environment reads, so it is called /// exactly once, from [`crate::service::GitService::new`], which every host runs /// while it is still wiring itself up — before any listener is bound. pub fn init() { INIT.call_once(|| { let count = match std::env::var("GIT_CONFIG_COUNT") { Err(_) => 0, Ok(raw) => match raw.trim().parse::() { Ok(value) => value, Err(_) => { tracing::warn!( value = raw.as_str(), "GIT_CONFIG_COUNT is not a number; leaving git config env untouched" ); return; } }, }; let key_slot = format!("GIT_CONFIG_KEY_{count}"); unsafe { std::env::set_var(&key_slot, "core.quotepath"); std::env::set_var(format!("GIT_CONFIG_VALUE_{count}"), "off"); std::env::set_var("GIT_CONFIG_COUNT", (count + 1).to_string()); } tracing::debug!(slot = key_slot.as_str(), "forced core.quotepath=off for git children"); }); } #[cfg(test)] mod tests { use super::*; /// What the fix actually promises is about a *child* process, so ask one: /// `git config --get` reads the same environment slots `ls-tree` will, with /// no repository involved. If this returns `off`, the quoting is really gone /// for every git command `gitbaby` runs on our behalf. #[test] fn child_git_sees_quotepath_off() { init(); let probe = std::process::Command::new("git") .args(["config", "--get", "core.quotepath"]) .output() .expect("git on PATH"); assert!( probe.status.success(), "`git config --get core.quotepath` failed: {}", String::from_utf8_lossy(&probe.stderr) ); assert_eq!( String::from_utf8_lossy(&probe.stdout).trim(), "off", "the appended GIT_CONFIG slot must reach the child" ); } }