use super::*;
use crate::test_support::{fixture, git, git_context, remove, repository};
fn pair(name: &str, value: &str) -> (OsString, OsString) {
return (OsString::from(name), OsString::from(value));
}
fn sample_plan(config_parameters: ConfigParameters, skip_hook_lock: bool) -> HookDispatchPlan {
return HookDispatchPlan {
git_path: String::from("/usr/bin/git"),
hooks_path: String::from("/repo/.git/hooks"),
disabled_events: vec![String::from("commit-msg")],
config_parameters,
worktree_root: String::from("/repo"),
lease: String::from("{\"directory\":\"/d\",\"token\":\"t\"}"),
hook_lock_path: String::from("/repo/.git/cli-git/hook.lock"),
skip_hook_lock,
};
}
#[test]
fn quoting_closes_the_quote_around_quotes_and_bangs() {
assert_eq!(quote_config_parameter("user.name=O'Brien"), "'user.name=O'\\''Brien'");
assert_eq!(quote_config_parameter("a.b=x!y"), "'a.b=x'\\!'y'");
assert_eq!(quote_config_parameter("a.b=1"), "'a.b=1'");
}
#[test]
fn global_overrides_pair_each_dash_c_with_its_next_token() {
let arguments: Vec<OsString> = vec![
OsString::from("-c"),
OsString::from("a.b=1"),
OsString::from("--no-pager"),
];
assert_eq!(global_config_overrides(arguments.as_slice()), Ok(vec![String::from("a.b=1")]));
let repeated: Vec<OsString> = vec![
OsString::from("-c"),
OsString::from("-c"),
OsString::from("x"),
];
assert_eq!(
global_config_overrides(repeated.as_slice()),
Ok(vec![String::from("-c"), String::from("x")])
);
let dangling: Vec<OsString> = vec![OsString::from("-c")];
assert_eq!(global_config_overrides(dangling.as_slice()), Ok(Vec::new()));
}
#[cfg(unix)]
#[test]
fn global_overrides_refuse_non_utf8_values() {
use std::os::unix::ffi::OsStringExt;
let arguments: Vec<OsString> = vec![
OsString::from("-c"),
OsString::from_vec(vec![0xff, 0xfe]),
];
assert_eq!(
global_config_overrides(arguments.as_slice()),
Err(HookPlanError::NotUtf8("global -c option"))
);
}
#[test]
fn combined_parameters_keep_git_order_and_absence() {
assert_eq!(
combine_config_parameters("", &[]),
Ok(ConfigParameters::Absent)
);
assert_eq!(
combine_config_parameters("", &[String::from("a.b=1")]),
Ok(ConfigParameters::Present(String::from("'a.b=1'")))
);
assert_eq!(
combine_config_parameters("'x=1'", &[String::from("a.b=1")]),
Ok(ConfigParameters::Present(String::from("'x=1' 'a.b=1'")))
);
assert_eq!(
combine_config_parameters("'x=1'", &[]),
Ok(ConfigParameters::Present(String::from("'x=1'")))
);
}
#[test]
fn inherited_parameters_come_from_the_injected_environment() {
let environment: Vec<(OsString, OsString)> = vec![
pair("GIT_CONFIG_PARAMETERS", "'old=1'"),
pair("GIT_CONFIG_PARAMETERS", "'x=1'"),
];
assert_eq!(
inherited_config_parameters(environment.as_slice()),
Ok(String::from("'x=1'"))
);
assert_eq!(inherited_config_parameters(&[]), Ok(String::new()));
}
#[test]
fn plans_spell_the_incumbent_fields_in_order() {
let absent: String = encode_hook_dispatch_plan(&sample_plan(ConfigParameters::Absent, false));
assert_eq!(
absent,
"{\"schemaVersion\":1,\"gitPath\":\"/usr/bin/git\",\"hooksPath\":\"/repo/.git/hooks\",\
\"disabledEvents\":[\"commit-msg\"],\"configParameters\":{\"kind\":\"absent\"},\
\"worktreeRoot\":\"/repo\",\"lease\":\"{\\\"directory\\\":\\\"/d\\\",\\\"token\\\":\\\"t\\\"}\",\
\"hookLockPath\":\"/repo/.git/cli-git/hook.lock\",\"skipHookLock\":false}"
);
let present: String = encode_hook_dispatch_plan(&sample_plan(
ConfigParameters::Present(String::from("'a.b=\"1\"'")),
true,
));
assert!(present.contains(
"\"configParameters\":{\"kind\":\"present\",\"value\":\"'a.b=\\\"1\\\"'\"}"
));
assert!(present.ends_with("\"skipHookLock\":true}"));
}
#[test]
fn plans_round_trip_and_refuse_other_shapes() {
let plan: HookDispatchPlan = sample_plan(ConfigParameters::Present(String::from("'x'")), false);
assert_eq!(parse_hook_dispatch_plan(encode_hook_dispatch_plan(&plan).as_str()), Some(plan));
let other_version: String = encode_hook_dispatch_plan(&sample_plan(ConfigParameters::Absent, false))
.replace("\"schemaVersion\":1", "\"schemaVersion\":2");
assert_eq!(parse_hook_dispatch_plan(other_version.as_str()), None);
let unknown_event: String = encode_hook_dispatch_plan(&sample_plan(ConfigParameters::Absent, false))
.replace("\"commit-msg\"", "\"pre-push\"");
assert_eq!(parse_hook_dispatch_plan(unknown_event.as_str()), None);
assert_eq!(parse_hook_dispatch_plan("{}"), None);
assert_eq!(parse_hook_dispatch_plan("not json"), None);
}
#[test]
fn lock_and_hooks_paths_normalize_lexically() {
assert_eq!(
hook_lock_directory(Path::new("/repo/.git")),
PathBuf::from("/repo/.git/cli-git/hook.lock")
);
assert_eq!(
default_hooks_directory(Path::new("/repo/.git/")),
PathBuf::from("/repo/.git/hooks")
);
assert_eq!(
resolve_hooks_path(Path::new("/repo/./wt//"), Path::new("../shared/./hooks")),
PathBuf::from("/repo/shared/hooks")
);
assert_eq!(
resolve_hooks_path(Path::new("/repo"), Path::new("/abs/../x/")),
PathBuf::from("/x")
);
assert_eq!(
resolve_hooks_path(Path::new("/"), Path::new("../../up")),
PathBuf::from("/up")
);
}
#[test]
fn plans_read_real_git_configuration() {
let root: PathBuf = fixture("hook-plan-reads");
let repository_root: PathBuf = repository(root.as_path(), "repo");
git(
repository_root.as_path(),
&["config", "hook.pre-commit.enabled", "false"],
);
git(
repository_root.as_path(),
&["config", "hooks.concurrentCommits", "true"],
);
let common: PathBuf = repository_root.join(".git");
let plan: HookDispatchPlan = compute_hook_dispatch_plan(&HookPlanInputs {
context: &git_context(),
cwd: repository_root.as_path(),
global_arguments: &[OsString::from("-c"), OsString::from("x.y=2")],
common_directory: common.as_path(),
worktree_root: repository_root.as_path(),
lease: "{\"directory\":\"/d\",\"token\":\"t\"}",
concurrent_commits: true,
inherited_lease_valid: false,
environment: &[],
})
.expect("plan");
assert_eq!(plan.disabled_events, vec![String::from("pre-commit")]);
assert_eq!(plan.hooks_path, common.join("hooks").to_str().expect("UTF-8"));
assert_eq!(plan.config_parameters, ConfigParameters::Present(String::from("'x.y=2'")));
assert!(plan.skip_hook_lock);
assert_eq!(plan.hook_lock_path, common.join("cli-git").join("hook.lock").to_str().expect("UTF-8"));
remove(root.as_path());
}
#[test]
fn plans_resolve_relative_hooks_paths_and_inherited_leases() {
let root: PathBuf = fixture("hook-plan-relative");
let repository_root: PathBuf = repository(root.as_path(), "repo");
git(
repository_root.as_path(),
&["config", "core.hooksPath", "../shared-hooks"],
);
let plan: HookDispatchPlan = compute_hook_dispatch_plan(&HookPlanInputs {
context: &git_context(),
cwd: repository_root.as_path(),
global_arguments: &[],
common_directory: repository_root.join(".git").as_path(),
worktree_root: repository_root.as_path(),
lease: "lease",
concurrent_commits: false,
inherited_lease_valid: true,
environment: &[],
})
.expect("plan");
let expected: PathBuf = resolve_hooks_path(repository_root.as_path(), Path::new("../shared-hooks"));
assert_eq!(plan.hooks_path, expected.to_str().expect("UTF-8"));
assert!(plan.skip_hook_lock);
assert_eq!(plan.config_parameters, ConfigParameters::Absent);
remove(root.as_path());
}
#[test]
fn a_failing_config_read_stops_planning() {
let root: PathBuf = fixture("hook-plan-failure");
let repository_root: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repository_root.join(".git").join("config"), b"[core\n").expect("broken config");
let result = compute_hook_dispatch_plan(&HookPlanInputs {
context: &git_context(),
cwd: repository_root.as_path(),
global_arguments: &[],
common_directory: repository_root.join(".git").as_path(),
worktree_root: repository_root.as_path(),
lease: "lease",
concurrent_commits: false,
inherited_lease_valid: false,
environment: &[],
});
assert!(matches!(result, Err(HookPlanError::ConfigRead(_))));
remove(root.as_path());
}
#[test]
fn new_leases_name_the_directory_with_fresh_tokens() {
let directory: PathBuf = PathBuf::from("/repo/.git/cli-git-transactions/x");
let first: String = new_preparation_lease(directory.as_path()).expect("lease");
let second: String = new_preparation_lease(directory.as_path()).expect("lease");
assert!(first.starts_with("{\"directory\":\"/repo/.git/cli-git-transactions/x\",\"token\":\""));
assert_ne!(first, second);
}