use super::command_options::{
Arity, DEFAULT_MODE, OptionError, OptionSpec, ParsedOptions, parse_options, row,
};
pub(crate) use super::test_support::{REAL_GIT, remove};
use std::ffi::OsString;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
pub(crate) const ALL: u16 = 1;
pub(crate) const MESSAGE: u16 = 2;
pub(crate) const UNTRACKED: u16 = 3;
pub(crate) const CONTAINS: u16 = 4;
pub(crate) const HARD: u16 = 5;
pub(crate) const NO_VERIFY: u16 = 6;
pub(crate) const AMEND: u16 = 7;
pub(crate) const ALLOW_EMPTY: u16 = 8;
pub(crate) const ALLOW_EMPTY_MESSAGE: u16 = 9;
pub(crate) const SYNTHETIC_TABLE: &[OptionSpec] = &[
row(ALL, Some(b'a'), Some("all"), Arity::None, true),
row(MESSAGE, Some(b'm'), Some("message"), Arity::Required, true),
row(
UNTRACKED,
Some(b'u'),
Some("untracked-files"),
Arity::Optional,
true,
),
row(
CONTAINS,
None,
Some("contains"),
Arity::LastArgDefault,
false,
),
row(HARD, None, Some("hard"), Arity::None, false),
row(NO_VERIFY, Some(b'n'), Some("no-verify"), Arity::None, true),
row(AMEND, None, Some("amend"), Arity::None, true),
row(ALLOW_EMPTY, None, Some("allow-empty"), Arity::None, true),
row(
ALLOW_EMPTY_MESSAGE,
None,
Some("allow-empty-message"),
Arity::None,
true,
),
];
pub(crate) fn os_arguments(values: &[&str]) -> Vec<OsString> {
let mut result: Vec<OsString> = Vec::<OsString>::new();
for value in values {
result.push(OsString::from(value));
}
return result;
}
pub(crate) fn parse_synthetic(values: &[&str]) -> Result<ParsedOptions, OptionError> {
return parse_options(
os_arguments(values).as_slice(),
SYNTHETIC_TABLE,
DEFAULT_MODE,
&[],
);
}
#[cfg(unix)]
pub(crate) fn byte_argument(bytes: &[u8]) -> OsString {
use std::os::unix::ffi::OsStringExt;
return OsString::from_vec(bytes.to_vec());
}
pub(crate) fn fixture(name: &str) -> PathBuf {
crate::test_support::arm_watchdog();
let root: PathBuf =
std::env::temp_dir().join(format!("native-command-{}-{name}", std::process::id()));
if root.exists() {
std::fs::remove_dir_all(&root).expect("remove stale fixture");
}
std::fs::create_dir(&root).expect("fresh fixture");
return std::fs::canonicalize(&root).expect("canonical fixture");
}
fn git_command(directory: &Path) -> Command {
let mut command: Command = Command::new(REAL_GIT);
command
.current_dir(directory)
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/nonexistent-global-config")
.env("GIT_AUTHOR_NAME", "Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.env("GIT_EDITOR", "true");
return command;
}
pub(crate) fn git_output(directory: &Path, arguments: &[OsString]) -> Output {
return git_command(directory)
.args(arguments)
.stdin(Stdio::null())
.output()
.expect("native Git fixture command");
}
pub(crate) fn git(directory: &Path, arguments: &[&str]) -> Output {
let output: Output = git_output(directory, os_arguments(arguments).as_slice());
assert!(
output.status.success(),
"git {arguments:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return output;
}
pub(crate) fn git_status(directory: &Path, arguments: &[&str]) -> Output {
return git_output(directory, os_arguments(arguments).as_slice());
}
pub(crate) fn git_with_input(directory: &Path, arguments: &[OsString], input: &[u8]) -> Output {
let mut child: std::process::Child = git_command(directory)
.args(arguments)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("native Git fixture command");
child
.stdin
.take()
.expect("piped standard input")
.write_all(input)
.expect("write option specification");
return child.wait_with_output().expect("native Git fixture result");
}
pub(crate) fn repository(parent: &Path, name: &str) -> PathBuf {
let root: PathBuf = parent.join(name);
std::fs::create_dir(&root).expect("repository directory");
git(
root.as_path(),
&["init", "--quiet", "--initial-branch=main"],
);
git(
root.as_path(),
&["commit", "--quiet", "--allow-empty", "--message=initial"],
);
return root;
}
pub(crate) fn repository_with_tracked_file(name: &str) -> (PathBuf, PathBuf) {
let directory: PathBuf = fixture(name);
let root: PathBuf = repository(directory.as_path(), "repository");
std::fs::write(root.join("tracked.txt"), b"one\n").expect("write tracked file");
git(root.as_path(), &["add", "--", "tracked.txt"]);
git(
root.as_path(),
&["commit", "--quiet", "--message=base", "--", "tracked.txt"],
);
return (directory, root);
}
pub(crate) fn start_conflicted_merge(root: &Path) {
git(root, &["switch", "--quiet", "--create", "other"]);
std::fs::write(root.join("tracked.txt"), b"other\n").expect("write other side");
git(
root,
&["commit", "--quiet", "--message=other", "--", "tracked.txt"],
);
git(root, &["switch", "--quiet", "main"]);
std::fs::write(root.join("tracked.txt"), b"main\n").expect("write main side");
git(
root,
&["commit", "--quiet", "--message=main", "--", "tracked.txt"],
);
let merge: Output = git_status(root, &["merge", "--quiet", "other"]);
assert!(!merge.status.success(), "the merge must conflict");
}
pub(crate) fn output_text(output: &Output) -> String {
return String::from_utf8_lossy(&output.stdout).trim().to_owned();
}
pub(crate) fn assert_table_invariants(table: &[OptionSpec]) {
let mut shorts: Vec<u8> = Vec::<u8>::new();
let mut longs: Vec<&str> = Vec::<&str>::new();
for spec in table {
assert!(spec.short.is_some() || spec.long.is_some(), "{spec:?}");
if let Some(short) = spec.short {
assert!(short.is_ascii_graphic() && short != b'-', "{spec:?}");
assert!(!shorts.contains(&short), "duplicate short {spec:?}");
shorts.push(short);
}
if let Some(long) = spec.long {
assert!(!long.is_empty() && !long.contains('='), "{spec:?}");
assert!(!long.starts_with('-'), "{spec:?}");
assert!(!longs.contains(&long), "duplicate long {spec:?}");
longs.push(long);
}
}
}