Commit
Message
Changed Files (8)
-
modified CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cd6b8..c8d90cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ ### 🐛 Bug Fixes - *(builders)* Correct glob walking in script builder and add tests + +### 🚜 Refactor + +- *(render)* Improve Gemtext nesting logic and add rendering tests ## [0.10.2] - 2026-06-22 ### 🐛 Bug Fixes -
modified src/builders/mod.rs
diff --git a/src/builders/mod.rs b/src/builders/mod.rs index 3682996..781617e 100644 --- a/src/builders/mod.rs +++ b/src/builders/mod.rs @@ -1,9 +1,9 @@ -//! Builder types, the [`Builder`] trait, and the parallel execution model. +//! Builder types, the `Builder` trait, and the parallel execution model. //! //! Each `[[builders]]` entry in `abbaye.toml` deserialises into a //! [`BuilderEntry`] and is run as an independent Tokio task. This module //! owns the [`AnyBuilder`] enum (the TOML type-tag dispatch), the -//! [`LogEvent`] channel used for progress-bar updates, and the [`Builder`] +//! `LogEvent` channel used for progress-bar updates, and the `Builder` //! trait that every concrete builder must implement. //! //! --- @@ -114,7 +114,7 @@ //! `Some(false)` so its own dependents also skip. //! - **Done / Failed** → signals `Some(true)` or `Some(false)`. //! -//! After all tasks finish, collected [`ArtifactPath`]s are classified: +//! After all tasks finish, collected `ArtifactPath` values are classified: //! //! - **File** artifacts → `dist/` (distribution binaries, archives, etc.). //! - **Directory** artifacts → `docs/` (rustdoc output, rendered Markdown, …). @@ -128,9 +128,9 @@ //! The UI is built with the `indicatif` crate. Each builder task owns: //! //! - A **parent spinner** inserted *above* a shared summary bar. -//! - An **mpsc log channel** ([`LogSender`]) over which the builder streams +//! - An **mpsc log channel** (`LogSender`) over which the builder streams //! events. -//! - A **log-consumer task** that receives [`LogEvent`]s and updates the +//! - A **log-consumer task** that receives `LogEvent` values and updates the //! spinner. //! //! ### `LogEvent` variants @@ -159,7 +159,7 @@ //! file lock. //! 2. Emits a `ChildStart` event so the UI creates a dedicated sub-spinner. //! 3. Uses `line_bridge` to adapt the plain-string stderr stream from -//! `run_cargo_build` into `ChildLine` events on the parent [`LogSender`]. +//! `run_cargo_build` into `ChildLine` events on the parent `LogSender`. //! 4. After the build, copies artifacts to stable `target/<triple>/release/` //! paths before the `TempDir` is dropped. //! 5. Emits a `ChildFinish` event when done. -
modified src/cli.rs
diff --git a/src/cli.rs b/src/cli.rs index 2c86429..19094e9 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -58,7 +58,7 @@ pub enum CliCommand { #[arg(short, long)] check: bool, }, - /// Print a usage spec (https://usage.jdx.dev) to stdout. + /// Print a usage spec (<https://usage.jdx.dev>) to stdout. /// /// Pipe the output to the `usage` CLI to generate shell completions, man pages, or docs: /// `abbaye usage-spec | usage generate completion bash` -
modified src/config.rs
diff --git a/src/config.rs b/src/config.rs index b3bd6ef..9d8ccc4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -122,6 +122,20 @@ pub struct GitUiConfig { pub prefix: String, } +impl Default for GitUiConfig { + fn default() -> Self { + Self { + default_branch: "main".to_string(), + max_commits: 200, + repo_path: None, + clone_url: None, + exclude: vec![], + include: vec![], + prefix: "repository".to_string(), + } + } +} + fn default_prefix() -> String { "repository".to_string() } @@ -161,7 +175,7 @@ fn default_max_commits() -> usize { /// /// You can learn more about each builder type in the [builders module documentation](crate::builders). /// -#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] pub struct AbbayeConfig { /// Metadata about the site. pub site: SiteConfig, @@ -184,8 +198,8 @@ fn abbaye_output_dir() -> PathBuf { /// Load the Abbaye2 configuration from the current working directory. /// -/// Looks for `.abbaye.toml` first, then `abbaye.toml`; when both are present -/// `abbaye.toml` takes precedence (last merge wins). +/// Looks for `.abbaye.toml` first, then `abbaye.toml`, then `.abbaye/abbaye.toml`; +/// when multiple are present, the last one wins (figment merge order). pub fn load_config() -> Result<AbbayeConfig> { let cwd = std::env::current_dir().into_diagnostic()?; Figment::new() -
modified src/git_browse.rs
diff --git a/src/git_browse.rs b/src/git_browse.rs index 2cdc0e1..2bd3d08 100644 --- a/src/git_browse.rs +++ b/src/git_browse.rs @@ -1,7 +1,7 @@ use std::path::Path; use gix::bstr::ByteSlice; -use miette::{IntoDiagnostic, Result}; +use miette::{IntoDiagnostic, Result, miette}; use serde::Serialize; use tera::{Context, Tera}; @@ -299,12 +299,17 @@ fn render_blob_page( ) -> Result<()> { const MAX_BLOB_BYTES: usize = 1024 * 1024; - let data: Vec<u8> = std::process::Command::new("git") + let output = std::process::Command::new("git") .current_dir(repo_path) .args(["cat-file", "blob", &oid.to_string()]) .output() - .map(|o| o.stdout) - .unwrap_or_default(); + .into_diagnostic()?; + + if !output.status.success() { + return Err(miette!("failed to read blob object: {}", oid)); + } + + let data = output.stdout; let is_binary = data[..data.len().min(8192)].contains(&0u8); let too_large = data.len() > MAX_BLOB_BYTES; -
modified src/git_ui.rs
diff --git a/src/git_ui.rs b/src/git_ui.rs index e98878f..919e3b3 100644 --- a/src/git_ui.rs +++ b/src/git_ui.rs @@ -132,7 +132,7 @@ struct RefBadge { #[derive(Serialize)] struct BranchNav { short_name: String, - /// HTML filename for this branch ("index.html" or "<name>.html"). + /// HTML filename for this branch (`index.html` or `<name>.html`). filename: String, is_current: bool, } @@ -255,7 +255,7 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi // Compute clone URL before the blocking task so we can pass it into the // browse page generator without re-deriving it. - let clone_url = generate_clone_command(config, git_cfg); + let clone_url = generate_clone_url(config, git_cfg); // ── All gix work happens inside one blocking task (Repository is !Send) ─── // @@ -542,21 +542,14 @@ pub async fn build_git_repository_ui(config: &AbbayeConfig, git_cfg: &GitUiConfi Ok(()) } -pub fn generate_clone_command(config: &AbbayeConfig, git_cfg: &GitUiConfig) -> Option<String> { - let clone_url: Option<String> = git_cfg.clone_url.clone().or_else(|| { - config.site.base_url.as_ref().map(|base| { - format!( - "{}/repository.git {}", - base.trim_end_matches('/'), - if config.site.name.contains(" ") { - format!("'{}'", config.site.name) - } else { - config.site.name.clone() - } - ) - }) - }); - clone_url +pub fn generate_clone_url(config: &AbbayeConfig, git_cfg: &GitUiConfig) -> Option<String> { + git_cfg.clone_url.clone().or_else(|| { + config + .site + .base_url + .as_ref() + .map(|base| format!("{}/repository.git", base.trim_end_matches('/'))) + }) } // ── Git data collection ─────────────────────────────────────────────────────── @@ -1140,3 +1133,152 @@ fn parse_message(raw: &str) -> (String, Option<String>) { (raw.trim().to_string(), None) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_clone_url_custom() { + let config = AbbayeConfig::default(); + let git_cfg = GitUiConfig { + clone_url: Some("https://git.example.com/repo.git".into()), + ..Default::default() + }; + let url = generate_clone_url(&config, &git_cfg); + assert_eq!(url.as_deref(), Some("https://git.example.com/repo.git")); + } + + #[test] + fn test_generate_clone_url_derived() { + let config = AbbayeConfig { + site: crate::config::SiteConfig { + name: String::new(), + base_url: Some("https://example.com".into()), + ..Default::default() + }, + ..Default::default() + }; + let git_cfg = GitUiConfig::default(); + let url = generate_clone_url(&config, &git_cfg); + assert_eq!(url.as_deref(), Some("https://example.com/repository.git")); + } + + #[test] + fn test_generate_clone_url_neither() { + let config = AbbayeConfig::default(); + let git_cfg = GitUiConfig::default(); + let url = generate_clone_url(&config, &git_cfg); + assert!(url.is_none()); + } + + #[test] + fn test_parse_message_subject_only() { + let (subj, body) = parse_message("fix: a bug"); + assert_eq!(subj, "fix: a bug"); + assert!(body.is_none()); + } + + #[test] + fn test_parse_message_subject_and_body() { + let (subj, body) = parse_message("feat: add widget\n\nThis is a long description."); + assert_eq!(subj, "feat: add widget"); + assert_eq!(body.as_deref(), Some("This is a long description.")); + } + + #[test] + fn test_parse_message_empty_body() { + let (subj, body) = parse_message("chore: bump\n\n "); + assert_eq!(subj, "chore: bump"); + assert!(body.is_none()); + } + + #[test] + fn test_parse_message_multi_paragraph() { + let msg = "major: breaking\n\nFirst paragraph.\n\nSecond paragraph."; + let (subj, body) = parse_message(msg); + assert_eq!(subj, "major: breaking"); + assert_eq!( + body.as_deref(), + Some("First paragraph.\n\nSecond paragraph.") + ); + } + + #[test] + fn test_parse_diff_output_empty() { + let files = parse_diff_output(""); + assert!(files.is_empty()); + } + + #[test] + fn test_parse_diff_output_single_file() { + let diff = "\ +diff --git a/src/main.rs b/src/main.rs +new file mode 100644 +index 0000000..e69de29 +--- /dev/null ++++ b/src/main.rs +@@ -0,0 +1 @@ ++hello +"; + let files = parse_diff_output(diff); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, "src/main.rs"); + assert_eq!(files[0].status, "added"); + assert!(files[0].diff_lines.iter().any(|l| l.content == "+hello")); + } + + #[test] + fn test_parse_diff_output_multiple_files() { + let diff = "\ +diff --git a/a.txt b/a.txt +deleted file mode 100644 +--- a/a.txt ++++ /dev/null +@@ -1 +0,0 @@ +-abc +diff --git a/b.txt b/b.txt +new file mode 100644 +--- /dev/null ++++ b/b.txt +@@ -0,0 +1 @@ ++xyz +"; + let files = parse_diff_output(diff); + assert_eq!(files.len(), 2); + assert_eq!(files[0].path, "a.txt"); + assert_eq!(files[0].status, "deleted"); + assert_eq!(files[1].path, "b.txt"); + assert_eq!(files[1].status, "added"); + } + + #[test] + fn test_parse_diff_output_rename() { + let diff = "\ +diff --git a/old.rs b/new.rs +rename from old.rs +rename to new.rs +"; + let files = parse_diff_output(diff); + assert_eq!(files.len(), 1); + assert_eq!(files[0].status, "renamed"); + assert_eq!(files[0].path, "new.rs"); + } + + #[test] + fn test_parse_diff_output_binary() { + let diff = "\ +diff --git a/data.bin b/data.bin +Binary files a/data.bin and b/data.bin differ +"; + let files = parse_diff_output(diff); + assert_eq!(files.len(), 1); + assert_eq!(files[0].path, "data.bin"); + assert!( + files[0] + .diff_lines + .iter() + .any(|l| l.content.contains("Binary")) + ); + } +} -
modified src/templates/git_log.html.j2
diff --git a/src/templates/git_log.html.j2 b/src/templates/git_log.html.j2 index 9efbfd5..9e3402f 100644 --- a/src/templates/git_log.html.j2 +++ b/src/templates/git_log.html.j2 @@ -14,7 +14,7 @@ {% if clone_url %} <div class="clone-box"> <span class="clone-label">Clone</span> - <span class="clone-url">git clone {{ clone_url }}</span> + <span class="clone-url">git clone {{ clone_url }} {{ project_name }}</span> </div> {% endif %} -
modified src/version_extractors/mod.rs
diff --git a/src/version_extractors/mod.rs b/src/version_extractors/mod.rs index 8b1ab61..7ff5c40 100644 --- a/src/version_extractors/mod.rs +++ b/src/version_extractors/mod.rs @@ -60,6 +60,12 @@ pub enum AnyVersionExtractor { Git(GitVersionConfig), } +impl Default for AnyVersionExtractor { + fn default() -> Self { + Self::Cargo(CargoVersionConfig::default()) + } +} + impl AnyVersionExtractor { pub async fn extract(&self) -> Result<VersionInfo> { match self {