| @@ -206,18 +206,46 @@ fn wildcard_match(pattern: &str, text: &str) -> bool { · 205 unmodified lines |
| 206 | 206 | pattern.ends_with('*') || remainder.is_empty() |
| 207 | 207 | } |
| 208 | 208 | |
| 209 | | −/// A wrapper for a running git-daemon process which is killed automatically on drop. |
| 209 | +/// A wrapper for a running git-daemon which is stopped automatically on drop. |
| 210 | 210 | /// |
| 211 | 211 | /// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed. |
| 212 | 212 | pub struct GitDaemon { |
| 213 | | − child: std::process::Child, |
| 213 | + process: GitDaemonProcess, |
| 214 | 214 | /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. |
| 215 | 215 | pub url: String, |
| 216 | 216 | } |
| 217 | 217 | |
| 218 | +enum GitDaemonProcess { |
| 219 | + #[cfg(not(unix))] |
| 220 | + Child(std::process::Child), |
| 221 | + #[cfg(unix)] |
| 222 | + Inetd { |
| 223 | + shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>, |
| 224 | + server_addr: std::net::SocketAddr, |
| 225 | + listener_thread: Option<std::thread::JoinHandle<()>>, |
| 226 | + }, |
| 227 | +} |
| 228 | + |
| 218 | 229 | impl Drop for GitDaemon { |
| 219 | 230 | fn drop(&mut self) { |
| 220 | | − self.child.kill().ok(); |
| 231 | + match &mut self.process { |
| 232 | + #[cfg(not(unix))] |
| 233 | + GitDaemonProcess::Child(child) => { |
| 234 | + child.kill().ok(); |
| 235 | + } |
| 236 | + #[cfg(unix)] |
| 237 | + GitDaemonProcess::Inetd { |
| 238 | + shutdown, |
| 239 | + server_addr, |
| 240 | + listener_thread, |
| 241 | + } => { |
| 242 | + shutdown.store(true, std::sync::atomic::Ordering::SeqCst); |
| 243 | + std::net::TcpStream::connect(*server_addr).ok(); |
| 244 | + if let Some(listener_thread) = listener_thread.take() { |
| 245 | + listener_thread.join().ok(); |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 221 | 249 | } |
| 222 | 250 | } |
| 223 | 251 | |
| @@ -317,6 +345,17 @@ const GIT_PROGRAM: &str = "git.exe"; · 93 unmodified lines |
| 317 | 345 | #[cfg(not(windows))] |
| 318 | 346 | const GIT_PROGRAM: &str = "git"; |
| 319 | 347 | |
| 348 | +const DISABLE_AUTO_MAINTENANCE_CONFIG: &[(&str, &str)] = &[("maintenance.auto", "false"), ("gc.auto", "0")]; |
| 349 | + |
| 350 | +const ISOLATED_GIT_CONFIG: &[(&str, &str)] = &[ |
| 351 | + ("commit.gpgsign", "false"), |
| 352 | + ("tag.gpgsign", "false"), |
| 353 | + ("init.defaultBranch", "main"), |
| 354 | + ("protocol.file.allow", "always"), |
| 355 | + ("maintenance.auto", "false"), |
| 356 | + ("gc.auto", "0"), |
| 357 | +]; |
| 358 | + |
| 320 | 359 | static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| { |
| 321 | 360 | let output = std::process::Command::new(GIT_PROGRAM) |
| 322 | 361 | .arg("--exec-path") |
| @@ -421,7 +460,8 @@ impl Drop for AutoRevertToPreviousCWD { · 98 unmodified lines |
| 421 | 460 | |
| 422 | 461 | /// Run `git` in `working_dir` with all provided `args`. |
| 423 | 462 | pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> { |
| 424 | | − std::process::Command::new(GIT_PROGRAM) |
| 463 | + let mut cmd = std::process::Command::new(GIT_PROGRAM); |
| 464 | + apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) |
| 425 | 465 | .current_dir(working_dir) |
| 426 | 466 | .args(args) |
| 427 | 467 | .status() |
| @@ -436,7 +476,8 @@ pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::proces · 8 unmodified lines |
| 436 | 476 | /// |
| 437 | 477 | /// This function expects the script to succeed and will panic otherwise. |
| 438 | 478 | pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) { |
| 439 | | − let status = std::process::Command::new(bash_program()) |
| 479 | + let mut cmd = std::process::Command::new(bash_program()); |
| 480 | + let status = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) |
| 440 | 481 | .current_dir(cwd) |
| 441 | 482 | .arg("-c") |
| 442 | 483 | .arg(script) |
| @@ -448,8 +489,22 @@ pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) { · 5 unmodified lines |
| 448 | 489 | assert!(status.success(), "bash script failed with {status}"); |
| 449 | 490 | } |
| 450 | 491 | |
| 451 | | −/// Spawn a git daemon process to host all repository at or below `working_dir`. |
| 492 | +/// Spawn a git daemon to host all repositories at or below `working_dir`. |
| 493 | +/// |
| 494 | +/// It runs in the background until the [`GitDaemon`] is dropped. |
| 452 | 495 | pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { |
| 496 | + #[cfg(unix)] |
| 497 | + { |
| 498 | + spawn_git_daemon_inetd(working_dir) |
| 499 | + } |
| 500 | + #[cfg(not(unix))] |
| 501 | + { |
| 502 | + spawn_git_daemon_process(working_dir) |
| 503 | + } |
| 504 | +} |
| 505 | + |
| 506 | +#[cfg(not(unix))] |
| 507 | +fn spawn_git_daemon_process(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { |
| 453 | 508 | let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); |
| 454 | 509 | fastrand::shuffle(&mut ports); |
| 455 | 510 | let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port)); |
| @@ -458,12 +513,15 @@ pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDae · 2 unmodified lines |
| 458 | 513 | listener.local_addr().expect("listener address is available").port() |
| 459 | 514 | }; |
| 460 | 515 | |
| 461 | | − let child = |
| 462 | | − std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })) |
| 516 | + let child = { |
| 517 | + let mut cmd = |
| 518 | + std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })); |
| 519 | + apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) |
| 463 | 520 | .current_dir(working_dir) |
| 464 | 521 | .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) |
| 465 | 522 | .arg(format!("--port={free_port}")) |
| 466 | | − .spawn()?; |
| 523 | + .spawn()? |
| 524 | + }; |
| 467 | 525 | |
| 468 | 526 | let server_addr = addr_at(free_port); |
| 469 | 527 | for time in gix_lock::backoff::Quadratic::default_with_random() { |
| @@ -473,11 +531,102 @@ pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDae · 3 unmodified lines |
| 473 | 531 | } |
| 474 | 532 | } |
| 475 | 533 | Ok(GitDaemon { |
| 476 | | − child, |
| 534 | + process: GitDaemonProcess::Child(child), |
| 477 | 535 | url: format!("git://{server_addr}"), |
| 478 | 536 | }) |
| 479 | 537 | } |
| 480 | 538 | |
| 539 | +#[cfg(unix)] |
| 540 | +fn spawn_git_daemon_inetd(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { |
| 541 | + use std::{ |
| 542 | + net::{TcpListener, TcpStream}, |
| 543 | + os::fd::{FromRawFd, IntoRawFd}, |
| 544 | + process::Stdio, |
| 545 | + sync::{ |
| 546 | + Arc, |
| 547 | + atomic::{AtomicBool, Ordering}, |
| 548 | + }, |
| 549 | + }; |
| 550 | + |
| 551 | + fn stream_to_stdio(stream: TcpStream) -> Stdio { |
| 552 | + // SAFETY: `into_raw_fd()` transfers ownership of the socket fd, and `Stdio` |
| 553 | + // takes over closing it in the spawned child. |
| 554 | + unsafe { Stdio::from_raw_fd(stream.into_raw_fd()) } |
| 555 | + } |
| 556 | + |
| 557 | + let working_dir = working_dir.as_ref().to_owned(); |
| 558 | + let listener = TcpListener::bind(("127.0.0.1", 0))?; |
| 559 | + let server_addr = listener.local_addr()?; |
| 560 | + let shutdown = Arc::new(AtomicBool::new(false)); |
| 561 | + let listener_thread = std::thread::spawn({ |
| 562 | + let shutdown = shutdown.clone(); |
| 563 | + move || { |
| 564 | + for incoming in listener.incoming() { |
| 565 | + let stream = match incoming { |
| 566 | + Ok(stream) => stream, |
| 567 | + Err(_) => break, |
| 568 | + }; |
| 569 | + if shutdown.load(Ordering::SeqCst) { |
| 570 | + break; |
| 571 | + } |
| 572 | + |
| 573 | + let peer_addr = stream.peer_addr().ok(); |
| 574 | + let stdin = match stream.try_clone() { |
| 575 | + Ok(stream) => stream_to_stdio(stream), |
| 576 | + Err(_) => continue, |
| 577 | + }; |
| 578 | + let stdout = stream_to_stdio(stream); |
| 579 | + let mut cmd = std::process::Command::new(GIT_PROGRAM); |
| 580 | + let Ok(mut child) = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG) |
| 581 | + .args([ |
| 582 | + "-c", |
| 583 | + "uploadpack.allowrefinwant", |
| 584 | + "daemon", |
| 585 | + "--inetd", |
| 586 | + "--verbose", |
| 587 | + "--base-path=.", |
| 588 | + "--export-all", |
| 589 | + "--user-path", |
| 590 | + ]) |
| 591 | + .current_dir(&working_dir) |
| 592 | + .stdin(stdin) |
| 593 | + .stdout(stdout) |
| 594 | + .stderr(Stdio::null()) |
| 595 | + .envs(remote_env(peer_addr)) |
| 596 | + .spawn() |
| 597 | + else { |
| 598 | + continue; |
| 599 | + }; |
| 600 | + |
| 601 | + std::thread::spawn(move || { |
| 602 | + let _ = child.wait(); |
| 603 | + }); |
| 604 | + } |
| 605 | + } |
| 606 | + }); |
| 607 | + |
| 608 | + Ok(GitDaemon { |
| 609 | + process: GitDaemonProcess::Inetd { |
| 610 | + shutdown, |
| 611 | + server_addr, |
| 612 | + listener_thread: Some(listener_thread), |
| 613 | + }, |
| 614 | + url: format!("git://{server_addr}"), |
| 615 | + }) |
| 616 | +} |
| 617 | + |
| 618 | +#[cfg(unix)] |
| 619 | +fn remote_env(peer_addr: Option<std::net::SocketAddr>) -> Vec<(&'static str, String)> { |
| 620 | + peer_addr |
| 621 | + .map(|addr| { |
| 622 | + vec![ |
| 623 | + ("REMOTE_ADDR", addr.ip().to_string()), |
| 624 | + ("REMOTE_PORT", addr.port().to_string()), |
| 625 | + ] |
| 626 | + }) |
| 627 | + .unwrap_or_default() |
| 628 | +} |
| 629 | + |
| 481 | 630 | /// Don't add a suffix to the archive name as `args` are platform dependent, non-deterministic, |
| 482 | 631 | /// or otherwise don't influence the content of the archive. |
| 483 | 632 | /// Note that this also means that `args` won't be used to control the hash of the archive itself. |
| @@ -1504,16 +1653,27 @@ fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>( · 1020 unmodified lines |
| 1504 | 1653 | .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000") |
| 1505 | 1654 | .env("GIT_COMMITTER_EMAIL", "committer@example.com") |
| 1506 | 1655 | .env("GIT_COMMITTER_NAME", "committer") |
| 1507 | | − .env("GIT_CONFIG_COUNT", "4") |
| 1508 | | − .env("GIT_CONFIG_KEY_0", "commit.gpgsign") |
| 1509 | | − .env("GIT_CONFIG_VALUE_0", "false") |
| 1510 | | − .env("GIT_CONFIG_KEY_1", "tag.gpgsign") |
| 1511 | | − .env("GIT_CONFIG_VALUE_1", "false") |
| 1512 | | − .env("GIT_CONFIG_KEY_2", "init.defaultBranch") |
| 1513 | | − .env("GIT_CONFIG_VALUE_2", "main") |
| 1514 | | − .env("GIT_CONFIG_KEY_3", "protocol.file.allow") |
| 1515 | | − .env("GIT_CONFIG_VALUE_3", "always") |
| 1516 | | − .env("GIT_DEFAULT_HASH", object_hash.to_string()) |
| 1656 | + .env("GIT_DEFAULT_HASH", object_hash.to_string()); |
| 1657 | + apply_git_config_by_environment(cmd, ISOLATED_GIT_CONFIG) |
| 1658 | +} |
| 1659 | + |
| 1660 | +/// Apply command-scoped Git `config` to `cmd`, and return it. |
| 1661 | +/// |
| 1662 | +/// This sets `GIT_CONFIG_COUNT` and matching `GIT_CONFIG_KEY_<n>` / |
| 1663 | +/// `GIT_CONFIG_VALUE_<n>` environment variables, which Git treats like |
| 1664 | +/// command-line `-c <key>=<value>` entries for the spawned process. Existing |
| 1665 | +/// values for these variables on `cmd` are overwritten for the configured |
| 1666 | +/// indices. |
| 1667 | +pub fn apply_git_config_by_environment<'a>( |
| 1668 | + cmd: &'a mut std::process::Command, |
| 1669 | + config: &[(&str, &str)], |
| 1670 | +) -> &'a mut std::process::Command { |
| 1671 | + cmd.env("GIT_CONFIG_COUNT", config.len().to_string()); |
| 1672 | + for (idx, (key, value)) in config.iter().enumerate() { |
| 1673 | + cmd.env(format!("GIT_CONFIG_KEY_{idx}"), key); |
| 1674 | + cmd.env(format!("GIT_CONFIG_VALUE_{idx}"), value); |
| 1675 | + } |
| 1676 | + cmd |
| 1517 | 1677 | } |
| 1518 | 1678 | |
| 1519 | 1679 | /// Get the path attempted as a `bash` interpreter, for fixture scripts having no `#!` we can use. |