johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! A repository's runs: what its runners ran, newest first, of every
//! workflow or of one, each with what it ran on; and the workflows on its
//! default branch, those that run only when asked among them, which no
//! push or pull shows.
use super::repo::found;
use super::*;
use knotless::workflow;
/// Runs a page shows unless asked for more or fewer.
const PAGE: usize = 30;
/// `dev.knotless.listRuns`'s answer.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Runs {
runs: Vec<RunView>,
/// Absent on the last page.
#[serde(skip_serializing_if = "Option::is_none")]
cursor: Option<String>,
/// The workflows in `.knotless/workflows` on the default branch, by
/// name.
workflows: Vec<WorkflowView>,
/// The minutes the repository's runners count for its owner.
#[serde(skip_serializing_if = "Vec::is_empty")]
minutes: Vec<MinutesView>,
}
/// `dev.knotless.listRuns#runView`: one run of a workflow, with what it
/// ran on.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunView {
check: CheckView,
/// The ref it ran at: for a pull's run its head branch, for a merge
/// queue batch's the branch it lands on.
#[serde(rename = "ref")]
refname: String,
/// For a run of a pull's head, the pull.
#[serde(skip_serializing_if = "Option::is_none")]
pull: Option<ItemViewBasic>,
/// For a run of a merge queue's batch, how many pulls it lands.
#[serde(skip_serializing_if = "Option::is_none")]
batch: Option<usize>,
/// For a run of a commit pushed to the ref, the first line of its
/// message, where this server has the commit.
#[serde(skip_serializing_if = "Option::is_none")]
subject: Option<String>,
}
/// `dev.knotless.listRuns#workflowView`: a workflow file on the default
/// branch.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkflowView {
name: String,
/// Whether it runs only when asked for by name, on no push or pull.
asked: bool,
/// The first sentence of the comment its file opens with.
#[serde(skip_serializing_if = "Option::is_none")]
about: Option<String>,
}
#[derive(Deserialize)]
pub struct RunsParams {
repo: String,
workflow: Option<String>,
limit: Option<usize>,
cursor: Option<String>,
}
/// `dev.knotless.listRuns`.
pub(super) async fn list_runs(
State(app): State<Arc<App>>,
Query(p): Query<RunsParams>,
) -> Result<Response> {
let repo = found(&app, &p.repo).await?;
let record = repo.record(&app).unwrap_or_default();
let runners: Vec<String> = record.runners.iter().map(|r| r.did.clone()).collect();
let filter = CheckFilter {
workflow: p.workflow.as_deref(),
..CheckFilter::default()
};
let limit = p.limit.unwrap_or(PAGE).clamp(1, 100);
let cursor = p.cursor.as_deref();
let found = checks_index(&app, &repo.uri()).and_then(|(index, private)| {
let found = index.checks(&repo.did, &repo.key, &runners, filter, limit, cursor);
let found = found.ok();
if let Some((entries, _)) = found.as_ref().filter(|_| !private) {
app.recheck(index, entries);
}
found
});
let (entries, cursor) = found.unwrap_or_default();
let made = assembled(&app, &repo).await?;
let items = &made.items;
let objects = repo.objects().ok();
let shown = repo.basic(&app).await;
let runs = entries.into_iter().filter_map(|e| {
let c: CheckRecord = serde_json::from_value(e.value).ok()?;
let pull = c
.pull
.as_deref()
.and_then(|uri| items.iter().find(|i| i.uri == uri));
let pushed = c.pull.is_none() && c.batch.is_empty();
let subject = objects
.as_ref()
.filter(|_| pushed)
.and_then(|o| subject(o, &c.commit));
Some(RunView {
refname: c.ref_name.clone(),
pull: pull.map(|i| ItemViewBasic {
uri: i.uri.clone(),
title: i.title.clone(),
}),
batch: (!c.batch.is_empty()).then_some(c.batch.len()),
subject,
check: check_view(&shown, e.uri, c)?,
})
});
let runs = runs.collect();
let workflows = objects.as_ref().map(workflows).unwrap_or_default();
let minutes = minutes(&app, &repo.uri(), &repo.did, &runners).await;
Ok(answer(Runs {
runs,
cursor,
workflows,
minutes,
}))
}
/// The first line of the message of the commit `sha`, if `o` has it.
fn subject(o: &Objects, sha: &str) -> Option<String> {
let commit = o.log(sha, "", 0, 1).ok()?.into_iter().next()?;
Some(
commit
.message
.lines()
.next()
.unwrap_or_default()
.to_string(),
)
}
/// The workflows in `.knotless/workflows` at `HEAD`, by name: those that
/// read as one, as a runner reads them.
fn workflows(o: &Objects) -> Vec<WorkflowView> {
let files = o.tree("HEAD", workflow::DIR).unwrap_or_default();
let mut found: Vec<WorkflowView> = files
.into_iter()
.filter(|e| e.kind == "blob" && (e.name.ends_with(".yml") || e.name.ends_with(".yaml")))
.filter_map(|e| {
let text = o
.blob("HEAD", &format!("{}/{}", workflow::DIR, e.name))
.ok()?;
let w = workflow::parse(&e.name, &text).ok()?;
let on = &w.on;
Some(WorkflowView {
asked: on.push.is_none() && on.pull.is_none() && on.asked.is_some(),
about: about(&String::from_utf8_lossy(&text)),
name: w.name,
})
})
.collect();
found.sort_by(|a, b| a.name.cmp(&b.name));
found
}
/// The first sentence of the comment `text` opens with, its lines joined.
fn about(text: &str) -> Option<String> {
let lines = text
.lines()
.map_while(|l| l.strip_prefix('#'))
.map(str::trim);
let said = lines
.take_while(|l| !l.is_empty())
.collect::<Vec<_>>()
.join(" ");
let first = said.split_inclusive(". ").next().unwrap_or_default().trim();
(!first.is_empty()).then(|| first.to_string())
}
#[cfg(test)]
mod tests {
use super::about;
#[test]
fn abouts() {
let text =
"# Checks the docs. Costs model usage,\n# so only when asked.\n#\n# More.\nimage: x\n";
assert_eq!(about(text).as_deref(), Some("Checks the docs."));
assert_eq!(
about("# One line, no stop\non: {}\n").as_deref(),
Some("One line, no stop")
);
assert_eq!(about("image: x\n# later\n"), None);
assert_eq!(about("#\n# after a blank\n"), None);
}
}