//! Body-input resolution shared by every verb that posts a body. //! Matches the bash `resolve_body` helper (#382): exactly one source //! between `--body`, `--body-file`, and piped stdin. Passing both //! `--body` and `--body-file` is a clear error. use std::io::{IsTerminal, Read}; use anyhow::{Context, Result, bail}; /// Resolve the body to send, given the user's explicit flags and the /// current stdin state. Returns `None` when none of the sources are /// available (the caller decides whether that's allowed — e.g. /// `issue-edit` treats absent body as "leave unchanged", while /// `comment` treats absent body as a hard error). pub fn resolve(body: Option<&str>, file: Option<&str>) -> Result> { if body.is_some() && file.is_some() { bail!("hive-forge: --body and --body-file are mutually exclusive"); } if let Some(b) = body { return Ok(Some(b.to_owned())); } if let Some(path) = file { if path == "-" { return Ok(Some(read_stdin().context("read stdin for --body-file -")?)); } let s = std::fs::read_to_string(path) .with_context(|| format!("read --body-file {path}"))?; return Ok(Some(s)); } if !std::io::stdin().is_terminal() { return Ok(Some(read_stdin().context("read piped stdin")?)); } Ok(None) } /// Resolve, then require a non-empty result (whitespace-trimmed). /// Used by `comment` / `comment-edit` which refuse to post empty /// bodies. pub fn resolve_required(body: Option<&str>, file: Option<&str>, verb: &str) -> Result { let resolved = resolve(body, file)?; let Some(s) = resolved else { bail!( "hive-forge {verb}: no body — pass --body , --body-file , or pipe via stdin" ); }; if s.trim().is_empty() { bail!("hive-forge {verb}: refusing to post empty body"); } Ok(s) } fn read_stdin() -> std::io::Result { let mut s = String::new(); std::io::stdin().read_to_string(&mut s)?; Ok(s) }