From c5a36e994f1541e553aa18b3f11623d69971ca51 Mon Sep 17 00:00:00 2001 From: gl0w Date: Thu, 10 Sep 2026 06:40:22 +0100 Subject: [PATCH] m6/header-config: repo-name + description flags/env (119 tests) --- DRAFT.md | 5 +- Dockerfile | 4 + entrypoint.sh | 9 ++ src/agents.rs | 11 +- src/build.rs | 115 +++++++++++------ src/commits.rs | 28 ++-- src/feed.rs | 39 ++++-- src/forge.rs | 88 +++++++++---- src/inbox.rs | 23 +++- src/layout.rs | 47 +++++-- src/main.rs | 27 +++- src/releases.rs | 24 +++- src/wiki.rs | 31 +++-- tests/header_config.rs | 283 +++++++++++++++++++++++++++++++++++++++++ 14 files changed, 619 insertions(+), 115 deletions(-) create mode 100644 tests/header_config.rs diff --git a/DRAFT.md b/DRAFT.md index fe17448..c0527b1 100644 --- a/DRAFT.md +++ b/DRAFT.md @@ -205,7 +205,10 @@ corners only (`border-radius: 0`). Semantic HTML so `curl` + reader-mode ``` hu13 build /data/repo -o /srv/www [--branch X] [--clone-url URL] - [--title S] [--max-log 100] + [--repo-name S] [--description S] [--max-log 100] + # repo-name default: HU13_REPO_NAME else repo dir name (never blank; + # --title still works as a deprecated alias) + # description default: HU13_DESCRIPTION else hidden (no empty

) # clone-url default: git remote get-url origin at build, else none # caps tunable: --max-title 300 --max-body 20000 --max-bytes 32768 # --rate-per-hour 2 --quota 500 (or HU13_* env) diff --git a/Dockerfile b/Dockerfile index 31b427c..f3997b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,6 +30,10 @@ RUN apt-get update \ COPY --from=builder /src/target/release/hu13 /usr/local/bin/hu13 COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh +# Header config defaults (m6, issues #21 + #24): empty means fallback — +# repo dir name for the name (never blank), hidden line for the description. +ENV HU13_REPO_NAME="" +ENV HU13_DESCRIPTION="" USER hu13 EXPOSE 39313 ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh index 3cc51e5..6d4f149 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -8,6 +8,9 @@ # Env: # HU13_BRANCH branch to build (default: main/master auto-detect) # HU13_CLONE_URL clone URL to advertise (default: repo origin remote) +# HU13_REPO_NAME repo display name (default: repo dir name; never blank) +# HU13_DESCRIPTION +# site description (default: hidden, no empty

) # HU13_LISTEN serve address (default: 0.0.0.0:39313) # HU13_MAX_TITLE / HU13_MAX_BODY / HU13_MAX_BYTES / # HU13_RATE_PER_HOUR / HU13_QUOTA @@ -30,6 +33,12 @@ fi if [ -n "${HU13_CLONE_URL:-}" ]; then BUILD_ARGS="$BUILD_ARGS --clone-url $HU13_CLONE_URL" fi +if [ -n "${HU13_REPO_NAME:-}" ]; then + BUILD_ARGS="$BUILD_ARGS --repo-name $HU13_REPO_NAME" +fi +if [ -n "${HU13_DESCRIPTION:-}" ]; then + BUILD_ARGS="$BUILD_ARGS --description $HU13_DESCRIPTION" +fi # shellcheck disable=SC2086 hu13 build /data/repo -o /srv/www $BUILD_ARGS diff --git a/src/agents.rs b/src/agents.rs index 67caf5a..f0f3646 100644 --- a/src/agents.rs +++ b/src/agents.rs @@ -15,7 +15,7 @@ use std::path::Path; use crate::build::markdown_to_html; -use crate::layout::{esc, page}; +use crate::layout::{esc, page_site}; /// License grant quoted verbatim from `DRAFT.md` (receiver-friendly v1). /// Single source: [`crate::inbox::LICENSE_TEXT`] — the forms, this page, @@ -103,6 +103,7 @@ pub fn render( clone_url: Option<&str>, branch: &str, short_sha: &str, + description: Option<&str>, ) -> Result<(), String> { let site_e = esc(site_name); let branch_e = esc(branch); @@ -113,7 +114,13 @@ pub fn render( // source so the raw `agents.md` and rendered `agents.html` agree. The // markdown renderer escapes raw HTML again, so `&` etc. stay safe. let html_body = markdown_to_html(&md); - let html = page(&format!("agents — {site_name}"), nav, &html_body); + let html = page_site( + &format!("agents — {site_name}"), + nav, + &html_body, + site_name, + description, + ); std::fs::create_dir_all(out).map_err(|e| format!("hu13: create out dir: {e}"))?; std::fs::write(out.join("agents.html"), html) .map_err(|e| format!("hu13: write agents.html: {e}"))?; diff --git a/src/build.rs b/src/build.rs index 09c8f51..d743377 100644 --- a/src/build.rs +++ b/src/build.rs @@ -33,12 +33,60 @@ pub struct BuildOptions { pub branch_flag: Option, /// `--clone-url` flag (else git origin remote, else none). pub clone_url_flag: Option, - /// `--title` site title override (else repo dir name). - pub title_override: Option, + /// `--repo-name` flag (else `$HU13_REPO_NAME`, else repo dir name). + /// `--title` is accepted as a deprecated alias for the same field. + /// Empty/whitespace always falls back (the header is never blank). + pub repo_name_flag: Option, + /// `--description` flag (else `$HU13_DESCRIPTION`, else none). + /// Empty/whitespace means absent: the header description line is + /// hidden entirely (no empty `

`). + pub description_flag: Option, /// `--max-log`: max commits listed in `commits.html` (default 100). pub max_log: usize, } +/// Resolve the repo display name (m6, f0rge issue #24). +/// +/// Precedence: `--repo-name` flag > `$HU13_REPO_NAME` > directory name of +/// `repo` > `"repo"`. Flag and env values are trimmed; empty values fall +/// through so the header is never blank. +#[must_use] +pub fn resolve_repo_name(repo: &Path, flag: Option<&str>) -> String { + if let Some(f) = flag.map(str::trim).filter(|s| !s.is_empty()) { + return f.to_string(); + } + if let Ok(env) = std::env::var("HU13_REPO_NAME") { + let t = env.trim(); + if !t.is_empty() { + return t.to_string(); + } + } + repo.file_name() + .map(|n| n.to_string_lossy().into_owned()) + .map(|n| n.trim().to_string()) + .filter(|n| !n.is_empty()) + .unwrap_or_else(|| "repo".to_string()) +} + +/// Resolve the site description (m6, f0rge issue #21). +/// +/// Precedence: `--description` flag > `$HU13_DESCRIPTION` > absent +/// (`None`). Flag and env values are trimmed; empty values fall through +/// to absent, and an absent description hides the header line entirely. +#[must_use] +pub fn resolve_description(flag: Option<&str>) -> Option { + if let Some(f) = flag.map(str::trim).filter(|s| !s.is_empty()) { + return Some(f.to_string()); + } + if let Ok(env) = std::env::var("HU13_DESCRIPTION") { + let t = env.trim(); + if !t.is_empty() { + return Some(t.to_string()); + } + } + None +} + /// Summary of one successful build (for logs/tests). #[derive(Debug, Clone)] pub struct BuildSummary { @@ -181,14 +229,9 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result t.trim().to_string(), - _ => repo - .file_name() - .map(|n| n.to_string_lossy().into_owned()) - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| "repo".to_string()), - }; + let name = resolve_repo_name(repo, opts.repo_name_flag.as_deref()); + let description = resolve_description(opts.description_flag.as_deref()); + let desc_opt = description.as_deref(); // Walk the whole tree (dirs-first order from gitx), remembering each // dir's entries so dir pages need no second pass. @@ -216,21 +259,6 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result 200 { - l[..200].to_string() - } else { - l.to_string() - } - }) - .filter(|l| !l.is_empty()) - }); std::fs::create_dir_all(out.join("files")) .map_err(|e| format!("hu13: create files dir: {e}"))?; @@ -297,7 +325,8 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result\n"); - let html = crate::layout::page(&format!("{dir} — {name}"), &nav, &body); + let html = + crate::layout::page_site(&format!("{dir} — {name}"), &nav, &body, &name, desc_opt); let dest = out.join("files").join(format!("{dir}.html")); if let Some(parent) = dest.parent() { std::fs::create_dir_all(parent) @@ -349,7 +378,7 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result Result Result

{}

\n", esc(&name)); - match &description { - Some(d) => body.push_str(&format!("

{}

\n", esc(d))), - None => body.push_str("

No description.

\n"), + if let Some(d) = &description { + body.push_str(&format!("

{}

\n", esc(d))); } if let Some(url) = &clone_url { body.push_str(&format!( @@ -455,7 +491,7 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result\n"); - let index = crate::layout::page(&name, &nav, &body); + let index = crate::layout::page_site(&name, &nav, &body, &name, desc_opt); std::fs::write(out.join("index.html"), index) .map_err(|e| format!("hu13: write index.html: {e}"))?; @@ -518,7 +554,8 @@ mod tests { BuildOptions { branch_flag: None, clone_url_flag: None, - title_override: None, + repo_name_flag: None, + description_flag: None, max_log: 100, } } diff --git a/src/commits.rs b/src/commits.rs index a98c017..5abea19 100644 --- a/src/commits.rs +++ b/src/commits.rs @@ -8,7 +8,7 @@ use std::path::Path; use crate::gitx; -use crate::layout::{esc, page}; +use crate::layout::{esc, page_site}; /// Short display form of a full sha (first 7 hex chars, git-style). #[must_use] @@ -30,6 +30,7 @@ pub fn render_loaded( log: &[gitx::Commit], site_name: &str, nav: &str, + description: Option<&str>, ) -> Result { let mut body = String::from("

Commits

\n"); if log.is_empty() { @@ -48,7 +49,13 @@ pub fn render_loaded( } body.push_str("\n\n"); } - let html = page(&format!("commits — {site_name}"), nav, &body); + let html = page_site( + &format!("commits — {site_name}"), + nav, + &body, + site_name, + description, + ); std::fs::create_dir_all(out).map_err(|e| format!("hu13: create out dir: {e}"))?; std::fs::write(out.join("commits.html"), html) .map_err(|e| format!("hu13: write commits.html: {e}"))?; @@ -67,10 +74,12 @@ pub fn render_loaded( esc(&message), esc(&diff), ); - let html = page( + let html = page_site( &format!("commit {} — {site_name}", short_sha(&c.sha)), nav, &detail, + site_name, + description, ); std::fs::write(out.join("commit").join(format!("{}.html", c.sha)), html) .map_err(|e| format!("hu13: write commit page {}: {e}", short_sha(&c.sha)))?; @@ -90,9 +99,10 @@ pub fn render( max_log: usize, site_name: &str, nav: &str, + description: Option<&str>, ) -> Result { let log = load(repo, branch, max_log)?; - render_loaded(repo, out, &log, site_name, nav) + render_loaded(repo, out, &log, site_name, nav, description) } #[cfg(test)] @@ -169,7 +179,7 @@ mod tests { fn list_order_newest_first_with_links() { let repo = fixture_repo(); let out = mktemp("out"); - let n = render(&repo, &out, "main", 100, "test", "