refactor: tighten web/http maintainability and reframe Hooks as Checks
commit 56f5fb1
refactor: tighten web/http maintainability and reframe Hooks as Checks
An adversarial maintainability pass over the server crate. Collapse
duplicated constants and predicates to a single source of truth, and
replace the git-hooks framing of the CI tab with scaffolding for the
object-graph model: signed CI run records in the git-metadata fanout
keyed by (config_oid, code_oid), config on refs/ents/config, drafts on
refs/ents/ci/draft. No runner is wired up yet, so the Checks panels are
empty states.
refactor: share MAX_REPO_DEPTH between the gateway and web routing
refactor: define git_output in terms of git_output_bytes
refactor: derive is_git_path from is_service_request
refactor: add RepoMeta::name() for the repository short name
fix: report binary blob sizes in human-readable units on the blob page
feat: rename the Hooks tab to Checks
refactor: drop the .gitents/hooks.toml probe from repository metadata
Assisted-by: Claude:claude-opus-4-8
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents-server/src/http.rs
@@ -178,7 +178,8 @@
}
/// Greatest repository nesting depth: `repo`, `org/repo`, or `org/team/repo`.
-const MAX_REPO_DEPTH: usize = 3;
+/// Shared by the push gateway and the web UI's routing/discovery.
+pub(crate) const MAX_REPO_DEPTH: usize = 3;
/// Whether a GET should be answered with the HTML web UI rather than handed to
/// `git http-backend`.
@@ -211,12 +212,7 @@
/// rather than the browser-facing web UI. Anything matching here is delegated
/// to `git http-backend`; everything else is rendered as HTML.
fn is_git_path(path: &str, query: &str) -> bool {
- path.ends_with("/info/refs")
- || path.ends_with("/git-upload-pack")
- || path.ends_with("/git-receive-pack")
- || path.ends_with("/HEAD")
- || path.contains("/objects/")
- || query.contains("service=")
+ is_service_request(path, query) || path.ends_with("/HEAD") || path.contains("/objects/")
}
/// Whether this request is a push: the smart-HTTP receive-pack advertisement
crates/git-ents-server/src/web/git.rs
@@ -7,23 +7,14 @@
use tokio::process::Command;
-use super::MAX_DEPTH;
-use crate::http::is_bare_repo;
+use crate::http::{MAX_REPO_DEPTH, is_bare_repo};
-/// Run `git -C <repo> <args>` and return its stdout, or `None` on failure.
+/// Run `git -C <repo> <args>` and return its stdout as lossy UTF-8, or `None` on
+/// failure.
pub(super) async fn git_output(repo: &Path, args: &[&str]) -> Option<String> {
- let out = Command::new("git")
- .arg("-C")
- .arg(repo)
- .args(args)
- .stderr(Stdio::null())
- .output()
+ git_output_bytes(repo, args)
.await
- .ok()?;
- if !out.status.success() {
- return None;
- }
- Some(String::from_utf8_lossy(&out.stdout).into_owned())
+ .map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
}
/// Run `git -C <repo> <args>` and return its raw stdout bytes, or `None` on
@@ -91,7 +82,7 @@
/// All bare repositories under `root`, as relative slash paths, sorted.
pub(super) fn discover_repos(root: &Path) -> Vec<String> {
let mut repos = Vec::new();
- collect_repos(root, root, MAX_DEPTH, &mut repos);
+ collect_repos(root, root, MAX_REPO_DEPTH, &mut repos);
repos.sort();
repos
}
crates/git-ents-server/src/web/mod.rs
@@ -19,15 +19,12 @@
use maud::{DOCTYPE, Markup, PreEscaped, html};
use crate::AppState;
-use crate::http::{is_bare_repo, valid_segment};
+use crate::http::{MAX_REPO_DEPTH, is_bare_repo, valid_segment};
use self::assets::{COPY_SCRIPT, FONTS, STYLE};
use self::git::{discover_repos, git_output, git_output_bytes};
use self::icons::{icon_branch, icon_chevron, icon_folder, icon_logo, icon_repo, icon_search};
-/// Greatest repository nesting depth served: `repo`, `org/repo`, `org/team/repo`.
-const MAX_DEPTH: usize = 3;
-
/// Render the page for `path`: the repository index at the root, a repository
/// overview, or one of its browse views (`tree`, `blob`, `commit`). `host` is
/// the request's `Host` header, used to build a copy-pasteable clone URL.
@@ -37,11 +34,11 @@
return index(state).into_response();
}
- // The repository is the shortest valid prefix (up to `MAX_DEPTH` segments)
- // that names a bare repo on disk; anything after it selects a browse view.
- // Resolving the boundary this way keeps a repo named `tree`/`blob`/`commit`
- // distinct from the route markers of the same name.
- let depth_limit = segments.len().min(MAX_DEPTH);
+ // The repository is the shortest valid prefix (up to `MAX_REPO_DEPTH`
+ // segments) that names a bare repo on disk; anything after it selects a
+ // browse view. Resolving the boundary this way keeps a repo named
+ // `tree`/`blob`/`commit` distinct from the route markers of the same name.
+ let depth_limit = segments.len().min(MAX_REPO_DEPTH);
for depth in 1..=depth_limit {
let Some(repo_segs) = segments.get(..depth) else {
break;
@@ -74,7 +71,7 @@
Some((&"blob", sub)) => pages::blob_page(repo, &meta, sub).await,
Some((&"commit", &[sha])) => pages::commit_page(repo, &meta, sha).await,
Some((&"releases", &[])) => pages::releases_page(repo, &meta).await.into_response(),
- Some((&"hooks", &[])) => pages::hooks_page(repo, &meta).await.into_response(),
+ Some((&"checks", &[])) => pages::checks_page(&meta).into_response(),
Some((&"issues", &[])) => pages::issues_page(&meta).into_response(),
Some((&"settings", &[])) => pages::settings_page(&meta).into_response(),
_ => not_found().into_response(),
@@ -87,7 +84,7 @@
Overview,
Files,
Releases,
- Hooks,
+ Checks,
Issues,
Settings,
}
@@ -101,7 +98,13 @@
topics: Vec<String>,
releases: usize,
issues: usize,
- has_hooks: bool,
+}
+
+impl RepoMeta {
+ /// The repository's short name: the final segment of its path.
+ fn name(&self) -> &str {
+ self.rel.rsplit('/').next().unwrap_or(&self.rel)
+ }
}
/// Collect the header/tab metadata for the repository at `rel`.
@@ -128,10 +131,6 @@
.await
.map(|s| s.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0);
- let has_hooks = git_output(repo, &["cat-file", "-t", "HEAD:.gitents/hooks.toml"])
- .await
- .as_deref()
- == Some("blob\n");
RepoMeta {
rel: rel.to_owned(),
branch,
@@ -139,7 +138,6 @@
topics,
releases,
issues: 0,
- has_hooks,
}
}
@@ -203,10 +201,7 @@
"Releases"
@if meta.releases > 0 { span.tab-count { (meta.releases) } }
}
- a.tab.active[active == Tab::Hooks] href={ "/" (rel) "/hooks" } {
- "Hooks"
- @if meta.has_hooks { span.tab-dot {} }
- }
+ a.tab.active[active == Tab::Checks] href={ "/" (rel) "/checks" } { "Checks" }
a.tab.active[active == Tab::Issues] href={ "/" (rel) "/issues" } {
"Issues"
@if meta.issues > 0 { span.tab-count { (meta.issues) } }
crates/git-ents-server/src/web/pages.rs
@@ -32,7 +32,7 @@
let clone_url = clone_url(host, rel);
let langs = languages(repo).await;
let latest = latest_release(repo).await;
- let name = rel.rsplit('/').next().unwrap_or(rel);
+ let name = meta.name();
let main = html! {
@if is_empty {
@@ -282,7 +282,7 @@
},
};
- let name = rel.rsplit('/').next().unwrap_or(rel);
+ let name = meta.name();
repo_shell(
meta,
Tab::Files,
@@ -377,7 +377,7 @@
if entries.is_empty() && !dir.is_empty() {
return not_found().into_response();
}
- let name = rel.rsplit('/').next().unwrap_or(rel);
+ let name = meta.name();
repo_shell(
meta,
Tab::Files,
@@ -421,7 +421,7 @@
};
let name = path.rsplit('/').next().unwrap_or(&path);
let body = if is_binary(&bytes) {
- html! { div.blob { div.binary { "Binary file (" (bytes.len()) " bytes) not shown." } } }
+ html! { div.blob { div.binary { "Binary file (" (human_size(bytes.len())) ") not shown." } } }
} else {
let text = String::from_utf8_lossy(&bytes);
match crate::asciidoc::is_asciidoc(name)
@@ -576,39 +576,34 @@
)
}
-/// The Hooks tab: CI is defined as plain git hooks, configured in
-/// `.gitents/hooks.toml`. Run logs need a store that does not exist yet, so the
-/// run list is an empty state; the configuration shown is the real file.
-pub(super) async fn hooks_page(repo: &Path, meta: &RepoMeta) -> Markup {
- let config = git_output_bytes(repo, &["cat-file", "-p", "HEAD:.gitents/hooks.toml"])
- .await
- .map(|b| String::from_utf8_lossy(&b).into_owned());
+/// The Checks tab: CI in the object-graph model. A run is a signed record in the
+/// git-metadata fanout keyed by `(config_oid, code_oid)`, so identical
+/// config/code pairs dedupe and skip re-execution. The privileged config lives
+/// on `refs/ents/config` and unprivileged drafts on `refs/ents/ci/draft/<name>`.
+/// None of this is wired up yet, so every panel is an empty state.
+pub(super) fn checks_page(meta: &RepoMeta) -> Markup {
repo_shell(
meta,
- Tab::Hooks,
- "Hooks",
+ Tab::Checks,
+ "Checks",
html! {
- div.page-header { h1.page-title { "Hooks" } }
+ div.page-header { h1.page-title { "Checks" } }
p.shell-note {
- "CI runs as plain "
- code { "pre-receive" } " / " code { "post-receive" }
- " git hooks, configured in " code { ".gitents/hooks.toml" }
- ". Runs and logs appear here once hooks execute on a push."
+ "CI runs are signed records in the git-metadata fanout, keyed by "
+ code { "(config_oid, code_oid)" }
+ ". Configuration is read from " code { "refs/ents/config" }
+ " and draft pipelines from " code { "refs/ents/ci/draft/*" }
+ ". Runs appear here once the runner records them."
}
- div.hooks-grid {
+ div.checks-grid {
div.card {
div.card-header { "Recent runs" }
div.card-row.muted { "No runs recorded yet." }
}
- @if let Some(config) = &config {
- div.card {
- div.card-header { (icon_file()) " .gitents/hooks.toml" }
- (blob_body("hooks.toml", config))
- }
- } @else {
- div.card {
- div.card-header { "Configuration" }
- div.card-row.muted { "No " code { ".gitents/hooks.toml" } " in this repository." }
+ div.card {
+ div.card-header { "Configuration" }
+ div.card-row.muted {
+ "No signed config on " code { "refs/ents/config" } " yet."
}
}
}
@@ -656,7 +651,7 @@
/// exist yet, so the controls reflect the repository's current real values and
/// are presented read-only.
pub(super) fn settings_page(meta: &RepoMeta) -> Markup {
- let name = meta.rel.rsplit('/').next().unwrap_or(&meta.rel);
+ let name = meta.name();
repo_shell(
meta,
Tab::Settings,
@@ -686,7 +681,7 @@
div.card-header { "Features" }
(feature_row("Bug reports", "Track and triage bugs.", meta.issues > 0))
(feature_row("Releases", "Publish tagged releases.", meta.releases > 0))
- (feature_row("Hooks (CI)", "Run git hooks on push.", meta.has_hooks))
+ (feature_row("Checks (CI)", "Run signed CI records on push.", false))
(feature_row("Wiki", "A separate documentation space.", false))
}