use std::path::PathBuf;
use crate::core::{Context, FlagError};
use clap::{Args, CommandFactory};
#[derive(Args, Debug)]
pub struct ManArgs {
#[arg(short = 'o', long = "output", value_name = "DIR")]
pub output: PathBuf,
}
pub fn run(ctx: &Context, args: ManArgs) -> anyhow::Result<()> {
std::fs::create_dir_all(&args.output).map_err(|e| {
FlagError::new(format!(
"could not create output directory {}: {e}",
args.output.display()
))
})?;
let pages = render_pages();
let count = pages.len();
for (name, roff) in pages {
let path = args.output.join(&name);
std::fs::write(&path, roff)
.map_err(|e| FlagError::new(format!("could not write {}: {e}", path.display())))?;
}
ctx.io.println(&format!(
"Wrote {count} man pages to {}",
args.output.display()
));
Ok(())
}
fn render_pages() -> Vec<(String, Vec<u8>)> {
let root = crate::cli::Cli::command();
let mut pages = Vec::new();
render_into(&root, "", &mut pages);
pages
}
fn render_into(cmd: &clap::Command, prefix: &str, pages: &mut Vec<(String, Vec<u8>)>) {
let name = if prefix.is_empty() {
cmd.get_name().to_owned()
} else {
format!("{prefix}-{}", cmd.get_name())
};
let mut roff = Vec::new();
let static_name: &'static str = String::leak(name.clone());
let cmd_full = cmd.clone().name(static_name);
let _ = clap_mangen::Man::new(cmd_full).render(&mut roff);
pages.push((format!("{name}.1"), roff));
for sub in cmd.get_subcommands() {
render_into(sub, &name, pages);
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::api::testing::FakeTransport;
use crate::config::FileConfig;
use crate::core::{ConfigProvider, GitClient, Transport};
use crate::git::{ShellGit, StubRunner};
use super::*;
use crate::testsupport::{test_context, ScriptedPrompter};
#[test]
fn render_pages_covers_root_and_nested_commands() {
let pages = render_pages();
let names: Vec<&str> = pages.iter().map(|(n, _)| n.as_str()).collect();
for expected in ["bb.1", "bb-pr.1", "bb-pr-create.1", "bb-completion.1"] {
assert!(
names.contains(&expected),
"missing {expected}; got {names:?}"
);
}
for (name, roff) in &pages {
assert!(!roff.is_empty(), "{name} is empty");
let text = String::from_utf8_lossy(roff);
assert!(text.contains(".TH"), "{name} missing .TH header");
}
}
#[test]
fn nested_page_title_uses_full_dashed_name() {
let pages = render_pages();
let (_, roff) = pages
.iter()
.find(|(n, _)| n == "bb-pr-create.1")
.expect("bb-pr-create.1 page");
let text = String::from_utf8_lossy(roff).replace('\\', "");
let lower = text.to_lowercase();
assert!(
lower.contains("bb-pr-create"),
"title/name should use the full dashed path; got:\n{text}"
);
let th_line = text
.lines()
.find(|l| l.starts_with(".TH"))
.expect(".TH line");
assert!(
th_line.to_lowercase().contains("bb-pr-create"),
".TH title should be the full dashed path; got: {th_line}"
);
}
#[test]
fn run_writes_files_and_reports_count() {
let transport: Arc<dyn Transport> = Arc::new(FakeTransport::new());
let git: Arc<dyn GitClient> = Arc::new(ShellGit::new(Arc::new(StubRunner::new())));
let config: Arc<dyn ConfigProvider> = Arc::new(FileConfig::blank());
let prompter = Arc::new(ScriptedPrompter::new());
let (ctx, bufs) = test_context(transport, git, config, prompter, false);
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("man");
run(
&ctx,
ManArgs {
output: out.clone(),
},
)
.unwrap();
assert!(out.join("bb.1").is_file(), "bb.1 should exist");
assert!(out.join("bb-pr-create.1").is_file(), "nested page exists");
let n = std::fs::read_dir(&out).unwrap().count();
assert!(
bufs.stdout_string()
.contains(&format!("Wrote {n} man pages")),
"summary should report the count"
);
}
}