use super::*;
use crate::test_support::{fixture, remove};
use std::path::PathBuf;
fn words(items: &[&str]) -> Vec<OsString> {
return items.iter().map(OsString::from).collect();
}
fn config(pairs: &[(&str, &str)]) -> Settings {
return pairs
.iter()
.map(|(key, value)| {
return ((*key).to_owned(), (*value).to_owned());
})
.collect();
}
struct Stub<'a> {
answers: &'a [(&'a str, &'a str)],
failing: &'a [&'a str],
}
fn plan_in(
directory: &Path,
global_prefix: &[&str],
region: &[&str],
environment: &[(OsString, OsString)],
settings: &Settings,
stub: &Stub<'_>,
) -> (Result<Option<UploadPlan>, PlanError>, usize) {
let mut asked: usize = 0;
let mut query = |arguments: &[&str]| -> Result<String, String> {
asked += 1;
let key: String = if arguments[0] == "for-each-ref" {
format!("for-each-ref:{}", arguments[arguments.len() - 1])
} else {
arguments[0].to_owned()
};
if stub.failing.contains(&key.as_str()) {
return Err(format!("{key} failed"));
}
return Ok(stub
.answers
.iter()
.find(|(name, _)| {
return *name == key;
})
.map_or(String::new(), |(_, answer)| {
return (*answer).to_owned();
}));
};
let planned = plan_upload(words(global_prefix).as_slice(), words(region).as_slice(), environment, directory, settings.as_slice(), &mut query);
return (planned, asked);
}
fn plan_of(region: &[&str], settings: &[(&str, &str)], answers: &[(&str, &str)]) -> UploadPlan {
let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(settings), &Stub { answers, failing: &[] });
return planned.expect("the plan is worked out").expect("a plan");
}
fn no_plan(region: &[&str], settings: &[(&str, &str)], answers: &[(&str, &str)]) {
let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(settings), &Stub { answers, failing: &[] });
assert_eq!(planned.expect("the plan is worked out"), None, "{region:?}");
}
#[test]
fn every_run_uses_all_with_the_refs_so_no_tracking_ref_hides_an_object() {
let plan: UploadPlan = plan_of(&["origin", "main", "topic:refs/heads/other"], &[], &[]);
assert_eq!(plan.refs, words(&["main", "topic"]));
let mut expected: Vec<OsString> = words(&["push", "--all"]);
expected.push(plan.remotes[0].clone());
expected.extend(plan.refs.iter().cloned());
assert_eq!(plan.arguments(plan.remotes[0].as_os_str()), expected);
let (planned, asked) = plan_in(Path::new("/work"), &[], &["origin", "main"], &[], &config(&[]), &Stub { answers: &[], failing: &[] });
assert_eq!((planned.expect("planned").is_some(), asked), (true, 0), "named refs need no Git start");
}
#[test]
fn forced_deleting_and_tag_refspecs_read_as_git_reads_them() {
assert_eq!(plan_of(&["origin", "+HEAD:refs/heads/x"], &[], &[]).refs, words(&["HEAD"]));
no_plan(&["origin", ":gone"], &[], &[("symbolic-ref", "refs/heads/work")]);
no_plan(&["--delete", "origin", "gone"], &[], &[]);
assert_eq!(plan_of(&["origin", "tag", "v1"], &[], &[]).refs, words(&["refs/tags/v1"]));
}
#[test]
fn a_source_with_a_colon_is_split_at_the_last_colon() {
let plan: UploadPlan = plan_of(&["origin", ":/unique message:refs/heads/new"], &[], &[]);
assert_eq!(plan.refs, words(&[":/unique message"]));
assert_eq!(plan_of(&["origin", "HEAD^{/a:b}:refs/heads/x"], &[], &[]).refs, words(&["HEAD^{/a:b}"]));
}
#[test]
fn the_matching_refspec_uploads_the_branches_and_is_not_a_deletion() {
let heads: &[(&str, &str)] = &[("for-each-ref:refs/heads", "refs/heads/main\nrefs/heads/topic")];
for spec in [":", "+:"] {
assert_eq!(plan_of(&["origin", spec], &[], heads).refs, words(&["refs/heads/main", "refs/heads/topic"]), "{spec}");
}
let configured = plan_of(&["origin"], &[("remote.origin.push", ":")], heads);
assert_eq!(configured.refs, words(&["refs/heads/main", "refs/heads/topic"]));
}
#[test]
fn a_star_crosses_slashes_as_a_refspec_does() {
let heads: &[(&str, &str)] = &[(
"for-each-ref:refs/heads",
"refs/heads/main\nrefs/heads/feature/nested\nrefs/heads/feature/deep/er\nrefs/heads/other",
)];
let all: UploadPlan = plan_of(&["origin", "refs/heads/*:refs/heads/*"], &[], heads);
assert_eq!(all.refs.len(), 4);
let nested: UploadPlan = plan_of(&["origin", "refs/heads/feature/*:refs/heads/f/*"], &[], &[("for-each-ref:refs/heads/feature", "refs/heads/feature/nested\nrefs/heads/feature/deep/er")]);
assert_eq!(nested.refs, words(&["refs/heads/feature/nested", "refs/heads/feature/deep/er"]));
let suffixed: UploadPlan = plan_of(&["origin", "refs/heads/*-ok:refs/heads/*-ok"], &[], &[("for-each-ref:refs/heads", "refs/heads/a-ok\nrefs/heads/b-no\nrefs/heads/c/d-ok")]);
assert_eq!(suffixed.refs, words(&["refs/heads/a-ok", "refs/heads/c/d-ok"]));
}
#[test]
fn branch_and_tag_options_expand_and_mirror_uploads_everything() {
let heads: &[(&str, &str)] = &[("for-each-ref:refs/heads", "refs/heads/main\nrefs/heads/other"), ("for-each-ref:refs/tags", "refs/tags/v1")];
assert_eq!(plan_of(&["--all", "origin"], &[], heads).refs, words(&["refs/heads/main", "refs/heads/other"]));
let tags: UploadPlan = plan_of(&["--tags", "origin"], &[], heads);
assert_eq!((tags.refs, tags.everything), (words(&["refs/tags/v1"]), false));
no_plan(&["--tags", "origin"], &[], &[]);
let mirror: UploadPlan = plan_of(&["--mirror", "origin"], &[], &[]);
assert!(mirror.everything);
assert_eq!(mirror.arguments(mirror.remotes[0].as_os_str()), words(&["push", "--all", "origin"]));
}
#[test]
fn a_bare_push_resolves_its_remote_and_refs_the_way_git_does() {
let branch: &[(&str, &str)] = &[("symbolic-ref", "refs/heads/work"), ("for-each-ref:refs/heads", "refs/heads/work\nrefs/heads/side")];
let remote_of = |pairs: &[(&str, &str)]| -> OsString {
return plan_of(&[], pairs, branch).remotes[0].clone();
};
let layered: &[(&str, &str)] = &[
("branch.work.remote", "upstream"),
("remote.pushdefault", "shared"),
("branch.work.pushremote", "mine"),
];
assert_eq!(remote_of(layered), OsString::from("mine"));
assert_eq!(remote_of(&layered[..2]), OsString::from("shared"));
assert_eq!(remote_of(&layered[..1]), OsString::from("upstream"));
assert_eq!(remote_of(&[("branch.other.remote", "elsewhere")]), OsString::from("origin"));
assert_eq!(plan_of(&[], &[], branch).refs, words(&["refs/heads/work"]));
assert_eq!(plan_of(&[], &[], &[("symbolic-ref", "refs/tags/work")]).refs, words(&["HEAD"]), "a tag is not a branch");
assert_eq!(plan_of(&[], &[], &[]).refs, words(&["HEAD"]), "a detached HEAD");
let matching = plan_of(&[], &[("push.default", "matching")], branch);
assert_eq!(matching.refs, words(&["refs/heads/work", "refs/heads/side"]));
let configured = plan_of(&["origin"], &[("remote.origin.push", "refs/heads/*:refs/heads/*"), ("remote.other.push", "x:y")], branch);
assert_eq!(configured.refs, words(&["refs/heads/work", "refs/heads/side"]));
let mapped = plan_of(&["origin"], &[("remote.origin.push", "other:refs/heads/mapped")], branch);
assert_eq!(mapped.refs, words(&["other"]), "the source side is uploaded");
for spelling in ["true", "yes", "on", "1", ""] {
let mirrored = plan_of(&["origin"], &[("remote.origin.mirror", spelling)], branch);
assert!(mirrored.everything, "mirror={spelling:?}");
}
assert!(!plan_of(&["origin"], &[("remote.origin.mirror", "false")], branch).everything);
let explicit = plan_of(&["origin", "main"], &[("remote.origin.push", "other:y"), ("remote.origin.mirror", "true")], branch);
assert_eq!(explicit.refs, words(&["main"]), "a refspec typed on the command line wins over the configuration");
}
#[test]
fn a_failed_query_fails_closed() {
let cases: [(&[&str], &str); 5] = [
(&["--all", "origin"], "for-each-ref:refs/heads"),
(&["--tags", "origin"], "for-each-ref:refs/tags"),
(&["origin", "refs/heads/*:refs/heads/*"], "for-each-ref:refs/heads"),
(&["origin", ":"], "for-each-ref:refs/heads"),
(&["origin"], "symbolic-ref"),
];
for (region, failing) in cases {
let (planned, _) = plan_in(Path::new("/work"), &[], region, &[], &config(&[]), &Stub { answers: &[], failing: &[failing] });
let error: PlanError = planned.expect_err(&format!("{region:?} must not plan on a failed query"));
assert!(error.0.contains(failing.split(':').next().expect("a name")), "{error:?}");
}
let (config_failure, _) = plan_in(Path::new("/work"), &[], &["origin"], &[], &config(&[]), &Stub { answers: &[], failing: &[] });
assert!(config_failure.is_ok());
let mut refusing = |_arguments: &[&str]| -> Result<String, String> { return Err(String::from("no start")); };
assert!(read_settings(&mut refusing).is_err());
}
#[test]
fn a_remote_with_several_push_urls_is_uploaded_to_each() {
let two: UploadPlan = plan_of(&["origin", "main"], &[("remote.origin.pushurl", "https://a.invalid/r.git"), ("remote.origin.pushurl", "https://b.invalid/r.git")], &[]);
assert_eq!(two.remotes, words(&["https://a.invalid/r.git", "https://b.invalid/r.git"]));
let one: UploadPlan = plan_of(&["origin", "main"], &[("remote.origin.pushurl", "https://a.invalid/r.git")], &[]);
assert_eq!(one.remotes, words(&["origin"]));
assert_eq!(plan_of(&["origin", "main"], &[], &[]).remotes, words(&["origin"]));
}
#[test]
fn a_path_destination_becomes_an_encoded_file_url() {
let root = fixture("lfs-plan-paths");
let (backup, hashed): (PathBuf, PathBuf) = (root.join("backup.git"), root.join("hash#dir.git"));
std::fs::create_dir(&backup).expect("backup directory");
std::fs::create_dir(&hashed).expect("hash directory");
let remote_of = |typed: &str, settings: &[(&str, &str)]| -> OsString {
let (planned, _) = plan_in(root.as_path(), &[], &[typed, "main"], &[], &config(settings), &Stub { answers: &[], failing: &[] });
return planned.expect("planned").expect("a plan").remotes[0].clone();
};
let prefix: String = format!("file://{}", root.display());
assert_eq!(remote_of("backup.git", &[]), OsString::from(format!("{prefix}/backup.git")));
assert_eq!(remote_of("./backup.git", &[]), OsString::from(format!("{prefix}/backup.git")));
assert_eq!(remote_of("hash#dir.git", &[]), OsString::from(format!("{prefix}/hash%23dir.git")));
assert_eq!(remote_of("backup.git", &[("remote.backup.git.url", "x")]), OsString::from("backup.git"));
assert_eq!(remote_of("origin", &[]), OsString::from("origin"), "no such directory: a remote name");
assert_eq!(remote_of("https://example.invalid/r.git", &[]), OsString::from("https://example.invalid/r.git"));
assert_eq!(remote_of("git@example.invalid:r.git", &[]), OsString::from("git@example.invalid:r.git"));
remove(root.as_path());
}
#[test]
fn the_repository_is_read_as_git_reads_it() {
let positional: UploadPlan = plan_of(&["--repo=second", "origin", "main"], &[], &[]);
assert_eq!((positional.remotes, positional.refs), (words(&["origin"]), words(&["main"])));
for spelling in ["--repo=elsewhere", "--rep=elsewhere"] {
let option_only: UploadPlan = plan_of(&[spelling, "--force"], &[], &[("symbolic-ref", "refs/heads/work")]);
assert_eq!(option_only.remotes, words(&["elsewhere"]), "{spelling}");
}
}
#[test]
fn submodule_pushes_are_marked_and_bypasses_plan_nothing() {
for (value, marked) in [("on-demand", true), ("only", true), ("check", false), ("no", false)] {
let option: String = format!("--recurse-submodules={value}");
assert_eq!(plan_of(&[option.as_str(), "origin", "main"], &[], &[]).submodules, marked, "{value}");
}
for region in [vec!["--no-verify", "origin", "main"], vec!["--no-ver", "origin", "main"], vec!["--dry-run", "origin", "main"]] {
no_plan(region.as_slice(), &[], &[]);
}
let environment = |pairs: &[(&str, &str)]| -> Vec<(OsString, OsString)> {
return pairs
.iter()
.map(|(name, value)| {
return (OsString::from(name), OsString::from(value));
})
.collect();
};
let stub: Stub<'_> = Stub { answers: &[], failing: &[] };
let off: &[&str] = &["-c", "hook.cli-git-lfs-pre-push.enabled=false"];
assert_eq!(plan_in(Path::new("/work"), off, &["origin", "main"], &[], &config(&[]), &stub).0, Ok(None));
let skip = environment(&[("GIT_LFS_SKIP_PUSH", "1")]);
assert_eq!(plan_in(Path::new("/work"), &[], &["origin", "main"], skip.as_slice(), &config(&[]), &stub).0, Ok(None));
let pairs = environment(&[("GIT_CONFIG_COUNT", "1"), ("GIT_CONFIG_KEY_0", "hook.cli-git-lfs-pre-push.enabled"), ("GIT_CONFIG_VALUE_0", "false")]);
assert_eq!(plan_in(Path::new("/work"), &[], &["origin", "main"], pairs.as_slice(), &config(&[]), &stub).0, Ok(None));
assert_eq!(plan_in(Path::new("/work"), &[], &["--no-such-option"], &[], &config(&[]), &stub).0, Ok(None), "Git refuses it");
}
#[test]
fn only_the_keys_the_plan_reads_are_taken_from_a_snapshot() {
for key in [
"branch.work.pushremote",
"branch.a.b.remote",
"remote.pushdefault",
"remote.origin.push",
"remote.origin.pushurl",
"remote.origin.mirror",
"remote.o.r.url",
"push.default",
] {
assert!(crate::lfs_upload_settings::wanted_key(key), "{key}");
}
for key in ["remote.origin.fetch", "branch.work.merge", "push.followtags", "core.bare", "remote.origin.tagopt"] {
assert!(!crate::lfs_upload_settings::wanted_key(key), "{key}");
}
}