1#![cfg_attr(
14 all(doc, feature = "document-features"),
15 doc = ::document_features::document_features!()
16)]
17#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
18#![deny(missing_docs)]
19
20use std::{
21 collections::{BTreeMap, HashMap},
22 env,
23 ffi::{OsStr, OsString},
24 io::Read,
25 path::{Path, PathBuf},
26 str::FromStr,
27 time::Duration,
28};
29
30pub use bstr;
31use bstr::ByteSlice;
32use io_close::Close;
33
34pub use is_ci;
35use parking_lot::Mutex;
36use std::sync::LazyLock;
37
38pub use tempfile;
39
40const ARCHIVE_DIR_NAME: &str = "generated-archives";
41
42pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
57
58pub type PostResult<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
63
64pub fn build_example_for_test(package: &str, example: &str, target_tmpdir: impl Into<PathBuf>) -> PathBuf {
69 let mut cargo = std::process::Command::new(env::var_os("CARGO").unwrap_or_else(|| OsString::from(env!("CARGO"))));
70 let res = cargo
71 .args(["build", "-p", package, "--example", example])
72 .status()
73 .expect("cargo should run fine");
74 assert!(res.success(), "cargo invocation should be successful");
75
76 let target_tmpdir = target_tmpdir.into();
77 let shared_path = target_tmpdir
78 .ancestors()
79 .nth(1)
80 .expect("first parent in target dir")
81 .join("debug")
82 .join("examples")
83 .join(format!("{example}{}", std::env::consts::EXE_SUFFIX));
84
85 let stable_path = target_tmpdir.join(format!(
86 "{example}-{}{}",
87 std::process::id(),
88 std::env::consts::EXE_SUFFIX
89 ));
90 let mut last_err = None;
91 for _ in 0..10 {
92 match std::fs::copy(&shared_path, &stable_path) {
93 Ok(_) => return stable_path,
94 Err(err) => {
95 last_err = Some(err);
96 std::thread::sleep(Duration::from_millis(50));
97 }
98 }
99 }
100 panic!(
101 "driver at {} could be copied for stable test execution: {last_err:?}",
102 shared_path.display()
103 );
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum FixtureState<'a> {
109 Uninitialized(&'a Path),
114 Fresh(&'a Path),
119}
120
121impl FixtureState<'_> {
122 pub fn path(&self) -> &Path {
124 match self {
125 FixtureState::Uninitialized(path) | FixtureState::Fresh(path) => path,
126 }
127 }
128
129 pub fn is_uninitialized(&self) -> bool {
131 matches!(self, FixtureState::Uninitialized(_))
132 }
133}
134
135trait IsExcluded {
142 fn is_excluded(&self, archive: &Path) -> bool;
144}
145
146#[cfg(not(feature = "worktree-exclusions"))]
152fn is_excluded_by_lines(lines: &str, archive: &Path) -> bool {
153 let archive = archive.to_string_lossy().replace('\\', "/");
154 let filename = archive.rsplit('/').next().unwrap_or(&archive);
155 lines.lines().any(|line| {
156 let pattern = line.trim();
157 if pattern.is_empty() || pattern.starts_with('#') {
158 return false;
159 }
160 let pattern = pattern.trim_start_matches('/');
161 let candidate = if pattern.contains('/') {
162 archive.as_str()
163 } else {
164 filename
165 };
166 wildcard_match(pattern, candidate)
167 })
168}
169
170#[cfg(not(feature = "worktree-exclusions"))]
179fn wildcard_match(pattern: &str, text: &str) -> bool {
180 if !pattern.contains('*') {
181 return pattern == text;
182 }
183
184 let mut remainder = text;
185 let mut parts = pattern.split('*').peekable();
186 let first = parts.next().expect("split yields at least one item");
187 if !first.is_empty() {
188 let Some(stripped) = remainder.strip_prefix(first) else {
189 return false;
190 };
191 remainder = stripped;
192 }
193
194 while let Some(part) = parts.next() {
195 if part.is_empty() {
196 continue;
197 }
198 let Some(pos) = remainder.find(part) else {
199 return false;
200 };
201 remainder = &remainder[pos + part.len()..];
202 if parts.peek().is_none() && !pattern.ends_with('*') {
203 return remainder.is_empty();
204 }
205 }
206 pattern.ends_with('*') || remainder.is_empty()
207}
208
209pub struct GitDaemon {
213 process: GitDaemonProcess,
214 pub url: String,
216}
217
218enum 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
229impl Drop for GitDaemon {
230 fn drop(&mut self) {
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 }
249 }
250}
251
252static SCRIPT_IDENTITY: LazyLock<Mutex<BTreeMap<PathBuf, u32>>> = LazyLock::new(|| Mutex::new(BTreeMap::new()));
253
254#[cfg(feature = "worktree-exclusions")]
255static EXCLUDE_LUT: LazyLock<Mutex<Option<gix_worktree::Stack>>> = LazyLock::new(|| {
256 let cache = (|| {
257 let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?;
258 let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories();
259 let work_tree = work_tree?.canonicalize().ok()?;
260
261 let mut buf = Vec::with_capacity(512);
262 let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case {
263 gix_worktree::ignore::glob::pattern::Case::Fold
264 } else {
265 Default::default()
266 };
267 let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new(
268 Default::default(),
269 gix_worktree::ignore::Search::from_git_dir(
270 &gix_dir,
271 None,
272 &mut buf,
273 gix_worktree::stack::state::ignore::ParseIgnore {
274 support_precious: false,
275 },
276 )
277 .ok()?,
278 None,
279 gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
280 Default::default(),
281 ));
282 Some(gix_worktree::Stack::new(
283 work_tree,
284 state,
285 case,
286 buf,
287 Default::default(),
288 ))
289 })();
290 Mutex::new(cache)
291});
292
293#[cfg(feature = "worktree-exclusions")]
294struct WorktreeExclusions;
295
296#[cfg(feature = "worktree-exclusions")]
297impl IsExcluded for WorktreeExclusions {
298 fn is_excluded(&self, archive: &Path) -> bool {
299 let mut lut = EXCLUDE_LUT.lock();
300 lut.as_mut()
301 .and_then(|cache| {
302 let archive = env::current_dir().ok()?.join(archive);
303 let relative_path = archive.strip_prefix(cache.base()).ok()?;
304 cache
305 .at_path(
306 relative_path,
307 Some(gix_worktree::index::entry::Mode::FILE),
308 &gix_worktree::object::find::Never,
309 )
310 .ok()?
311 .is_excluded()
312 .into()
313 })
314 .unwrap_or(false)
315 }
316}
317
318#[cfg(feature = "worktree-exclusions")]
319fn default_excludes() -> &'static dyn IsExcluded {
320 static WORKTREE_EXCLUSIONS: WorktreeExclusions = WorktreeExclusions;
321 &WORKTREE_EXCLUSIONS
322}
323
324#[cfg(not(feature = "worktree-exclusions"))]
325struct GitignoreExclusions;
326
327#[cfg(not(feature = "worktree-exclusions"))]
328impl IsExcluded for GitignoreExclusions {
329 fn is_excluded(&self, archive: &Path) -> bool {
330 let Some(parent) = archive.parent() else {
331 return false;
332 };
333 std::fs::read_to_string(parent.join(".gitignore")).is_ok_and(|lines| is_excluded_by_lines(&lines, archive))
334 }
335}
336
337#[cfg(not(feature = "worktree-exclusions"))]
338fn default_excludes() -> &'static dyn IsExcluded {
339 static GITIGNORE_EXCLUSIONS: GitignoreExclusions = GitignoreExclusions;
340 &GITIGNORE_EXCLUSIONS
341}
342
343#[cfg(windows)]
344const GIT_PROGRAM: &str = "git.exe";
345#[cfg(not(windows))]
346const GIT_PROGRAM: &str = "git";
347
348const DISABLE_AUTO_MAINTENANCE_CONFIG: &[(&str, &str)] = &[("maintenance.auto", "false"), ("gc.auto", "0")];
349
350const 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
359static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
360 let output = std::process::Command::new(GIT_PROGRAM)
361 .arg("--exec-path")
362 .output()
363 .expect("can execute `git --exec-path`");
364
365 assert!(output.status.success(), "`git --exec-path` failed");
366
367 output
368 .stdout
369 .strip_suffix(b"\n")
370 .expect("`git --exec-path` output to be well-formed")
371 .to_os_str()
372 .expect("no invalid UTF-8 in `--exec-path` except as OS allows")
373 .into()
374});
375
376pub static GIT_VERSION: LazyLock<(u8, u8, u8)> =
378 LazyLock::new(|| parse_git_version().expect("git version to be parsable"));
379
380pub enum Creation {
383 CopyFromReadOnly,
390 Execute,
392}
393
394pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool {
402 if is_ci::cached() {
403 return false; }
405 *GIT_VERSION < (major, minor, patch)
406}
407
408fn parse_git_version() -> Result<(u8, u8, u8)> {
409 let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?;
410 git_version_from_bytes(&output.stdout)
411}
412
413fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> {
414 let mut numbers = bytes
415 .split(|b| *b == b' ' || *b == b'\n')
416 .nth(2)
417 .expect("git version <version>")
418 .split(|b| *b == b'.')
419 .take(3)
420 .map(|n| std::str::from_utf8(n).expect("valid utf8 in version number"))
421 .map(u8::from_str);
422
423 Ok((|| -> Result<_> {
424 Ok((
425 numbers.next().expect("major")?,
426 numbers.next().expect("minor")?,
427 numbers.next().expect("patch")?,
428 ))
429 })()
430 .map_err(|err| {
431 format!(
432 "Could not parse version from output of 'git --version' ({:?}) with error: {}",
433 bytes.to_str_lossy(),
434 err
435 )
436 })?)
437}
438
439pub fn set_current_dir(new_cwd: impl AsRef<Path>) -> std::io::Result<AutoRevertToPreviousCWD> {
441 let cwd = env::current_dir()?;
442 env::set_current_dir(new_cwd)?;
443 Ok(AutoRevertToPreviousCWD(cwd))
444}
445
446#[derive(Debug)]
452#[must_use]
453pub struct AutoRevertToPreviousCWD(PathBuf);
454
455impl Drop for AutoRevertToPreviousCWD {
456 fn drop(&mut self) {
457 env::set_current_dir(&self.0).unwrap();
458 }
459}
460
461pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> {
463 let mut cmd = std::process::Command::new(GIT_PROGRAM);
464 apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
465 .current_dir(working_dir)
466 .args(args)
467 .status()
468}
469
470pub fn invoke_bash(cwd: impl AsRef<Path>, script: &str) {
479 let mut cmd = std::process::Command::new(bash_program());
480 let status = apply_git_config_by_environment(&mut cmd, DISABLE_AUTO_MAINTENANCE_CONFIG)
481 .current_dir(cwd)
482 .arg("-c")
483 .arg(script)
484 .stdin(std::process::Stdio::null())
485 .stdout(std::process::Stdio::inherit())
486 .stderr(std::process::Stdio::inherit())
487 .status()
488 .expect("can run bash script");
489 assert!(status.success(), "bash script failed with {status}");
490}
491
492pub 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))]
507fn spawn_git_daemon_process(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
508 let mut ports: Vec<_> = (9419u16..9419 + 100).collect();
509 fastrand::shuffle(&mut ports);
510 let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port));
511 let free_port = {
512 let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::<Vec<_>>().as_slice())?;
513 listener.local_addr().expect("listener address is available").port()
514 };
515
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)
520 .current_dir(working_dir)
521 .args(["--verbose", "--base-path=.", "--export-all", "--user-path"])
522 .arg(format!("--port={free_port}"))
523 .spawn()?
524 };
525
526 let server_addr = addr_at(free_port);
527 for time in gix_lock::backoff::Quadratic::default_with_random() {
528 std::thread::sleep(time);
529 if std::net::TcpStream::connect(server_addr).is_ok() {
530 break;
531 }
532 }
533 Ok(GitDaemon {
534 process: GitDaemonProcess::Child(child),
535 url: format!("git://{server_addr}"),
536 })
537}
538
539#[cfg(unix)]
540fn 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 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)]
619fn 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
630#[derive(Copy, Clone)]
634enum ArgsInHash {
635 Yes,
636 No,
637}
638
639pub fn fixture_path(path: impl AsRef<Path>) -> PathBuf {
641 fixture_base().join(path.as_ref())
642}
643
644fn fixture_base() -> PathBuf {
645 PathBuf::from("tests").join("fixtures")
646}
647
648pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> {
650 match std::fs::read(fixture_path(path.as_ref())) {
651 Ok(res) => res,
652 Err(_) => panic!("File at '{}' not found", path.as_ref().display()),
653 }
654}
655
656pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> {
679 scripted_fixture_read_only_with_args(script_name, None::<String>)
680}
681
682pub fn scripted_fixture_read_only_needs_archive(script_name: impl AsRef<Path>) -> Result<PathBuf> {
695 scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
696 script_name,
697 None::<String>,
698 None,
699 ArgsInHash::Yes,
700 default_excludes(),
701 None::<(u32, _)>,
702 true,
703 )
704 .map(|(dir, _)| dir)
705}
706
707pub fn scripted_fixture_writable(script_name: impl AsRef<Path>) -> Result<tempfile::TempDir> {
712 scripted_fixture_writable_with_args(script_name, None::<String>, Creation::CopyFromReadOnly)
713}
714
715pub fn scripted_fixture_writable_with_args(
718 script_name: impl AsRef<Path>,
719 args: impl IntoIterator<Item = impl Into<String>>,
720 mode: Creation,
721) -> Result<tempfile::TempDir> {
722 scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
723 script_name,
724 args,
725 mode,
726 ArgsInHash::Yes,
727 default_excludes(),
728 None::<(u32, _)>,
729 )
730 .map(|(dir, _)| dir)
731}
732
733pub fn scripted_fixture_writable_with_args_single_archive(
738 script_name: impl AsRef<Path>,
739 args: impl IntoIterator<Item = impl Into<String>>,
740 mode: Creation,
741) -> Result<tempfile::TempDir> {
742 scripted_fixture_writable_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
743 script_name,
744 args,
745 mode,
746 ArgsInHash::No,
747 default_excludes(),
748 None::<(u32, _)>,
749 )
750 .map(|(dir, _)| dir)
751}
752
753fn scripted_fixture_writable_with_args_inner<F, T>(
754 script_name: impl AsRef<Path>,
755 args: impl IntoIterator<Item = impl Into<String>>,
756 mode: Creation,
757 args_in_hash: ArgsInHash,
758 excludes: &dyn IsExcluded,
759 mut post_process: Option<(u32, F)>,
760) -> Result<(tempfile::TempDir, Option<T>)>
761where
762 F: FnMut(FixtureState<'_>) -> PostResult<T>,
763{
764 let dst = tempfile::TempDir::new()?;
765 Ok(match mode {
766 Creation::CopyFromReadOnly => {
767 let (ro_dir, _res_ignored) = scripted_fixture_read_only_with_args_inner(
769 script_name,
770 args,
771 None,
772 args_in_hash,
773 excludes,
774 post_process.as_mut().map(|(v, f)| (*v, f)),
775 false,
776 )?;
777 copy_recursively_into_existing_dir(ro_dir, dst.path())?;
778 (dst, _res_ignored)
779 }
780 Creation::Execute => {
781 let (_, post_result) = scripted_fixture_read_only_with_args_inner(
783 script_name,
784 args,
785 dst.path().into(),
786 args_in_hash,
787 excludes,
788 post_process.as_mut().map(|(v, f)| (*v, f)),
789 false,
790 )?;
791 (dst, post_result)
792 }
793 })
794}
795
796pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef<Path>, dst_dir: impl AsRef<Path>) -> std::io::Result<()> {
798 fs_extra::copy_items(
799 &std::fs::read_dir(src_dir)?
800 .map(|e| e.map(|e| e.path()))
801 .collect::<std::result::Result<Vec<_>, _>>()?,
802 dst_dir,
803 &fs_extra::dir::CopyOptions {
804 overwrite: false,
805 skip_exist: false,
806 copy_inside: false,
807 content_only: false,
808 ..Default::default()
809 },
810 )
811 .map_err(std::io::Error::other)?;
812 Ok(())
813}
814
815pub fn scripted_fixture_read_only_with_args(
817 script_name: impl AsRef<Path>,
818 args: impl IntoIterator<Item = impl Into<String>>,
819) -> Result<PathBuf> {
820 scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
821 script_name,
822 args,
823 None,
824 ArgsInHash::Yes,
825 default_excludes(),
826 None::<(u32, _)>,
827 false,
828 )
829 .map(|(dir, _)| dir)
830}
831
832pub fn scripted_fixture_read_only_with_args_single_archive(
844 script_name: impl AsRef<Path>,
845 args: impl IntoIterator<Item = impl Into<String>>,
846) -> Result<PathBuf> {
847 scripted_fixture_read_only_with_args_inner::<fn(FixtureState<'_>) -> PostResult, ()>(
848 script_name,
849 args,
850 None,
851 ArgsInHash::No,
852 default_excludes(),
853 None::<(u32, _)>,
854 false,
855 )
856 .map(|(dir, _)| dir)
857}
858
859pub fn scripted_fixture_read_only_with_post<T>(
868 script_name: impl AsRef<Path>,
869 version: u32,
870 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
871) -> Result<(PathBuf, T)> {
872 scripted_fixture_read_only_with_args_inner(
873 script_name,
874 None::<String>,
875 None,
876 ArgsInHash::Yes,
877 default_excludes(),
878 Some((version, post_process)),
879 false,
880 )
881 .map(|(path, opt)| (path, opt.expect("post_process was provided")))
882}
883
884pub fn scripted_fixture_read_only_with_args_with_post<T>(
888 script_name: impl AsRef<Path>,
889 args: impl IntoIterator<Item = impl Into<String>>,
890 version: u32,
891 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
892) -> Result<(PathBuf, T)> {
893 scripted_fixture_read_only_with_args_inner(
894 script_name,
895 args,
896 None,
897 ArgsInHash::Yes,
898 default_excludes(),
899 Some((version, post_process)),
900 false,
901 )
902 .map(|(path, opt)| (path, opt.expect("post_process was provided")))
903}
904
905pub fn scripted_fixture_read_only_with_args_single_archive_with_post<T>(
909 script_name: impl AsRef<Path>,
910 args: impl IntoIterator<Item = impl Into<String>>,
911 version: u32,
912 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
913) -> Result<(PathBuf, T)> {
914 scripted_fixture_read_only_with_args_inner(
915 script_name,
916 args,
917 None,
918 ArgsInHash::No,
919 default_excludes(),
920 Some((version, post_process)),
921 false,
922 )
923 .map(|(path, opt)| (path, opt.expect("post_process was provided")))
924}
925
926pub fn scripted_fixture_writable_with_post<T>(
935 script_name: impl AsRef<Path>,
936 version: u32,
937 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
938) -> Result<(tempfile::TempDir, T)> {
939 scripted_fixture_writable_with_args_inner(
940 script_name,
941 None::<String>,
942 Creation::CopyFromReadOnly,
943 ArgsInHash::Yes,
944 default_excludes(),
945 Some((version, post_process)),
946 )
947 .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
948}
949
950pub fn scripted_fixture_writable_with_args_with_post<T>(
954 script_name: impl AsRef<Path>,
955 args: impl IntoIterator<Item = impl Into<String>>,
956 mode: Creation,
957 version: u32,
958 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
959) -> Result<(tempfile::TempDir, T)> {
960 scripted_fixture_writable_with_args_inner(
961 script_name,
962 args,
963 mode,
964 ArgsInHash::Yes,
965 default_excludes(),
966 Some((version, post_process)),
967 )
968 .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
969}
970
971pub fn scripted_fixture_writable_with_args_single_archive_with_post<T>(
975 script_name: impl AsRef<Path>,
976 args: impl IntoIterator<Item = impl Into<String>>,
977 mode: Creation,
978 version: u32,
979 post_process: impl FnMut(FixtureState<'_>) -> PostResult<T>,
980) -> Result<(tempfile::TempDir, T)> {
981 scripted_fixture_writable_with_args_inner(
982 script_name,
983 args,
984 mode,
985 ArgsInHash::No,
986 default_excludes(),
987 Some((version, post_process)),
988 )
989 .map(|(tmp, opt)| (tmp, opt.expect("post_process was provided")))
990}
991
992pub fn rust_fixture_read_only<T, F>(name: &str, version: u32, make_fixture: F) -> Result<(PathBuf, T)>
1031where
1032 F: FnOnce(FixtureState<'_>) -> PostResult<T>,
1033{
1034 rust_fixture_read_only_inner(name, version, None, make_fixture, None, default_excludes())
1035}
1036
1037pub fn rust_fixture_writable<T, F>(
1066 name: &str,
1067 version: u32,
1068 mode: Creation,
1069 make_fixture: F,
1070) -> Result<(tempfile::TempDir, T)>
1071where
1072 F: FnMut(FixtureState<'_>) -> PostResult<T>,
1073{
1074 rust_fixture_writable_inner(name, version, None, make_fixture, mode, default_excludes())
1075}
1076
1077fn rust_fixture_writable_inner<T, F>(
1078 name: &str,
1079 version: u32,
1080 object_hash: Option<gix_hash::Kind>,
1081 mut make_fixture: F,
1082 mode: Creation,
1083 excludes: &dyn IsExcluded,
1084) -> Result<(tempfile::TempDir, T)>
1085where
1086 F: FnMut(FixtureState<'_>) -> PostResult<T>,
1087{
1088 let dst = tempfile::TempDir::new()?;
1089 let res = match mode {
1090 Creation::CopyFromReadOnly => {
1091 let (ro_dir, _res_ignored) =
1092 rust_fixture_read_only_inner(name, version, object_hash, &mut make_fixture, None, excludes)?;
1093 copy_recursively_into_existing_dir(ro_dir, dst.path())?;
1094 make_fixture(FixtureState::Fresh(dst.path()))?
1095 }
1096 Creation::Execute => {
1097 let (_, res) =
1098 rust_fixture_read_only_inner(name, version, object_hash, make_fixture, Some(dst.path()), excludes)?;
1099 res
1100 }
1101 };
1102 Ok((dst, res))
1103}
1104
1105fn rust_fixture_read_only_inner<T, F>(
1106 name: &str,
1107 version: u32,
1108 object_hash: Option<gix_hash::Kind>,
1109 make_fixture: F,
1110 destination_dir: Option<&Path>,
1111 excludes: &dyn IsExcluded,
1112) -> Result<(PathBuf, T)>
1113where
1114 F: FnOnce(FixtureState<'_>) -> PostResult<T>,
1115{
1116 gix_tempfile::signal::setup(
1118 gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour,
1119 );
1120
1121 let script_identity = version;
1124 let archive_name = format!("rust-{name}");
1125 let fixture_base = fixture_base();
1126
1127 let archive_file_path = fixture_base
1128 .join(ARCHIVE_DIR_NAME)
1129 .join(format!("{archive_name}.{}", tar_extension()));
1130 let (force_run, script_result_directory) = force_and_dir(
1131 destination_dir,
1132 &fixture_base,
1133 &archive_name,
1134 object_hash,
1135 &script_identity,
1136 None,
1137 );
1138 let _marker = marker_if_needed(destination_dir, archive_name)?;
1139
1140 run_fixture_generator_with_marker_handling(
1141 &archive_file_path,
1142 &script_result_directory,
1143 script_identity,
1144 force_run,
1145 false,
1146 excludes,
1147 &format!("using Rust closure '{name}'"),
1148 make_fixture,
1149 )
1150 .map(|res| (script_result_directory, res))
1151}
1152
1153fn marker_if_needed(
1156 destination_dir: Option<&Path>,
1157 archive_name: impl AsRef<Path>,
1158) -> Result<Option<gix_lock::Marker>> {
1159 Ok(destination_dir
1160 .is_none()
1161 .then(|| {
1162 gix_lock::Marker::acquire_to_hold_resource(
1163 archive_name,
1164 gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)),
1165 None,
1166 )
1167 })
1168 .transpose()?)
1169}
1170
1171fn force_and_dir(
1172 destination_dir: Option<&Path>,
1173 fixture_base: &Path,
1174 archive_name: impl AsRef<Path>,
1175 object_hash: Option<gix_hash::Kind>,
1176 script_identity: &dyn std::fmt::Display,
1177 cache_variant: Option<&str>,
1178) -> (bool, PathBuf) {
1179 destination_dir.map_or_else(
1180 || {
1181 let mut dir = fixture_base.join(
1182 Path::new("generated-do-not-edit")
1183 .join(archive_name)
1184 .join(object_hash.unwrap_or_else(self::object_hash).to_string()),
1185 );
1186 if let Some(cache_variant) = cache_variant {
1187 dir = dir.join(cache_variant);
1188 }
1189 let dir = dir.join(format!("{}-{}", script_identity, family_name()));
1190 (false, dir)
1191 },
1192 |d| (true, d.to_owned()),
1193 )
1194}
1195
1196#[expect(clippy::too_many_arguments)]
1197fn run_fixture_generator_with_marker_handling<T, F>(
1198 archive_file_path: &Path,
1199 script_result_directory: &Path,
1200 script_identity: u32,
1201 force_run: bool,
1202 needs_archive: bool,
1203 excludes: &dyn IsExcluded,
1204 description: &str,
1205 make_fixture: F,
1206) -> Result<T>
1207where
1208 F: FnOnce(FixtureState<'_>) -> PostResult<T>,
1209{
1210 let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_");
1211 if force_run || !script_result_directory.is_dir() || failure_marker.is_file() {
1212 if failure_marker.is_file() {
1213 std::fs::remove_dir_all(script_result_directory).map_err(|err| {
1214 format!(
1215 "Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
1216 script_result_directory = script_result_directory.display()
1217 )
1218 })?;
1219 }
1220 std::fs::create_dir_all(script_result_directory)?;
1221 match extract_archive(
1222 archive_file_path,
1223 script_result_directory,
1224 script_identity,
1225 needs_archive,
1226 ) {
1227 Ok((archive_id, platform)) => {
1228 eprintln!(
1229 "Extracted fixture from archive '{}' ({}, {:?})",
1230 archive_file_path.display(),
1231 archive_id,
1232 platform
1233 );
1234 make_fixture(FixtureState::Fresh(script_result_directory))
1235 }
1236 Err(err) => {
1237 if err.kind() != std::io::ErrorKind::NotFound {
1238 eprintln!("failed to extract '{}': {}", archive_file_path.display(), err);
1239 std::fs::remove_dir_all(script_result_directory).map_err(|err| {
1240 format!(
1241 "Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
1242 script_result_directory = script_result_directory.display()
1243 )
1244 })?;
1245 std::fs::create_dir_all(script_result_directory)?;
1246 } else if !excludes.is_excluded(archive_file_path) {
1247 eprintln!(
1248 "Archive at '{}' not found, creating fixture {}",
1249 archive_file_path.display(),
1250 description
1251 );
1252 }
1253 let res = match make_fixture(FixtureState::Uninitialized(script_result_directory)) {
1254 Ok(value) => value,
1255 Err(err) => {
1256 write_failure_marker(&failure_marker);
1257 return Err(err);
1258 }
1259 };
1260 create_archive_if_we_should(script_result_directory, archive_file_path, script_identity, excludes)
1261 .inspect_err(|_err| {
1262 write_failure_marker(&failure_marker);
1263 })?;
1264 Ok(res)
1265 }
1266 }
1267 } else {
1268 make_fixture(FixtureState::Fresh(script_result_directory))
1269 }
1270}
1271
1272fn scripted_fixture_read_only_with_args_inner<F, T>(
1273 script_name: impl AsRef<Path>,
1274 args: impl IntoIterator<Item = impl Into<String>>,
1275 destination_dir: Option<&Path>,
1276 args_in_hash: ArgsInHash,
1277 excludes: &dyn IsExcluded,
1278 post_process: Option<(u32, F)>,
1279 needs_archive: bool,
1280) -> Result<(PathBuf, Option<T>)>
1281where
1282 F: FnMut(FixtureState<'_>) -> PostResult<T>,
1283{
1284 gix_tempfile::signal::setup(
1286 gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour,
1287 );
1288
1289 let object_hash = object_hash();
1290
1291 let script_location = script_name.as_ref();
1292 let fixture_base = fixture_base();
1293 let script_path = fixture_path(script_location);
1294
1295 let args: Vec<String> = args.into_iter().map(Into::into).collect();
1297 let post_version = post_process.as_ref().map(|(v, _)| *v);
1298 let script_identity = {
1299 let mut map = SCRIPT_IDENTITY.lock();
1300 let init = if object_hash == gix_hash::Kind::Sha1 {
1301 script_path.clone()
1302 } else {
1303 script_path.clone().join(object_hash.to_string())
1304 };
1305 let key = args.iter().fold(init, |p, a| p.join(a));
1306 let key = if let Some(v) = post_version {
1308 key.join(format!("post-v{v}"))
1309 } else {
1310 key
1311 };
1312 map.entry(key)
1313 .or_insert_with(|| {
1314 let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM);
1315 let mut crc_digest = crc_value.digest();
1316 crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| {
1317 panic!(
1318 "file {script_path} in CWD '{cwd}' could not be read: {err}",
1319 cwd = env::current_dir().expect("valid cwd").display(),
1320 script_path = script_path.display(),
1321 )
1322 }));
1323 for arg in &args {
1324 crc_digest.update(arg.as_bytes());
1325 }
1326 if let Some(v) = post_version {
1328 crc_digest.update(&v.to_le_bytes());
1329 }
1330 crc_digest.finalize()
1331 })
1332 .to_owned()
1333 };
1334
1335 let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str());
1336 let archive_file_path = fixture_base.join(ARCHIVE_DIR_NAME).join({
1337 let suffix = match args_in_hash {
1338 ArgsInHash::Yes => {
1339 let mut suffix = args.join("_");
1340 if !suffix.is_empty() {
1341 suffix.insert(0, '_');
1342 }
1343 suffix.replace(['\\', '/', ' ', '.'], "_")
1344 }
1345 ArgsInHash::No => "".into(),
1346 };
1347 let potential_hash_suffix = if object_hash == gix_hash::Kind::Sha1 {
1348 "".into()
1349 } else {
1350 format!("_{object_hash}")
1351 };
1352 format!(
1353 "{}{suffix}{potential_hash_suffix}.{}",
1354 script_basename.to_str().expect("valid UTF-8"),
1355 tar_extension()
1356 )
1357 });
1358 let (force_run, script_result_directory) = force_and_dir(
1359 destination_dir,
1360 &fixture_base,
1361 script_basename,
1362 Some(object_hash),
1363 &script_identity,
1364 needs_archive.then_some("archive"),
1365 );
1366 let _marker = marker_if_needed(destination_dir, script_basename)?;
1367
1368 let script_identity_for_archive = match args_in_hash {
1369 ArgsInHash::Yes => script_identity,
1370 ArgsInHash::No => 0,
1371 };
1372 let script_absolute_path = env::current_dir()?.join(&script_path);
1373 let post_process_closure = post_process.map(|(_, f)| f);
1374
1375 let res = run_fixture_generator_with_marker_handling(
1376 &archive_file_path,
1377 &script_result_directory,
1378 script_identity_for_archive,
1379 force_run,
1380 needs_archive,
1381 excludes,
1382 &format!("using script '{}'", script_location.display()),
1383 |fixture_state| {
1384 if let FixtureState::Uninitialized(dir) = fixture_state {
1385 let mut cmd = std::process::Command::new(&script_absolute_path);
1386 let output = match configure_command(&mut cmd, object_hash, &args, dir).output() {
1387 Ok(out) => out,
1388 Err(err)
1389 if err.kind() == std::io::ErrorKind::PermissionDenied
1390 || err.raw_os_error() == Some(193) =>
1391 {
1392 cmd = std::process::Command::new(bash_program());
1393 configure_command(cmd.arg(&script_absolute_path), object_hash, &args, dir).output()?
1394 }
1395 Err(err) => return Err(err.into()),
1396 };
1397 if !output.status.success() {
1398 eprintln!("stdout: {}", output.stdout.as_bstr());
1399 eprintln!("stderr: {}", output.stderr.as_bstr());
1400 return Err(format!("fixture script of {cmd:?} failed").into());
1401 }
1402 }
1403 if let Some(mut f) = post_process_closure {
1404 f(fixture_state).map(Some)
1405 } else {
1406 Ok(None)
1407 }
1408 },
1409 )?;
1410
1411 Ok((script_result_directory, res))
1412}
1413
1414pub fn object_hash_from_env() -> Option<gix_hash::Kind> {
1426 static FIXTURE_HASH: LazyLock<Option<gix_hash::Kind>> = LazyLock::new(|| {
1427 env::var_os("GIX_TEST_FIXTURE_HASH").and_then(|value| value.into_string().ok()).map(|object_kind| {
1428 gix_hash::Kind::from_str(&object_kind).unwrap_or_else(|_| {
1429 panic!(
1430 "GIX_TEST_FIXTURE_HASH was set to {object_kind} which is an invalid value. Valid values are {}. Exiting.",
1431 gix_hash::Kind::all().iter().map(std::string::ToString::to_string).collect::<Vec<_>>().join(", ")
1432 )
1433 })
1434 })
1435 });
1436 *FIXTURE_HASH
1437}
1438
1439pub fn object_hash() -> gix_hash::Kind {
1441 object_hash_from_env().unwrap_or_default()
1442}
1443
1444pub fn git(current_dir: impl AsRef<Path>, arguments: &str) -> Result<String> {
1451 let args = split_git_arguments(arguments)?;
1452 let cwd = current_dir.as_ref();
1453 let mut cmd = std::process::Command::new(GIT_PROGRAM);
1454 let output = configure_command(&mut cmd, object_hash(), args.iter().map(String::as_str), cwd)
1455 .current_dir(cwd)
1456 .output()?;
1457 if !output.status.success() {
1458 return Err(format!(
1459 "{cmd:?} failed with status {}\nstdout: {}\nstderr: {}",
1460 output.status,
1461 output.stdout.as_bstr(),
1462 output.stderr.as_bstr()
1463 )
1464 .into());
1465 }
1466 Ok(String::from_utf8(output.stdout)?)
1467}
1468
1469fn split_git_arguments(input: &str) -> Result<Vec<String>> {
1470 let mut args = Vec::new();
1471 let mut arg = String::new();
1472 let mut quote = None;
1473 let mut has_arg = false;
1474 let mut chars = input.chars();
1475
1476 while let Some(ch) = chars.next() {
1477 match quote {
1478 Some('\'') => {
1479 if ch == '\'' {
1480 quote = None;
1481 } else {
1482 arg.push(ch);
1483 }
1484 }
1485 Some('"') => {
1486 if ch == '"' {
1487 quote = None;
1488 } else if ch == '\\' {
1489 if let Some(next) = chars.next() {
1490 arg.push(next);
1491 }
1492 } else {
1493 arg.push(ch);
1494 }
1495 }
1496 Some(_) => unreachable!("only single and double quotes are set"),
1497 None => {
1498 if ch.is_whitespace() {
1499 if has_arg {
1500 args.push(std::mem::take(&mut arg));
1501 has_arg = false;
1502 }
1503 } else if matches!(ch, '\'' | '"') {
1504 quote = Some(ch);
1505 has_arg = true;
1506 } else if ch == '\\' {
1507 if let Some(next) = chars.next() {
1508 arg.push(next);
1509 }
1510 has_arg = true;
1511 } else {
1512 arg.push(ch);
1513 has_arg = true;
1514 }
1515 }
1516 }
1517 }
1518
1519 if let Some(quote) = quote {
1520 return Err(format!("unterminated {quote:?} quote in git arguments").into());
1521 }
1522 if has_arg {
1523 args.push(arg);
1524 }
1525 Ok(args)
1526}
1527
1528pub fn normalize_debug_snapshot(value: &dyn std::fmt::Debug) -> (String, Vec<gix_hash::ObjectId>) {
1536 normalize_hashes(&format!("{value:#?}"))
1537}
1538
1539pub fn normalize_hashes(input: &str) -> (String, Vec<gix_hash::ObjectId>) {
1543 let mut out = String::with_capacity(input.len());
1544 let mut seen = HashMap::<gix_hash::ObjectId, usize>::new();
1545 let mut removed = Vec::<gix_hash::ObjectId>::new();
1546 let mut chars = input.chars().peekable();
1547 let mut hex = String::new();
1548
1549 while let Some(ch) = chars.next() {
1550 if ch.is_ascii_hexdigit() {
1551 hex.clear();
1552 hex.push(ch);
1553 while let Some(ch) = chars.next_if(char::is_ascii_hexdigit) {
1554 hex.push(ch);
1555 }
1556
1557 if let Some(oid) = raw_object_id(&hex) {
1558 strip_debug_hash_wrapper(&mut out, &mut chars);
1559 push_normalized_oid(oid, &mut seen, &mut removed, &mut out);
1560 } else {
1561 out.push_str(&hex);
1562 }
1563 } else {
1564 out.push(ch);
1565 }
1566 }
1567 (out, removed)
1568}
1569
1570fn raw_object_id(input: &str) -> Option<gix_hash::ObjectId> {
1571 if !matches!(input.len(), 40 | 64) {
1572 return None;
1573 }
1574 gix_hash::ObjectId::from_hex(input.as_bytes()).ok()
1575}
1576
1577fn strip_debug_hash_wrapper(out: &mut String, chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
1578 if !matches!(chars.peek(), Some(')')) {
1579 return;
1580 }
1581 let consume_closing_parenthesis = if out.ends_with("Sha1(") {
1584 out.truncate(out.len() - "Sha1(".len());
1585 true
1586 } else if out.ends_with("Sha256(") {
1587 out.truncate(out.len() - "Sha256(".len());
1588 true
1589 } else {
1590 false
1591 };
1592 if consume_closing_parenthesis {
1593 chars.next();
1594 }
1595}
1596
1597fn push_normalized_oid(
1598 oid: gix_hash::ObjectId,
1599 seen: &mut HashMap<gix_hash::ObjectId, usize>,
1600 removed: &mut Vec<gix_hash::ObjectId>,
1601 out: &mut String,
1602) {
1603 let normalized = *seen.entry(oid).or_insert_with(|| {
1604 let current = removed.len();
1605 removed.push(oid);
1606 current
1607 });
1608
1609 out.push_str("Oid(");
1610 out.push_str(&(normalized + 1).to_string());
1611 out.push(')');
1612}
1613
1614#[cfg(windows)]
1615const NULL_DEVICE: &str = "nul"; #[cfg(not(windows))]
1617const NULL_DEVICE: &str = "/dev/null";
1618
1619fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
1620 cmd: &'a mut std::process::Command,
1621 object_hash: gix_hash::Kind,
1622 args: I,
1623 script_result_directory: &Path,
1624) -> &'a mut std::process::Command {
1625 let mut msys_for_git_bash_on_windows = env::var_os("MSYS").unwrap_or_default();
1629 msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
1630 cmd.args(args)
1631 .stdout(std::process::Stdio::piped())
1632 .stderr(std::process::Stdio::piped())
1633 .current_dir(script_result_directory)
1634 .env_remove("GIT_DIR")
1635 .env_remove("GIT_INDEX_FILE")
1636 .env_remove("GIT_OBJECT_DIRECTORY")
1637 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
1638 .env_remove("GIT_WORK_TREE")
1639 .env_remove("GIT_COMMON_DIR")
1640 .env_remove("GIT_ASKPASS")
1641 .env_remove("SSH_ASKPASS")
1642 .env("MSYS", msys_for_git_bash_on_windows)
1643 .env(
1644 "XDG_CONFIG_HOME",
1645 script_result_directory.join(".gix-testtools-xdg-config"),
1646 )
1647 .env("GIT_CONFIG_NOSYSTEM", "1")
1648 .env("GIT_CONFIG_GLOBAL", NULL_DEVICE)
1649 .env("GIT_TERMINAL_PROMPT", "false")
1650 .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
1651 .env("GIT_AUTHOR_EMAIL", "author@example.com")
1652 .env("GIT_AUTHOR_NAME", "author")
1653 .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
1654 .env("GIT_COMMITTER_EMAIL", "committer@example.com")
1655 .env("GIT_COMMITTER_NAME", "committer")
1656 .env("GIT_DEFAULT_HASH", object_hash.to_string());
1657 apply_git_config_by_environment(cmd, ISOLATED_GIT_CONFIG)
1658}
1659
1660pub 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
1677}
1678
1679pub fn bash_program() -> &'static Path {
1708 static GIT_BASH: LazyLock<PathBuf> = LazyLock::new(|| {
1711 if cfg!(windows) {
1712 GIT_CORE_DIR
1713 .ancestors()
1714 .nth(3)
1715 .map(OsStr::new)
1716 .iter()
1717 .flat_map(|prefix| {
1718 ["/bin/bash.exe", "/usr/bin/bash.exe"].into_iter().map(|suffix| {
1720 let mut raw_path = (*prefix).to_owned();
1721 raw_path.push(suffix);
1722 raw_path
1723 })
1724 })
1725 .map(PathBuf::from)
1726 .find(|bash| bash.is_file())
1727 .unwrap_or_else(|| "bash.exe".into())
1728 } else {
1729 "bash".into()
1730 }
1731 });
1732 GIT_BASH.as_ref()
1733}
1734
1735fn write_failure_marker(failure_marker: &Path) {
1736 std::fs::write(failure_marker, []).ok();
1737}
1738
1739fn should_skip_all_archive_creation() -> bool {
1740 cfg!(windows) || (is_ci::cached() && env::var_os("GIX_TEST_CREATE_ARCHIVES_EVEN_ON_CI").is_none())
1745}
1746
1747fn is_lfs_pointer_file(path: &Path) -> bool {
1748 const PREFIX: &[u8] = b"version https://git-lfs";
1749 let mut buf = [0_u8; PREFIX.len()];
1750 std::fs::OpenOptions::new()
1751 .read(true)
1752 .open(path)
1753 .is_ok_and(|mut f| f.read_exact(&mut buf).is_ok_and(|_| buf.starts_with(PREFIX)))
1754}
1755
1756fn create_archive_if_we_should(
1759 source_dir: &Path,
1760 archive: &Path,
1761 script_identity: u32,
1762 excludes: &dyn IsExcluded,
1763) -> std::io::Result<()> {
1764 if should_skip_all_archive_creation() || excludes.is_excluded(archive) {
1765 return Ok(());
1766 }
1767 if is_lfs_pointer_file(archive) {
1768 eprintln!(
1769 "Refusing to overwrite `gix-lfs` pointer file at \"{}\" - git lfs might not be properly installed.",
1770 archive.display()
1771 );
1772 return Ok(());
1773 }
1774 std::fs::create_dir_all(archive.parent().expect("archive is a file"))?;
1775
1776 let meta_dir = populate_meta_dir(source_dir, script_identity)?;
1777 let res = (move || {
1778 let mut buf = Vec::<u8>::new();
1779 {
1780 let mut ar = tar::Builder::new(&mut buf);
1781 ar.mode(tar::HeaderMode::Deterministic);
1782 ar.follow_symlinks(false);
1783 ar.append_dir_all(".", source_dir)?;
1784 ar.finish()?;
1785 }
1786 #[cfg_attr(feature = "xz", allow(unused_mut))]
1787 let mut archive = std::fs::OpenOptions::new()
1788 .write(true)
1789 .create(true)
1790 .truncate(true)
1791 .open(archive)?;
1792 #[cfg(feature = "xz")]
1793 {
1794 let mut xz_write = xz2::write::XzEncoder::new(archive, 3);
1795 std::io::copy(&mut &*buf, &mut xz_write)?;
1796 xz_write.finish()?.close()
1797 }
1798 #[cfg(not(feature = "xz"))]
1799 {
1800 use std::io::Write;
1801 archive.write_all(&buf)?;
1802 archive.close()
1803 }
1804 })();
1805 #[cfg(not(windows))]
1806 std::fs::remove_dir_all(meta_dir)?;
1807 #[cfg(windows)]
1808 std::fs::remove_dir_all(meta_dir).ok(); res
1811}
1812
1813const META_DIR_NAME: &str = "__gitoxide_meta__";
1814const META_IDENTITY: &str = "identity";
1815const META_GIT_VERSION: &str = "git-version";
1816
1817fn populate_meta_dir(destination_dir: &Path, script_identity: u32) -> std::io::Result<PathBuf> {
1818 let meta_dir = destination_dir.join(META_DIR_NAME);
1819 std::fs::create_dir_all(&meta_dir)?;
1820 std::fs::write(
1821 meta_dir.join(META_IDENTITY),
1822 format!("{}-{}", script_identity, family_name()).as_bytes(),
1823 )?;
1824 std::fs::write(
1825 meta_dir.join(META_GIT_VERSION),
1826 std::process::Command::new(GIT_PROGRAM)
1827 .arg("--version")
1828 .output()?
1829 .stdout,
1830 )?;
1831 Ok(meta_dir)
1832}
1833
1834fn extract_archive(
1837 archive: &Path,
1838 destination_dir: &Path,
1839 required_script_identity: u32,
1840 needs_archive: bool,
1841) -> std::io::Result<(u32, Option<String>)> {
1842 let archive_buf: Vec<u8> = {
1843 let mut buf = Vec::new();
1844 #[cfg_attr(feature = "xz", allow(unused_mut))]
1845 let mut input_archive = std::fs::File::open(archive)?;
1846 if !needs_archive && env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() {
1847 return Err(std::io::Error::other(format!(
1848 "Ignoring archive at '{}' as GIX_TEST_IGNORE_ARCHIVES is set.",
1849 archive.display()
1850 )));
1851 }
1852 #[cfg(feature = "xz")]
1853 {
1854 let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(input_archive));
1855 std::io::copy(&mut decoder, &mut buf)?;
1856 }
1857 #[cfg(not(feature = "xz"))]
1858 {
1859 input_archive.read_to_end(&mut buf)?;
1860 }
1861 buf
1862 };
1863
1864 let mut entry_buf = Vec::<u8>::new();
1865 let (archive_identity, platform): (u32, _) = tar::Archive::new(std::io::Cursor::new(&mut &*archive_buf))
1866 .entries_with_seek()?
1867 .filter_map(std::result::Result::ok)
1868 .find_map(|mut e: tar::Entry<'_, _>| {
1869 let path = e.path().ok()?;
1870 if path.parent()?.file_name()? == META_DIR_NAME && path.file_name()? == META_IDENTITY {
1871 entry_buf.clear();
1872 e.read_to_end(&mut entry_buf).ok()?;
1873 let mut tokens = entry_buf.to_str().ok()?.trim().splitn(2, '-');
1874 match (tokens.next(), tokens.next()) {
1875 (Some(id), platform) => Some((id.parse().ok()?, platform.map(ToOwned::to_owned))),
1876 _ => None,
1877 }
1878 } else {
1879 None
1880 }
1881 })
1882 .ok_or_else(|| std::io::Error::other("BUG: Could not find meta directory in our own archive"))
1883 .map_err(|err| {
1884 std::io::Error::other(format!(
1885 "Could not extract archive at '{archive}': {err}",
1886 archive = archive.display()
1887 ))
1888 })?;
1889 if archive_identity != required_script_identity {
1890 eprintln!(
1891 "Ignoring archive at '{}' as its generating script changed",
1892 archive.display()
1893 );
1894 return Err(std::io::ErrorKind::NotFound.into());
1895 }
1896
1897 for entry in tar::Archive::new(&mut &*archive_buf).entries()? {
1898 let mut entry = entry?;
1899 let path = entry.path()?;
1900 if path.to_str() == Some(META_DIR_NAME) || path.parent().and_then(Path::to_str) == Some(META_DIR_NAME) {
1901 continue;
1902 }
1903 entry.unpack_in(destination_dir)?;
1904 }
1905 Ok((archive_identity, platform))
1906}
1907
1908fn family_name() -> &'static str {
1909 if cfg!(windows) { "windows" } else { "unix" }
1910}
1911
1912#[derive(Default)]
1914pub struct Env<'a> {
1915 altered_vars: Vec<(&'a str, Option<OsString>)>,
1916}
1917
1918fn set_var(var: &str, value: impl AsRef<OsStr>) {
1919 unsafe { env::set_var(var, value) };
1922}
1923
1924fn remove_var(var: &str) {
1925 unsafe { env::remove_var(var) };
1928}
1929
1930impl<'a> Env<'a> {
1931 pub fn new() -> Self {
1933 Env {
1934 altered_vars: Vec::new(),
1935 }
1936 }
1937
1938 pub fn set(mut self, var: &'a str, value: impl Into<String>) -> Self {
1940 let prev = env::var_os(var);
1941 set_var(var, value.into());
1942 self.altered_vars.push((var, prev));
1943 self
1944 }
1945
1946 pub fn unset(mut self, var: &'a str) -> Self {
1948 let prev = env::var_os(var);
1949 remove_var(var);
1950 self.altered_vars.push((var, prev));
1951 self
1952 }
1953}
1954
1955impl Drop for Env<'_> {
1956 fn drop(&mut self) {
1957 for (var, prev_value) in self.altered_vars.iter().rev() {
1958 match prev_value {
1959 Some(value) => set_var(var, value),
1960 None => remove_var(var),
1961 }
1962 }
1963 }
1964}
1965
1966pub fn size_ok(actual_size: usize, expected_64_bit_size: usize) -> bool {
1984 #[cfg(target_pointer_width = "64")]
1985 return actual_size == expected_64_bit_size;
1986 #[cfg(target_pointer_width = "32")]
1987 return actual_size <= expected_64_bit_size;
1988}
1989
1990#[cfg(unix)]
1992pub fn umask() -> u32 {
1993 let output = std::process::Command::new("/bin/sh")
1994 .args(["-c", "umask"])
1995 .output()
1996 .expect("can execute `sh -c umask`");
1997 assert!(output.status.success(), "`sh -c umask` failed");
1998 assert_eq!(output.stderr.as_bstr(), "", "`sh -c umask` unexpected message");
1999 let text = output.stdout.to_str().expect("valid Unicode").trim();
2000 u32::from_str_radix(text, 8).expect("parses as octal number")
2001}
2002
2003fn tar_extension() -> &'static str {
2004 if cfg!(feature = "xz") { "tar.xz" } else { "tar" }
2005}
2006
2007#[cfg(test)]
2008mod tests;