use super::*;
use crate::repo_read_layout::read_layout_in;
use crate::test_support::{fixture, git, remove};
fn repository(name: &str) -> (PathBuf, PathBuf) {
let root = fixture(name);
let remote = root.join("remote.git");
let repo = root.join("repo");
git(&root, &["init", "--bare", "--quiet", "--initial-branch=main", "remote.git"]);
git(&root, &["init", "--quiet", "--initial-branch=main", "repo"]);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=base"]);
git(&repo, &["remote", "add", "origin", remote.to_str().expect("utf-8")]);
git(&repo, &["push", "--quiet", "--set-upstream", "origin", "main"]);
return (root, repo);
}
fn oid(repo: &Path, revision: &str) -> String {
return String::from_utf8_lossy(&git(repo, &["rev-parse", revision]).stdout).trim().to_owned();
}
fn git_says_ancestor(repo: &Path, ancestor: &str, descendant: &str) -> bool {
return crate::test_support::git_output(repo, &["merge-base", "--is-ancestor", ancestor, descendant]).status.success();
}
fn watch(repo: &Path) -> TipWatch {
let overlay: Vec<(OsString, OsString)> = crate::test_support::isolated_overlay();
return watch_tip(repo, Path::new("/usr/bin/git"), overlay.as_slice(), &[]).expect("a branch with a tip");
}
#[test]
fn the_walk_agrees_with_git_about_ancestry() {
let (root, repo) = repository("atr-walk");
let base = oid(&repo, "HEAD");
git(&repo, &["switch", "--quiet", "--create", "side"]);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=side"]);
let side = oid(&repo, "HEAD");
git(&repo, &["switch", "--quiet", "main"]);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=main"]);
git(&repo, &["merge", "--quiet", "--no-ff", "--no-edit", "side"]);
let merged = oid(&repo, "HEAD");
let layout = read_layout_in(&repo, crate::test_support::isolated_overlay().as_slice()).expect("layout");
let database = ObjectDatabase::open(layout.common_dir.as_path(), None, layout.object_format).expect("database");
for (from, target) in [(&merged, &base), (&merged, &side), (&side, &base), (&base, &side), (&side, &merged), (&base, &base)] {
assert_eq!(
reaches(&database, &layout, from, target, WALK_BUDGET),
Some(git_says_ancestor(&repo, target, from)),
"{target} from {from}"
);
}
assert_eq!(reaches(&database, &layout, &merged, &base, 0), None, "a walk past its budget does not guess");
remove(&root);
}
#[test]
fn a_branch_that_grew_ahead_of_its_upstream_is_handed_over() {
let (root, repo) = repository("atr-grew");
let watched = watch(&repo);
assert_eq!(tip_to_push(&watched), None, "nothing moved");
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=more"]);
assert_eq!(tip_to_push(&watched), Some(oid(&repo, "HEAD")));
remove(&root);
}
#[test]
fn a_branch_that_moved_backwards_is_not_handed_over() {
let (root, repo) = repository("atr-back");
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=more"]);
git(&repo, &["push", "--quiet", "origin", "main"]);
let watched = watch(&repo);
git(&repo, &["reset", "--quiet", "--hard", "HEAD~1"]);
assert_eq!(tip_to_push(&watched), None);
remove(&root);
}
#[test]
fn a_rewritten_branch_is_not_handed_over() {
let (root, repo) = repository("atr-rewrite");
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=local"]);
let watched = watch(&repo);
git(&repo, &["commit", "--quiet", "--amend", "--allow-empty", "--message=local, rewritten"]);
assert_eq!(tip_to_push(&watched), None);
remove(&root);
}
#[test]
fn a_diverged_branch_and_a_branch_without_an_upstream_are_not_handed_over() {
let (root, repo) = repository("atr-diverged");
git(&repo, &["switch", "--quiet", "--create", "loose"]);
let loose = watch(&repo);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=more"]);
assert_eq!(tip_to_push(&loose), None, "no upstream");
git(&repo, &["switch", "--quiet", "main"]);
let watched = watch(&repo);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=mine"]);
git(&repo, &["switch", "--quiet", "--detach", "HEAD~1"]);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=theirs"]);
let theirs = oid(&repo, "HEAD");
git(&repo, &["update-ref", "refs/remotes/origin/main", &theirs]);
git(&repo, &["switch", "--quiet", "main"]);
assert_eq!(tip_to_push(&watched), None);
remove(&root);
}
fn overlay_with_fsmonitor(file: &Path) -> Vec<(OsString, OsString)> {
std::fs::write(file, "[core]\n\tfsmonitor = true\n[user]\n\tname = Someone\n").expect("global configuration");
let mut overlay: Vec<(OsString, OsString)> = crate::test_support::isolated_overlay()
.into_iter()
.filter(|(name, _)| { return name != "GIT_CONFIG_GLOBAL"; })
.collect();
overlay.push((OsString::from("GIT_CONFIG_GLOBAL"), file.as_os_str().to_os_string()));
return overlay;
}
#[test]
fn a_user_level_setting_the_reader_cannot_prove_does_not_stop_the_trigger() {
let (root, repo) = repository("atr-fsmonitor");
git(&repo, &["config", "extensions.worktreeConfig", "true"]);
git(&repo, &["config", "push.default", "matching"]);
git(&repo, &["worktree", "add", "--quiet", "--detach", "../linked"]);
git(&repo, &["switch", "--quiet", "--create", "topic"]);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=topic"]);
git(&repo, &["switch", "--quiet", "main"]);
let overlay: Vec<(OsString, OsString)> = overlay_with_fsmonitor(root.join("global.gitconfig").as_path());
assert!(
read_layout_in(&repo, overlay.as_slice()).is_none(),
"the premise: the reader that checks every value declines this user's configuration"
);
let watched: TipWatch = try_watch_tip(&repo, Path::new("/usr/bin/git"), overlay.as_slice(), &[]).expect("the watch starts");
git(&repo, &["merge", "--quiet", "--no-ff", "--no-edit", "topic"]);
assert_eq!(tip_to_push_or_why(&watched), Ok(oid(&repo, "HEAD")));
remove(&root);
}
#[test]
fn every_decline_names_its_reason() {
let (root, repo) = repository("atr-reasons");
let reason = |watched: &TipWatch| -> &'static str { return tip_to_push_or_why(watched).expect_err("declined").reason; };
let at_base: TipWatch = watch(&repo);
assert_eq!(reason(&at_base), "tip-unchanged");
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=more"]);
git(&repo, &["push", "--quiet", "origin", "main"]);
assert_eq!(reason(&at_base), "upstream-equals-tip");
let published: TipWatch = watch(&repo);
git(&repo, &["commit", "--quiet", "--allow-empty", "--amend", "--message=rewritten"]);
assert_eq!(reason(&published), "not-grown");
let rewritten: TipWatch = watch(&repo);
git(&repo, &["commit", "--quiet", "--allow-empty", "--message=on the rewrite"]);
assert_eq!(reason(&rewritten), "not-ahead");
git(&repo, &["config", "--unset", "branch.main.merge"]);
assert_eq!(reason(&rewritten), "upstream-not-single");
git(&repo, &["switch", "--quiet", "--detach"]);
assert_eq!(reason(&rewritten), "head-detached");
let empty = root.join("nowhere");
std::fs::create_dir(&empty).expect("directory");
let declined = try_watch_tip(&empty, Path::new("/usr/bin/git"), crate::test_support::isolated_overlay().as_slice(), &[]).expect_err("no repository");
assert_eq!(declined.reason, "layout-declined");
remove(&root);
}
#[test]
fn a_trigger_record_is_one_line_that_names_the_decision() {
use crate::push_trigger_log::{Outcome as Ending, trigger_line};
let line: String = trigger_line("merge", Ending::Declined, "upstream-equals-tip", "a \"quoted\"\nname", Some("refs/heads/main"));
assert_eq!(line.matches('\n').count(), 1, "one line: {line:?}");
assert!(line.ends_with("}\n"));
for part in ["\"schemaVersion\":1", "\"command\":\"merge\"", "\"outcome\":\"declined\"", "\"reason\":\"upstream-equals-tip\"", "\"branch\":\"refs/heads/main\"", "\\\"quoted\\\"\\n"] {
assert!(line.contains(part), "{part} in {line}");
}
assert!(!trigger_line("pull", Ending::HandedOver, "ahead-of-upstream", "", None).contains("\"branch\""));
assert!(trigger_line("pull", Ending::HandedOver, "ahead-of-upstream", "", None).contains("\"outcome\":\"handed-over\""));
}