diff --git a/Cargo.lock b/Cargo.lock index d2fa693c9e4..05dbb9a6dab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,9 +487,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.11" +version = "4.5.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35723e6a11662c2afb578bcf0b88bf6ea8e21282a953428f240574fcc3a2b5b3" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" dependencies = [ "clap_builder", "clap_derive", @@ -497,9 +497,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.11" +version = "4.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49eb96cbfa7cfa35017b7cd548c75b14c3118c98b423041d70562665e07fb0fa" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" dependencies = [ "anstream", "anstyle", @@ -518,9 +518,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.11" +version = "4.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d029b67f89d30bbb547c89fd5161293c0aec155fc691d7924b64550662db93e" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" dependencies = [ "heck", "proc-macro2", @@ -3089,6 +3089,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "internal-tools" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "gix", +] + [[package]] name = "io-close" version = "0.3.7" diff --git a/Cargo.toml b/Cargo.toml index 56dd00e2f0a..deb1210ee72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -289,6 +289,7 @@ members = [ "gix-revwalk", "gix-fsck", "tests/tools", + "tests/it", "gix-diff/tests", "gix-pack/tests", "gix-odb/tests", diff --git a/tests/it/Cargo.lock b/tests/it/Cargo.lock new file mode 100644 index 00000000000..65565a2bfb6 --- /dev/null +++ b/tests/it/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "jtt" +version = "0.1.0" diff --git a/tests/it/Cargo.toml b/tests/it/Cargo.toml new file mode 100644 index 00000000000..6c3cc4cccfe --- /dev/null +++ b/tests/it/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "internal-tools" +description = "internal CLI tooling to help generated test-cases" +version = "0.0.0" +authors = ["Sebastian Thiel "] +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false + +[[bin]] +name = "it" +path = "src/main.rs" + +[dependencies] +clap = { version = "4.5.16", features = ["derive"] } +anyhow = "1.0.86" + +gix = { version = "0.64.0", path = "../../gix", default-features = false, features = ["attributes"] } + diff --git a/tests/it/src/main.rs b/tests/it/src/main.rs new file mode 100644 index 00000000000..2edb3dce269 --- /dev/null +++ b/tests/it/src/main.rs @@ -0,0 +1,185 @@ +use clap::Parser; + +mod commands { + pub(crate) mod royal_copy { + use anyhow::Context; + use gix::fs::Stack; + use gix::pathspec::Pattern; + use std::path::{Path, PathBuf}; + + pub fn doit( + dry_run: bool, + worktree_dir: &Path, + destination_dir: PathBuf, + patterns: Vec, + ) -> anyhow::Result<()> { + let prefix = if dry_run { "WOULD" } else { "Will" }; + let repo = gix::open(&worktree_dir)?; + let index = repo.index()?; + let mut specs = repo.pathspec( + true, + // TODO: ideally this could accept patterns already. + patterns.into_iter().map(|p| p.to_bstring()), + true, + &index, + gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + )?; + let mut create_dir = CreateDir { dry_run }; + let mut stack = gix::fs::Stack::new(destination_dir); + for (rela_path, _entry) in specs + .index_entries_with_paths(&index) + .context("Didn't find a single entry to copy")? + { + let rela_path = gix::path::from_bstr(rela_path); + let src = worktree_dir.join(&rela_path); + stack.make_relative_path_current(&rela_path, &mut create_dir)?; + let dst = stack.current(); + + eprintln!( + "{prefix} copy '{src}' to '{dst}'", + src = src.display(), + dst = dst.display() + ); + if !dry_run { + let content = std::fs::read_to_string(&src).with_context(|| { + format!( + "Need UTF-8 decodable content in '{src}' - skip binaries with pathspec", + src = src.display() + ) + })?; + std::fs::write(&dst, remapped(content))? + } + } + Ok(()) + } + + fn remapped(i: String) -> String { + i.chars() + .filter_map(|c| { + Some(if c.is_alphabetic() { + if c.is_uppercase() { + match (c as u32) % 10 { + 0 => 'A', + 1 => 'E', + 2 => 'I', + 3 => 'O', + 4 => 'U', + 5 => 'X', + 6 => 'R', + 7 => 'S', + 8 => 'T', + 9 => 'Y', + _ => unreachable!(), + } + } else { + match (c as u32) % 10 { + 0 => 'a', + 1 => 'e', + 2 => 'i', + 3 => 'o', + 4 => 'u', + 5 => 'x', + 6 => 'r', + 7 => 's', + 8 => 't', + 9 => 'y', + _ => unreachable!(), + } + } + } else if c.is_whitespace() || c.is_ascii_punctuation() || c.is_digit(10) { + c + } else { + return None; + }) + }) + .collect() + } + + struct CreateDir { + dry_run: bool, + } + + impl gix::fs::stack::Delegate for CreateDir { + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { + if !self.dry_run && !stack.current().is_dir() { + std::fs::create_dir(stack.current())?; + } + Ok(()) + } + + fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { + Ok(()) + } + + fn pop_directory(&mut self) {} + } + } +} + +fn main() -> anyhow::Result<()> { + let args: Args = Args::parse(); + match args.cmd { + Subcommands::RoyalCopy { + dry_run, + worktree_dir: worktree_root, + destination_dir, + patterns, + } => commands::royal_copy::doit(dry_run, &worktree_root, destination_dir, patterns), + } +} + +mod args { + use clap::builder::{OsStringValueParser, TypedValueParser}; + use clap::{Arg, Command, Error}; + use std::ffi::OsStr; + use std::path::PathBuf; + + #[derive(Debug, clap::Parser)] + #[clap(name = "it", about = "internal tools to help create test cases")] + pub struct Args { + #[clap(subcommand)] + pub cmd: Subcommands, + } + + #[derive(Debug, clap::Subcommand)] + pub enum Subcommands { + /// Copy a tree so that it diffs the same but can't be traced back uniquely to its source. + /// + /// The idea is that we don't want to deal with licensing, it's more about patterns in order to + /// reproduce cases for tests. + #[clap(visible_alias = "rc")] + RoyalCopy { + /// Don't really copy anything. + #[clap(long, short = 'n')] + dry_run: bool, + /// The git root whose tracked files to copy. + worktree_dir: PathBuf, + /// The directory into which to copy the files. + destination_dir: PathBuf, + /// The pathspecs to determine which paths to copy from `worktree_dir`. + /// + /// None will copy everything. + #[clap(value_parser = AsPathSpec)] + patterns: Vec, + }, + } + + #[derive(Clone)] + pub struct AsPathSpec; + + impl TypedValueParser for AsPathSpec { + type Value = gix::pathspec::Pattern; + + fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { + let pathspec_defaults = + gix::pathspec::Defaults::from_environment(&mut |n| std::env::var_os(n)).unwrap_or_default(); + OsStringValueParser::new() + .try_map(move |arg| { + let arg: &std::path::Path = arg.as_os_str().as_ref(); + gix::pathspec::parse(gix::path::into_bstr(arg).as_ref(), pathspec_defaults) + }) + .parse_ref(cmd, arg, value) + } + } +} +use args::{Args, Subcommands};