feat(checks): add a CLI view of check runs and fix two run-visibility bugs
commit
c60dc57feat(checks): add a CLI view of check runs and fix two run-visibility bugs
The live Checks tab was blanking its entire Recent-runs list over one
stale-format run, and fetching several refs/meta/runs/* refs at once
was getting corrupted in transit.
fix: stop Store::history() at the first format-incompatible commit instead of erroring the whole read
fix: forward Content-Encoding to git http-backend so multi-ref fetches don’t arrive gzip’d and garbled
feat: add git ents checks runs to list recorded check runs from a remote
Assisted-by: Claude:claude-sonnet-5
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents-server/src/http.rs
@@ -123,6 +123,14 @@
let content_type = header_value(headers, "Content-Type");
let content_length = header_value(headers, "Content-Length");
+ // `Content-Type`/`Content-Length` are CGI-special-cased env vars with no
+ // `HTTP_` prefix; every other header (including `Content-Encoding`) maps to
+ // `HTTP_<NAME>`. Without it, a gzip'd request body — which git's client
+ // sends once a negotiation grows past its size threshold, e.g. `fetch`ing
+ // many refs at once — reaches `git-upload-pack` still compressed, which it
+ // reads as raw (garbled) pkt-lines: "protocol error: bad line length
+ // character".
+ let content_encoding = header_value(headers, "Content-Encoding");
let mut cmd = Command::new("git");
cmd.arg("http-backend")
@@ -146,6 +154,9 @@
if let Some(value) = &content_length {
cmd.env("CONTENT_LENGTH", value);
}
+ if let Some(value) = &content_encoding {
+ cmd.env("HTTP_CONTENT_ENCODING", value);
+ }
// Push these through `GIT_CONFIG_*` rather than `git -c` so they reach the
// `receive-pack` and `pre-receive` processes http-backend spawns, where the
crates/git-ents/src/main.rs
@@ -209,6 +209,13 @@
#[arg(default_value = "origin")]
remote: String,
},
+ /// Show recorded check runs (queued/running/pass/fail/error) from
+ /// `refs/meta/runs/*` on a remote, newest first.
+ Runs {
+ /// Remote to read `refs/meta/runs/*` from.
+ #[arg(default_value = "origin")]
+ remote: String,
+ },
}
#[derive(Subcommand)]
@@ -339,9 +346,35 @@
} => add_check(name, command, &remote),
ChecksAction::Remove { name, remote } => remove::<Checks>(&name, &remote),
ChecksAction::Debug { remote } => checks_debug(&remote),
+ ChecksAction::Runs { remote } => checks_runs(&remote),
}
}
+/// Print every recorded check run on `remote`, newest commit first and
+/// (within a commit) newest run first, as `<commit> <when> <check>=<status> …`.
+fn checks_runs(remote: &str) -> Result<(), String> {
+ let repo = repo()?;
+ sync_namespace(remote, checks::RUNS_NS)?;
+ let commits = checks::runs(&repo).map_err(|error| error.to_string())?;
+ if commits.is_empty() {
+ println!("no check runs on {remote}");
+ return Ok(());
+ }
+ for commit_runs in commits {
+ for run in &commit_runs.runs {
+ let when = ago(run.at);
+ let results = run
+ .results
+ .iter()
+ .map(|outcome| format!("{}={}", outcome.name, outcome.status))
+ .collect::<Vec<_>>()
+ .join(" ");
+ println!("{} {when} {results}", short_id(&commit_runs.commit));
+ }
+ }
+ Ok(())
+}
+
fn run_comment(action: CommentAction) -> Result<(), String> {
match action {
CommentAction::Add {
@@ -1264,6 +1297,23 @@
.map_or(0, |elapsed| elapsed.as_secs())
}
+/// `at` (seconds since the Unix epoch) as a relative "N units ago" string.
+fn ago(at: u64) -> String {
+ let secs = now_seconds().saturating_sub(at);
+ let mins = secs / 60;
+ let hours = mins / 60;
+ let days = hours / 24;
+ if mins == 0 {
+ "just now".to_owned()
+ } else if hours == 0 {
+ format!("{mins}m ago")
+ } else if days == 0 {
+ format!("{hours}h ago")
+ } else {
+ format!("{days}d ago")
+ }
+}
+
/// Fail-fast check, ahead of any network sync, that `value` is a well-formed
/// OpenSSH `allowed_signers` timestamp — the same rule [`Member::validate`]
/// (via [`members::store`]) checks again before the write actually lands, and
crates/git-store/src/lib.rs
@@ -355,13 +355,22 @@
/// The documents on `refname`'s commit chain as `(committer date, value)`
/// pairs, newest first — one entry per commit, following first parents.
+ ///
+ /// Stops (without erroring) at the first commit that predates an
+ /// incompatible format change to `T`: such a commit is unreadable forever,
+ /// not transiently, so returning the readable prefix beats letting one
+ /// stale commit blank out every newer entry that parses fine. A real I/O
+ /// or repository-corruption error still propagates.
pub fn history<T: for<'a> Facet<'a>>(&self, refname: &str) -> Result<Vec<(u64, T)>, Error> {
let mut out = Vec::new();
let mut cursor = self.ref_commit(refname)?;
while let Some(oid) = cursor {
let commit = self.read_commit(&oid)?;
- let value = facet_git_tree::deserialize(&commit.tree, &self.odb)?;
- out.push((commit.seconds, value));
+ match facet_git_tree::deserialize(&commit.tree, &self.odb) {
+ Ok(value) => out.push((commit.seconds, value)),
+ Err(facet_git_tree::Error::Message(_)) => break,
+ Err(error) => return Err(error.into()),
+ }
cursor = commit.parents.into_iter().next();
}
Ok(out)