Skip to content

Commit ba91274

Browse files
committed
Add it copy-royal a degenerative copy tool
Its main purpose is to help build test-cases from real-world repositories.
1 parent 3a339da commit ba91274

File tree

8 files changed

+222
-6
lines changed

8 files changed

+222
-6
lines changed

Cargo.lock

Lines changed: 15 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ members = [
289289
"gix-revwalk",
290290
"gix-fsck",
291291
"tests/tools",
292+
"tests/it",
292293
"gix-diff/tests",
293294
"gix-pack/tests",
294295
"gix-odb/tests",

tests/it/Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/it/Cargo.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "internal-tools"
3+
description = "internal CLI tooling to help generated test-cases"
4+
version = "0.0.0"
5+
authors = ["Sebastian Thiel <[email protected]>"]
6+
edition = "2021"
7+
license = "MIT OR Apache-2.0"
8+
publish = false
9+
10+
[[bin]]
11+
name = "it"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
clap = { version = "4.5.16", features = ["derive"] }
16+
anyhow = "1.0.86"
17+
18+
gix = { version = "0.64.0", path = "../../gix", default-features = false, features = ["attributes"] }
19+

tests/it/src/args.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
use clap::builder::{OsStringValueParser, TypedValueParser};
2+
use clap::{Arg, Command, Error};
3+
use std::ffi::OsStr;
4+
use std::path::PathBuf;
5+
6+
#[derive(Debug, clap::Parser)]
7+
#[clap(name = "it", about = "internal tools to help create test cases")]
8+
pub struct Args {
9+
#[clap(subcommand)]
10+
pub cmd: Subcommands,
11+
}
12+
13+
#[derive(Debug, clap::Subcommand)]
14+
pub enum Subcommands {
15+
/// Copy a tree so that it diffs the same but can't be traced back uniquely to its source.
16+
///
17+
/// The idea is that we don't want to deal with licensing, it's more about patterns in order to
18+
/// reproduce cases for tests.
19+
#[clap(visible_alias = "cr")]
20+
CopyRoyal {
21+
/// Don't really copy anything.
22+
#[clap(long, short = 'n')]
23+
dry_run: bool,
24+
/// The git root whose tracked files to copy.
25+
worktree_dir: PathBuf,
26+
/// The directory into which to copy the files.
27+
destination_dir: PathBuf,
28+
/// The pathspecs to determine which paths to copy from `worktree_dir`.
29+
///
30+
/// None will copy everything.
31+
#[clap(value_parser = AsPathSpec)]
32+
patterns: Vec<gix::pathspec::Pattern>,
33+
},
34+
}
35+
36+
#[derive(Clone)]
37+
pub struct AsPathSpec;
38+
39+
impl TypedValueParser for AsPathSpec {
40+
type Value = gix::pathspec::Pattern;
41+
42+
fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
43+
let pathspec_defaults =
44+
gix::pathspec::Defaults::from_environment(&mut |n| std::env::var_os(n)).unwrap_or_default();
45+
OsStringValueParser::new()
46+
.try_map(move |arg| {
47+
let arg: &std::path::Path = arg.as_os_str().as_ref();
48+
gix::pathspec::parse(gix::path::into_bstr(arg).as_ref(), pathspec_defaults)
49+
})
50+
.parse_ref(cmd, arg, value)
51+
}
52+
}

tests/it/src/commands/copy_royal.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use anyhow::Context;
2+
use gix::fs::Stack;
3+
use gix::pathspec::Pattern;
4+
use std::path::{Path, PathBuf};
5+
6+
pub fn doit(
7+
dry_run: bool,
8+
worktree_dir: &Path,
9+
destination_dir: PathBuf,
10+
patterns: Vec<Pattern>,
11+
) -> anyhow::Result<()> {
12+
let prefix = if dry_run { "WOULD" } else { "Will" };
13+
let repo = gix::open(worktree_dir)?;
14+
let index = repo.index()?;
15+
let mut specs = repo.pathspec(
16+
true,
17+
// TODO: ideally this could accept patterns already.
18+
patterns.into_iter().map(|p| p.to_bstring()),
19+
true,
20+
&index,
21+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
22+
)?;
23+
let mut create_dir = CreateDir { dry_run };
24+
let mut stack = gix::fs::Stack::new(destination_dir);
25+
for (rela_path, _entry) in specs
26+
.index_entries_with_paths(&index)
27+
.context("Didn't find a single entry to copy")?
28+
{
29+
let rela_path = gix::path::from_bstr(rela_path);
30+
let src = worktree_dir.join(&rela_path);
31+
stack.make_relative_path_current(&rela_path, &mut create_dir)?;
32+
let dst = stack.current();
33+
34+
eprintln!(
35+
"{prefix} copy '{src}' to '{dst}'",
36+
src = src.display(),
37+
dst = dst.display()
38+
);
39+
if !dry_run {
40+
let content = std::fs::read_to_string(&src).with_context(|| {
41+
format!(
42+
"Need UTF-8 decodable content in '{src}' - skip binaries with pathspec",
43+
src = src.display()
44+
)
45+
})?;
46+
std::fs::write(dst, remapped(content))?
47+
}
48+
}
49+
Ok(())
50+
}
51+
52+
fn remapped(i: String) -> String {
53+
i.chars()
54+
.filter_map(|c| {
55+
Some(if c.is_alphabetic() {
56+
if c.is_uppercase() {
57+
match (c as u32) % 10 {
58+
0 => 'A',
59+
1 => 'E',
60+
2 => 'I',
61+
3 => 'O',
62+
4 => 'U',
63+
5 => 'X',
64+
6 => 'R',
65+
7 => 'S',
66+
8 => 'T',
67+
9 => 'Y',
68+
_ => unreachable!(),
69+
}
70+
} else {
71+
match (c as u32) % 10 {
72+
0 => 'a',
73+
1 => 'e',
74+
2 => 'i',
75+
3 => 'o',
76+
4 => 'u',
77+
5 => 'x',
78+
6 => 'r',
79+
7 => 's',
80+
8 => 't',
81+
9 => 'y',
82+
_ => unreachable!(),
83+
}
84+
}
85+
} else if c.is_whitespace() || c.is_ascii_punctuation() || c.is_ascii_digit() {
86+
c
87+
} else {
88+
return None;
89+
})
90+
})
91+
.collect()
92+
}
93+
94+
struct CreateDir {
95+
dry_run: bool,
96+
}
97+
98+
impl gix::fs::stack::Delegate for CreateDir {
99+
fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> {
100+
if !self.dry_run && !stack.current().is_dir() {
101+
std::fs::create_dir(stack.current())?;
102+
}
103+
Ok(())
104+
}
105+
106+
fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> {
107+
Ok(())
108+
}
109+
110+
fn pop_directory(&mut self) {}
111+
}

tests/it/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub(crate) mod copy_royal;

tests/it/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use clap::Parser;
2+
3+
mod commands;
4+
5+
fn main() -> anyhow::Result<()> {
6+
let args: Args = Args::parse();
7+
match args.cmd {
8+
Subcommands::CopyRoyal {
9+
dry_run,
10+
worktree_dir: worktree_root,
11+
destination_dir,
12+
patterns,
13+
} => commands::copy_royal::doit(dry_run, &worktree_root, destination_dir, patterns),
14+
}
15+
}
16+
17+
mod args;
18+
use args::{Args, Subcommands};

0 commit comments

Comments
 (0)