johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! Branches that take a pull only once it is reviewed: approvals from
//! those who may write, code owners from CODEOWNERS, standing requests for
//! changes, and approvals gone stale, as `merge`, `merge --check` and the
//! merge queue read them.
use super::*;
/// What `args` says on failing, as `who` runs them.
fn refusal(who: &Sandbox, args: &[&str]) -> String {
let (done, out, err) = who.knotless(args, b"");
assert!(!done, "{args:?}: {out}");
err
}
#[test]
fn protected_branches_take_reviewed_pulls() {
// Started first, so its index sees the repositories made after.
let server = gateway_with(None, true);
let [mut owner, mut alice, mut bob, mut stranger] = people();
for who in [&mut owner, &mut alice, &mut bob, &mut stranger] {
who.set("KNOTLESS_APPVIEW", &server.base);
}
let url = owner.url("reviewed");
let work = owner.work("w", 1);
std::fs::create_dir_all(work.join(".knotless")).unwrap();
let a = &alice.account.handle;
let codeowners = format!(
"# Who owns what\n* {}\nweb/ @{a} @org/team @nobody.invalid\ndocs/ @org/team\nweb/vendor/\n",
owner.account.did
);
owner.commit_file(&work, ".knotless/CODEOWNERS", &codeowners);
owner.git(&work, &["push", "-q", &url, "main"]);
// Sharing for writing first delegates the owner's writes to the
// appview, as `delegate` does, and says so; once is enough.
for (n, who) in [&alice, &bob].into_iter().enumerate() {
let share = ["repo", "share", &url, &who.account.handle, "--write"];
let (code, out) = owner.cli(&share, b"");
assert_eq!(code, 0, "{out}");
let delegated = format!("delegated to {}\nshared", server.did);
assert_eq!(out.contains(&delegated), n == 0, "{out}");
}
// What a rule asks of reviews, as `rules` says it.
let asks = [
"branch",
"protect",
&url,
"main",
"--reviews",
"1",
"--code-owners",
];
let (code, out) = owner.cli(&asks, b"");
assert_eq!(
(code, out.as_str()),
(0, "main\tlinear\treviews 1\tcode owners")
);
let rules = owner.record("dev.knotless.repo", "reviewed")["rules"].clone();
let reviews = json!({"approvals": 1, "codeOwners": true});
assert_eq!(rules[0]["reviews"], reviews);
for bad in [["--reviews", "11"], ["--reviews", "x"]] {
let args = [&["branch", "protect", &url, "main"][..], &bad[..]].concat();
assert_eq!(owner.cli(&args, b"").0, 1, "{args:?}");
}
// Bob's pull changes a file of the owner's and one of alice's.
let clone = bob.path("clone");
bob.git(bob.dir.path(), &["clone", "-q", &url, "clone"]);
bob.git(&clone, &["checkout", "-q", "-b", "x"]);
std::fs::create_dir_all(clone.join("web/vendor")).unwrap();
bob.commit_file(&clone, "web/a.css", "a\n");
bob.git(&clone, &["push", "-q", "origin", "x"]);
bob.commit_file(&clone, "web/vendor/lib.js", "lib\n");
bob.commit_file(&clone, "b", "b\n");
bob.git(&clone, &["push", "-q", "origin", "x"]);
let (code, pull) = bob.cli(&["pull", "create", &url, "main", &url, "x", "X"], b"");
assert_eq!(code, 0, "{pull}");
wait_until(|| owner.cli(&["pull", "list", &url], b"").1.contains(&pull));
// Nobody has reviewed it: it lacks an approval and both owners.
let owner_did = &owner.account.did;
let checked = owner.cli(&["pull", "merge", &url, &pull, "--check"], b"").1;
let lines: Vec<&str> = checked.lines().collect();
assert_eq!(lines[0], "blocked\tsquash\tchecks none\treviews blocked");
assert_eq!(lines[1], "missing\treviews 0 of 1");
assert_eq!(lines[2], format!("missing\towners of *: {owner_did}"));
assert_eq!(lines[3], format!("missing\towners of web/: @{a}"));
for skipped in [
"warning\t.knotless/CODEOWNERS line 3: @org/team is a team, and knotless has no teams; skipped",
"warning\t.knotless/CODEOWNERS line 3: @nobody.invalid does not resolve to an account; skipped",
"warning\t.knotless/CODEOWNERS line 4: docs/ has none of its owners left; skipped",
] {
assert!(checked.contains(skipped), "{checked}");
}
assert!(
checked.ends_with("the owner, its author or not, merges past it with `--force`"),
"{checked}"
);
let err = refusal(&bob, &["pull", "merge", &url, &pull]);
assert!(
err.contains(&format!(
"{pull} is not reviewed as main asks: reviews 0 of 1; owners of *"
)),
"{err}"
);
// The owner's own pull of what they alone own can never have a code
// owner's approval, as an author's never counts: its check says so.
owner.git(&work, &["checkout", "-q", "-b", "own"]);
owner.commit_file(&work, "o", "o\n");
owner.git(&work, &["push", "-q", &url, "own"]);
let (code, own) = owner.cli(&["pull", "create", &url, "main", &url, "own", "Own"], b"");
assert_eq!(code, 0, "{own}");
wait_until(|| owner.cli(&["pull", "list", &url], b"").1.contains(&own));
let checked = owner.cli(&["pull", "merge", &url, &own, "--check"], b"").1;
let alone = format!(
"missing\towners of *: {owner_did}, its author alone, whose own approval never counts, so * needs another owner in CODEOWNERS"
);
assert!(checked.contains(&alone), "{checked}");
assert_eq!(owner.cli(&["pull", "close", &url, &own], b"").0, 0);
// Past the client, the gateway holds bob to the same: his own squash
// of the pull, a commit of his on main, and one through createCommit.
bob.git(&clone, &["checkout", "-q", "-B", "own", "origin/main"]);
bob.git(&clone, &["merge", "-q", "--squash", "x"]);
bob.git(&clone, &["commit", "-q", "-m", "squashed"]);
let err = refused(&bob, &clone, "own:main").unwrap();
let held = format!("protected branch: {pull} is not reviewed as main asks");
assert!(err.contains(&held), "{err}");
bob.commit_file(&clone, "d", "d\n");
let err = refused(&bob, &clone, "own:main").unwrap();
let plain = "protected branch: changes land on it only through reviewed pulls";
assert!(err.contains(plain), "{err}");
let committed = [
"repo", "commit", &url, "-b", "main", "-m", "drop f", "--delete", "f",
];
let err = refusal(&bob, &committed);
assert!(
err.contains("main takes changes only through reviewed pulls"),
"{err}"
);
bob.git(&clone, &["checkout", "-q", "x"]);
// A stranger's approval counts for nothing; alice's for an approval
// and for web/.
let approve = ["comment", "add", &url, &pull, "fine", "--approve"];
assert_eq!(stranger.cli(&approve, b"").0, 0);
assert_eq!(alice.cli(&approve, b"").0, 0);
let only_owner = format!("missing\towners of *: {owner_did}");
// Its merge says why the stranger's does not count, once both
// approvals are seen: alice's may be before the stranger's.
let not = format!(
"uncounted\t@{}'s approval does not count: only the owner's and collaborators' approvals count",
stranger.account.handle
);
wait_until(|| {
let checked = owner.cli(&["pull", "merge", &url, &pull, "--check"], b"").1;
!checked.contains("reviews 0 of 1")
&& checked.contains(&only_owner)
&& checked.contains(¬)
});
let err = refusal(&bob, &["pull", "merge", &url, &pull]);
assert!(err.contains(¬[10..]), "{err}");
// The owner asks for changes: that blocks it, by their handle.
let changes = ["comment", "add", &url, &pull, "not yet", "--changes"];
assert_eq!(owner.cli(&changes, b"").0, 0);
let asked = format!("missing\tchanges asked by @{}", owner.account.handle);
wait_until(|| {
owner
.cli(&["pull", "merge", &url, &pull, "--check"], b"")
.1
.contains(&asked)
});
// Queued, it is turned away, saying why.
let ci = &stranger.account.handle;
assert_eq!(owner.cli(&["runner", "add", &url, ci], b"").0, 0);
// Bob reads the reviews for himself, and may not have the owner's
// yet: his refusal comes to name it.
let on = [&asks[..], &["--queue"][..]].concat();
assert_eq!(owner.cli(&on, b"").0, 0);
let queued = || refusal(&bob, &["pull", "merge", &url, &pull, "--queue"]);
wait_until(|| queued().contains("changes asked by @"));
let err = queued();
assert!(err.contains("Queue it again once it is"), "{err}");
// Once the owner approves, it merges, the queue off again.
assert_eq!(owner.cli(&asks, b"").0, 0);
assert_eq!(owner.cli(&approve, b"").0, 0);
wait_until(|| {
owner
.cli(&["pull", "merge", &url, &pull, "--check"], b"")
.1
.starts_with("clean\tsquash\tchecks none\treviews passed")
});
// Only approvals of its head count, once the rule says so: a push
// leaves it with none.
let stale = [
"branch",
"protect",
&url,
"main",
"--reviews",
"1",
"--dismiss-stale",
];
let (code, out) = owner.cli(&stale, b"");
assert_eq!(
(code, out.as_str()),
(0, "main\tlinear\treviews 1\tdismiss stale")
);
bob.commit_file(&clone, "c", "c\n");
bob.git(&clone, &["push", "-q", "origin", "x"]);
wait_until(|| {
owner
.cli(&["pull", "merge", &url, &pull, "--check"], b"")
.1
.contains("missing\treviews 0 of 1")
});
let err = refusal(&bob, &["pull", "merge", &url, &pull]);
assert!(err.contains("reviews 0 of 1"), "{err}");
// Approved again, bob merges it, through the gateway.
assert_eq!(alice.cli(&approve, b"").0, 0);
assert_eq!(owner.cli(&approve, b"").0, 0);
wait_until(|| {
owner
.cli(&["pull", "merge", &url, &pull, "--check"], b"")
.1
.starts_with("clean")
});
let (code, merged) = bob.cli(&["pull", "merge", &url, &pull], b"");
assert_eq!(code, 0, "{merged}");
let (_, rules) = owner.cli(&["branch", "protect", &url, "main", "--undo"], b"");
assert_eq!(rules, "");
}