feat: list authorized signers on the repository Settings tab
commit
e7ea7e4feat: list authorized signers on the repository Settings tab
The Settings page now reads the refs/meta/auth signer set and renders
it as an Authorized signers card, mirroring git ents auth list.
Assisted-by: Claude:claude-opus-4-8
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents-server/src/web/mod.rs
@@ -73,7 +73,7 @@
Some((&"releases", &[])) => pages::releases_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(),
+ Some((&"settings", &[])) => pages::settings_page(repo, &meta).await.into_response(),
_ => not_found().into_response(),
}
}
crates/git-ents-server/src/web/pages.rs
@@ -703,8 +703,9 @@
/// The Settings tab. Persisting changes needs a config store that does not
/// 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 {
+pub(super) async fn settings_page(repo: &Path, meta: &RepoMeta) -> Markup {
let name = meta.name();
+ let signers = load_signers(repo).await;
repo_shell(
meta,
Tab::Settings,
@@ -738,6 +739,33 @@
(feature_row("Wiki", "A separate documentation space.", false))
}
+ div.card {
+ div.card-header {
+ "Authorized signers"
+ @if let Ok(signers) = &signers { span.count { (signers.len()) } }
+ }
+ p.shell-note {
+ "Keys on " code { "refs/meta/auth" } " whose signed pushes are accepted "
+ "(" code { "git ents auth list" } ")."
+ }
+ @match &signers {
+ Err(err) => div.card-row.muted { "Could not read signers: " (err) }
+ Ok(signers) if signers.is_empty() => {
+ div.card-row.muted {
+ "No authorized signers — pushes are open until the first key is added."
+ }
+ }
+ Ok(signers) => {
+ @for signer in signers {
+ div.card-row {
+ code { (signer.fingerprint) }
+ span.muted { (signer_label(&signer.key)) }
+ }
+ }
+ }
+ }
+ }
+
div.card {
div.card-header { "Visibility" }
(visibility_row("Public", "Anyone can read this repository.", true))
@@ -766,6 +794,29 @@
)
}
+/// Load the authorized signer set off the async runtime, since `signers::load`
+/// shells out to git and reads the object database synchronously.
+async fn load_signers(repo: &Path) -> Result<Vec<git_ents::signers::Signer>, String> {
+ let repo = repo.to_owned();
+ tokio::task::spawn_blocking(move || git_ents::signers::load(&repo))
+ .await
+ .map_err(|err| err.to_string())?
+ .map_err(|err| err.to_string())
+}
+
+/// A short label for a signer's key: its type and trailing comment, dropping the
+/// long base64 body that would not fit on the row.
+fn signer_label(key: &str) -> String {
+ let mut parts = key.split_whitespace();
+ let kind = parts.next().unwrap_or_default();
+ let comment = parts.nth(1).unwrap_or_default();
+ if comment.is_empty() {
+ kind.to_owned()
+ } else {
+ format!("{kind} · {comment}")
+ }
+}
+
/// A Features row with a static toggle reflecting `on`.
fn feature_row(title: &str, desc: &str, on: bool) -> Markup {
html! {