69f12aa
/// 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_<n>` / `GIT_CONFIG_VALUE_<n>` 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::<usize>() {
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"
);
}
}