| @@ -206,18 +206,46 @@ fn wildcard_match(pattern: &str, text: &str) -> bool { · 205 unmodified lines |
| 206 | pattern.ends_with('*') || remainder.is_empty() | 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 | /// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed. | 211 | /// Note that we will swallow any errors, assuming that the test would have failed if the daemon crashed. |
| 212 | pub struct GitDaemon { | 212 | pub struct GitDaemon { |
| 213 | child: std::process::Child, | 213 | process: GitDaemonProcess, |
| 214 | /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. | 214 | /// The base url under which all repositories are hosted, typically `git://127.0.0.1:port`. |
| 215 | pub url: String, | 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 | impl Drop for GitDaemon { | 229 | impl Drop for GitDaemon { |
| 219 | fn drop(&mut self) { | 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 | #[cfg(not(windows))] | 345 | #[cfg(not(windows))] |
| 318 | const GIT_PROGRAM: &str = "git"; | 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 | static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| { | 359 | static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| { |
| 321 | let output = std::process::Command::new(GIT_PROGRAM) | 360 | let output = std::process::Command::new(GIT_PROGRAM) |
| 322 | .arg("--exec-path") | 361 | .arg("--exec-path") |
| @@ -421,7 +460,8 @@ impl Drop for AutoRevertToPreviousCWD { · 98 unmodified lines |
| 421 | | 460 | |
| 422 | /// Run `git` in `working_dir` with all provided `args`. | 461 | /// Run `git` in `working_dir` with all provided `args`. |
| 423 | pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> { | 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 | .current_dir(working_dir) | 465 | .current_dir(working_dir) |
| 426 | .args(args) | 466 | .args(args) |
| 427 | .status() | 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 | /// This function expects the script to succeed and will panic otherwise. | 477 | /// This function expects the script to succeed and will panic otherwise. |
| 438 | pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) { | 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 | .current_dir(cwd) | 481 | .current_dir(cwd) |
| 441 | .arg("-c") | 482 | .arg("-c") |
| 442 | .arg(script) | 483 | .arg(script) |
| @@ -448,8 +489,22 @@ pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) { · 5 unmodified lines |
| 448 | assert!(status.success(), "bash script failed with {status}"); | 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 | pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> { | 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 | let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); | 508 | let mut ports: Vec<_> = (9419u16..9419 + 100).collect(); |
| 454 | fastrand::shuffle(&mut ports); | 509 | fastrand::shuffle(&mut ports); |
| 455 | let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port)); | 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 | listener.local_addr().expect("listener address is available").port() | 513 | listener.local_addr().expect("listener address is available").port() |
| 459 | }; | 514 | }; |
| 460 | | 515 | |
| 461 | let child = | 516 | let child = { |
| 462 | std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" })) | 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 | .current_dir(working_dir) | 520 | .current_dir(working_dir) |
| 464 | .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) | 521 | .args(["--verbose", "--base-path=.", "--export-all", "--user-path"]) |
| 465 | .arg(format!("--port={free_port}")) | 522 | .arg(format!("--port={free_port}")) |
| 466 | .spawn()?; | 523 | .spawn()? |
| | 524 | }; |
| 467 | | 525 | |
| 468 | let server_addr = addr_at(free_port); | 526 | let server_addr = addr_at(free_port); |
| 469 | for time in gix_lock::backoff::Quadratic::default_with_random() { | 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 | Ok(GitDaemon { | 533 | Ok(GitDaemon { |
| 476 | child, | 534 | process: GitDaemonProcess::Child(child), |
| 477 | url: format!("git://{server_addr}"), | 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 | /// Don't add a suffix to the archive name as `args` are platform dependent, non-deterministic, | 630 | /// Don't add a suffix to the archive name as `args` are platform dependent, non-deterministic, |
| 482 | /// or otherwise don't influence the content of the archive. | 631 | /// or otherwise don't influence the content of the archive. |
| 483 | /// Note that this also means that `args` won't be used to control the hash of the archive itself. | 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 | .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000") | 1653 | .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000") |
| 1505 | .env("GIT_COMMITTER_EMAIL", "committer@example.com") | 1654 | .env("GIT_COMMITTER_EMAIL", "committer@example.com") |
| 1506 | .env("GIT_COMMITTER_NAME", "committer") | 1655 | .env("GIT_COMMITTER_NAME", "committer") |
| 1507 | .env("GIT_CONFIG_COUNT", "4") | 1656 | .env("GIT_DEFAULT_HASH", object_hash.to_string()); |
| 1508 | .env("GIT_CONFIG_KEY_0", "commit.gpgsign") | 1657 | apply_git_config_by_environment(cmd, ISOLATED_GIT_CONFIG) |
| 1509 | .env("GIT_CONFIG_VALUE_0", "false") | 1658 | } |
| 1510 | .env("GIT_CONFIG_KEY_1", "tag.gpgsign") | 1659 | |
| 1511 | .env("GIT_CONFIG_VALUE_1", "false") | 1660 | /// Apply command-scoped Git `config` to `cmd`, and return it. |
| 1512 | .env("GIT_CONFIG_KEY_2", "init.defaultBranch") | 1661 | /// |
| 1513 | .env("GIT_CONFIG_VALUE_2", "main") | 1662 | /// This sets `GIT_CONFIG_COUNT` and matching `GIT_CONFIG_KEY_<n>` / |
| 1514 | .env("GIT_CONFIG_KEY_3", "protocol.file.allow") | 1663 | /// `GIT_CONFIG_VALUE_<n>` environment variables, which Git treats like |
| 1515 | .env("GIT_CONFIG_VALUE_3", "always") | 1664 | /// command-line `-c <key>=<value>` entries for the spawned process. Existing |
| 1516 | .env("GIT_DEFAULT_HASH", object_hash.to_string()) | 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 | /// Get the path attempted as a `bash` interpreter, for fixture scripts having no `#!` we can use. | 1679 | /// Get the path attempted as a `bash` interpreter, for fixture scripts having no `#!` we can use. |