use super::commit::{remove_settled, wrapped_commit};
use super::support::{Fixture, Observed, fixture, git, observe, repository, settle_service_work, start_fixture_service, FixtureService};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
struct World {
fixture: Fixture,
repo: PathBuf,
remote: PathBuf,
other: PathBuf,
service: FixtureService,
}
fn world(name: &str) -> World {
let fixture: Fixture = fixture(name);
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
let remote: PathBuf = fixture.root.join("remote.git");
git(&fixture, fixture.root.as_path(), &["init", "--quiet", "--bare", "--initial-branch=main", remote.to_str().expect("UTF-8")]);
std::fs::write(repo.join("a.txt"), "base\n").expect("a");
git(&fixture, &repo, &["add", "a.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=base"]);
git(&fixture, &repo, &["config", "maintenance.autoDetach", "false"]);
git(&fixture, &repo, &["remote", "add", "origin", remote.to_str().expect("UTF-8")]);
git(&fixture, &repo, &["push", "--quiet", "--set-upstream", "origin", "main"]);
git(&fixture, fixture.root.as_path(), &["clone", "--quiet", remote.to_str().expect("UTF-8"), "other"]);
let other: PathBuf = fixture.root.join("other");
git(&fixture, &repo, &["switch", "--quiet", "--create", "topic"]);
std::fs::write(repo.join("b.txt"), "topic\n").expect("b");
git(&fixture, &repo, &["add", "b.txt"]);
git(&fixture, &repo, &["commit", "--quiet", "--message=topic work"]);
git(&fixture, &repo, &["switch", "--quiet", "main"]);
let service: FixtureService = start_fixture_service(&fixture);
service.serve(&fixture, repo.join(".git").as_path());
return World { fixture, repo, remote, other, service };
}
fn wrapped_in(world: &World, directory: &Path, arguments: &[&str]) -> Observed {
let mut command = wrapped_commit(&world.fixture, directory, arguments);
command.env("XDG_RUNTIME_DIR", &world.service.runtime);
return observe(&mut command, b"");
}
fn run(world: &World, arguments: &[&str]) -> Observed {
return wrapped_in(world, world.repo.as_path(), arguments);
}
fn tip(world: &World, repository: &Path, name: &str) -> String {
return String::from_utf8_lossy(&git(&world.fixture, repository, &["rev-parse", name]).stdout).trim().to_owned();
}
fn push_records(world: &World) -> usize {
let work: PathBuf = world.repo.join(".git/cli-git/work");
return ["pending", "claimed", "recovering", "done", "failed"]
.iter()
.map(|state| {
return std::fs::read_dir(work.join(state)).map_or(0, |listing| {
return listing.flatten().filter(|entry| { return entry.file_name().to_string_lossy().starts_with("push-"); }).count();
});
})
.sum();
}
fn assert_pushed(world: &World, observed: &Observed) {
let text: String = String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
assert_eq!(observed.code, Some(0), "{text}");
settle_service_work(world.repo.as_path());
let local: String = tip(world, world.repo.as_path(), "main");
assert_eq!(tip(world, world.remote.as_path(), "main"), local, "the remote follows the branch: {text}");
}
fn assert_not_pushed(world: &World, observed: &Observed, remote_before: &str) {
let text: String = String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
assert_eq!(observed.code, Some(0), "{text}");
settle_service_work(world.repo.as_path());
assert_eq!(push_records(world), 0, "no push was handed over: {text}");
assert_eq!(tip(world, world.remote.as_path(), "main"), remote_before, "the remote did not move: {text}");
}
#[test]
fn a_merge_commit_is_pushed() {
let world: World = world("trigger-merge");
let observed: Observed = run(&world, &["merge", "--no-ff", "--no-edit", "topic"]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn a_fast_forward_merge_is_pushed() {
let world: World = world("trigger-fast-forward");
let observed: Observed = run(&world, &["merge", "--ff-only", "topic"]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn a_cherry_pick_is_pushed() {
let world: World = world("trigger-cherry-pick");
let observed: Observed = run(&world, &["cherry-pick", "topic"]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn a_revert_is_pushed() {
let world: World = world("trigger-revert");
git(&world.fixture, &world.repo, &["merge", "--quiet", "--ff-only", "topic"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
let observed: Observed = run(&world, &["revert", "--no-edit", "HEAD"]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn an_applied_patch_is_pushed() {
let world: World = world("trigger-am");
let patch = git(&world.fixture, &world.repo, &["format-patch", "--stdout", "main..topic"]).stdout;
let file: PathBuf = world.fixture.root.join("topic.patch");
std::fs::write(&file, patch).expect("patch");
let observed: Observed = run(&world, &["am", file.to_str().expect("UTF-8")]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn a_pull_that_merges_is_pushed() {
let world: World = world("trigger-pull-merge");
std::fs::write(world.other.join("c.txt"), "remote\n").expect("c");
git(&world.fixture, &world.other, &["add", "c.txt"]);
git(&world.fixture, &world.other, &["commit", "--quiet", "--message=remote work"]);
git(&world.fixture, &world.other, &["push", "--quiet", "origin", "main"]);
std::fs::write(world.repo.join("d.txt"), "local\n").expect("d");
git(&world.fixture, &world.repo, &["add", "d.txt"]);
git(&world.fixture, &world.repo, &["commit", "--quiet", "--message=local work"]);
let observed: Observed = run(&world, &["pull", "--no-rebase", "--no-edit"]);
assert_pushed(&world, &observed);
remove_settled(&world.fixture);
}
#[test]
fn a_pull_that_rebases_is_not_pushed() {
let world: World = world("trigger-pull-rebase");
std::fs::write(world.other.join("c.txt"), "remote\n").expect("c");
git(&world.fixture, &world.other, &["add", "c.txt"]);
git(&world.fixture, &world.other, &["commit", "--quiet", "--message=remote work"]);
git(&world.fixture, &world.other, &["push", "--quiet", "origin", "main"]);
std::fs::write(world.repo.join("d.txt"), "local\n").expect("d");
git(&world.fixture, &world.repo, &["add", "d.txt"]);
git(&world.fixture, &world.repo, &["commit", "--quiet", "--message=local work"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["pull", "--rebase"]);
assert_not_pushed(&world, &observed, remote_before.as_str());
remove_settled(&world.fixture);
}
#[test]
fn a_rebase_is_not_pushed() {
let world: World = world("trigger-rebase");
std::fs::write(world.repo.join("d.txt"), "local\n").expect("d");
git(&world.fixture, &world.repo, &["add", "d.txt"]);
git(&world.fixture, &world.repo, &["commit", "--quiet", "--message=local work"]);
git(&world.fixture, &world.repo, &["switch", "--quiet", "topic"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["rebase", "main"]);
assert_not_pushed(&world, &observed, remote_before.as_str());
remove_settled(&world.fixture);
}
#[test]
fn a_conflicting_merge_is_not_pushed() {
let world: World = world("trigger-conflict");
git(&world.fixture, &world.repo, &["switch", "--quiet", "topic"]);
std::fs::write(world.repo.join("a.txt"), "topic side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=topic edit"]);
git(&world.fixture, &world.repo, &["switch", "--quiet", "main"]);
std::fs::write(world.repo.join("a.txt"), "main side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=main edit"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["merge", "--no-edit", "topic"]);
assert_eq!(observed.code, Some(1));
settle_service_work(world.repo.as_path());
assert_eq!(push_records(&world), 0);
assert_eq!(tip(&world, world.remote.as_path(), "main"), remote_before);
remove_settled(&world.fixture);
}
#[test]
fn a_merge_that_moves_nothing_is_not_pushed() {
let world: World = world("trigger-up-to-date");
git(&world.fixture, &world.repo, &["merge", "--quiet", "--ff-only", "topic"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["merge", "topic"]);
assert_not_pushed(&world, &observed, remote_before.as_str());
remove_settled(&world.fixture);
}
#[test]
fn a_merge_into_a_diverged_branch_is_not_pushed() {
let world: World = world("trigger-diverged");
std::fs::write(world.other.join("c.txt"), "remote\n").expect("c");
git(&world.fixture, &world.other, &["add", "c.txt"]);
git(&world.fixture, &world.other, &["commit", "--quiet", "--message=remote work"]);
git(&world.fixture, &world.other, &["push", "--quiet", "origin", "main"]);
git(&world.fixture, &world.repo, &["fetch", "--quiet", "origin"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["merge", "--no-ff", "--no-edit", "topic"]);
assert_not_pushed(&world, &observed, remote_before.as_str());
remove_settled(&world.fixture);
}
#[test]
fn a_merge_into_a_branch_without_an_upstream_is_not_pushed() {
let world: World = world("trigger-no-upstream");
git(&world.fixture, &world.repo, &["switch", "--quiet", "--create", "loose"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let observed: Observed = run(&world, &["merge", "--no-ff", "--no-edit", "topic"]);
assert_not_pushed(&world, &observed, remote_before.as_str());
remove_settled(&world.fixture);
}
#[test]
fn a_merge_the_wrapper_completes_with_staged_changes_is_pushed() {
let world: World = world("trigger-isolated-merge");
std::fs::write(world.repo.join("main-side.txt"), "main\n").expect("file");
git(&world.fixture, &world.repo, &["add", "main-side.txt"]);
git(&world.fixture, &world.repo, &["commit", "--quiet", "--message=main side"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
std::fs::write(world.repo.join("staged.txt"), "staged\n").expect("file");
git(&world.fixture, &world.repo, &["add", "staged.txt"]);
let observed: Observed = run(&world, &["merge", "--no-ff", "--no-edit", "topic"]);
assert_pushed(&world, &observed);
let status: String = String::from_utf8_lossy(&git(&world.fixture, &world.repo, &["status", "--porcelain"]).stdout).into_owned();
assert_eq!(status.trim(), "A staged.txt", "the staged file is still staged");
remove_settled(&world.fixture);
}
#[test]
fn a_cherry_pick_continued_after_a_conflict_is_pushed() {
let world: World = world("trigger-continue");
git(&world.fixture, &world.repo, &["switch", "--quiet", "topic"]);
std::fs::write(world.repo.join("a.txt"), "topic side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=topic edit"]);
git(&world.fixture, &world.repo, &["switch", "--quiet", "main"]);
std::fs::write(world.repo.join("a.txt"), "main side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=main edit"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
let remote_before: String = tip(&world, world.remote.as_path(), "main");
let stopped: Observed = run(&world, &["cherry-pick", "topic"]);
assert_eq!(stopped.code, Some(1));
settle_service_work(world.repo.as_path());
assert_eq!(tip(&world, world.remote.as_path(), "main"), remote_before, "a stopped pick pushes nothing");
std::fs::write(world.repo.join("a.txt"), "resolved\n").expect("a");
git(&world.fixture, &world.repo, &["add", "a.txt"]);
let mut command = wrapped_commit(&world.fixture, world.repo.as_path(), &["cherry-pick", "--continue"]);
command.env("XDG_RUNTIME_DIR", &world.service.runtime).env("GIT_EDITOR", "true");
let continued: Observed = observe(&mut command, b"");
assert_pushed(&world, &continued);
remove_settled(&world.fixture);
}
fn user_configuration(world: &World) -> PathBuf {
let file: PathBuf = world.fixture.root.join("user.gitconfig");
std::fs::write(&file, "[diff]\n\tdirstat = changes\n").expect("user configuration");
return file;
}
#[test]
fn a_merge_is_pushed_for_a_user_whose_configuration_the_reader_cannot_prove() {
let world: World = world("trigger-user-configuration");
git(&world.fixture, &world.repo, &["config", "extensions.worktreeConfig", "true"]);
git(&world.fixture, &world.repo, &["config", "push.default", "matching"]);
git(&world.fixture, &world.repo, &["worktree", "add", "--quiet", "--detach", world.fixture.root.join("linked").to_str().expect("UTF-8")]);
let file: PathBuf = user_configuration(&world);
let mut command = wrapped_commit(&world.fixture, world.repo.as_path(), &["merge", "--no-ff", "--no-edit", "topic"]);
command.env("XDG_RUNTIME_DIR", &world.service.runtime).env("GIT_CONFIG_GLOBAL", &file);
let observed: Observed = observe(&mut command, b"");
assert_pushed(&world, &observed);
let record: String = std::fs::read_to_string(world.repo.join(".git/cli-git-push-trigger.jsonl")).expect("the trigger's record");
assert!(record.contains("\"outcome\":\"handed-over\"") && record.contains("\"command\":\"merge\""), "{record}");
remove_settled(&world.fixture);
}
#[test]
fn a_declined_trigger_leaves_its_reason_in_the_record() {
let world: World = world("trigger-record");
git(&world.fixture, &world.repo, &["merge", "--quiet", "--ff-only", "topic"]);
git(&world.fixture, &world.repo, &["push", "--quiet", "origin", "main"]);
let observed: Observed = run(&world, &["merge", "topic"]);
assert_eq!(observed.code, Some(0));
let record: String = std::fs::read_to_string(world.repo.join(".git/cli-git-push-trigger.jsonl")).expect("the trigger's record");
assert!(record.contains("\"outcome\":\"declined\"") && record.contains("\"reason\":\"tip-unchanged\""), "{record}");
assert!(record.contains("\"branch\":\"refs/heads/main\""), "{record}");
let text: String = String::from_utf8_lossy(observed.stderr.as_slice()).into_owned();
assert!(!text.contains("tip-unchanged"), "the terminal is not told: {text}");
remove_settled(&world.fixture);
}
#[test]
fn a_failed_command_is_recorded_as_such() {
let world: World = world("trigger-failed-record");
git(&world.fixture, &world.repo, &["switch", "--quiet", "topic"]);
std::fs::write(world.repo.join("a.txt"), "topic side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=topic edit"]);
git(&world.fixture, &world.repo, &["switch", "--quiet", "main"]);
std::fs::write(world.repo.join("a.txt"), "main side\n").expect("a");
git(&world.fixture, &world.repo, &["commit", "--quiet", "--all", "--message=main edit"]);
let observed: Observed = run(&world, &["merge", "--no-edit", "topic"]);
assert_eq!(observed.code, Some(1));
let record: String = std::fs::read_to_string(world.repo.join(".git/cli-git-push-trigger.jsonl")).expect("the trigger's record");
assert!(record.contains("\"reason\":\"command-failed\"") && record.contains("status 1"), "{record}");
remove_settled(&world.fixture);
}
#[test]
fn a_watch_that_could_not_start_is_recorded() {
let world: World = world("trigger-detached-record");
git(&world.fixture, &world.repo, &["switch", "--quiet", "--detach"]);
let observed: Observed = run(&world, &["merge", "--no-ff", "--no-edit", "topic"]);
assert_eq!(observed.code, Some(0), "{}", String::from_utf8_lossy(observed.stderr.as_slice()));
let record: String = std::fs::read_to_string(world.repo.join(".git/cli-git-push-trigger.jsonl")).expect("the trigger's record");
assert!(record.contains("\"reason\":\"head-detached\"") && record.contains("\"command\":\"merge\""), "{record}");
remove_settled(&world.fixture);
}