1#![cfg_attr(
5 all(doc, feature = "document-features"),
6 doc = ::document_features::document_features!()
7)]
8#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg))]
9#![deny(missing_docs)]
10
11use std::{
12 collections::BTreeMap,
13 env,
14 ffi::{OsStr, OsString},
15 io::Read,
16 path::{Path, PathBuf},
17 str::FromStr,
18 time::Duration,
19};
20
21pub use bstr;
22use bstr::ByteSlice;
23use io_close::Close;
24pub use is_ci;
25use parking_lot::Mutex;
26use std::sync::LazyLock;
27pub use tempfile;
28
29pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
44
45pub struct GitDaemon {
49 child: std::process::Child,
50 pub url: String,
52}
53
54impl Drop for GitDaemon {
55 fn drop(&mut self) {
56 self.child.kill().ok();
57 }
58}
59
60static SCRIPT_IDENTITY: LazyLock<Mutex<BTreeMap<PathBuf, u32>>> = LazyLock::new(|| Mutex::new(BTreeMap::new()));
61
62static EXCLUDE_LUT: LazyLock<Mutex<Option<gix_worktree::Stack>>> = LazyLock::new(|| {
63 let cache = (|| {
64 let (repo_path, _) = gix_discover::upwards(Path::new(".")).ok()?;
65 let (gix_dir, work_tree) = repo_path.into_repository_and_work_tree_directories();
66 let work_tree = work_tree?.canonicalize().ok()?;
67
68 let mut buf = Vec::with_capacity(512);
69 let case = if gix_fs::Capabilities::probe(&work_tree).ignore_case {
70 gix_worktree::ignore::glob::pattern::Case::Fold
71 } else {
72 Default::default()
73 };
74 let state = gix_worktree::stack::State::IgnoreStack(gix_worktree::stack::state::Ignore::new(
75 Default::default(),
76 gix_worktree::ignore::Search::from_git_dir(
77 &gix_dir,
78 None,
79 &mut buf,
80 gix_worktree::stack::state::ignore::ParseIgnore {
81 support_precious: false,
82 },
83 )
84 .ok()?,
85 None,
86 gix_worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped,
87 Default::default(),
88 ));
89 Some(gix_worktree::Stack::new(
90 work_tree,
91 state,
92 case,
93 buf,
94 Default::default(),
95 ))
96 })();
97 Mutex::new(cache)
98});
99
100#[cfg(windows)]
101const GIT_PROGRAM: &str = "git.exe";
102#[cfg(not(windows))]
103const GIT_PROGRAM: &str = "git";
104
105static GIT_CORE_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
106 let output = std::process::Command::new(GIT_PROGRAM)
107 .arg("--exec-path")
108 .output()
109 .expect("can execute `git --exec-path`");
110
111 assert!(output.status.success(), "`git --exec-path` failed");
112
113 output
114 .stdout
115 .strip_suffix(b"\n")
116 .expect("`git --exec-path` output to be well-formed")
117 .to_os_str()
118 .expect("no invalid UTF-8 in `--exec-path` except as OS allows")
119 .into()
120});
121
122pub static GIT_VERSION: LazyLock<(u8, u8, u8)> =
124 LazyLock::new(|| parse_git_version().expect("git version to be parsable"));
125
126pub enum Creation {
128 CopyFromReadOnly,
135 ExecuteScript,
137}
138
139pub fn should_skip_as_git_version_is_smaller_than(major: u8, minor: u8, patch: u8) -> bool {
147 if is_ci::cached() {
148 return false; }
150 *GIT_VERSION < (major, minor, patch)
151}
152
153fn parse_git_version() -> Result<(u8, u8, u8)> {
154 let output = std::process::Command::new(GIT_PROGRAM).arg("--version").output()?;
155 git_version_from_bytes(&output.stdout)
156}
157
158fn git_version_from_bytes(bytes: &[u8]) -> Result<(u8, u8, u8)> {
159 let mut numbers = bytes
160 .split(|b| *b == b' ' || *b == b'\n')
161 .nth(2)
162 .expect("git version <version>")
163 .split(|b| *b == b'.')
164 .take(3)
165 .map(|n| std::str::from_utf8(n).expect("valid utf8 in version number"))
166 .map(u8::from_str);
167
168 Ok((|| -> Result<_> {
169 Ok((
170 numbers.next().expect("major")?,
171 numbers.next().expect("minor")?,
172 numbers.next().expect("patch")?,
173 ))
174 })()
175 .map_err(|err| {
176 format!(
177 "Could not parse version from output of 'git --version' ({:?}) with error: {}",
178 bytes.to_str_lossy(),
179 err
180 )
181 })?)
182}
183
184pub fn set_current_dir(new_cwd: impl AsRef<Path>) -> std::io::Result<AutoRevertToPreviousCWD> {
186 let cwd = env::current_dir()?;
187 env::set_current_dir(new_cwd)?;
188 Ok(AutoRevertToPreviousCWD(cwd))
189}
190
191#[derive(Debug)]
197#[must_use]
198pub struct AutoRevertToPreviousCWD(PathBuf);
199
200impl Drop for AutoRevertToPreviousCWD {
201 fn drop(&mut self) {
202 env::set_current_dir(&self.0).unwrap();
203 }
204}
205
206pub fn run_git(working_dir: &Path, args: &[&str]) -> std::io::Result<std::process::ExitStatus> {
208 std::process::Command::new(GIT_PROGRAM)
209 .current_dir(working_dir)
210 .args(args)
211 .status()
212}
213
214pub fn spawn_git_daemon(working_dir: impl AsRef<Path>) -> std::io::Result<GitDaemon> {
216 let mut ports: Vec<_> = (9419u16..9419 + 100).collect();
217 fastrand::shuffle(&mut ports);
218 let addr_at = |port| std::net::SocketAddr::from(([127, 0, 0, 1], port));
219 let free_port = {
220 let listener = std::net::TcpListener::bind(ports.into_iter().map(addr_at).collect::<Vec<_>>().as_slice())?;
221 listener.local_addr().expect("listener address is available").port()
222 };
223
224 let child =
225 std::process::Command::new(GIT_CORE_DIR.join(if cfg!(windows) { "git-daemon.exe" } else { "git-daemon" }))
226 .current_dir(working_dir)
227 .args(["--verbose", "--base-path=.", "--export-all", "--user-path"])
228 .arg(format!("--port={free_port}"))
229 .spawn()?;
230
231 let server_addr = addr_at(free_port);
232 for time in gix_lock::backoff::Quadratic::default_with_random() {
233 std::thread::sleep(time);
234 if std::net::TcpStream::connect(server_addr).is_ok() {
235 break;
236 }
237 }
238 Ok(GitDaemon {
239 child,
240 url: format!("git://{server_addr}"),
241 })
242}
243
244#[derive(Copy, Clone)]
245enum DirectoryRoot {
246 IntegrationTest,
247 StandaloneTest,
248}
249
250#[derive(Copy, Clone)]
254enum ArgsInHash {
255 Yes,
256 No,
257}
258
259pub fn fixture_path(path: impl AsRef<Path>) -> PathBuf {
261 fixture_path_inner(path, DirectoryRoot::IntegrationTest)
262}
263
264pub fn fixture_path_standalone(path: impl AsRef<Path>) -> PathBuf {
266 fixture_path_inner(path, DirectoryRoot::StandaloneTest)
267}
268fn fixture_path_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> PathBuf {
270 match root {
271 DirectoryRoot::StandaloneTest => PathBuf::from("fixtures").join(path.as_ref()),
272 DirectoryRoot::IntegrationTest => PathBuf::from("tests").join("fixtures").join(path.as_ref()),
273 }
274}
275
276pub fn fixture_bytes(path: impl AsRef<Path>) -> Vec<u8> {
278 fixture_bytes_inner(path, DirectoryRoot::IntegrationTest)
279}
280
281pub fn fixture_bytes_standalone(path: impl AsRef<Path>) -> Vec<u8> {
283 fixture_bytes_inner(path, DirectoryRoot::StandaloneTest)
284}
285
286fn fixture_bytes_inner(path: impl AsRef<Path>, root: DirectoryRoot) -> Vec<u8> {
287 match std::fs::read(fixture_path_inner(path.as_ref(), root)) {
288 Ok(res) => res,
289 Err(_) => panic!("File at '{}' not found", path.as_ref().display()),
290 }
291}
292
293pub fn scripted_fixture_read_only(script_name: impl AsRef<Path>) -> Result<PathBuf> {
316 scripted_fixture_read_only_with_args(script_name, None::<String>)
317}
318
319pub fn scripted_fixture_read_only_standalone(script_name: impl AsRef<Path>) -> Result<PathBuf> {
321 scripted_fixture_read_only_with_args_standalone(script_name, None::<String>)
322}
323
324pub fn scripted_fixture_writable(script_name: impl AsRef<Path>) -> Result<tempfile::TempDir> {
329 scripted_fixture_writable_with_args(script_name, None::<String>, Creation::CopyFromReadOnly)
330}
331
332pub fn scripted_fixture_writable_standalone(script_name: &str) -> Result<tempfile::TempDir> {
334 scripted_fixture_writable_with_args_standalone(script_name, None::<String>, Creation::CopyFromReadOnly)
335}
336
337pub fn scripted_fixture_writable_with_args(
340 script_name: impl AsRef<Path>,
341 args: impl IntoIterator<Item = impl Into<String>>,
342 mode: Creation,
343) -> Result<tempfile::TempDir> {
344 scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::Yes)
345}
346
347pub fn scripted_fixture_writable_with_args_single_archive(
352 script_name: impl AsRef<Path>,
353 args: impl IntoIterator<Item = impl Into<String>>,
354 mode: Creation,
355) -> Result<tempfile::TempDir> {
356 scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::IntegrationTest, ArgsInHash::No)
357}
358
359pub fn scripted_fixture_writable_with_args_standalone(
361 script_name: &str,
362 args: impl IntoIterator<Item = impl Into<String>>,
363 mode: Creation,
364) -> Result<tempfile::TempDir> {
365 scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::Yes)
366}
367
368pub fn scripted_fixture_writable_with_args_standalone_single_archive(
372 script_name: &str,
373 args: impl IntoIterator<Item = impl Into<String>>,
374 mode: Creation,
375) -> Result<tempfile::TempDir> {
376 scripted_fixture_writable_with_args_inner(script_name, args, mode, DirectoryRoot::StandaloneTest, ArgsInHash::No)
377}
378
379fn scripted_fixture_writable_with_args_inner(
380 script_name: impl AsRef<Path>,
381 args: impl IntoIterator<Item = impl Into<String>>,
382 mode: Creation,
383 root: DirectoryRoot,
384 args_in_hash: ArgsInHash,
385) -> Result<tempfile::TempDir> {
386 let dst = tempfile::TempDir::new()?;
387 Ok(match mode {
388 Creation::CopyFromReadOnly => {
389 let ro_dir = scripted_fixture_read_only_with_args_inner(script_name, args, None, root, args_in_hash)?;
390 copy_recursively_into_existing_dir(ro_dir, dst.path())?;
391 dst
392 }
393 Creation::ExecuteScript => {
394 scripted_fixture_read_only_with_args_inner(script_name, args, dst.path().into(), root, args_in_hash)?;
395 dst
396 }
397 })
398}
399
400pub fn copy_recursively_into_existing_dir(src_dir: impl AsRef<Path>, dst_dir: impl AsRef<Path>) -> std::io::Result<()> {
402 fs_extra::copy_items(
403 &std::fs::read_dir(src_dir)?
404 .map(|e| e.map(|e| e.path()))
405 .collect::<std::result::Result<Vec<_>, _>>()?,
406 dst_dir,
407 &fs_extra::dir::CopyOptions {
408 overwrite: false,
409 skip_exist: false,
410 copy_inside: false,
411 content_only: false,
412 ..Default::default()
413 },
414 )
415 .map_err(std::io::Error::other)?;
416 Ok(())
417}
418
419pub fn scripted_fixture_read_only_with_args(
421 script_name: impl AsRef<Path>,
422 args: impl IntoIterator<Item = impl Into<String>>,
423) -> Result<PathBuf> {
424 scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::Yes)
425}
426
427pub fn scripted_fixture_read_only_with_args_single_archive(
439 script_name: impl AsRef<Path>,
440 args: impl IntoIterator<Item = impl Into<String>>,
441) -> Result<PathBuf> {
442 scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::IntegrationTest, ArgsInHash::No)
443}
444
445pub fn scripted_fixture_read_only_with_args_standalone(
447 script_name: impl AsRef<Path>,
448 args: impl IntoIterator<Item = impl Into<String>>,
449) -> Result<PathBuf> {
450 scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::Yes)
451}
452
453pub fn scripted_fixture_read_only_with_args_standalone_single_archive(
455 script_name: impl AsRef<Path>,
456 args: impl IntoIterator<Item = impl Into<String>>,
457) -> Result<PathBuf> {
458 scripted_fixture_read_only_with_args_inner(script_name, args, None, DirectoryRoot::StandaloneTest, ArgsInHash::No)
459}
460
461fn scripted_fixture_read_only_with_args_inner(
462 script_name: impl AsRef<Path>,
463 args: impl IntoIterator<Item = impl Into<String>>,
464 destination_dir: Option<&Path>,
465 root: DirectoryRoot,
466 args_in_hash: ArgsInHash,
467) -> Result<PathBuf> {
468 gix_tempfile::signal::setup(
470 gix_tempfile::signal::handler::Mode::DeleteTempfilesOnTerminationAndRestoreDefaultBehaviour,
471 );
472
473 let script_location = script_name.as_ref();
474 let script_path = fixture_path_inner(script_location, root);
475
476 let args: Vec<String> = args.into_iter().map(Into::into).collect();
478 let script_identity = {
479 let mut map = SCRIPT_IDENTITY.lock();
480 map.entry(args.iter().fold(script_path.clone(), |p, a| p.join(a)))
481 .or_insert_with(|| {
482 let crc_value = crc::Crc::<u32>::new(&crc::CRC_32_CKSUM);
483 let mut crc_digest = crc_value.digest();
484 crc_digest.update(&std::fs::read(&script_path).unwrap_or_else(|err| {
485 panic!(
486 "file {script_path} in CWD '{cwd}' could not be read: {err}",
487 cwd = env::current_dir().expect("valid cwd").display(),
488 script_path = script_path.display(),
489 )
490 }));
491 for arg in &args {
492 crc_digest.update(arg.as_bytes());
493 }
494 crc_digest.finalize()
495 })
496 .to_owned()
497 };
498
499 let script_basename = script_location.file_stem().unwrap_or(script_location.as_os_str());
500 let archive_file_path = fixture_path_inner(
501 {
502 let suffix = match args_in_hash {
503 ArgsInHash::Yes => {
504 let mut suffix = args.join("_");
505 if !suffix.is_empty() {
506 suffix.insert(0, '_');
507 }
508 suffix.replace(['\\', '/', ' ', '.'], "_")
509 }
510 ArgsInHash::No => "".into(),
511 };
512 Path::new("generated-archives").join(format!(
513 "{}{suffix}.tar{}",
514 script_basename.to_str().expect("valid UTF-8"),
515 if cfg!(feature = "xz") { ".xz" } else { "" }
516 ))
517 },
518 root,
519 );
520 let (force_run, script_result_directory) = destination_dir.map_or_else(
521 || {
522 let dir = fixture_path_inner(
523 Path::new("generated-do-not-edit").join(script_basename).join(format!(
524 "{}-{}",
525 script_identity,
526 family_name()
527 )),
528 root,
529 );
530 (false, dir)
531 },
532 |d| (true, d.to_owned()),
533 );
534
535 let _marker = destination_dir
538 .is_none()
539 .then(|| {
540 gix_lock::Marker::acquire_to_hold_resource(
541 script_basename,
542 gix_lock::acquire::Fail::AfterDurationWithBackoff(Duration::from_secs(6 * 60)),
543 None,
544 )
545 })
546 .transpose()?;
547 let failure_marker = script_result_directory.join("_invalid_state_due_to_script_failure_");
548 if force_run || !script_result_directory.is_dir() || failure_marker.is_file() {
549 if failure_marker.is_file() {
550 std::fs::remove_dir_all(&script_result_directory).map_err(|err| {
551 format!("Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
552 script_result_directory = script_result_directory.display())
553 })?;
554 }
555 std::fs::create_dir_all(&script_result_directory)?;
556 let script_identity_for_archive = match args_in_hash {
557 ArgsInHash::Yes => script_identity,
558 ArgsInHash::No => 0,
559 };
560 match extract_archive(
561 &archive_file_path,
562 &script_result_directory,
563 script_identity_for_archive,
564 ) {
565 Ok((archive_id, platform)) => {
566 eprintln!(
567 "Extracted fixture from archive '{}' ({}, {:?})",
568 archive_file_path.display(),
569 archive_id,
570 platform
571 );
572 }
573 Err(err) => {
574 if err.kind() != std::io::ErrorKind::NotFound {
575 eprintln!("failed to extract '{}': {}", archive_file_path.display(), err);
576 std::fs::remove_dir_all(&script_result_directory)
577 .map_err(|err| {
578 format!("Failed to remove '{script_result_directory}', please try to do that by hand. Original error: {err}",
579 script_result_directory = script_result_directory.display())
580 })?;
581 std::fs::create_dir_all(&script_result_directory)?;
582 } else if !is_excluded(&archive_file_path) {
583 eprintln!(
584 "Archive at '{}' not found, creating fixture using script '{}'",
585 archive_file_path.display(),
586 script_location.display()
587 );
588 }
589 let script_absolute_path = env::current_dir()?.join(script_path);
590 let mut cmd = std::process::Command::new(&script_absolute_path);
591 let output = match configure_command(&mut cmd, &args, &script_result_directory).output() {
592 Ok(out) => out,
593 Err(err)
594 if err.kind() == std::io::ErrorKind::PermissionDenied || err.raw_os_error() == Some(193) =>
595 {
596 cmd = std::process::Command::new(bash_program());
597 configure_command(cmd.arg(script_absolute_path), &args, &script_result_directory).output()?
598 }
599 Err(err) => return Err(err.into()),
600 };
601 if !output.status.success() {
602 write_failure_marker(&failure_marker);
603 eprintln!("stdout: {}", output.stdout.as_bstr());
604 eprintln!("stderr: {}", output.stderr.as_bstr());
605 return Err(format!("fixture script of {cmd:?} failed").into());
606 }
607 create_archive_if_we_should(
608 &script_result_directory,
609 &archive_file_path,
610 script_identity_for_archive,
611 )
612 .inspect_err(|_err| {
613 write_failure_marker(&failure_marker);
614 })?;
615 }
616 }
617 }
618 Ok(script_result_directory)
619}
620
621#[cfg(windows)]
622const NULL_DEVICE: &str = "nul"; #[cfg(not(windows))]
624const NULL_DEVICE: &str = "/dev/null";
625
626fn configure_command<'a, I: IntoIterator<Item = S>, S: AsRef<OsStr>>(
627 cmd: &'a mut std::process::Command,
628 args: I,
629 script_result_directory: &Path,
630) -> &'a mut std::process::Command {
631 let mut msys_for_git_bash_on_windows = env::var_os("MSYS").unwrap_or_default();
635 msys_for_git_bash_on_windows.push(" winsymlinks:nativestrict");
636 cmd.args(args)
637 .stdout(std::process::Stdio::piped())
638 .stderr(std::process::Stdio::piped())
639 .current_dir(script_result_directory)
640 .env_remove("GIT_DIR")
641 .env_remove("GIT_INDEX_FILE")
642 .env_remove("GIT_OBJECT_DIRECTORY")
643 .env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES")
644 .env_remove("GIT_WORK_TREE")
645 .env_remove("GIT_COMMON_DIR")
646 .env_remove("GIT_ASKPASS")
647 .env_remove("SSH_ASKPASS")
648 .env("MSYS", msys_for_git_bash_on_windows)
649 .env("GIT_CONFIG_NOSYSTEM", "1")
650 .env("GIT_CONFIG_GLOBAL", NULL_DEVICE)
651 .env("GIT_TERMINAL_PROMPT", "false")
652 .env("GIT_AUTHOR_DATE", "2000-01-01 00:00:00 +0000")
653 .env("GIT_AUTHOR_EMAIL", "author@example.com")
654 .env("GIT_AUTHOR_NAME", "author")
655 .env("GIT_COMMITTER_DATE", "2000-01-02 00:00:00 +0000")
656 .env("GIT_COMMITTER_EMAIL", "committer@example.com")
657 .env("GIT_COMMITTER_NAME", "committer")
658 .env("GIT_CONFIG_COUNT", "4")
659 .env("GIT_CONFIG_KEY_0", "commit.gpgsign")
660 .env("GIT_CONFIG_VALUE_0", "false")
661 .env("GIT_CONFIG_KEY_1", "tag.gpgsign")
662 .env("GIT_CONFIG_VALUE_1", "false")
663 .env("GIT_CONFIG_KEY_2", "init.defaultBranch")
664 .env("GIT_CONFIG_VALUE_2", "main")
665 .env("GIT_CONFIG_KEY_3", "protocol.file.allow")
666 .env("GIT_CONFIG_VALUE_3", "always")
667}
668
669pub fn bash_program() -> &'static Path {
698 static GIT_BASH: LazyLock<PathBuf> = LazyLock::new(|| {
701 if cfg!(windows) {
702 GIT_CORE_DIR
703 .ancestors()
704 .nth(3)
705 .map(OsStr::new)
706 .iter()
707 .flat_map(|prefix| {
708 ["/bin/bash.exe", "/usr/bin/bash.exe"].into_iter().map(|suffix| {
710 let mut raw_path = (*prefix).to_owned();
711 raw_path.push(suffix);
712 raw_path
713 })
714 })
715 .map(PathBuf::from)
716 .find(|bash| bash.is_file())
717 .unwrap_or_else(|| "bash.exe".into())
718 } else {
719 "bash".into()
720 }
721 });
722 GIT_BASH.as_ref()
723}
724
725fn write_failure_marker(failure_marker: &Path) {
726 std::fs::write(failure_marker, []).ok();
727}
728
729fn should_skip_all_archive_creation() -> bool {
730 cfg!(windows) || (is_ci::cached() && env::var_os("GIX_TEST_CREATE_ARCHIVES_EVEN_ON_CI").is_none())
735}
736
737fn is_lfs_pointer_file(path: &Path) -> bool {
738 const PREFIX: &[u8] = b"version https://git-lfs";
739 let mut buf = [0_u8; PREFIX.len()];
740 std::fs::OpenOptions::new()
741 .read(true)
742 .open(path)
743 .is_ok_and(|mut f| f.read_exact(&mut buf).is_ok_and(|_| buf.starts_with(PREFIX)))
744}
745
746fn create_archive_if_we_should(source_dir: &Path, archive: &Path, script_identity: u32) -> std::io::Result<()> {
749 if should_skip_all_archive_creation() || is_excluded(archive) {
750 return Ok(());
751 }
752 if is_lfs_pointer_file(archive) {
753 eprintln!(
754 "Refusing to overwrite `gix-lfs` pointer file at \"{}\" - git lfs might not be properly installed.",
755 archive.display()
756 );
757 return Ok(());
758 }
759 std::fs::create_dir_all(archive.parent().expect("archive is a file"))?;
760
761 let meta_dir = populate_meta_dir(source_dir, script_identity)?;
762 let res = (move || {
763 let mut buf = Vec::<u8>::new();
764 {
765 let mut ar = tar::Builder::new(&mut buf);
766 ar.mode(tar::HeaderMode::Deterministic);
767 ar.follow_symlinks(false);
768 ar.append_dir_all(".", source_dir)?;
769 ar.finish()?;
770 }
771 #[cfg_attr(feature = "xz", allow(unused_mut))]
772 let mut archive = std::fs::OpenOptions::new()
773 .write(true)
774 .create(true)
775 .truncate(true)
776 .open(archive)?;
777 #[cfg(feature = "xz")]
778 {
779 let mut xz_write = xz2::write::XzEncoder::new(archive, 3);
780 std::io::copy(&mut &*buf, &mut xz_write)?;
781 xz_write.finish()?.close()
782 }
783 #[cfg(not(feature = "xz"))]
784 {
785 use std::io::Write;
786 archive.write_all(&buf)?;
787 archive.close()
788 }
789 })();
790 #[cfg(not(windows))]
791 std::fs::remove_dir_all(meta_dir)?;
792 #[cfg(windows)]
793 std::fs::remove_dir_all(meta_dir).ok(); res
796}
797
798fn is_excluded(archive: &Path) -> bool {
799 let mut lut = EXCLUDE_LUT.lock();
800 lut.as_mut()
801 .and_then(|cache| {
802 let archive = env::current_dir().ok()?.join(archive);
803 let relative_path = archive.strip_prefix(cache.base()).ok()?;
804 cache
805 .at_path(
806 relative_path,
807 Some(gix_worktree::index::entry::Mode::FILE),
808 &gix_worktree::object::find::Never,
809 )
810 .ok()?
811 .is_excluded()
812 .into()
813 })
814 .unwrap_or(false)
815}
816
817const META_DIR_NAME: &str = "__gitoxide_meta__";
818const META_IDENTITY: &str = "identity";
819const META_GIT_VERSION: &str = "git-version";
820
821fn populate_meta_dir(destination_dir: &Path, script_identity: u32) -> std::io::Result<PathBuf> {
822 let meta_dir = destination_dir.join(META_DIR_NAME);
823 std::fs::create_dir_all(&meta_dir)?;
824 std::fs::write(
825 meta_dir.join(META_IDENTITY),
826 format!("{}-{}", script_identity, family_name()).as_bytes(),
827 )?;
828 std::fs::write(
829 meta_dir.join(META_GIT_VERSION),
830 std::process::Command::new(GIT_PROGRAM)
831 .arg("--version")
832 .output()?
833 .stdout,
834 )?;
835 Ok(meta_dir)
836}
837
838fn extract_archive(
841 archive: &Path,
842 destination_dir: &Path,
843 required_script_identity: u32,
844) -> std::io::Result<(u32, Option<String>)> {
845 let archive_buf: Vec<u8> = {
846 let mut buf = Vec::new();
847 #[cfg_attr(feature = "xz", allow(unused_mut))]
848 let mut input_archive = std::fs::File::open(archive)?;
849 if env::var_os("GIX_TEST_IGNORE_ARCHIVES").is_some() {
850 return Err(std::io::Error::other(format!(
851 "Ignoring archive at '{}' as GIX_TEST_IGNORE_ARCHIVES is set.",
852 archive.display()
853 )));
854 }
855 #[cfg(feature = "xz")]
856 {
857 let mut decoder = xz2::bufread::XzDecoder::new(std::io::BufReader::new(input_archive));
858 std::io::copy(&mut decoder, &mut buf)?;
859 }
860 #[cfg(not(feature = "xz"))]
861 {
862 input_archive.read_to_end(&mut buf)?;
863 }
864 buf
865 };
866
867 let mut entry_buf = Vec::<u8>::new();
868 let (archive_identity, platform): (u32, _) = tar::Archive::new(std::io::Cursor::new(&mut &*archive_buf))
869 .entries_with_seek()?
870 .filter_map(std::result::Result::ok)
871 .find_map(|mut e: tar::Entry<'_, _>| {
872 let path = e.path().ok()?;
873 if path.parent()?.file_name()? == META_DIR_NAME && path.file_name()? == META_IDENTITY {
874 entry_buf.clear();
875 e.read_to_end(&mut entry_buf).ok()?;
876 let mut tokens = entry_buf.to_str().ok()?.trim().splitn(2, '-');
877 match (tokens.next(), tokens.next()) {
878 (Some(id), platform) => Some((id.parse().ok()?, platform.map(ToOwned::to_owned))),
879 _ => None,
880 }
881 } else {
882 None
883 }
884 })
885 .ok_or_else(|| std::io::Error::other("BUG: Could not find meta directory in our own archive"))
886 .map_err(|err| {
887 std::io::Error::other(format!(
888 "Could not extract archive at '{archive}': {err}",
889 archive = archive.display()
890 ))
891 })?;
892 if archive_identity != required_script_identity {
893 eprintln!(
894 "Ignoring archive at '{}' as its generating script changed",
895 archive.display()
896 );
897 return Err(std::io::ErrorKind::NotFound.into());
898 }
899
900 for entry in tar::Archive::new(&mut &*archive_buf).entries()? {
901 let mut entry = entry?;
902 let path = entry.path()?;
903 if path.to_str() == Some(META_DIR_NAME) || path.parent().and_then(Path::to_str) == Some(META_DIR_NAME) {
904 continue;
905 }
906 entry.unpack_in(destination_dir)?;
907 }
908 Ok((archive_identity, platform))
909}
910
911pub fn to_bstr_err(
913 err: winnow::error::ErrMode<winnow::error::TreeError<&[u8], winnow::error::StrContext>>,
914) -> winnow::error::TreeError<&winnow::stream::BStr, winnow::error::StrContext> {
915 let err = err.into_inner().expect("not a streaming parser");
916 err.map_input(winnow::stream::BStr::new)
917}
918
919fn family_name() -> &'static str {
920 if cfg!(windows) {
921 "windows"
922 } else {
923 "unix"
924 }
925}
926
927#[derive(Default)]
929pub struct Env<'a> {
930 altered_vars: Vec<(&'a str, Option<OsString>)>,
931}
932
933impl<'a> Env<'a> {
934 pub fn new() -> Self {
936 Env {
937 altered_vars: Vec::new(),
938 }
939 }
940
941 pub fn set(mut self, var: &'a str, value: impl Into<String>) -> Self {
943 let prev = env::var_os(var);
944 env::set_var(var, value.into());
945 self.altered_vars.push((var, prev));
946 self
947 }
948
949 pub fn unset(mut self, var: &'a str) -> Self {
951 let prev = env::var_os(var);
952 env::remove_var(var);
953 self.altered_vars.push((var, prev));
954 self
955 }
956}
957
958impl Drop for Env<'_> {
959 fn drop(&mut self) {
960 for (var, prev_value) in self.altered_vars.iter().rev() {
961 match prev_value {
962 Some(value) => env::set_var(var, value),
963 None => env::remove_var(var),
964 }
965 }
966 }
967}
968
969pub fn size_ok(actual_size: usize, expected_64_bit_size: usize) -> bool {
987 #[cfg(target_pointer_width = "64")]
988 return actual_size == expected_64_bit_size;
989 #[cfg(target_pointer_width = "32")]
990 return actual_size <= expected_64_bit_size;
991}
992
993#[cfg(unix)]
995pub fn umask() -> u32 {
996 let output = std::process::Command::new("/bin/sh")
997 .args(["-c", "umask"])
998 .output()
999 .expect("can execute `sh -c umask`");
1000 assert!(output.status.success(), "`sh -c umask` failed");
1001 assert_eq!(output.stderr.as_bstr(), "", "`sh -c umask` unexpected message");
1002 let text = output.stdout.to_str().expect("valid Unicode").trim();
1003 u32::from_str_radix(text, 8).expect("parses as octal number")
1004}
1005
1006#[cfg(test)]
1007mod tests {
1008 use super::*;
1009
1010 #[test]
1011 fn parse_version() {
1012 assert_eq!(git_version_from_bytes(b"git version 2.37.2").unwrap(), (2, 37, 2));
1013 assert_eq!(
1014 git_version_from_bytes(b"git version 2.32.1 (Apple Git-133)").unwrap(),
1015 (2, 32, 1)
1016 );
1017 }
1018
1019 #[test]
1020 fn parse_version_with_trailing_newline() {
1021 assert_eq!(git_version_from_bytes(b"git version 2.37.2\n").unwrap(), (2, 37, 2));
1022 }
1023
1024 const SCOPE_ENV_VALUE: &str = "gitconfig";
1025
1026 fn populate_ad_hoc_config_files(dir: &Path) {
1027 const CONFIG_DATA: &[u8] = b"[foo]\n\tbar = baz\n";
1028
1029 let paths: &[PathBuf] = if cfg!(windows) {
1030 let unc_literal_nul = dir.canonicalize().expect("directory exists").join("nul");
1031 &[dir.join(SCOPE_ENV_VALUE), dir.join("-"), unc_literal_nul]
1032 } else {
1033 &[dir.join(SCOPE_ENV_VALUE), dir.join("-"), dir.join(":")]
1034 };
1035 for path in paths {
1037 std::fs::write(path, CONFIG_DATA).expect("can write contents");
1038 }
1039 for path in paths {
1041 let buf = std::fs::read(path).expect("the file really exists");
1042 assert_eq!(buf, CONFIG_DATA, "{path:?} should be a config file");
1043 }
1044 }
1045
1046 #[test]
1047 fn configure_command_clears_external_config() {
1048 let temp = tempfile::TempDir::new().expect("can create temp dir");
1049 populate_ad_hoc_config_files(temp.path());
1050
1051 let mut cmd = std::process::Command::new(GIT_PROGRAM);
1052 cmd.env("GIT_CONFIG_SYSTEM", SCOPE_ENV_VALUE);
1053 cmd.env("GIT_CONFIG_GLOBAL", SCOPE_ENV_VALUE);
1054 configure_command(&mut cmd, ["config", "-l", "--show-origin"], temp.path());
1055
1056 let output = cmd.output().expect("can run git");
1057 let lines: Vec<_> = output
1058 .stdout
1059 .to_str()
1060 .expect("valid UTF-8")
1061 .lines()
1062 .filter(|line| !line.starts_with("command line:\t"))
1063 .collect();
1064 let status = output.status.code().expect("terminated normally");
1065 assert_eq!(lines, Vec::<&str>::new(), "should be no config variables from files");
1066 assert_eq!(status, 0, "reading the config should succeed");
1067 }
1068
1069 #[test]
1070 #[cfg(windows)]
1071 fn bash_program_ok_for_platform() {
1072 let path = bash_program();
1073 assert!(path.is_absolute());
1074
1075 let for_version = std::process::Command::new(path)
1076 .arg("--version")
1077 .output()
1078 .expect("can pass it `--version`");
1079 assert!(for_version.status.success(), "passing `--version` succeeds");
1080 let version_line = for_version
1081 .stdout
1082 .lines()
1083 .nth(0)
1084 .expect("`--version` output has first line");
1085 assert!(
1086 version_line.ends_with(b"-pc-msys)"), "it is an MSYS bash (such as Git Bash)"
1088 );
1089
1090 let for_uname_os = std::process::Command::new(path)
1091 .args(["-c", "uname -o"])
1092 .output()
1093 .expect("can tell it to run `uname -o`");
1094 assert!(for_uname_os.status.success(), "telling it to run `uname -o` succeeds");
1095 assert_eq!(
1096 for_uname_os.stdout.trim_end(),
1097 b"Msys",
1098 "it runs commands in an MSYS environment"
1099 );
1100 }
1101
1102 #[test]
1103 #[cfg(not(windows))]
1104 fn bash_program_ok_for_platform() {
1105 assert_eq!(bash_program(), Path::new("bash"));
1106 }
1107
1108 #[test]
1109 fn bash_program_unix_path() {
1110 let path = bash_program()
1111 .to_str()
1112 .expect("This test depends on the bash path being valid Unicode");
1113 assert!(
1114 !path.contains('\\'),
1115 "The path to bash should have no backslashes, barring very unusual environments"
1116 );
1117 }
1118
1119 fn is_rooted_relative(path: impl AsRef<Path>) -> bool {
1120 let p = path.as_ref();
1121 p.is_relative() && p.has_root()
1122 }
1123
1124 #[test]
1125 #[cfg(windows)]
1126 fn unix_style_absolute_is_rooted_relative() {
1127 assert!(is_rooted_relative("/bin/bash"), "can detect paths like /bin/bash");
1128 }
1129
1130 #[test]
1131 fn bash_program_absolute_or_unrooted() {
1132 let bash = bash_program();
1133 assert!(!is_rooted_relative(bash), "{bash:?}");
1134 }
1135}