Skip to content

Commit f84839c

Browse files
committed
fix: isolate anonymous Git probes
1 parent e5559e7 commit f84839c

4 files changed

Lines changed: 170 additions & 49 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ git clone https://github.com/owner/repository.git
3030

3131
线路失效时在 GitBoost 中重新检测或切换节点即可,仓库里的 `origin` 不变。
3232

33+
“仅加速清单”沿用 Git `insteadOf` 的 URL 前缀匹配语义,不按仓库身份做精确匹配。例如,加入 `owner/repo` 后,`owner/repo-private` 也可能命中同一条路由。这是当前系统的既定路由特性,不作为待修复问题;如果同一 owner 下存在名称前缀相同且可能包含私有内容的仓库,请不要添加该前缀较短的仓库。
34+
3335
### Claude Code 的 Plugin 也需要访问 GitHub
3436

3537
[Claude Code Marketplace](https://code.claude.com/docs/en/plugin-marketplaces) 支持用 GitHub 的 `owner/repo` 作为来源。例如 [Superpowers Marketplace](https://github.com/obra/superpowers-marketplace) 的安装命令是:

‎docs/USER_GUIDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ https://github.com/owner/repository.git
5050

5151
“全局加速”会让所有 GitHub HTTPS 读取经过第三方节点。Git 无法判断仓库是否公开;只有确定不会访问私有仓库时,才使用这个模式。
5252

53-
清单按 URL 前缀匹配。例如,`owner/repo` 也可能匹配 `owner/repo-private`。如果公开仓库和私有仓库存在这种命名关系,不要把前者加入清单。
53+
清单沿用 Git `insteadOf` 的 URL 前缀匹配语义,不按仓库身份做精确匹配。例如,`owner/repo` 也可能匹配 `owner/repo-private`。这是当前系统的既定路由特性,不作为待修复问题;如果公开仓库和私有仓库存在这种命名关系,不要把前者加入清单。
5454

5555
### 第三步:开启加速
5656

‎src-tauri/src/core.rs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3682,6 +3682,25 @@ mod tests {
36823682

36833683
let catalog = fetch_system_catalog().unwrap();
36843684
core.apply_system_catalog(&catalog).unwrap();
3685+
3686+
// Production probes must remain anonymous even when the launching user has
3687+
// a more-specific rewrite, repository context, or credential helper configured.
3688+
let clean_global = std::env::var_os("GIT_CONFIG_GLOBAL").unwrap();
3689+
let hostile_global = directory.path().join("hostile.gitconfig");
3690+
fs::write(
3691+
&hostile_global,
3692+
"[credential]\n\thelper = inherited-helper\n",
3693+
)
3694+
.unwrap();
3695+
std::env::set_var("GIT_CONFIG_GLOBAL", &hostile_global);
3696+
std::env::set_var("GIT_CONFIG_COUNT", "2");
3697+
std::env::set_var("GIT_CONFIG_KEY_0", "url.http://127.0.0.1:9/.insteadOf");
3698+
std::env::set_var("GIT_CONFIG_VALUE_0", TEST_REPOSITORY);
3699+
std::env::set_var("GIT_CONFIG_KEY_1", "credential.helper");
3700+
std::env::set_var("GIT_CONFIG_VALUE_1", "dynamic-helper");
3701+
std::env::set_var("GIT_DIR", directory.path().join("missing.git"));
3702+
std::env::set_var("GIT_WORK_TREE", directory.path());
3703+
36853704
let progress = Mutex::new(Vec::new());
36863705
let started = std::sync::Barrier::new(2);
36873706
let release = std::sync::Barrier::new(2);
@@ -3705,6 +3724,20 @@ mod tests {
37053724
core.test_all_nodes_or_join_with_progress(|_, _| {})
37063725
})
37073726
.unwrap();
3727+
3728+
std::env::set_var("GIT_CONFIG_GLOBAL", clean_global);
3729+
for key in [
3730+
"GIT_CONFIG_COUNT",
3731+
"GIT_CONFIG_KEY_0",
3732+
"GIT_CONFIG_VALUE_0",
3733+
"GIT_CONFIG_KEY_1",
3734+
"GIT_CONFIG_VALUE_1",
3735+
"GIT_DIR",
3736+
"GIT_WORK_TREE",
3737+
] {
3738+
std::env::remove_var(key);
3739+
}
3740+
37083741
let owner_result = owner_result.into_inner().unwrap().unwrap();
37093742
assert_eq!(
37103743
owner_result

‎src-tauri/src/git.rs‎

Lines changed: 134 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const SLOW_THRESHOLD_MS: u64 = 2_500;
2020
#[cfg(target_os = "windows")]
2121
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
2222

23+
#[derive(Clone, Copy)]
24+
enum GitEnvironment {
25+
User,
26+
AnonymousProbe,
27+
}
28+
2329
pub fn git_version() -> Option<String> {
2430
run_git(["--version"], None, Duration::from_secs(5))
2531
.ok()
@@ -70,10 +76,11 @@ pub fn test_node(node: &NodeDefinition, previous: &HealthSummary) -> HealthSumma
7076
for _ in 0..2 {
7177
let started = Instant::now();
7278
let config = format!("url.{}.insteadOf=https://github.com/", node.rewrite_base);
73-
let result = run_git(
79+
let result = run_git_with_environment(
7480
["-c", config.as_str(), "ls-remote", TEST_REPOSITORY, "HEAD"],
7581
None,
7682
COMMAND_TIMEOUT,
83+
GitEnvironment::AnonymousProbe,
7784
);
7885
let elapsed = started.elapsed().as_millis() as u64;
7986
next.attempt_count = next.attempt_count.saturating_add(1);
@@ -483,25 +490,25 @@ fn affects_github_https(rewrite_prefix: &str) -> bool {
483490
}
484491

485492
pub fn effective_urls(config_path: &Path, original_url: &str) -> Result<(String, String), String> {
486-
effective_urls_with_global_config(config_path, original_url, None)
493+
effective_urls_with_environment(config_path, original_url, GitEnvironment::User)
487494
}
488495

489496
pub fn effective_urls_isolated(
490497
config_path: &Path,
491498
original_url: &str,
492499
) -> Result<(String, String), String> {
493-
effective_urls_with_global_config(config_path, original_url, Some(config_path))
500+
effective_urls_with_environment(config_path, original_url, GitEnvironment::AnonymousProbe)
494501
}
495502

496-
fn effective_urls_with_global_config(
503+
fn effective_urls_with_environment(
497504
config_path: &Path,
498505
original_url: &str,
499-
global_config: Option<&Path>,
506+
environment: GitEnvironment,
500507
) -> Result<(String, String), String> {
501508
let temp = tempfile::tempdir().map_err(|error| format!("无法创建诊断目录:{error}"))?;
502509
let directory = temp.path().to_string_lossy();
503510
let include = format!("include.path={}", config_path.display());
504-
let init = run_git_with_global_config(
511+
let init = run_git_with_environment(
505512
[
506513
"-c",
507514
include.as_str(),
@@ -510,14 +517,14 @@ fn effective_urls_with_global_config(
510517
"init",
511518
"-q",
512519
],
513-
None,
520+
Some(temp.path()),
514521
Duration::from_secs(8),
515-
global_config,
522+
environment,
516523
)?;
517524
if !init.status.success() {
518525
return Err(command_error(&init));
519526
}
520-
let add = run_git_with_global_config(
527+
let add = run_git_with_environment(
521528
[
522529
"-c",
523530
include.as_str(),
@@ -528,14 +535,14 @@ fn effective_urls_with_global_config(
528535
"origin",
529536
original_url,
530537
],
531-
None,
538+
Some(temp.path()),
532539
Duration::from_secs(8),
533-
global_config,
540+
environment,
534541
)?;
535542
if !add.status.success() {
536543
return Err(command_error(&add));
537544
}
538-
let fetch = run_git_with_global_config(
545+
let fetch = run_git_with_environment(
539546
[
540547
"-c",
541548
include.as_str(),
@@ -545,11 +552,11 @@ fn effective_urls_with_global_config(
545552
"get-url",
546553
"origin",
547554
],
548-
None,
555+
Some(temp.path()),
549556
Duration::from_secs(8),
550-
global_config,
557+
environment,
551558
)?;
552-
let push = run_git_with_global_config(
559+
let push = run_git_with_environment(
553560
[
554561
"-c",
555562
include.as_str(),
@@ -560,9 +567,9 @@ fn effective_urls_with_global_config(
560567
"--push",
561568
"origin",
562569
],
563-
None,
570+
Some(temp.path()),
564571
Duration::from_secs(8),
565-
global_config,
572+
environment,
566573
)?;
567574
if !fetch.status.success() || !push.status.success() {
568575
return Err("无法解析 Git 有效地址".into());
@@ -679,32 +686,70 @@ fn run_git<const N: usize>(
679686
current_dir: Option<&Path>,
680687
timeout: Duration,
681688
) -> Result<Output, String> {
682-
run_git_with_global_config(args, current_dir, timeout, None)
689+
run_git_with_environment(args, current_dir, timeout, GitEnvironment::User)
683690
}
684691

685-
fn run_git_with_global_config<const N: usize>(
692+
fn run_git_with_environment<const N: usize>(
686693
args: [&str; N],
687694
current_dir: Option<&Path>,
688695
timeout: Duration,
689-
global_config: Option<&Path>,
696+
environment: GitEnvironment,
690697
) -> Result<Output, String> {
698+
let isolation = match environment {
699+
GitEnvironment::User => None,
700+
GitEnvironment::AnonymousProbe => {
701+
let directory =
702+
tempfile::tempdir().map_err(|error| format!("无法创建 Git 隔离目录:{error}"))?;
703+
fs::write(directory.path().join("global.gitconfig"), "")
704+
.map_err(|error| format!("无法创建 Git 隔离配置:{error}"))?;
705+
Some(directory)
706+
}
707+
};
691708
let mut command = Command::new("git");
692-
// GitBoost's own probes and configuration checks are operational noise, not user traffic.
693-
command.env("GIT_TRACE2_EVENT", "0");
694709
command
695710
.args(args)
711+
// GitBoost's own probes and configuration checks are operational noise, not user traffic.
712+
.env("GIT_TRACE2_EVENT", "0")
696713
.env("GIT_TERMINAL_PROMPT", "0")
697714
.stdout(Stdio::piped())
698715
.stderr(Stdio::piped());
699-
if let Some(config) = global_config {
716+
717+
if let Some(directory) = isolation.as_ref() {
718+
for (key, _) in std::env::vars_os() {
719+
if key
720+
.to_string_lossy()
721+
.to_ascii_uppercase()
722+
.starts_with("GIT_")
723+
{
724+
command.env_remove(key);
725+
}
726+
}
700727
command
701-
.env("GIT_CONFIG_GLOBAL", config)
702-
.env("GIT_CONFIG_NOSYSTEM", "1");
728+
.env_remove("SSH_ASKPASS")
729+
.env_remove("SSH_ASKPASS_REQUIRE")
730+
.env("HOME", directory.path())
731+
.env("USERPROFILE", directory.path())
732+
.env("XDG_CONFIG_HOME", directory.path())
733+
.env(
734+
"GIT_CONFIG_GLOBAL",
735+
directory.path().join("global.gitconfig"),
736+
)
737+
.env("GIT_CONFIG_NOSYSTEM", "1")
738+
.env("GIT_CONFIG_COUNT", "1")
739+
.env("GIT_CONFIG_KEY_0", "credential.helper")
740+
.env("GIT_CONFIG_VALUE_0", "")
741+
.env("GIT_TRACE2_EVENT", "0")
742+
.env("GIT_TERMINAL_PROMPT", "0");
703743
}
704-
hide_console(&mut command);
705744
if let Some(directory) = current_dir {
706745
command.current_dir(directory);
746+
} else if let Some(directory) = isolation.as_ref() {
747+
command.current_dir(directory.path());
707748
}
749+
if matches!(environment, GitEnvironment::AnonymousProbe) {
750+
command.env_remove("HOMEDRIVE").env_remove("HOMEPATH");
751+
}
752+
hide_console(&mut command);
708753
let mut child = command
709754
.spawn()
710755
.map_err(|error| format!("无法启动系统 Git:{error}"))?;
@@ -801,42 +846,83 @@ mod tests {
801846

802847
#[test]
803848
fn isolated_effective_urls_ignore_the_registered_live_config() {
849+
const MARKER: &str = "GITBOOST_ISOLATED_URLS_CHILD";
850+
if std::env::var_os(MARKER).is_none() {
851+
let directory = tempfile::tempdir().unwrap();
852+
let live = directory.path().join("live.gitconfig");
853+
let global = directory.path().join("global.gitconfig");
854+
fs::write(
855+
&live,
856+
"[url \"https://old.example/https://github.com/openai/codex\"]\n\tinsteadOf = https://github.com/openai/codex\n[credential]\n\thelper = inherited-helper\n",
857+
)
858+
.unwrap();
859+
fs::write(
860+
&global,
861+
format!(
862+
"[include]\n\tpath = \"{}\"\n",
863+
escape_subsection(&live.to_string_lossy())
864+
),
865+
)
866+
.unwrap();
867+
let status = Command::new(std::env::current_exe().unwrap())
868+
.args([
869+
"--exact",
870+
"git::tests::isolated_effective_urls_ignore_the_registered_live_config",
871+
"--nocapture",
872+
])
873+
.env(MARKER, "1")
874+
.env("GIT_CONFIG_GLOBAL", &global)
875+
.env("GIT_CONFIG_NOSYSTEM", "1")
876+
.env("GIT_CONFIG_COUNT", "2")
877+
.env(
878+
"GIT_CONFIG_KEY_0",
879+
"url.https://dynamic.example/https://github.com/openai/codex.git.insteadOf",
880+
)
881+
.env("GIT_CONFIG_VALUE_0", "https://github.com/openai/codex.git")
882+
.env("GIT_CONFIG_KEY_1", "credential.helper")
883+
.env("GIT_CONFIG_VALUE_1", "dynamic-helper")
884+
.env("GIT_DIR", directory.path().join("missing.git"))
885+
.env("GIT_WORK_TREE", directory.path())
886+
.env("GIT_ASKPASS", directory.path().join("missing-askpass"))
887+
.env("SSH_ASKPASS", directory.path().join("missing-ssh-askpass"))
888+
.status()
889+
.unwrap();
890+
assert!(status.success(), "isolated Git environment test failed");
891+
return;
892+
}
893+
804894
let directory = tempfile::tempdir().unwrap();
805-
let live = directory.path().join("live.gitconfig");
806895
let candidate = directory.path().join("candidate.gitconfig");
807-
let global = directory.path().join("global.gitconfig");
808-
fs::write(
809-
&live,
810-
"[url \"https://old.example/https://github.com/openai/codex\"]\n\tinsteadOf = https://github.com/openai/codex\n",
811-
)
812-
.unwrap();
813896
fs::write(
814897
&candidate,
815898
"[url \"https://new.example/https://github.com/openai/codex\"]\n\tinsteadOf = https://github.com/openai/codex\n",
816899
)
817900
.unwrap();
818-
fs::write(
819-
&global,
820-
format!(
821-
"[include]\n\tpath = \"{}\"\n",
822-
escape_subsection(&live.to_string_lossy())
823-
),
824-
)
825-
.unwrap();
826901

827902
let original = "https://github.com/openai/codex.git";
828-
let (conflicted, _) =
829-
effective_urls_with_global_config(&candidate, original, Some(&global)).unwrap();
830-
assert_eq!(
831-
conflicted,
832-
"https://old.example/https://github.com/openai/codex.git"
833-
);
834-
835903
let (fetch, _) = effective_urls_isolated(&candidate, original).unwrap();
836904
assert_eq!(
837905
fetch,
838906
"https://new.example/https://github.com/openai/codex.git"
839907
);
908+
909+
let helpers = run_git_with_environment(
910+
["config", "--get-all", "credential.helper"],
911+
None,
912+
Duration::from_secs(5),
913+
GitEnvironment::AnonymousProbe,
914+
)
915+
.unwrap();
916+
assert!(helpers.status.success());
917+
assert_eq!(String::from_utf8_lossy(&helpers.stdout).trim(), "");
918+
919+
std::env::remove_var("GIT_DIR");
920+
std::env::remove_var("GIT_WORK_TREE");
921+
let (conflicted, _) = effective_urls(&candidate, original).unwrap();
922+
assert_eq!(
923+
conflicted,
924+
"https://dynamic.example/https://github.com/openai/codex.git"
925+
);
840926
}
841927

842928
#[test]

0 commit comments

Comments
 (0)