Commit
Message
Changed Files (7)
-
modified CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index a01e549..213394e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ### π Features - Add multi-format output support (HTML + Gemtext) + +### π Refactor + +- Split builder running & output rendering into own modules +- *(archive_builder)* Change output message format. ## [0.7.1] - 2026-06-16 ### π Features -
modified src/builders/archive.rs
diff --git a/src/builders/archive.rs b/src/builders/archive.rs index 84f82f3..4abd6d6 100644 --- a/src/builders/archive.rs +++ b/src/builders/archive.rs @@ -23,7 +23,7 @@ pub struct ArchiveBuilderConfig { pub source_dir: Option<PathBuf>, /// Output path for the generated `.tar.gz` archive. - /// Defaults to `source.tar.gz` in the current working directory. + /// Defaults to `{prefix}-{version}.tar.gz` in the current working directory. pub output: Option<PathBuf>, /// Prefix applied to every entry path inside the archive. @@ -62,7 +62,7 @@ impl Builder for ArchiveBuilder { async fn build( &self, config: Self::ConfigType, - _version: &str, + version: &str, log: LogSender, ) -> Result<Vec<ArtifactPath>> { let source_dir = config @@ -71,10 +71,6 @@ impl Builder for ArchiveBuilder { .canonicalize() .into_diagnostic()?; - let output = config - .output - .unwrap_or_else(|| PathBuf::from("../source.tar.gz")); - let prefix = config.prefix.unwrap_or_else(|| { source_dir .file_name() @@ -82,10 +78,14 @@ impl Builder for ArchiveBuilder { .unwrap_or_else(|| "source".to_owned()) }); + let output = config + .output + .unwrap_or_else(|| PathBuf::from(format!("{prefix}-{version}.tar.gz"))); + let ignore_set = build_ignore_set(&config.ignore_patterns)?; let _ = log.send(LogEvent::Line(format!( - "archiving {} β {}", + "{} β {}", source_dir.display(), output.display() ))); @@ -94,10 +94,6 @@ impl Builder for ArchiveBuilder { }) .await .into_diagnostic()??; - let _ = log.send(LogEvent::Line(format!( - "archive written: {}", - archive_path.display() - ))); let name = archive_path .file_name() -
modified src/builders/orchestrator.rs
diff --git a/src/builders/orchestrator.rs b/src/builders/orchestrator.rs index 84e1a3f..f11968f 100644 --- a/src/builders/orchestrator.rs +++ b/src/builders/orchestrator.rs @@ -5,6 +5,7 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use miette::{IntoDiagnostic, Result}; use tokio::sync::{mpsc, watch}; use tokio::task::JoinSet; +use tracing::{info, warn}; use crate::builders::{ArtifactPath, BuilderEntry, LogEvent}; use crate::cli::{COLOURS, GREEN, RED, RESET, YELLOW}; @@ -18,9 +19,6 @@ pub(crate) async fn run_builders( builders: &[BuilderEntry], version: &str, ) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { - let mut dist_artifacts = Vec::new(); - let mut doc_artifacts = Vec::new(); - let id_to_idx: HashMap<&str, usize> = builders .iter() .enumerate() @@ -86,6 +84,22 @@ pub(crate) async fn run_builders( } let total = builders.len(); + + if crate::utils::is_interactive() { + run_builders_with_bars(builders, version, completion_txs, completion_rxs, total).await + } else { + run_builders_with_logs(builders, version, completion_txs, completion_rxs, total).await + } +} + +/// Interactive variant: uses `indicatif` progress bars and spinners. +async fn run_builders_with_bars( + builders: &[BuilderEntry], + version: &str, + mut completion_txs: HashMap<String, watch::Sender<Option<bool>>>, + completion_rxs: HashMap<String, watch::Receiver<Option<bool>>>, + total: usize, +) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { let multi = MultiProgress::new(); let summary = multi.add(ProgressBar::new(total as u64)); @@ -221,6 +235,107 @@ pub(crate) async fn run_builders( }); } + collect_and_finish(join_set, &summary).await +} + +/// Non-interactive variant: logs progress via `tracing` instead of spinners. +async fn run_builders_with_logs( + builders: &[BuilderEntry], + version: &str, + mut completion_txs: HashMap<String, watch::Sender<Option<bool>>>, + completion_rxs: HashMap<String, watch::Receiver<Option<bool>>>, + total: usize, +) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { + info!("Running {total} builder(s) β¦"); + + let mut join_set: JoinSet<Result<Vec<ArtifactPath>>> = JoinSet::new(); + + for entry in builders.iter() { + let label = entry.id.as_deref().unwrap_or(entry.label()).to_owned(); + + let (log_tx, mut log_rx) = mpsc::unbounded_channel::<LogEvent>(); + + let log_label = label.clone(); + tokio::spawn(async move { + while let Some(event) = log_rx.recv().await { + match event { + LogEvent::Line(line) => info!("[{log_label}] {line}"), + LogEvent::ChildStart { label: child, .. } => { + info!("[{log_label}] [{child}] starting β¦"); + } + LogEvent::ChildLine { id, line } => { + info!("[{log_label}] [{id}] {line}"); + } + LogEvent::ChildFinish { + id, + success, + summary, + } => { + if success { + info!("[{log_label}] [{id}] {summary}"); + } else { + warn!("[{log_label}] [{id}] failed: {summary}"); + } + } + } + } + }); + + let dep_receivers: Vec<(String, watch::Receiver<Option<bool>>)> = entry + .depends_on + .iter() + .filter_map(|dep_id| { + completion_rxs + .get(dep_id) + .map(|rx| (dep_id.clone(), rx.clone())) + }) + .collect(); + + let my_tx: Option<watch::Sender<Option<bool>>> = + entry.id.as_ref().and_then(|id| completion_txs.remove(id)); + + let entry = entry.clone(); + let version = version.to_owned(); + let task_label = label.clone(); + + join_set.spawn(async move { + for (dep_id, mut rx) in dep_receivers { + info!("[{task_label}] waiting for '{dep_id}' β¦"); + + let resolved = rx.wait_for(|v| v.is_some()).await; + + let succeeded = match resolved { + Err(_) => false, + Ok(r) => r.unwrap_or(false), + }; + + if !succeeded { + info!("[{task_label}] skipped (dependency '{dep_id}' failed)"); + if let Some(tx) = &my_tx { + let _ = tx.send(Some(false)); + } + return Ok(vec![]); + } + } + + info!("[{task_label}] running β¦"); + let result = entry.build(&version, log_tx).await; + let succeeded = result.is_ok(); + + if let Some(tx) = my_tx { + let _ = tx.send(Some(succeeded)); + } + + match &result { + Ok(artifacts) => info!("[{task_label}] done ({} artifact(s))", artifacts.len()), + Err(e) => warn!("[{task_label}] failed: {e}"), + } + result + }); + } + + let mut dist_artifacts = Vec::new(); + let mut doc_artifacts = Vec::new(); let mut errors: Vec<miette::Report> = Vec::new(); while let Some(res) = join_set.join_next().await { match res.into_diagnostic()? { @@ -237,6 +352,44 @@ pub(crate) async fn run_builders( } } + if errors.is_empty() { + info!("All builders completed successfully."); + } else { + warn!("Some builders failed."); + } + + if let Some(first_err) = errors.into_iter().next() { + return Err(first_err); + } + + Ok((dist_artifacts, doc_artifacts)) +} + +/// Collect results from the `JoinSet` and finish the summary bar (interactive +/// variant only). +async fn collect_and_finish( + mut join_set: JoinSet<Result<Vec<ArtifactPath>>>, + summary: &ProgressBar, +) -> Result<(Vec<ArtifactPath>, Vec<ArtifactPath>)> { + let mut errors: Vec<miette::Report> = Vec::new(); + let mut dist_artifacts = Vec::new(); + let mut doc_artifacts = Vec::new(); + + while let Some(res) = join_set.join_next().await { + match res.into_diagnostic()? { + Ok(artifacts) => { + for artifact in artifacts { + if artifact.path.is_dir() { + doc_artifacts.push(artifact); + } else { + dist_artifacts.push(artifact); + } + } + } + Err(e) => errors.push(e), + } + } + let summary_msg = if errors.is_empty() { format!("{GREEN}\u{2713} all done{RESET}") } else { -
modified src/git_ui.rs
diff --git a/src/git_ui.rs b/src/git_ui.rs index 26c0b8f..0f902d7 100644 --- a/src/git_ui.rs +++ b/src/git_ui.rs @@ -27,7 +27,7 @@ use miette::{IntoDiagnostic, Result}; use crate::cli::{CYAN, GREEN, RED, RESET}; use serde::Serialize; use tera::{Context, Tera}; -use tracing::warn; +use tracing::{info, warn}; use crate::config::{AbbayeConfig, GitUiConfig}; @@ -148,7 +148,6 @@ struct BranchEntry { // ββ Public entry point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ -/// Generate the repository web UI and bare clone into `config.site.output_dir`. /// Shared spinner style - matches the builder spinners in `site.rs`. fn make_spinner(label: &str) -> ProgressBar { let pb = ProgressBar::new_spinner(); @@ -158,6 +157,53 @@ fn make_spinner(label: &str) -> ProgressBar { pb } +/// A lightweight progress reporter that either drives an `indicatif` spinner +/// (interactive terminals) or logs via `tracing::info!` (dumb / piped terminals). +enum Progress { + /// Interactive terminal β show a spinner. + Spinner(ProgressBar), + /// Non-interactive terminal β just log messages. + Log, +} + +impl Progress { + fn new(label: &str) -> Self { + if crate::utils::is_interactive() { + let pb = make_spinner(label); + pb.set_message("startingβ¦"); + Self::Spinner(pb) + } else { + info!("[{label}] starting β¦"); + Self::Log + } + } + + fn set_message(&self, msg: impl Into<String>) { + match self { + Self::Spinner(pb) => pb.set_message(msg.into()), + Self::Log => info!("[git ui] {}", msg.into()), + } + } + + fn finish_done(&self) { + match self { + Self::Spinner(pb) => { + pb.finish_with_message(format!("{GREEN}\u{2713} done{RESET}")); + } + Self::Log => info!("[git ui] done"), + } + } + + fn finish_failed(&self, detail: &miette::Report) { + match self { + Self::Spinner(pb) => { + pb.finish_with_message(format!("{RED}\u{2717} failed: {detail}{RESET}")); + } + Self::Log => warn!("[git ui] failed: {detail}"), + } + } +} + pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfig) -> Result<()> { let output_dir = &config.site.output_dir; let ui_dir = output_dir.join("repository"); @@ -269,10 +315,10 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi return Ok(()); } - let pb = make_spinner("git ui"); + let progress = Progress::new("git ui"); // ββ Bare clone + dumb HTTP setup ββββββββββββββββββββββββββββββββββββββββββ - pb.set_message("cloning bare repositoryβ¦"); + progress.set_message("cloning bare repositoryβ¦"); if let Err(e) = export_bare_clone( &repo_path, &bare_dir, @@ -282,7 +328,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi ) .await { - pb.finish_with_message(format!("{RED}\u{2717} failed:{RESET} {e}")); + progress.finish_failed(&e); return Err(e); } @@ -323,7 +369,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi }); // ββ Render one log page per branch (per format) βββββββββββββββββββββββββββ - pb.set_message("rendering log pagesβ¦"); + progress.set_message("rendering log pagesβ¦"); for (short_name, filename, commits) in &branch_pages { let truncated = commits.len() >= max_commits; @@ -381,7 +427,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi } // ββ Render per-commit detail pages (per format) βββββββββββββββββββββββββββ - pb.set_message(format!("rendering {} commit pagesβ¦", unique_commits.len())); + progress.set_message(format!("rendering {} commit pagesβ¦", unique_commits.len())); for commit_info in &unique_commits { let changed_files = get_changed_files(&commit_info.hash).await?; let has_browse = !commit_info.ref_badges.is_empty(); @@ -411,7 +457,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi // ββ Tree browser (browse/<hash>/) βββββββββββββββββββββββββββββββββββββββββ if !browse_revisions.is_empty() { - pb.set_message(format!( + progress.set_message(format!( "building browse pages for {} revision(s)β¦", browse_revisions.len() )); @@ -443,7 +489,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi .into_diagnostic()?? } - pb.finish_with_message(format!("{GREEN}\u{2713} done{RESET}")); + progress.finish_done(); Ok(()) } -
modified src/main.rs
diff --git a/src/main.rs b/src/main.rs index d9bf332..a93bd8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -309,9 +309,15 @@ async fn main() -> Result<()> { setup_panic!(); let cli_args = cli::CliArgs::parse(); + let default_level = match cli_args.verbose { + 0 => "warn", + 1 => "info", + _ => "debug", + }; + let log_filter = std::env::var("RUST_LOG").unwrap_or_else(|_| default_level.to_string()); tracing_subscriber::fmt() .with_timer(tracing_subscriber::fmt::time::SystemTime) - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_env_filter(tracing_subscriber::EnvFilter::builder().parse_lossy(log_filter)) .init(); match cli_args.command { @@ -361,6 +367,7 @@ async fn main() -> Result<()> { if repository_only { match &config.git_ui { Some(git_ui_cfg) => { + info!("Building repository UI only β¦"); git_ui::build_git_repository_ui(&config, git_ui_cfg).await?; } None => { @@ -370,10 +377,13 @@ async fn main() -> Result<()> { } } } else { + info!("Building site β¦"); site::build_site(config.clone()).await?; if let Some(git_ui_cfg) = &config.git_ui { + info!("Building repository UI β¦"); git_ui::build_git_repository_ui(&config, git_ui_cfg).await?; } + info!("Build complete."); } } cli::CliCommand::BuildAll => { -
modified src/site.rs
diff --git a/src/site.rs b/src/site.rs index e9749fd..563b4a0 100644 --- a/src/site.rs +++ b/src/site.rs @@ -9,7 +9,7 @@ use indicatif::ProgressStyle; use miette::{IntoDiagnostic, Result}; use sha2::{Digest, Sha256}; use tera::{Context, Tera}; -use tracing::warn; +use tracing::{info, warn}; use crate::{ changelog::ChangelogExtractor, @@ -97,10 +97,12 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { .into_diagnostic()?; // ββ 1. Version ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + info!("Extracting version β¦"); let version_info = config.version_extractor.extract().await?; let version = version_info.version.clone(); // ββ 2. Tera setup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ + info!("Setting up templates β¦"); let mut tera = Tera::default(); let theme_path = PathBuf::from(".abbaye").join("theme"); @@ -138,10 +140,12 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { } // ββ 3. Builders (run in parallel, respecting depends_on ordering) βββββββββ + info!("Running builders for version {version} β¦"); let (dist_artifacts, doc_artifacts) = crate::builders::orchestrator::run_builders(&config.builders, &version).await?; // ββ 4. Lay out dist/ ββββββββββββββββββββββββββββββββββββββββββββββββββββββ + info!("Laying out {} dist artifact(s) β¦", dist_artifacts.len()); let version_dir = output_dir.join(&version); let dist_dir = version_dir.join("dist"); tokio::fs::create_dir_all(&dist_dir) @@ -171,6 +175,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { let has_dist = !dist_file_infos.is_empty(); // ββ 5. Lay out docs/ ββββββββββββββββββββββββββββββββββββββββββββββββββββββ + info!("Laying out {} doc artifact(s) β¦", doc_artifacts.len()); let has_docs = !doc_artifacts.is_empty(); let has_docs_tarball; @@ -247,6 +252,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { }; // ββ 8. Version pages (per format) βββββββββββββββββββββββββββββββββββββββββ + info!("Rendering version pages β¦"); // Git UI integration: derive the clone URL once and compute the tag name // for the current version so the templates can link into the repository UI. @@ -306,6 +312,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { } // ββ 9. Root index.html + Atom feed ββββββββββββββββββββββββββββββββββββββ + info!("Rendering root index and Atom feed β¦"); // Collect every known version from the version extractor, ensure the // current version is present (handles untagged builds), then sort newest-first. let mut all_versions = config.version_extractor.extract_all().await?; -
modified src/utils.rs
diff --git a/src/utils.rs b/src/utils.rs index 021aba2..d5a3793 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +use std::io::IsTerminal; use std::path::{Path, PathBuf}; use flate2::Compression; @@ -83,3 +84,13 @@ pub(crate) fn archive_dir(src: &Path, dest: &Path) -> Result<()> { .into_diagnostic()?; Ok(()) } + +/// Check whether the program is running in an interactive terminal suitable +/// for progress bars and spinners. +/// +/// Returns `false` when stdout is not a terminal (piped to a file or another +/// process) or when `TERM` is set to `dumb`. This lets callers gracefully +/// fall back to plain log output instead of using ANSI-spinner-based UI. +pub fn is_interactive() -> bool { + std::io::stdout().is_terminal() && std::env::var("TERM").ok().as_deref() != Some("dumb") +}