Skip to content
Clone

fix(git): force core.quotepath=off for git children

gitbaby parses path names out of `git ls-tree` text output, and with the
default core.quotePath=true every non-ASCII byte comes back octally
escaped *with the surrounding quotes*: 说明.md arrives over the pipe as
"\350\257\264\346\230\216.md". That quoting then propagates into the tree
listing, breadcrumbs, blob and history lookups and the JSON tree API, and
is most visible on markdown: an opened 说明.md no longer ends in .md, so
the viewer stops treating it as a document.

gitbaby only passes -c core.quotepath=off for its two log commands and
the ls-tree argv is not ours to change, so pin it through
GIT_CONFIG_KEY_<n>/GIT_CONFIG_VALUE_<n>, which git reads with the same
precedence as -c and which survives GIT_CONFIG_NOSYSTEM. The slot is
appended after any operator-supplied GIT_CONFIG_* rather than resetting
the count, and the set_var happens once from GitService::new while hosts
are still wiring up, before any listener is bound.
ZhenYi committed
parent 75ab6b6 commit 563bb99 Browse files

3 files changed

+97 −0

File tree 3
Modified lib/git/src/service/mod.rs View lib/git/src/service/mod.rs
@@ -13,6 +13,10 @@
1313
1414impl GitService {
1515 pub fn new(state: AppState) -> Self {
16 // Every host builds a GitService before it can touch a repository, so
17 // this is the one place that can still shape the environment the `git`
18 // children will inherit. See `share::git_env` for what it pins and why.
19 crate::share::git_env::init();
1620 Self { state }
1721 }
1822
Added lib/git/src/share/git_env.rs View lib/git/src/share/git_env.rs
@@ -0,0 +1,92 @@
1/// Process-wide `git` configuration for the child processes this service spawns.
2///
3/// The forge does not build the argv for those: `gitbaby` does, and it parses
4/// path names out of `git ls-tree` **text** output. See [`init`] for why one of
5/// those settings has to be forced from the environment.
6use std::sync::Once;
7
8static INIT: Once = Once::new();
9
10/// Force `core.quotepath=off` for every `git` child process of this service.
11///
12/// With the default `core.quotePath=true`, git prints any path holding a byte it
13/// considers non-printable as a quoted, octal-escaped C string: `说明.md` comes
14/// back over the pipe as the literal `"\350\257\264\346\230\216.md"`, **quotes
15/// included**. `gitbaby`'s `ls-tree` parser takes everything after the tab
16/// verbatim as the entry name, so that quoting becomes the name the whole forge
17/// then works with — the directory listing, the breadcrumb, the blob and history
18/// lookups, and the JSON tree API. It is visible most clearly on markdown: an
19/// opened `说明.md` no longer ends in `.md`, so the viewer stops treating it as a
20/// document and shows it as source.
21///
22/// `gitbaby` already passes `-c core.quotepath=off` for the two commands that
23/// report paths (`log` for last-commit and path history); `ls-tree` is not one of
24/// them and its argv is not ours to change. Environment config is the supported
25/// way to reach a subprocess we do not build arguments for: git reads
26/// `GIT_CONFIG_KEY_<n>` / `GIT_CONFIG_VALUE_<n>` with the same precedence as `-c`
27/// (later entries win), and `gitbaby`'s `build_env` forwards the whole process
28/// environment to the child. It also survives `GIT_CONFIG_NOSYSTEM`, which only
29/// suppresses the system config *file* — verified against both.
30///
31/// Operator-supplied `GIT_CONFIG_*` slots are preserved: this appends one more
32/// rather than resetting the count, so ours simply takes precedence. If the
33/// count is already present but unreadable the environment is not ours to guess
34/// at, and nothing is changed.
35///
36/// # Safety
37///
38/// `std::env::set_var` races with concurrent environment reads, so it is called
39/// exactly once, from [`crate::service::GitService::new`], which every host runs
40/// while it is still wiring itself up — before any listener is bound.
41pub fn init() {
42 INIT.call_once(|| {
43 let count = match std::env::var("GIT_CONFIG_COUNT") {
44 Err(_) => 0,
45 Ok(raw) => match raw.trim().parse::<usize>() {
46 Ok(value) => value,
47 Err(_) => {
48 tracing::warn!(
49 value = raw.as_str(),
50 "GIT_CONFIG_COUNT is not a number; leaving git config env untouched"
51 );
52 return;
53 }
54 },
55 };
56 let key_slot = format!("GIT_CONFIG_KEY_{count}");
57 unsafe {
58 std::env::set_var(&key_slot, "core.quotepath");
59 std::env::set_var(format!("GIT_CONFIG_VALUE_{count}"), "off");
60 std::env::set_var("GIT_CONFIG_COUNT", (count + 1).to_string());
61 }
62 tracing::debug!(slot = key_slot.as_str(), "forced core.quotepath=off for git children");
63 });
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 /// What the fix actually promises is about a *child* process, so ask one:
71 /// `git config --get` reads the same environment slots `ls-tree` will, with
72 /// no repository involved. If this returns `off`, the quoting is really gone
73 /// for every git command `gitbaby` runs on our behalf.
74 #[test]
75 fn child_git_sees_quotepath_off() {
76 init();
77 let probe = std::process::Command::new("git")
78 .args(["config", "--get", "core.quotepath"])
79 .output()
80 .expect("git on PATH");
81 assert!(
82 probe.status.success(),
83 "`git config --get core.quotepath` failed: {}",
84 String::from_utf8_lossy(&probe.stderr)
85 );
86 assert_eq!(
87 String::from_utf8_lossy(&probe.stdout).trim(),
88 "off",
89 "the appended GIT_CONFIG slot must reach the child"
90 );
91 }
92}
Modified lib/git/src/share/mod.rs View lib/git/src/share/mod.rs
@@ -1,5 +1,6 @@
11pub mod access;
22pub mod errors;
3pub mod git_env;
34pub mod identity;
45pub mod policy;
56pub mod validate;