use super::commit::{git_text, keep_maintenance_in_the_foreground, remove_settled, run_commit, wrapped_commit};
use super::support::{Fixture, Observed, REAL_GIT, decline_in_process_lfs_scan, direct, executable, fixture, git, observe, repository, stderr_of};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
pub(super) struct World {
pub(super) repo: PathBuf,
pub(super) origin: PathBuf,
pub(super) record: PathBuf,
}
#[derive(Debug)]
pub(super) struct Run {
arguments: String,
input: Vec<String>,
origin_main: String,
}
fn stand_in_source(world_origin: &Path, record: &Path, status: i32, from_run: usize) -> String {
return format!(
"#!/bin/sh\n\
count=$(grep -c '^run ' '{record}' 2>/dev/null || true)\n\
{{\n\
printf 'run arguments=%s\\n' \"$*\"\n\
sed 's/^/input /'\n\
printf 'origin-main=%s\\n' \"$({REAL_GIT} --git-dir='{origin}' rev-parse --verify --quiet refs/heads/main || echo absent)\"\n\
}} >> '{record}'\n\
if [ \"${{count:-0}}\" -ge {from_run} ]; then exit {status}; fi\n\
exit 0\n",
origin = world_origin.display(),
record = record.display(),
);
}
pub(super) fn world(fixture: &Fixture, lfs: bool, program: Option<i32>) -> World {
return world_failing_from(fixture, lfs, program, 0);
}
fn world_failing_from(fixture: &Fixture, lfs: bool, program: Option<i32>, from_run: usize) -> World {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
let origin: PathBuf = fixture.root.join("origin.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", "origin.git"]);
git(fixture, repo.as_path(), &["remote", "add", "origin", origin.to_str().expect("UTF-8")]);
if lfs {
git(fixture, repo.as_path(), &["config", "lfs.url", "http://127.0.0.1:9/never-contacted"]);
}
keep_maintenance_in_the_foreground(fixture, repo.as_path());
let record: PathBuf = fixture.root.join("lfs-record.txt");
if lfs {
decline_in_process_lfs_scan(fixture, repo.as_path());
}
if let Some(status) = program {
executable(
fixture.root.join("bin/git-lfs").as_path(),
stand_in_source(origin.as_path(), record.as_path(), status, from_run).as_bytes(),
);
}
return World { repo, origin, record };
}
pub(super) fn runs(world: &World) -> Vec<Run> {
let text: String = std::fs::read_to_string(&world.record).unwrap_or_default();
let mut found: Vec<Run> = Vec::new();
for line in text.lines() {
if let Some(arguments) = line.strip_prefix("run arguments=") {
found.push(Run { arguments: arguments.to_owned(), input: Vec::new(), origin_main: String::new() });
} else if let Some(input) = line.strip_prefix("input ") {
found.last_mut().expect("a run precedes its input").input.push(input.to_owned());
} else if let Some(main) = line.strip_prefix("origin-main=") {
found.last_mut().expect("a run precedes its record").origin_main = main.to_owned();
}
}
return found;
}
pub(super) fn ref_of(fixture: &Fixture, repo: &Path, name: &str) -> String {
let output = direct(fixture)
.current_dir(repo)
.args(["rev-parse", "--verify", "--quiet", name])
.output()
.expect("rev-parse starts");
let text: String = String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
return if text.is_empty() { String::from("absent") } else { text };
}
pub(super) fn commit_directly(fixture: &Fixture, repo: &Path, name: &str, text: &str) {
std::fs::write(repo.join(name), text).expect("fixture file");
git(fixture, repo, &["add", "--", name]);
git(fixture, repo, &["commit", "--quiet", &format!("--message={name}"), "--", name]);
}
pub(super) fn wrapped(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Observed {
return observe(&mut wrapped_commit(fixture, directory, arguments), b"");
}
#[test]
fn a_push_runs_the_upload_before_any_ref_moves() {
let fixture: Fixture = fixture("lfs-push");
let world: World = world(&fixture, true, Some(0));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
let head: String = ref_of(&fixture, world.repo.as_path(), "refs/heads/main");
let recorded: Vec<Run> = runs(&world);
assert_eq!(recorded.len(), 1, "one upload for one push (the scope probe must not upload): {recorded:?}");
assert_eq!(recorded[0].arguments, "push origin main", "the upload names the remote and the refs the push names");
assert!(recorded[0].input.is_empty(), "git lfs push reads no update lines: {recorded:?}");
assert_eq!(recorded[0].origin_main, "absent", "the upload runs before the ref moves");
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), head);
remove_settled(&fixture);
}
#[test]
fn a_failing_upload_blocks_the_push_and_names_every_way_forward() {
let fixture: Fixture = fixture("lfs-push-fails");
let world: World = world(&fixture, true, Some(3));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
let stderr: String = stderr_of(&pushed);
assert_ne!(pushed.code, Some(0), "{stderr}");
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent", "{stderr}");
assert!(stderr.contains("`git-lfs push` exited with status 3"), "{stderr}");
assert!(stderr.contains("nothing was published"), "{stderr}");
assert!(stderr.contains("git push --no-verify"), "{stderr}");
assert_eq!(runs(&world).len(), 1);
remove_settled(&fixture);
}
#[test]
fn a_forced_push_runs_the_upload_before_any_ref_moves() {
let fixture: Fixture = fixture("lfs-push-forced");
let world: World = world(&fixture, true, Some(0));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "--force", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
let recorded: Vec<Run> = runs(&world);
assert_eq!(recorded.len(), 1, "{recorded:?}");
assert_eq!(recorded[0].origin_main, "absent");
remove_settled(&fixture);
}
#[test]
fn a_failing_upload_blocks_a_forced_push() {
let fixture: Fixture = fixture("lfs-push-forced-fails");
let world: World = world(&fixture, true, Some(5));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "--force", "origin", "main"]);
assert_ne!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent");
remove_settled(&fixture);
}
#[test]
fn a_dry_run_push_uploads_nothing() {
let fixture: Fixture = fixture("lfs-push-dry-run");
let world: World = world(&fixture, true, Some(0));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "--dry-run", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert!(runs(&world).is_empty(), "{:?}", runs(&world));
remove_settled(&fixture);
}
#[test]
fn no_verify_skips_the_upload() {
let fixture: Fixture = fixture("lfs-push-no-verify");
let world: World = world(&fixture, true, Some(9));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "--no-verify", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert!(runs(&world).is_empty());
assert_ne!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent");
remove_settled(&fixture);
}
#[test]
fn the_caller_can_switch_the_upload_off_for_one_push() {
let fixture: Fixture = fixture("lfs-push-disabled");
let world: World = world(&fixture, true, Some(9));
let pushed: Observed = wrapped(
&fixture,
world.repo.as_path(),
&["-c", "hook.cli-git-lfs-pre-push.enabled=false", "push", "origin", "main"],
);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert!(runs(&world).is_empty(), "{:?}", runs(&world));
assert_ne!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent");
remove_settled(&fixture);
}
#[test]
fn a_global_lfs_filter_alone_runs_no_upload() {
let fixture: Fixture = fixture("lfs-push-global-only");
let world: World = world(&fixture, false, None);
let global: PathBuf = fixture.root.join("global-gitconfig");
std::fs::write(&global, "[filter \"lfs\"]\n\tprocess = git-lfs filter-process\n\trequired = true\n").expect("global file");
let mut command = wrapped_commit(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
command.env("GIT_CONFIG_GLOBAL", &global);
let pushed: Observed = observe(&mut command, b"");
assert_eq!(pushed.code, Some(0), "no git-lfs is installed here, so any upload attempt would stop the push: {}", stderr_of(&pushed));
assert_ne!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent");
remove_settled(&fixture);
}
#[test]
fn a_repository_without_lfs_keys_runs_no_upload() {
let fixture: Fixture = fixture("lfs-push-none");
let world: World = world(&fixture, false, Some(9));
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
assert_eq!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert!(runs(&world).is_empty(), "{:?}", runs(&world));
remove_settled(&fixture);
}
#[test]
fn a_missing_git_lfs_blocks_the_push_and_names_the_remedies() {
let fixture: Fixture = fixture("lfs-push-missing");
let world: World = world(&fixture, true, None);
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
let stderr: String = stderr_of(&pushed);
assert_ne!(pushed.code, Some(0), "{stderr}");
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent", "{stderr}");
assert!(stderr.contains("`git-lfs` was not found in any directory of PATH"), "{stderr}");
assert!(stderr.contains("install Git LFS"), "{stderr}");
assert!(stderr.contains("git lfs uninstall"), "{stderr}");
remove_settled(&fixture);
}
#[test]
fn the_auto_push_after_a_commit_runs_the_upload_before_any_ref_moves() {
let fixture: Fixture = fixture("lfs-auto-push");
let world: World = world(&fixture, true, Some(0));
std::fs::write(world.repo.join("a.txt"), b"one\n").expect("a.txt");
git(&fixture, world.repo.as_path(), &["add", "--", "a.txt"]);
let landed: Observed = run_commit(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=one", "--", "a.txt"]);
let stderr: String = stderr_of(&landed);
assert_eq!(landed.code, Some(0), "{stderr}");
assert!(!stderr.contains("auto-push to origin failed"), "{stderr}");
super::support::settle_service_work(world.repo.as_path());
let head: String = ref_of(&fixture, world.repo.as_path(), "refs/heads/main");
let recorded: Vec<Run> = runs(&world);
assert_eq!(recorded.len(), 1, "{recorded:?}");
assert_eq!(recorded[0].arguments, "push origin refs/heads/main", "the auto-push names its remote and the branch it pushes, by name");
assert_eq!(recorded[0].origin_main, "absent");
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), head);
remove_settled(&fixture);
}
#[test]
fn a_failing_upload_keeps_the_auto_pushed_commit_local() {
let fixture: Fixture = fixture("lfs-auto-push-fails");
let world: World = world(&fixture, true, Some(4));
std::fs::write(world.repo.join("a.txt"), b"one\n").expect("a.txt");
git(&fixture, world.repo.as_path(), &["add", "--", "a.txt"]);
let landed: Observed = run_commit(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=one", "--", "a.txt"]);
let stderr: String = stderr_of(&landed);
assert_eq!(landed.code, Some(0), "the commit itself succeeds: {stderr}");
let report: String = super::support::push_report(world.repo.as_path(), &stderr);
assert!(report.contains(super::support::push_failed_words()), "{report}");
assert!(report.contains("`git-lfs push` exited with status 4"), "{report}");
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent", "{report}");
assert_eq!(git_text(&fixture, world.repo.as_path(), &["rev-list", "--count", "HEAD"]).trim(), "2");
remove_settled(&fixture);
}
#[test]
fn an_auto_push_without_lfs_keys_runs_no_upload() {
let fixture: Fixture = fixture("lfs-auto-push-none");
let world: World = world(&fixture, false, Some(9));
std::fs::write(world.repo.join("a.txt"), b"one\n").expect("a.txt");
git(&fixture, world.repo.as_path(), &["add", "--", "a.txt"]);
let landed: Observed = run_commit(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=one", "--", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
assert!(runs(&world).is_empty());
assert_ne!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), "absent");
remove_settled(&fixture);
}
fn lost_race(fixture: &Fixture, world: &World) -> (PathBuf, String) {
git(fixture, world.repo.as_path(), &["push", "--quiet", "--set-upstream", "origin", "main"]);
let rival: PathBuf = fixture.root.join("rival");
git(
fixture,
fixture.root.as_path(),
&["clone", "--quiet", world.origin.to_str().expect("UTF-8"), "rival"],
);
commit_directly(fixture, rival.as_path(), "rival.txt", "rival\n");
git(fixture, rival.as_path(), &["push", "--quiet", "origin", "HEAD:main"]);
let rival_tip: String = ref_of(fixture, rival.as_path(), "HEAD");
return (rival, rival_tip);
}
#[test]
fn the_merge_push_after_a_lost_race_runs_the_upload_before_the_ref_moves() {
let fixture: Fixture = fixture("lfs-lost-race");
let world: World = world(&fixture, true, Some(0));
let (_rival, rival_tip) = lost_race(&fixture, &world);
commit_directly(&fixture, world.repo.as_path(), "local.txt", "local\n");
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
let stderr: String = stderr_of(&pushed);
assert_eq!(pushed.code, Some(0), "{stderr}");
let tip: String = ref_of(&fixture, world.origin.as_path(), "refs/heads/main");
assert_ne!(tip, rival_tip, "the merge was published: {stderr}");
let recorded: Vec<Run> = runs(&world);
let last: &Run = recorded.last().expect("an upload ran");
assert_eq!(recorded.len(), 2, "the rejected push and the merge push each ran the hook once, the probe never: {recorded:?}");
assert_eq!(last.arguments, format!("push origin {tip}"), "the merge is uploaded for by its object name: {recorded:?}");
assert_eq!(last.origin_main, rival_tip, "the merge was uploaded for before the ref moved");
remove_settled(&fixture);
}
#[test]
fn a_failing_upload_blocks_the_merge_push_after_a_lost_race() {
let fixture: Fixture = fixture("lfs-lost-race-fails");
let world: World = world_failing_from(&fixture, true, Some(6), 1);
let (_rival, rival_tip) = lost_race(&fixture, &world);
commit_directly(&fixture, world.repo.as_path(), "local.txt", "local\n");
let pushed: Observed = wrapped(&fixture, world.repo.as_path(), &["push", "origin", "main"]);
assert_ne!(pushed.code, Some(0), "{}", stderr_of(&pushed));
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), rival_tip);
remove_settled(&fixture);
}
#[test]
fn the_reconcile_push_of_an_auto_push_runs_the_upload_before_the_ref_moves() {
let fixture: Fixture = fixture("lfs-reconcile");
let world: World = world(&fixture, true, Some(0));
commit_directly(&fixture, world.repo.as_path(), "a.txt", "one\n");
let (_rival, rival_tip) = lost_race(&fixture, &world);
std::fs::write(world.repo.join("a.txt"), b"one\ntwo\n").expect("edit");
let landed: Observed = run_commit(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=two", "--", "a.txt"]);
let stderr: String = stderr_of(&landed);
assert_eq!(landed.code, Some(0), "{stderr}");
super::support::settle_service_work(world.repo.as_path());
let tip: String = ref_of(&fixture, world.origin.as_path(), "refs/heads/main");
let report: String = super::support::push_report(world.repo.as_path(), &stderr);
assert_ne!(tip, rival_tip, "the merge was published: {report}");
let recorded: Vec<Run> = runs(&world);
let last: &Run = recorded.last().expect("an upload ran");
assert_eq!(recorded.len(), 2, "the rejected push and the reconcile push each ran the hook once: {recorded:?}");
assert_eq!(last.arguments, format!("push origin {tip}"), "{recorded:?}");
assert_eq!(last.origin_main, rival_tip);
remove_settled(&fixture);
}
#[test]
fn a_failing_upload_blocks_the_reconcile_push_of_an_auto_push() {
let fixture: Fixture = fixture("lfs-reconcile-fails");
let world: World = world_failing_from(&fixture, true, Some(8), 1);
commit_directly(&fixture, world.repo.as_path(), "a.txt", "one\n");
let (_rival, rival_tip) = lost_race(&fixture, &world);
std::fs::write(world.repo.join("a.txt"), b"one\ntwo\n").expect("edit");
let landed: Observed = run_commit(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=two", "--", "a.txt"]);
assert_eq!(landed.code, Some(0), "{}", stderr_of(&landed));
super::support::settle_service_work(world.repo.as_path());
assert_eq!(ref_of(&fixture, world.origin.as_path(), "refs/heads/main"), rival_tip);
remove_settled(&fixture);
}