git-ents.gitmain
⌘K
foforge
commit 7e0aa6e
roots: surface recorded results as a checks card on the commit page

Every ResultRecord whose stored target field names the shown commit — the canonical refs/meta/results namespace and every member’s self-run mirror — renders as a pass/fail/error status chip linking to its effect’s page, matched on the tree field the gate itself binds rather than the refname segment. No results renders nothing: a result is only written by a run, so no checks is the ordinary state of a commit, not a pending one.

Assisted-by: Claude:claude-fable-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/cli/ents-web/tests/router.rs @@ -12,10 +12,11 @@ use axum::body::Body; use axum::http::{Request, StatusCode, header}; use ents_kiln::Toolchain; -use ents_model::{Account, Effect, MemberId, Provenance, Redaction}; +use ents_model::{Account, Effect, MemberId, Provenance, Redaction, ResultRecord, Status}; use ents_receive::{Mode, NullEventSink}; use ents_testutil::{ - CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity, + CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, record_result, write_commit, + write_meta_entity, }; use ents_web::identity::SigningIdentity; use ents_web::state::AppState; @@ -2084,6 +2085,82 @@ ); } +/// The commit page's Checks card (`model.result-identity`, +/// `model.result-taxonomy`): a recorded result whose stored target names +/// the shown commit renders as a status chip and its effect's link -- +/// one row per taxonomy value here -- a self-run mirror row additionally +/// names its member, and a result targeting a different commit stays off +/// the page (the tree's own `target` field is what is matched, not the +/// refname). +#[tokio::test] +async fn commit_page_lists_recorded_results_as_checks() { + let dir = seed_repo(&[("src/main.rs", "fn main() {}\n")]); + let oid = head_oid(dir.path()); + let refs = MemRefStore::default(); + let objects = ObjectStore::default(); + record_result(&refs, &objects, "unit", &oid, Status::Pass, None, 1_000); + record_result(&refs, &objects, "lint", &oid, Status::Fail, None, 1_001); + record_result(&refs, &objects, "deploy", &oid, Status::Error, None, 1_002); + // Same effect, different target commit: filtered out by the stored + // target field. + record_result( + &refs, + &objects, + "unit", + "aaaaaaaa", + Status::Pass, + None, + 1_003, + ); + // A self-run mirror row names the member that ran it. + let member = MemberId::new("joey"); + let target = gix::ObjectId::from_hex(oid.as_bytes()).expect("head oid is hex"); + let self_ref = ents_model::namespace::self_result_ref(&member, "unit", &oid).expect("valid"); + let mirror = ResultRecord::new("unit", target, Status::Pass); + write_meta_entity(&refs, &objects, self_ref, &mirror, None, 1_004); + + let state = Arc::new(AppState::new( + Box::new(refs), + objects, + Box::new(NullEventSink), + Mode::Advisory, + Box::new(FixtureIdentity { + name: "local-user", + key: Keypair::from_seed(1), + }), + dir.path().to_owned(), + )); + let router = ents_web::router(state); + + let body = get_body(&router, &format!("/commit/{oid}")).await; + assert!(body.contains("Checks"), "the Checks card renders"); + for (chip, effect) in [ + ("status-pass", "unit"), + ("status-fail", "lint"), + ("status-error", "deploy"), + ] { + assert!( + body.contains(chip), + "the {effect} row carries its {chip} chip" + ); + assert!( + body.contains(&format!("/effects/{effect}")), + "the {effect} row links to its effect page" + ); + } + assert!( + body.contains("self-run by joey"), + "the mirror row names its member" + ); + // The chip class appears once in the canonical unit row and once in + // the self-run mirror row -- never for the other commit's result. + assert_eq!( + body.matches("status-pass").count(), + 2, + "the other commit's result stays off the page" + ); +} + /// `GET /commit/{oid}` on a malformed id is a 404, never a panic or 500. #[tokio::test] async fn commit_show_on_an_invalid_oid_is_not_found_not_a_crash() {
crates/cli/ents-web/src/assets/ents.css @@ -57,6 +57,9 @@ --s-prop: #076678; --diff-add: #4e9a0622; --diff-del: #cc241d22; + --status-pass: #4e9a06; + --status-fail: #cc241d; + --status-error: #b57614; } @media (prefers-color-scheme: dark) { :root { @@ -82,6 +85,9 @@ --s-prop: #83a598; --diff-add: #b8bb2620; --diff-del: #fb493420; + --status-pass: #b8bb26; + --status-fail: #fb4934; + --status-error: #fabd2f; } } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } @@ -439,6 +445,12 @@ * (`crate::pages::{comments,issues,commits}`). */ .comment-state, .verdict { display: inline-block; padding: .05rem .5rem; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-size: .78rem; font-weight: 600; text-transform: lowercase; color: var(--color-text-muted); } .verdict { color: var(--color-text); } +/* Check-status chips (`crate::pages::commits::checks_section`): one color + * per value of the closed pass/fail/error result taxonomy. */ +.status { display: inline-block; font-family: var(--font-mono); font-size: .72rem; font-weight: 700; border-radius: var(--radius-pill); padding: .05rem .6rem; text-transform: lowercase; } +.status-pass { color: var(--status-pass); background: color-mix(in srgb, var(--status-pass) 12%, transparent); } +.status-fail { color: var(--status-fail); background: color-mix(in srgb, var(--status-fail) 12%, transparent); } +.status-error { color: var(--status-error); background: color-mix(in srgb, var(--status-error) 14%, transparent); } /* The reply/resolve/reopen and comment composer forms on a thread card. */ .comment-actions { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .6rem; padding: .7rem 1.1rem; border-top: 1px solid var(--color-border); }
crates/cli/ents-web/src/pages/commits.rs @@ -258,6 +258,7 @@ let old_tree_ref = old_tree.as_ref().unwrap_or(&empty_tree); let (diff, truncated) = diff_sections(&repo, old_tree_ref, &new_tree); let comments = super::comments::for_commit(&state, object_id); + let checks = checks_section(&state, object_id); let reviews = reviews_section(&state, &session, object_id, &oid); let (sidebar_rows, _older) = commit_rows(&state, None, PAGE_SIZE); @@ -301,6 +302,7 @@ } } } + (checks) (reviews) } (diff) @@ -338,6 +340,109 @@ } } +/// One row of the commit page's "Checks" card: a recorded result targeting +/// the shown commit. +struct CheckRow { + /// The recording effect's name ([`ents_model::ResultRecord`]'s own + /// `effect` field), the row's `/effects/{name}` link. + effect: String, + /// The run's outcome, one of the closed taxonomy's three values. + status: ents_model::Status, + /// The self-run mirror's `<member>` segment when the result lives + /// there rather than the canonical namespace (`effect.self-run`). + self_run: Option<String>, + /// The result ref tip's author time, for [`super::ago`]. + seconds: Option<i64>, +} + +/// A [`ents_model::Status`]'s display word, doubling as its +/// `.status-<word>` chip class -- the closed pass/fail/error taxonomy +/// (`model.result-taxonomy`), spelled out here rather than through +/// `Debug`. +fn status_label(status: ents_model::Status) -> &'static str { + match status { + ents_model::Status::Pass => "pass", + ents_model::Status::Fail => "fail", + ents_model::Status::Error => "error", + } +} + +/// The "Checks" card on `GET /commit/{oid}`: every recorded result +/// (`model.result-identity`) whose stored `target` field names this +/// commit -- the canonical `refs/meta/results/<effect>/<short-oid>` +/// namespace and every member's self-run mirror +/// (`refs/meta/self/<member>/...`), matched on the tree's own `target` +/// field (the same binding the gate verifies), never the refname's +/// short-oid segment. Renders nothing at all when no result targets the +/// commit: a result is only ever written by a run +/// (`effect.result-taxonomy`), so "no checks" is the ordinary state of +/// most commits, not a pending one. Best effort: a result ref whose tree +/// cannot be read back is skipped from this card (it still lists on +/// `git ents effect log`). +// @relation(model.result-identity, model.result-taxonomy, scope=function) +fn checks_section<O: Find + Write>(state: &AppState<O>, commit_id: ObjectId) -> Markup { + let mut rows: Vec<CheckRow> = Vec::new(); + for prefix in ["refs/meta/results/", "refs/meta/self/"] { + let Ok(iter) = state.refs.iter_prefix(prefix) else { + continue; + }; + for entry in iter { + let Ok((name, tip)) = entry else { continue }; + // One `state.objects()` lock per read -- the same + // non-reentrant-`Mutex` care `crate::pages::effects::read_all` + // documents. + let record = { + let objects = state.objects(); + super::commit_tree(&*objects, tip).ok().and_then(|tree| { + facet_git_tree::deserialize::<ents_model::ResultRecord>(&tree, &*objects).ok() + }) + }; + let Some(record) = record else { continue }; + if record.target() != commit_id { + continue; + } + let path = name.as_bstr().to_string(); + let self_run = path + .strip_prefix("refs/meta/self/") + .and_then(|rest| rest.split('/').next()) + .map(str::to_owned); + let seconds = super::commit_authorship(&*state.objects(), tip) + .ok() + .map(|(_author, seconds)| seconds); + rows.push(CheckRow { + effect: record.effect, + status: record.status, + self_run, + seconds, + }); + } + } + if rows.is_empty() { + return html! {}; + } + rows.sort_by(|a, b| (&a.effect, &a.self_run).cmp(&(&b.effect, &b.self_run))); + html! { + div.card { + div.card-header { "Checks" } + @for row in &rows { + div.card-row { + span class={ "status status-" (status_label(row.status)) } { + (status_label(row.status)) + } + " " + a href={ "/effects/" (row.effect) } { (row.effect) } + @if let Some(member) = &row.self_run { + span.muted { " \u{b7} self-run by " (member) } + } + @if let Some(seconds) = row.seconds { + span.entry-size { (super::ago(seconds)) } + } + } + } + } + } +} + /// Every review targeting `commit_id` (`ents_forge::review::list` filtered /// to this commit, `model.review`), each rendering its verdict prominently, /// its body as AsciiDoc, and its reviewer (from the review ref's own tip