~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
238 lines · 9.9 KB Raw History
//! 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(&not)
    });
    let err = refusal(&bob, &["pull", "merge", &url, &pull]);
    assert!(err.contains(&not[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, "");
}