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

chadtmiller.com/knotless

git that lives in your Atmosphere account

knotless.dev
1 collaborator

knotless 0.13.0 Latest 2 packages, 1 image

1
10

A pull's merge box reads what the server holds, not every PDS again

Merged chadtmiller.com asked to merge into main from chadtmiller.com/knotless:serve/merge-status-speed
crates/knotless/src/approvals.rs+3−3
@@ -14,7 +14,7 @@
use crate::codeowners::{self, CodeOwners};
use crate::env::Ctx;
use crate::forge::{Item, Target};
use crate::forge::{Facts, Item};
use crate::git::Git;
use crate::identity;
use crate::records::Reviews;
@@ -173,7 +173,7 @@ pub fn stand(
/// none, when it asks nothing of them.
pub async fn of(
ctx: &Ctx,
t: &Target,
t: Facts<'_>,
item: &Item,
git: &Git,
base: &str,
@@ -190,7 +190,7 @@ pub async fn of(
),
false => (None, Vec::new()),
};
let owner = t.remote.did();
let owner = t.owner;
let writer = |did: &str| did == owner || t.record.is_collaborator(did);
let mut standing = stand(
rule,
crates/knotless/src/forge.rs+46−14
@@ -667,6 +667,19 @@ async fn previous(
Ok(agreed.then_some(from))
}
/// A repository as what is decided about it goes by: where it is, its
/// owner and key, its record, and whether it is private, however these
/// were read: from its PDS as a [`Target`] opens it, or as a server
/// holds them.
#[derive(Clone, Copy)]
pub struct Facts<'a> {
pub url: &'a RepoUrl,
pub owner: &'a str,
pub key: &'a str,
pub record: &'a RepoRecord,
pub private: bool,
}
/// A repository, opened, with its record.
pub struct Target {
pub url: RepoUrl,
@@ -704,6 +717,17 @@ impl Target {
})
}
/// What is decided about it goes by, as it was opened.
pub fn facts(&self) -> Facts<'_> {
Facts {
url: &self.url,
owner: self.remote.did(),
key: self.snap.key().expect("an opened repository has a key"),
record: &self.record,
private: self.remote.space.is_some(),
}
}
/// The repository as forge records name it, under the name it has now.
pub fn reference(&self) -> RepoRef {
self.snap
@@ -1839,7 +1863,7 @@ pub async fn label(
}
let (mine, me) = t.mine(ctx).await?;
let owner = t.remote.did();
crate::org::needs_public(ctx, &t, &me, "labelling").await?;
crate::org::needs_public(ctx, t.facts(), &me, "labelling").await?;
if let Some(label) = defined
.iter()
.find(|l| !t.record.may_label(owner, &me.did, l))
@@ -1990,7 +2014,7 @@ pub async fn merge(
}
let sides = sides(ctx, &t, &item).await?;
let (git, base, head) = (&sides.git, &sides.base, &sides.head);
crate::org::needs_public(ctx, &t, &me, "merging").await?;
crate::org::needs_public(ctx, t.facts(), &me, "merging").await?;
let branch = sides.refname.trim_start_matches("refs/heads/");
let squash = t.record.rule(branch).is_some_and(records::Rule::linear);
let fast = git.is_ancestor(base, head).await?;
@@ -2004,7 +2028,7 @@ pub async fn merge(
}
let reviewed = match gate.force {
true => None,
false => approvals::of(ctx, &t, &item, git, base, head).await?,
false => approvals::of(ctx, t.facts(), &item, git, base, head).await?,
};
if let Some(why) = reviewed.and_then(|s| approvals::refusal(target, pull, branch, &s)) {
bail!("{why}");
@@ -2029,7 +2053,7 @@ pub async fn merge(
force: false,
}];
let others = assemble(&t.record, t.remote.did(), &entries);
let deleted = (!keep).then(|| spent(&t, &item, &others)).flatten();
let deleted = (!keep).then(|| spent(t.facts(), &item, &others)).flatten();
let kept = kept(&t, &item, &others, keep).filter(|_| deleted.is_none());
if let Some(gone) = &deleted {
updates.push(Update {
@@ -2173,9 +2197,9 @@ pub(crate) fn refusal(
/// The head branch of `item`, once merged, when nothing needs it any more:
/// it is in the target itself, is neither the target branch nor the
/// default one, and no other open pull brings it in or targets it.
pub(crate) fn spent(t: &Target, item: &Item, items: &[Item]) -> Option<String> {
pub(crate) fn spent(t: Facts, item: &Item, items: &[Item]) -> Option<String> {
let head = item.head.as_ref()?;
let here = t.reference().key;
let here = t.key;
let mine = head_here(t, item).is_some();
let default = t.record.default_branch.as_deref().unwrap_or("main");
let named = |other: &Item| {
@@ -2203,7 +2227,13 @@ pub(crate) fn kept(t: &Target, item: &Item, items: &[Item], keep: bool) -> Optio
};
let named = items.iter().find(other).map(|o| o.uri.as_str());
let into = item.base.as_deref() == Some(branch) || branch == default;
why_kept(head, keep, head_here(t, item).is_some(), into, named)
why_kept(
head,
keep,
head_here(t.facts(), item).is_some(),
into,
named,
)
}
/// Why a merge keeps `head`: it was asked to, `keep`; it is in another
@@ -2229,19 +2259,21 @@ fn why_kept(
}
/// The branch `item`'s head is on, when it is in `t` itself.
pub(crate) fn head_here<'a>(t: &Target, item: &'a Item) -> Option<&'a str> {
pub(crate) fn head_here<'a>(t: Facts, item: &'a Item) -> Option<&'a str> {
let head = item.head.as_ref()?;
let owner = parse_uri(&head.repo.uri).ok()?.0;
(head.repo.key == t.reference().key && owner == t.remote.did()).then_some(&head.branch)
(head.repo.key == t.key && owner == t.owner).then_some(&head.branch)
}
/// A pull's two sides, in the target's mirror, with the head fetched from
/// wherever it is.
pub(crate) struct Sides {
pub(crate) git: Git,
pub(crate) refname: String,
pub(crate) base: String,
pub(crate) head: String,
pub struct Sides {
pub git: Git,
/// The branch it merges into, in full: `refs/heads/main`.
pub refname: String,
/// The commits its base and its head are at.
pub base: String,
pub head: String,
}
pub(crate) async fn sides(ctx: &Ctx, t: &Target, item: &Item) -> Result<Sides> {
crates/knotless/src/landing.rs+85−49
@@ -7,7 +7,7 @@
use crate::approvals;
use crate::checks::{self, Standing};
use crate::env::Ctx;
use crate::forge::{self, Item, Target};
use crate::forge::{self, Facts, Item, Sides, Target};
use crate::git::Git;
use crate::mirror::Mirror;
use crate::records::Rule;
@@ -107,16 +107,16 @@ pub struct May {
}
/// What `who` may do to `item` on `t`.
pub fn may(t: &Target, item: &Item, who: &str) -> May {
let owner = t.remote.did();
pub fn may(t: Facts, item: &Item, who: &str) -> May {
let owner = t.owner;
let at = crate::keys::tid();
let maintainer = forge::allowed(&t.record, owner, &item.author, who, "merged", &at);
let maintainer = forge::allowed(t.record, owner, &item.author, who, "merged", &at);
let runners = !t.record.runners.is_empty();
May {
merge: maintainer,
force: who == owner,
queue: maintainer && runners,
base: forge::allowed(&t.record, owner, &item.author, who, "closed", &at),
base: forge::allowed(t.record, owner, &item.author, who, "closed", &at),
head: who == item.author,
rerun: who == owner && runners,
}
@@ -129,29 +129,30 @@ pub fn may(t: &Target, item: &Item, who: &str) -> May {
pub async fn mergeable(ctx: &Ctx, target: &RepoUrl, pull: &str, who: &str) -> Result<Mergeable> {
let t = Target::open(ctx, target).await?;
let entries = t.reviewed(ctx, pull, who).await?;
let item = forge::thread(&t.record, t.remote.did(), &entries, pull)?.item;
let items = forge::assemble(&t.record, t.remote.did(), &entries);
let item = forge::thread_in(&t.record, t.remote.did(), &entries, &items, pull)?.item;
if item.state != "open" {
bail!("{pull} is {}", item.state);
}
let mut found = gauged(ctx, &t, &entries, &item, who).await?;
if item.draft {
found.state = "draft";
found.reason = Some(forge::drafted(target, pull));
}
if !found.may.merge {
let not = not_theirs(ctx, &t, who).await;
found.reason = Some(match found.reason {
Some(why) => format!("{why}; and {not}"),
None => not,
});
}
Ok(found)
let sides = forge::sides(ctx, &t, &item).await?;
let gauged = gauge(ctx, t.facts(), &items, &item, &sides, who).await?;
let checks = match gauged.workflows.is_empty() {
true => Vec::new(),
false => {
let filter = checks::Filter {
pull: Some(pull),
..Default::default()
};
checks::list(ctx, &t.url, filter, false).await?
}
};
Ok(decide(ctx, t.facts(), &item, &sides, gauged, &checks, who).await)
}
/// Why `who` may not merge on `t`: a hidden member of the organization it
/// is, that merging takes a public membership, in the words a merge would
/// refuse with; anyone else, that only its owner and collaborators do.
async fn not_theirs(ctx: &Ctx, t: &Target, who: &str) -> String {
async fn not_theirs(ctx: &Ctx, t: Facts<'_>, who: &str) -> String {
let me = crate::session::me(ctx)
.await
.ok()
@@ -161,21 +162,30 @@ async fn not_theirs(ctx: &Ctx, t: &Target, who: &str) -> String {
{
return e.to_string();
}
let target = &t.url;
let target = t.url;
format!("you may not merge it: only the owner and collaborators of {target} merge its pulls")
}
/// Whether the open pull `item` on `t` could merge now, were it ready, as
/// [`mergeable`] says, with `entries`, what is said about it.
async fn gauged(
/// Where an open pull stands before its checks are read, as [`gauge`]
/// finds it, and the workflows whose checks a merge of it waits for: none
/// when it waits for none, as when it conflicts.
pub struct Gauged {
found: Mergeable,
pub workflows: Vec<String>,
}
/// Whether the open pull `item` on `t`, among its repository's `items`,
/// could merge now at `sides`, were it ready, as [`mergeable`] says: all
/// but its checks, which [`decide`] reads.
pub async fn gauge(
ctx: &Ctx,
t: &Target,
entries: &[forge::Entry],
t: Facts<'_>,
items: &[Item],
item: &Item,
sides: &Sides,
who: &str,
) -> Result<Mergeable> {
) -> Result<Gauged> {
let pull = item.uri.as_str();
let sides = forge::sides(ctx, t, item).await?;
let (git, base, head) = (&sides.git, &sides.base, &sides.head);
let branch = sides.refname.trim_start_matches("refs/heads/");
let fast = git.is_ancestor(base, head).await?;
@@ -184,8 +194,6 @@ async fn gauged(
// has not moved since the head branched off it.
let methods = [["merge", "fastForward"], ["squash", "squash"]];
let method = methods[usize::from(squash)][usize::from(fast)];
let others = forge::assemble(&t.record, t.remote.did(), entries);
let deletes = forge::spent(t, item, &others);
let mut found = Mergeable {
state: "clean",
reason: None,
@@ -196,27 +204,31 @@ async fn gauged(
reviews_missing: Vec::new(),
reviews_uncounted: Vec::new(),
code_owners_warnings: Vec::new(),
deletes,
deletes: forge::spent(t, item, items),
base: base.clone(),
head: head.clone(),
may: may(t, item, who),
queue_blocked: None,
queue_only: t.record.queue_only(branch),
};
let none = |found| Gauged {
found,
workflows: Vec::new(),
};
let tree = match forge::merge_tree(git, base, head).await? {
Ok(tree) => tree,
Err(files) => {
found.state = "conflicting";
found.reason = Some(format!("merging conflicts in {}", files.join(", ")));
found.conflicts = files.into_iter().map(|path| Conflict { path }).collect();
return Ok(found);
return Ok(none(found));
}
};
let notes =
|all: Vec<String>| -> Vec<Note> { all.into_iter().map(|text| Note { text }).collect() };
if let Some(s) = approvals::of(ctx, t, item, git, base, head).await? {
found.reviews = if s.passed() { "passed" } else { "blocked" };
found.reason = approvals::refusal(&t.url, pull, branch, &s);
found.reason = approvals::refusal(t.url, pull, branch, &s);
found.reviews_missing = notes(s.missing());
found.reviews_uncounted = notes(s.uncounted());
found.code_owners_warnings = notes(s.warnings);
@@ -225,30 +237,54 @@ async fn gauged(
found.state = "blocked";
}
if t.record.runners.is_empty() {
return Ok(found);
return Ok(none(found));
}
let at = match fast {
true => head.clone(),
false => forge::checked_merge(git, &tree, base, head).await?,
};
let workflows = workflow::on_pull(git, &at, &sides.refname).await?;
if workflows.is_empty() {
return Ok(found);
Ok(Gauged { found, workflows })
}
/// What [`gauge`] found of the open pull `item` on `t`, with where `checks`,
/// the pull's own newest first, leave it, and who may merge it: as
/// [`mergeable`] says.
pub async fn decide(
ctx: &Ctx,
t: Facts<'_>,
item: &Item,
sides: &Sides,
gauged: Gauged,
checks: &[checks::Check],
who: &str,
) -> Mergeable {
let Gauged {
mut found,
workflows,
} = gauged;
if !workflows.is_empty() {
let standing = checks::standing(&workflows, checks, &sides.head, &sides.base);
found.checks = standing.name();
// A merge refuses for its reviews before it reads its checks.
let checked = forge::refusal(t.url, &item.uri, sides, &standing, checks);
found.reason = found.reason.or(checked);
if standing != Standing::Passed {
found.state = "blocked";
}
}
let filter = checks::Filter {
pull: Some(pull),
..Default::default()
};
let checks = checks::list(ctx, &t.url, filter, false).await?;
let standing = checks::standing(&workflows, &checks, head, base);
found.checks = standing.name();
// A merge refuses for its reviews before it reads its checks.
let checked = forge::refusal(&t.url, pull, &sides, &standing, &checks);
found.reason = found.reason.or(checked);
if standing != Standing::Passed {
found.state = "blocked";
if item.draft {
found.state = "draft";
found.reason = Some(forge::drafted(t.url, &item.uri));
}
if !found.may.merge {
let not = not_theirs(ctx, t, who).await;
found.reason = Some(match found.reason {
Some(why) => format!("{why}; and {not}"),
None => not,
});
}
Ok(found)
found
}
/// Why a gateway refuses `update`, `pusher`'s move of a branch of `t`, to
crates/knotless/src/org.rs+4−4
@@ -31,7 +31,7 @@
use crate::auth;
use crate::env::Ctx;
use crate::forge::Target;
use crate::forge::Facts;
use crate::gateway;
use crate::identity::{self, Identity};
use crate::manage::{Created, NewRepo};
@@ -457,12 +457,12 @@ fn pusher(did: String, shared: bool) -> OrgMemberRecord {
/// there take a public membership, as those reading it cannot see a hidden
/// one, and would pass over what they did. An appview that does not answer
/// refuses nothing.
pub async fn needs_public(ctx: &Ctx, t: &Target, me: &Session, what: &str) -> Result<()> {
pub async fn needs_public(ctx: &Ctx, t: Facts<'_>, me: &Session, what: &str) -> Result<()> {
let did = me.did.as_str();
if t.remote.space.is_some() || did == t.remote.did() || t.record.is_collaborator(did) {
if t.private || did == t.owner || t.record.is_collaborator(did) {
return Ok(());
}
let roster = asked(ctx, t.remote.did(), Some(me)).await.ok();
let roster = asked(ctx, t.owner, Some(me)).await.ok();
let hidden = |r: &&Roster| r.entry(did).is_some_and(|e| e.standing == Standing::Hidden);
if let Some(roster) = roster.as_ref().filter(hidden) {
let name = &roster.name;
crates/knotless/src/queue.rs+4−4
@@ -77,7 +77,7 @@ fn place(items: &[Item], pull: &str) -> Option<Place> {
/// collaborator, who may merge them.
async fn maintainer(ctx: &Ctx, t: &Target) -> Result<Session> {
let me = session::me(ctx).await?;
crate::org::needs_public(ctx, t, &me, "queueing pulls").await?;
crate::org::needs_public(ctx, t.facts(), &me, "queueing pulls").await?;
if me.did != t.remote.did() && !t.record.is_collaborator(&me.did) {
bail!(
"only the owner and collaborators of {} put pulls in its merge queue, as they are who may merge them",
@@ -187,7 +187,7 @@ async fn admit(ctx: &Ctx, t: &Target, item: &Item) -> Result<()> {
files.join(", ")
);
}
let reviewed = approvals::of(ctx, t, item, &sides.git, base, head).await?;
let reviewed = approvals::of(ctx, t.facts(), item, &sides.git, base, head).await?;
if let Some(why) = reviewed.and_then(|s| approvals::refusal(&t.url, &item.uri, into, &s)) {
bail!("{why}. Queue it again once it is");
}
@@ -723,7 +723,7 @@ pub async fn landing_refusal(
}
let spent: Vec<&str> = landed
.iter()
.filter_map(|i| forge::head_here(t, i))
.filter_map(|i| forge::head_here(t.facts(), i))
.collect();
for gone in deletes {
let name = gone.dst.strip_prefix("refs/heads/").unwrap_or(&gone.dst);
@@ -766,7 +766,7 @@ pub async fn land(ctx: &Ctx, url: &RepoUrl, git: &Git, branch: &str, stack: &Sta
let mut gone: Vec<String> = stack
.stacked
.iter()
.filter_map(|s| forge::spent(&t, &s.pull, &items))
.filter_map(|s| forge::spent(t.facts(), &s.pull, &items))
.collect();
gone.sort();
gone.dedup();
crates/knotless-run/src/queue.rs+2−1
@@ -620,7 +620,8 @@ impl Runner {
reviewed.push(item);
continue;
};
let standing = approvals::of(&self.ctx, on.target, &item, on.git, tip, &sha).await?;
let standing =
approvals::of(&self.ctx, on.target.facts(), &item, on.git, tip, &sha).await?;
let url = &on.watch.remote.url;
let why = standing.and_then(|s| approvals::refusal(url, &item.uri, on.branch, &s));
let eject = why
crates/knotless-serve/src/web/landing.rs+78−2
@@ -13,6 +13,7 @@ use super::*;
use crate::viewer::Viewer;
use anyhow::Context;
use axum::Extension;
use knotless::checks::Check;
use knotless::env::Ctx;
use knotless::landing;
use knotless::manage;
@@ -73,14 +74,89 @@ pub async fn get_merge_status(
.0
.ok_or(Shown(StatusCode::UNAUTHORIZED, why.into()))?;
let repo = found(&app, &p.repo).await?;
let found = landing::mergeable(&repo.reader, &url_of(&repo), &p.pull, &did).await;
let mut found = found.map_err(AppError::upstream)?;
let mut found = mergeable(&app, &repo, &p.pull, &did).await?;
if found.may.queue && undelegated(&app, &repo) {
found.queue_blocked = Some("undelegated");
}
Ok(answer(found))
}
/// Whether the open pull `pull` on `repo` can merge now, as
/// [`landing::mergeable`] decides it, read from what this server holds
/// rather than from the PDSes again: the repository as its mirror last
/// synced, the pull, its reviews and its checks as the index has them.
async fn mergeable(app: &App, repo: &Repo, pull: &str, who: &str) -> Result<landing::Mergeable> {
let mut made = assembled(app, repo).await?;
if !made.items.iter().any(|i| i.uri == pull) {
// Opened a moment ago, perhaps, before the index heard of it.
super::threads::reindex(app, repo, pull).await?;
made = assembled(app, repo).await?;
}
// As the mirror last synced, which a write from the pages syncs again.
let record = repo.synced.clone();
let (entries, items) = (&made.entries, &made.items);
let thread = forge::thread_in(&record, &repo.did, entries, items, pull);
let item = thread.map_err(not_found)?.item;
let (Some(base), Some(head), "open") = (&item.base, &item.head, item.state.as_str()) else {
let why = format!("{pull} is {}", item.state);
return Err(Shown(StatusCode::BAD_REQUEST, why));
};
let (git, base_sha, head_sha, _) = super::pull_range(app, repo, &item, base, head).await?;
let sides = forge::Sides {
git,
refname: format!("refs/heads/{base}"),
base: base_sha,
head: head_sha,
};
let url = url_of(repo);
let facts = forge::Facts {
url: &url,
owner: &repo.did,
key: &repo.key,
record: &record,
private: repo.in_space.is_some(),
};
let ctx = &repo.reader;
let gauged = landing::gauge(ctx, facts, items, &item, &sides, who).await;
let gauged = gauged.map_err(AppError::upstream)?;
let checks = match gauged.workflows.is_empty() {
true => Vec::new(),
false => checks_of(app, repo, &record, pull)?,
};
Ok(landing::decide(ctx, facts, &item, &sides, gauged, &checks, who).await)
}
/// The checks `record`'s runners ran of `pull` on `repo`, newest first, as
/// the index its pulls were read from has them: as many as a merge goes by, those of its head now
/// and of the heads just before it.
fn checks_of(app: &App, repo: &Repo, record: &RepoRecord, pull: &str) -> Result<Vec<Check>> {
let index = super::indexed(app, repo).expect("its pulls were read from an index");
let runners: Vec<String> = record.runners.iter().map(|r| r.did.clone()).collect();
let filter = CheckFilter {
pull: Some(pull),
..CheckFilter::default()
};
let found = index.checks(&repo.did, &repo.key, &runners, filter, CHECKS_READ, None);
let (entries, _) = found
.map_err(anyhow::Error::from)
.map_err(AppError::internal)?;
let checks = entries.into_iter().filter_map(|e| {
Some(Check {
record: serde_json::from_value(e.value).ok()?,
uri: e.uri,
runner: e.author,
log_text: None,
space: repo.in_space.clone(),
})
});
Ok(checks.collect())
}
/// How many of a pull's checks, newest first, its merge status reads: a
/// push runs one of each of its workflows, so these cover its head and
/// the pushes before it many times over.
const CHECKS_READ: usize = 200;
/// Whether the merge queue of `repo` lands through this server's gateway,
/// as no runner it names may push and it records no other gateway, while
/// its owner has not delegated writes here: then it lands nothing.
crates/knotless-serve/tests/forge.rs+3−0
@@ -733,6 +733,9 @@ fn merges_wait_for_their_checks() {
assert!(merged, "{err}");
assert_eq!(out, format!("{head}\ndeleted x"));
});
// Merged, it can no longer say whether it merges.
let (checked, _, err) = owner.knotless(&["merge", &url, &pull, "--check"], b"");
assert!(!checked && err.contains("is merged"), "{err}");
// The base moved: what is checked is the merge of the two.
// A workflow that does not load is waited for too, as its runner files
crates/knotless-serve/tests/gateway.rs+32−1
@@ -1783,6 +1783,9 @@ fn landing_pulls_from_the_pages() {
// Only someone asks whether it can merge. It names no runners, so
// nothing waits for its checks: it fast-forwards, and takes x along.
assert_eq!(status(None).0, 401);
// One there is none of, read again from its author's repo, is none.
let nothing = format!("repo={repo}&pull={pull}x");
assert_eq!(ask("getMergeStatus", &nothing, me).0, 404);
let (code, _, said) = status(me);
assert_eq!(code, 200, "{said}");
let may = json!({"merge": true, "force": true, "queue": false, "base": true, "head": true, "rerun": false});
@@ -1866,6 +1869,28 @@ fn landing_pulls_from_the_pages() {
);
assert_eq!(write("requestRerun", them, again).0, 400);
// Its runner files a check that passed at its head on main, which the
// box reads from the index once it hears of it.
let key = owner.record("dev.knotless.repo", "landed")["key"].clone();
let base = owner.git(&work, &["rev-parse", "main"]);
let key_str = key.as_str().unwrap();
let rkey = knotless::keys::check_rkey(
&owner.account.did,
key_str,
&pull,
&head,
Some(base.as_str()),
"ci",
);
let check = json!({
"$type": "dev.knotless.check", "repo": {"uri": repo, "key": key}, "ref": "refs/heads/x",
"pull": pull, "commit": head, "baseCommit": base, "workflow": "ci", "status": "success",
"startedAt": "2026-10-31T00:00:00.000Z",
});
ci.put_record("dev.knotless.check", &rkey, check);
wait_until(|| status(me).2["checks"] == "passed");
assert_eq!(status(me).2["state"], "clean");
// Its runner may not push, so lands the queue through this server,
// which does not write for its owner yet: queueing it would land
// nothing, and is refused, until they delegate.
@@ -1959,7 +1984,13 @@ fn landing_pulls_from_the_pages() {
code == 400 && again["message"].as_str().unwrap().contains("is merged"),
"{again}"
);
assert_eq!(status(me).0, 502);
// Nor can it say whether a merged pull merges.
let (code, _, said) = status(me);
assert_eq!(code, 400, "{said}");
assert!(
said["message"].as_str().unwrap().ends_with("is merged"),
"{said}"
);
// The pull it undoes names it once the revert merges.
assert!(as_me.get("revertedBy").is_none(), "{as_me}");
let landing = json!({"repo": repo, "pull": uri});
docs/design/serve.md+9−7
@@ -749,13 +749,15 @@ batch, which its `merged` state names as `batch`. Then the box asks the server,
and only for someone logged in, whether the pull can merge now
(`dev.knotless.getMergeStatus`), and says whether it conflicts with its
base: the files that do, each linked to its place in the pull's diff,
and the commands that bring the base into the branch to resolve them. The server answers through
the same code as `knotless merge --check`, which reads what a merge reads:
the pull from its author's repository, both sides in the server's mirror,
and the pull's checks at its head merged with its base as it is now. So
the box says what a merge would, without a merge's writes.
It is asked only of someone logged in because it is costly, a mirror's
sync and a merge of the two sides, and only they can act on it. It reads
and the commands that bring the base into the branch to resolve them. The server decides through
the same code as `knotless merge --check`, from what it holds rather
than what a merge reads afresh: the repository as its mirror last synced,
both sides in its mirrors, and the pull, its reviews and its checks as its
index has them. So the box says what a merge would, without a merge's
writes, and in a tenth of the time; a review or a check written a moment
ago shows once the index hears of it, while a merge reads them at once.
It is asked only of someone logged in, as it merges the two sides, and
only they can act on it. It reads
the repository as the pages read it for that viewer: a public one as the
server, a private one as the member asking, so it works for a private
repository's members. A pull into a repository whose runners have no
docs/reference/api.md+1−1