Commit
Message
Changed Files (7)
-
modified CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9a61e..f17575b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ### π Features - Add a static UI for the repository. + +### π Bug Fixes + +- Write site.css into theme_path/static, instead of theme_path ## [0.6.1] - 2026-06-04 ### π Features -
modified README.md
diff --git a/README.md b/README.md index fb32f67..433c7ef 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Here's an example file structure: . βββ index.html # the main page of the website, enabling choosing a version, defaults to "latest" (contains a list of available versions and a iframe to the selected version?) βββ releases.feed # the RSS feed of the releases +βββ repository/ # the directory containing the repository UI (if enabled in config) +βββ repository.git # Clonable git repository βββ latest -> v2.0.0 # symlink to the latest version (biggest version number) βββ v1.0.0/ # the directory containing the version 1.0.0 of the software β βββ index.html # the main page of the version 1.0.0, from the README.md file. @@ -84,9 +86,11 @@ type = "cargo_doc" no_deps = true # Don't include dependencies in the documentation [[builders]] -type = "archive" # creates a compressed tarball of the source code (can be of anything, really) +# creates a compressed tarball of the source code (can be of anything, really) +type = "archive" [[builders]] +# Just an example dumb script to showcase the `script` builder type type = "script" script = [ "echo $ABBAYE_BUILDING_VERSION > .version", @@ -100,19 +104,41 @@ Now you can copy the contents of `public/` to your web server to deploy the site To have a look at all the available configuration options, please refer to the documentation of [`config::AbbayeConfig`]. +#### A note on the repository UI + +The repository UI is **NOT** enabled by default. It must be enabled explicitly in the configuration with the following: + +```toml +[git_ui] # only this section is needed to enable the repository UI +``` +It has multiple options (each presented with their defaults, if any): + +```toml +[git_ui] +default_branch = "main" +max_commits = 200 +repo_path = "." +clone_url = "{{ site.base_url }}/repository.git" +``` + +When the git UI is enabled, pages permitting browsing the tree of the repository are available, _but only for the branches tips and the tags_, as to not generate too much content. It already is quite a lot of content to generate for a simple repository UI (about 10MBs for Abbaye's repository at the time of writing). + ### β¨ Customization β¨ You can dump the default theme/templates to your local filesystem with `abbaye dump-theme`. This will create a `.abbaye/theme/` directory in your current directory with the default templates, which you can then β¨customizeβ¨. -If this directory contains a `static/` directory, it will be copied to the output directory. So you can add custom static assets to your site, and even use a separate CSS! +If you don't want to customize every template, simply delete the ones you don't want to change. + +In `.abbaye/theme/static/`, you will find the CSS sheet used by the default theme, across all templates. Editing this file will change the look and feel of all your site's pages. ## Future plans - [x] Add support for theming - [ ] Add support for more site variables, such as the site title, description, and author, or even a custom footer and stuff. -- [ ] Add support for a `self-update`-like command to update the abbaye binary to the latest version. The mechanisms put in place for this goal should be usable to any user of `abbaye`. + - I added OpenGraph support, does that count? +- [x] Add support for a `self-update`-like command to update the abbaye binary to the latest version. The mechanisms put in place for this goal should be usable to any user of `abbaye`. ## Contributing -
modified src/builders/markdown.rs
diff --git a/src/builders/markdown.rs b/src/builders/markdown.rs index 79a973a..94f9fac 100644 --- a/src/builders/markdown.rs +++ b/src/builders/markdown.rs @@ -127,7 +127,8 @@ impl Builder for MarkdownBuilder { /// [`TEMPLATE_MARKDOWN`] constant β exactly the same override mechanism /// used by the site templates (`root_index.html.j2` / `version_index.html.j2`). fn load_tera() -> Result<Tera> { - let theme_file = PathBuf::from(".abbaye").join("theme").join(THEME_FILENAME); + let theme_path = PathBuf::from(".abbaye").join("theme"); + let theme_file = theme_path.join(THEME_FILENAME); let mut tera = Tera::default(); if theme_file.is_file() { tera.add_template_file(&theme_file, Some(TERA_NAME)) @@ -136,6 +137,7 @@ fn load_tera() -> Result<Tera> { tera.add_raw_template(TERA_NAME, TEMPLATE_MARKDOWN) .into_diagnostic()?; } + crate::site::load_extra_theme_templates(&mut tera, &theme_path, &[TERA_NAME])?; Ok(tera) } -
modified src/cli.rs
diff --git a/src/cli.rs b/src/cli.rs index dcaf3bb..180d9f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -3,6 +3,18 @@ use std::path::PathBuf; use clap::Parser; use clap::Subcommand; +pub const RESET: &str = "\x1b[0m"; + +pub const RED: &str = "\x1b[31m"; +pub const GREEN: &str = "\x1b[32m"; +pub const YELLOW: &str = "\x1b[33m"; +pub const BLUE: &str = "\x1b[34m"; +pub const MAGENTA: &str = "\x1b[35m"; +pub const CYAN: &str = "\x1b[36m"; + +/// Array to cycle through ANSI color escape codes for builder spinner prefixes. +pub const COLOURS: &[&str] = &[CYAN, GREEN, YELLOW, MAGENTA, BLUE, RED]; + #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] pub struct CliArgs { -
modified src/git_ui.rs
diff --git a/src/git_ui.rs b/src/git_ui.rs index 5afe8e6..df48aef 100644 --- a/src/git_ui.rs +++ b/src/git_ui.rs @@ -16,10 +16,14 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::time::Duration; use chrono::{DateTime, Utc}; use gix::bstr::ByteSlice; +use indicatif::{ProgressBar, ProgressStyle}; use miette::{IntoDiagnostic, Result}; + +use crate::cli::{CYAN, GREEN, RED, RESET}; use serde::Serialize; use tera::{Context, Tera}; use tracing::warn; @@ -164,6 +168,19 @@ 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(); + pb.set_style( + ProgressStyle::with_template("{spinner:.bold} {prefix} {msg}") + .expect("valid template") + .tick_chars("β β β Ήβ Έβ Όβ ΄β ¦β §β β "), + ); + pb.set_prefix(format!("{CYAN}[{label}]{RESET}")); + pb.enable_steady_tick(Duration::from_millis(100)); + pb +} + 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"); @@ -262,8 +279,14 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi return Ok(()); } + let pb = make_spinner("git ui"); + // ββ Bare clone + dumb HTTP setup ββββββββββββββββββββββββββββββββββββββββββ - export_bare_clone(&repo_path, &bare_dir).await?; + pb.set_message("cloning bare repositoryβ¦"); + if let Err(e) = export_bare_clone(&repo_path, &bare_dir).await { + pb.finish_with_message(format!("{RED}\u{2717} failed:{RESET} {e}")); + return Err(e); + } // ββ Tera setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ let mut tera = Tera::default(); @@ -281,6 +304,11 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi tera.add_raw_template(name, builtin).into_diagnostic()?; } } + crate::site::load_extra_theme_templates( + &mut tera, + &theme_path, + &["git_log.html", "git_commit.html", "git_refs.html"], + )?; // ββ Branch switcher nav (shared across all log pages) βββββββββββββββββββββ // @@ -296,6 +324,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi }); // ββ Render one log page per branch ββββββββββββββββββββββββββββββββββββββββ + pb.set_message("rendering log pagesβ¦"); for (short_name, filename, commits) in &branch_pages { let truncated = commits.len() >= max_commits; @@ -341,6 +370,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi } // ββ Render per-commit detail pages ββββββββββββββββββββββββββββββββββββββββ + pb.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(); @@ -367,6 +397,10 @@ 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!( + "building browse pages for {} revision(s)β¦", + browse_revisions.len() + )); let browse_dir = ui_dir.join("browse"); tokio::fs::create_dir_all(&browse_dir) .await @@ -390,9 +424,10 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi ) }) .await - .into_diagnostic()??; + .into_diagnostic()?? } + pb.finish_with_message(format!("{GREEN}\u{2713} done{RESET}")); Ok(()) } @@ -729,30 +764,36 @@ fn parse_diff_output(text: &str) -> Vec<ChangedFile> { // ββ Bare clone export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ async fn export_bare_clone(source: &Path, dest: &Path) -> Result<()> { + use tokio::process::Command; + if dest.exists() { tokio::fs::remove_dir_all(dest).await.into_diagnostic()?; } - let status = tokio::process::Command::new("git") + let status = Command::new("git") .arg("clone") .arg("--bare") .arg(source) .arg(dest) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) .status() .await .into_diagnostic()?; if !status.success() { return Err(miette::miette!( - "git clone --bare failed with status {status}" + "git clone --bare failed with exit status {status}" )); } // Enable dumb HTTP transport so the bare repo is clonable over plain HTTPS. - let status = tokio::process::Command::new("git") + let status = Command::new("git") .arg("-C") .arg(dest) .arg("update-server-info") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) .status() .await .into_diagnostic()?; @@ -801,6 +842,11 @@ fn build_browse_pages( .map_err(|e| miette::miette!("{e}"))?; } } + crate::site::load_extra_theme_templates( + &mut tera, + theme_path, + &["git_tree.html", "git_blob.html"], + )?; for (hash, oid) in revisions { let rev_dir = browse_dir.join(hash); -
modified src/main.rs
diff --git a/src/main.rs b/src/main.rs index 3d56fb8..e4a15e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ //! . //! βββ index.html # the main page of the website, enabling choosing a version, defaults to "latest" (contains a list of available versions and a iframe to the selected version?) //! βββ releases.feed # the RSS feed of the releases +//! βββ repository/ # the directory containing the repository UI (if enabled in config) +//! βββ repository.git # Clonable git repository //! βββ latest -> v2.0.0 # symlink to the latest version (biggest version number) //! βββ v1.0.0/ # the directory containing the version 1.0.0 of the software //! β βββ index.html # the main page of the version 1.0.0, from the README.md file. @@ -84,9 +86,11 @@ //! no_deps = true # Don't include dependencies in the documentation //! //! [[builders]] -//! type = "archive" # creates a compressed tarball of the source code (can be of anything, really) +//! # creates a compressed tarball of the source code (can be of anything, really) +//! type = "archive" //! //! [[builders]] +//! # Just an example dumb script to showcase the `script` builder type //! type = "script" //! script = [ //! "echo $ABBAYE_BUILDING_VERSION > .version", @@ -100,19 +104,41 @@ //! //! To have a look at all the available configuration options, please refer to the documentation of [`config::AbbayeConfig`]. //! +//! #### A note on the repository UI +//! +//! The repository UI is **NOT** enabled by default. It must be enabled explicitly in the configuration with the following: +//! +//! ```toml +//! [git_ui] # only this section is needed to enable the repository UI +//! ``` +//! It has multiple options (each presented with their defaults, if any): +//! +//! ```toml +//! [git_ui] +//! default_branch = "main" +//! max_commits = 200 +//! repo_path = "." +//! clone_url = "{{ site.base_url }}/repository.git" +//! ``` +//! +//! When the git UI is enabled, pages permitting browsing the tree of the repository are available, _but only for the branches tips and the tags_, as to not generate too much content. It already is quite a lot of content to generate for a simple repository UI (about 10MBs for Abbaye's repository at the time of writing). +//! //! ### β¨ Customization β¨ //! //! You can dump the default theme/templates to your local filesystem with `abbaye dump-theme`. //! //! This will create a `.abbaye/theme/` directory in your current directory with the default templates, which you can then β¨customizeβ¨. //! -//! If this directory contains a `static/` directory, it will be copied to the output directory. So you can add custom static assets to your site, and even use a separate CSS! +//! If you don't want to customize every template, simply delete the ones you don't want to change. +//! +//! In `.abbaye/theme/static/`, you will find the CSS sheet used by the default theme, across all templates. Editing this file will change the look and feel of all your site's pages. //! //! ## Future plans //! //! - [x] Add support for theming //! - [ ] Add support for more site variables, such as the site title, description, and author, or even a custom footer and stuff. -//! - [ ] Add support for a `self-update`-like command to update the abbaye binary to the latest version. The mechanisms put in place for this goal should be usable to any user of `abbaye`. +//! - I added OpenGraph support, does that count? +//! - [x] Add support for a `self-update`-like command to update the abbaye binary to the latest version. The mechanisms put in place for this goal should be usable to any user of `abbaye`. //! //! ## Contributing //! @@ -140,7 +166,8 @@ use crate::{ pub mod builders; /// Parses the changelog file and generates a changelog page for the site. pub mod changelog; -mod cli; +/// Stuff related to the CLI interface. Also contains colour escape codes for terminal output. +pub mod cli; /// Handles the `abbaye.toml` configuration file. pub mod config; /// Generates a static git web UI and clonable bare repository. -
modified src/site.rs
diff --git a/src/site.rs b/src/site.rs index a6e6141..8fdfed1 100644 --- a/src/site.rs +++ b/src/site.rs @@ -110,6 +110,11 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { tera.add_raw_template("version_index.html", TEMPLATE_VERSION_INDEX) .into_diagnostic()?; } + load_extra_theme_templates( + &mut tera, + &theme_path, + &["root_index.html", "version_index.html"], + )?; // Write shared CSS to static/ so the git UI templates can reference it. { let static_dir = output_dir.join("static"); @@ -130,16 +135,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { let mut doc_artifacts = Vec::new(); { - // Colour palette: ANSI foreground codes cycled across builders. - const COLORS: &[&str] = &[ - "\x1b[36m", // cyan - "\x1b[32m", // green - "\x1b[33m", // yellow - "\x1b[35m", // magenta - "\x1b[34m", // blue - "\x1b[31m", // red - ]; - const RESET: &str = "\x1b[0m"; + use crate::cli::{COLOURS, GREEN, RED, RESET, YELLOW}; // ββ Dependency validation βββββββββββββββββββββββββββββββββββββββββββββ // @@ -233,7 +229,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { JoinSet::new(); for (i, entry) in config.builders.iter().enumerate() { - let color = COLORS[i % COLORS.len()]; + let color = COLOURS[i % COLOURS.len()]; let label = entry.id.as_deref().unwrap_or(entry.label()); let colored_prefix = format!("{color}[{label}]{RESET}"); @@ -269,7 +265,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { pb_log.set_message(line); } LogEvent::ChildStart { id, label } => { - let child_color = COLORS[child_color_idx % COLORS.len()]; + let child_color = COLOURS[child_color_idx % COLOURS.len()]; child_color_idx += 1; let child_pb = multi_log.insert_after(&last_child_pb, ProgressBar::new_spinner()); @@ -297,11 +293,11 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { if let Some(child_pb) = child_pbs.remove(&id) { if success { child_pb.finish_with_message(format!( - "\x1b[32m\u{2713}\x1b[0m {summary}" + "{GREEN}\u{2713}{RESET} {summary}" )); } else { child_pb.finish_with_message(format!( - "\x1b[31m\u{2717}\x1b[0m {summary}" + "{RED}\u{2717}{RESET} {summary}" )); } } @@ -348,7 +344,7 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { if !succeeded { summary_task.inc(1); pb_task.finish_with_message(format!( - "\x1b[33m\u{29B8} skipped\x1b[0m (dependency '{dep_id}' failed)" + "{YELLOW}\u{29B8} skipped{RESET} (dependency '{dep_id}' failed)" )); if let Some(tx) = &my_tx { let _ = tx.send(Some(false)); @@ -371,11 +367,11 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { summary_task.inc(1); match &result { Ok(artifacts) => pb_task.finish_with_message(format!( - "\x1b[32m\u{2713} done\x1b[0m ({} artifact(s))", + "{GREEN}\u{2713} done{RESET} ({} artifact(s))", artifacts.len() )), Err(e) => { - pb_task.finish_with_message(format!("\x1b[31m\u{2717} failed:\x1b[0m {e}")) + pb_task.finish_with_message(format!("{RED}\u{2717} failed:{RESET} {e}")) } } result @@ -400,11 +396,12 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { } } - summary.finish_with_message(if errors.is_empty() { - "\x1b[32mall done\x1b[0m" + let summary_msg = if errors.is_empty() { + format!("{GREEN}all done{RESET}") } else { - "\x1b[31msome builders failed\x1b[0m" - }); + format!("{RED}some builders failed{RESET}") + }; + summary.finish_with_message(summary_msg); if let Some(first_err) = errors.into_iter().next() { return Err(first_err); @@ -524,17 +521,11 @@ pub async fn build_site(config: AbbayeConfig) -> Result<()> { // for the current version so the templates can link into the repository UI. let git_ui_clone_url: Option<String> = config.git_ui.as_ref().and_then(|cfg| { cfg.clone_url.clone().or_else(|| { - config.site.base_url.as_ref().map(|b| { - format!( - "{}/repository.git {}", - b.trim_end_matches('/'), - if config.site.name.contains(" ") { - format!("'{}'", config.site.name) - } else { - config.site.name.clone() - } - ) - }) + config + .site + .base_url + .as_ref() + .map(|b| format!("{}/repository.git", b.trim_end_matches('/'))) }) }); let version_tag = config.version_extractor.tag_name(&version); @@ -939,3 +930,37 @@ fn update_latest_symlink(output_dir: &Path, version_dir_name: &str) -> Result<() Ok(()) } + +/// Scans `theme_path` for any `*.j2` files whose stem (the name without the +/// `.j2` suffix, e.g. `"base.html"`) is **not** already listed in `skip`, and +/// loads each one into `tera` under that stem name. +/// +/// This makes user-supplied helper or base templates β e.g. a `base.html.j2` +/// referenced by `{% extends "base.html" %}` in a customised main template β +/// available at render time without the caller needing to enumerate them. +pub(crate) fn load_extra_theme_templates( + tera: &mut tera::Tera, + theme_path: &std::path::Path, + skip: &[&str], +) -> miette::Result<()> { + let entries = match std::fs::read_dir(theme_path) { + Ok(e) => e, + Err(_) => return Ok(()), // theme dir absent β nothing to do + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) != Some("j2") { + continue; + } + let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { + continue; + }; + if skip.contains(&stem) { + continue; + } + tera.add_template_file(&path, Some(stem)).map_err(|e| { + miette::miette!("failed to load theme template {}: {e}", path.display()) + })?; + } + Ok(()) +}