use super::support::{
Fixture, Observed, REAL_GIT, bounded_for, executable, fixture, git, observe, remove, repository, stderr_of,
wrapper_first_path,
};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::Command;
const PUSH_BOUND_SECONDS: &str = "120";
const MARKER_NAME: &str = "CLI_GIT_PUSH_ENVIRONMENT_MARKER";
const MARKER_VALUE: &str = "carried-by-the-invocation";
struct PushWorld {
repo: PathBuf,
bare: PathBuf,
record: PathBuf,
}
#[derive(Debug)]
struct Start {
service: String,
marker: String,
home: String,
path: String,
forward_target: String,
config_count: String,
}
fn shim_source(record: &Path) -> String {
return format!(
"#!/bin/sh\n\
printf 'service=%s|marker=%s|home=%s|path=%s|target=%s|count=%s\\n' \
\"$1\" \"${{{MARKER_NAME}-unset}}\" \"${{HOME-unset}}\" \"${{PATH-unset}}\" \
\"${{CLI_GIT_NATIVE_FORWARD_TARGET-unset}}\" \"${{GIT_CONFIG_COUNT-unset}}\" >> '{record}'\n\
[ \"${{{MARKER_NAME}-}}\" = '{MARKER_VALUE}' ] || {{ echo 'the push environment lost the marker' >&2; exit 1; }}\n\
exec {REAL_GIT} \"${{1#git-}}\" \"$2\"\n",
record = record.display(),
);
}
fn push_world(fixture: &Fixture) -> PushWorld {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
let bare: PathBuf = fixture.root.join("origin.git");
git(fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "origin.git"]);
let record: PathBuf = fixture.root.join("shim-record.txt");
let shim: PathBuf = fixture.root.join("shim.sh");
executable(shim.as_path(), shim_source(record.as_path()).as_bytes());
let url: String = format!("ext::{} %S {}", shim.display(), bare.display());
git(fixture, repo.as_path(), &["remote", "add", "origin", url.as_str()]);
git(fixture, repo.as_path(), &["config", "protocol.ext.allow", "always"]);
git(fixture, repo.as_path(), &["config", "maintenance.autoDetach", "false"]);
std::fs::write(repo.join("a.txt"), b"one\n").expect("a.txt");
git(fixture, repo.as_path(), &["add", "--", "a.txt"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base"]);
std::fs::write(repo.join("a.txt"), b"one\ntwo\n").expect("edit a.txt");
return PushWorld { repo, bare, record };
}
fn wrapped_with_marker(fixture: &Fixture, directory: &Path, arguments: &[&str]) -> Command {
let mut command: Command = bounded_for(
fixture,
fixture.root.join("bin/git").as_path(),
wrapper_first_path(fixture).as_os_str(),
PUSH_BOUND_SECONDS,
);
command.current_dir(directory).args(arguments).env(MARKER_NAME, MARKER_VALUE);
return command;
}
fn starts(world: &PushWorld) -> Vec<Start> {
let text: String = std::fs::read_to_string(world.record.as_path()).unwrap_or_default();
return text
.lines()
.map(|line| {
let field = |name: &str| -> String {
return line
.split('|')
.find_map(|part| { return part.strip_prefix(format!("{name}=").as_str()); })
.unwrap_or("missing")
.to_owned();
};
return Start {
service: field("service"),
marker: field("marker"),
home: field("home"),
path: field("path"),
forward_target: field("target"),
config_count: field("count"),
};
})
.collect();
}
fn assert_inherited(fixture: &Fixture, started: &[Start]) {
let expected_path: String = wrapper_first_path(fixture).to_string_lossy().into_owned();
let expected_home: String = fixture.root.join("home").to_string_lossy().into_owned();
for start in started {
assert_eq!(start.marker, MARKER_VALUE, "the marker reaches {start:?}");
assert_eq!(start.home, expected_home, "HOME reaches {start:?}");
assert!(start.path.contains(expected_path.as_str()), "PATH reaches {start:?}");
assert_ne!(start.forward_target, "unset", "the forward-target marker is added to {start:?}");
assert_ne!(start.config_count, "unset", "the lock PID injection is added to {start:?}");
}
}
#[test]
fn an_auto_push_after_a_landed_commit_runs_git_with_the_invocation_environment() {
let fixture: Fixture = fixture("push-env-auto");
let world: PushWorld = push_world(&fixture);
let landed: Observed = observe(
&mut wrapped_with_marker(&fixture, world.repo.as_path(), &["commit", "--message=second", "a.txt"]),
b"",
);
let stderr: String = stderr_of(&landed);
assert_eq!(landed.code, Some(0), "{stderr}");
assert!(!stderr.contains("auto-push to origin failed"), "{stderr}");
let head: String = git_head(&fixture, world.repo.as_path());
let published: String = git_head_of(&fixture, world.bare.as_path(), "refs/heads/main");
assert_eq!(published, head, "the landed commit is on origin: {stderr}");
let started: Vec<Start> = starts(&world);
assert!(
started.iter().any(|start| { return start.service == "git-receive-pack"; }),
"the push went through the shim: {started:?}"
);
assert_inherited(&fixture, started.as_slice());
remove(&fixture);
}
#[test]
fn a_manual_push_probes_and_pushes_with_the_invocation_environment() {
let fixture: Fixture = fixture("push-env-manual");
let world: PushWorld = push_world(&fixture);
git(&fixture, world.repo.as_path(), &["commit", "--quiet", "--message=second", "a.txt"]);
let pushed: Observed = observe(
&mut wrapped_with_marker(&fixture, world.repo.as_path(), &["push", "origin", "main"]),
b"",
);
let stderr: String = stderr_of(&pushed);
assert_eq!(pushed.code, Some(0), "{stderr}");
assert!(!stderr.contains("content-unavailable"), "{stderr}");
let head: String = git_head(&fixture, world.repo.as_path());
assert_eq!(git_head_of(&fixture, world.bare.as_path(), "refs/heads/main"), head, "{stderr}");
let started: Vec<Start> = starts(&world);
let services: Vec<&str> = started.iter().map(|start| { return start.service.as_str(); }).collect();
assert_eq!(
services,
["git-receive-pack", "git-upload-pack", "git-receive-pack"],
"probe dry run, confirmation query, then the push itself: {started:?}"
);
assert_inherited(&fixture, started.as_slice());
remove(&fixture);
}
fn git_head(fixture: &Fixture, repo: &Path) -> String {
return git_head_of(fixture, repo, "HEAD");
}
fn git_head_of(fixture: &Fixture, repo: &Path, reference: &str) -> String {
let output = super::support::direct(fixture)
.current_dir(repo)
.args(["rev-parse", "--verify", "--quiet", reference])
.output()
.expect("rev-parse starts");
return String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned();
}
#[test]
fn a_stalled_auto_push_is_stopped_and_the_commit_stays_saved() {
let fixture: Fixture = fixture("push-stalled");
let world: PushWorld = push_world(&fixture);
let shim: PathBuf = fixture.root.join("shim.sh");
executable(shim.as_path(), b"#!/bin/sh\nexec sleep 8\n");
let started: std::time::Instant = std::time::Instant::now();
let landed: Observed = observe(
wrapped_with_marker(&fixture, world.repo.as_path(), &["commit", "--message=second", "a.txt"])
.env("CLI_GIT_PUSH_DEADLINE_SECONDS", "1"),
b"",
);
let stderr: String = stderr_of(&landed);
assert!(started.elapsed() < std::time::Duration::from_secs(60), "{:?}", started.elapsed());
assert_eq!(landed.code, Some(0), "{stderr}");
assert!(stderr.contains("git push did not finish within 1 seconds and was stopped"), "{stderr}");
assert!(stderr.contains("auto-push to origin failed"), "{stderr}");
let head: String = git_head(&fixture, world.repo.as_path());
assert_ne!(head, "", "the commit is saved locally: {stderr}");
assert_eq!(git_head_of(&fixture, world.bare.as_path(), "refs/heads/main"), "", "nothing reached origin");
remove(&fixture);
}