git-ents.gitmain
⌘K
foforge
commit c60dc57
feat(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

Joseph D. Carpinelli · 1 month ago

Reviews

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

Start a review

verdict

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)