From 730647546ddc41d392c8f73caa01b3dc04714595 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 16:42:23 +0100 Subject: [PATCH 01/79] Cargo.toml: Add `miette` and `thiserror` as dependencies --- Cargo.lock | 158 +++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + 2 files changed, 160 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9c1dd825..8d02fe46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -102,6 +117,30 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "bender" version = "0.29.1" @@ -118,6 +157,7 @@ dependencies = [ "indexmap", "is-terminal", "itertools", + "miette", "pathdiff", "pretty_assertions", "semver", @@ -128,6 +168,7 @@ dependencies = [ "tabwriter", "tempfile", "tera", + "thiserror", "tokio", "typed-arena", "walkdir", @@ -546,6 +587,12 @@ dependencies = [ "wasip2", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "glob" version = "0.3.3" @@ -666,6 +713,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -752,6 +805,45 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.0" @@ -772,6 +864,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -790,6 +891,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1076,6 +1183,12 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustix" version = "1.1.2" @@ -1268,6 +1381,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.111" @@ -1328,6 +1462,24 @@ name = "termtree" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] [[package]] name = "thiserror" @@ -1401,6 +1553,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-segmentation" version = "1.12.0" diff --git a/Cargo.toml b/Cargo.toml index d0037bef..b6a36b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ glob = "0.3" walkdir = "2" subst = "0.3" tera = "1.19" +miette = { version = "7.6.0", features = ["fancy"] } +thiserror = "2.0.17" [target.'cfg(windows)'.dependencies] dunce = "1.0.4" From 5d67d0ebcd1165721c0954d95cc9775db9ecc7bd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 17:11:33 +0100 Subject: [PATCH 02/79] error: Initial implementation of Warnings and suppression --- src/cli.rs | 18 +++++++++++--- src/error.rs | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1e4079ee..bd9fb824 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,6 +4,8 @@ //! Main command line tool implementation. use std; +use std::collections::HashSet; +use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command as SysCommand; @@ -125,9 +127,19 @@ pub fn main() -> Result<()> { }) .collect(); - if suppressed_warnings.contains("all") || suppressed_warnings.contains("Wall") { - suppressed_warnings.extend((1..24).map(|i| format!("W{:02}", i))); - } + let diagnostics = Diagnostics::new(suppressed); + + diagnostics.emit(Warnings::Warning1); + diagnostics.emit(Warnings::Warning1); // should not be emitted again + diagnostics.emit(Warnings::Warning2); + + return Ok(()); + + let mut suppressed_warnings: IndexSet = matches + .get_many::("suppress") + .unwrap_or_default() + .map(|s| s.to_owned()) + .collect(); #[cfg(debug_assertions)] if cli.debug { diff --git a/src/error.rs b/src/error.rs index 778d56ca..7fabf181 100644 --- a/src/error.rs +++ b/src/error.rs @@ -159,3 +159,71 @@ macro_rules! stageln { pub fn println_stage(stage: &str, message: &str) { eprintln!("\x1B[32;1m{:>12}\x1B[0m {}", stage, message); } + +use std::cell::RefCell; +use std::collections::HashSet; + +use miette::Diagnostic; +use thiserror::Error; + +/// A diagnostics manager that handles warnings (and errors). +pub struct Diagnostics { + /// A set of suppressed warnings. + suppressed: HashSet, + /// Whether all warnings are suppressed. + all_suppressed: bool, + /// A set of already emitted warnings. + emitted: RefCell>, +} + +impl Diagnostics { + /// Create a new diagnostics manager. + pub fn new(suppressed: HashSet) -> Diagnostics { + Diagnostics { + all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), + suppressed: suppressed, + emitted: RefCell::new(HashSet::new()), + } + } + + /// Emit a warning if it is not suppressed or already emitted. + pub fn emit(&self, warning: Warnings) { + // Extract the code (e.g., "W07") automatically from the derived implementation + let code = warning.code().map(|c| c.to_string()); + + // Check whether the command is suppressed + if let Some(code) = &code { + if self.all_suppressed || self.suppressed.contains(code) { + return; + } + } + + // Check whether the warning was already emitted + let mut emitted = self.emitted.borrow_mut(); + if emitted.contains(&warning) { + return; + } + + // Record the emitted warning and print it + emitted.insert(warning.clone()); + eprintln!("{:?}", miette::Report::new(warning)); + } +} + +#[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] +pub enum Warnings { + #[error("This is warning 1")] + #[diagnostic( + severity(Warning), + code(W01), + help("Consider checking the configuration.") + )] + Warning1, + #[error("This is warning 2")] + #[diagnostic( + severity(Warning), + code(W02), + help("Consider updating your dependencies.") + )] + Warning2, +} From 026953d6eae71a01c55a687ed636a7407352473a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 17:42:51 +0100 Subject: [PATCH 03/79] errorr: Improve `emit` function --- src/error.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7fabf181..a56604dc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -188,25 +188,27 @@ impl Diagnostics { /// Emit a warning if it is not suppressed or already emitted. pub fn emit(&self, warning: Warnings) { - // Extract the code (e.g., "W07") automatically from the derived implementation - let code = warning.code().map(|c| c.to_string()); - // Check whether the command is suppressed - if let Some(code) = &code { - if self.all_suppressed || self.suppressed.contains(code) { + if let Some(code) = warning.code() { + if self.all_suppressed || self.suppressed.contains(&code.to_string()) { return; } } // Check whether the warning was already emitted - let mut emitted = self.emitted.borrow_mut(); - if emitted.contains(&warning) { - return; + // We scope the borrow so it drops immediately after the check + { + if self.emitted.borrow().contains(&warning) { + return; + } } - // Record the emitted warning and print it - emitted.insert(warning.clone()); - eprintln!("{:?}", miette::Report::new(warning)); + // Record the emitted warning + self.emitted.borrow_mut().insert(warning.clone()); + + // Print the warning report + let report = miette::Report::new(warning); + eprintln!("{:?}", report); } } From d8cc84d1d1c42b0fdfd92292296c68401fc44b30 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:01:41 +0100 Subject: [PATCH 04/79] Cargo.toml: Add `owo-colors` as dependency --- Cargo.lock | 1 + Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8d02fe46..9b87dc8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,7 @@ dependencies = [ "is-terminal", "itertools", "miette", + "owo-colors", "pathdiff", "pretty_assertions", "semver", diff --git a/Cargo.toml b/Cargo.toml index b6a36b3b..d42a104c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ subst = "0.3" tera = "1.19" miette = { version = "7.6.0", features = ["fancy"] } thiserror = "2.0.17" +owo-colors = "4.2.3" [target.'cfg(windows)'.dependencies] dunce = "1.0.4" From d3c5d40f423d5629fb4b72e884ea0d13061cc154 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:02:03 +0100 Subject: [PATCH 05/79] error: Nicely format warning messages --- src/cli.rs | 1 + src/error.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index bd9fb824..1db5fb7c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -127,6 +127,7 @@ pub fn main() -> Result<()> { }) .collect(); + miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); let diagnostics = Diagnostics::new(suppressed); diagnostics.emit(Warnings::Warning1); diff --git a/src/error.rs b/src/error.rs index a56604dc..453d6f63 100644 --- a/src/error.rs +++ b/src/error.rs @@ -163,7 +163,8 @@ pub fn println_stage(stage: &str, message: &str) { use std::cell::RefCell; use std::collections::HashSet; -use miette::Diagnostic; +use miette::{Diagnostic, ReportHandler}; +use owo_colors::OwoColorize; use thiserror::Error; /// A diagnostics manager that handles warnings (and errors). @@ -173,6 +174,7 @@ pub struct Diagnostics { /// Whether all warnings are suppressed. all_suppressed: bool, /// A set of already emitted warnings. + /// Implemented as a RefCell to allow interior mutability. emitted: RefCell>, } @@ -212,6 +214,46 @@ impl Diagnostics { } } +pub struct DiagnosticRenderer; + +impl ReportHandler for DiagnosticRenderer { + fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Determine severity and the resulting style + let (severity, style) = match diagnostic.severity().unwrap_or_default() { + miette::Severity::Error => ("error", owo_colors::Style::new().red().bold()), + miette::Severity::Warning => ("warning", owo_colors::Style::new().yellow().bold()), + miette::Severity::Advice => unimplemented!(), + }; + + // Write the severity prefix + write!(f, "{}", severity.style(style))?; + + // Writ the code, if any + if let Some(code) = diagnostic.code() { + write!(f, "{}", format!("[{}]: ", code).style(style))?; + } + + // Then, we write the diagnostic message + write!(f, "{}", diagnostic)?; + + // Below the message, there might be an additional help message + let _branch = " ├─›"; // Branching with arrow + let corner = " ╰─›"; // Final corner with arrow + + if let Some(help) = diagnostic.help() { + write!( + f, + "\n{} {} {}", + corner.dimmed(), + "help:".bold(), + help.dimmed() + )?; + } + + Ok(()) + } +} + #[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] pub enum Warnings { #[error("This is warning 1")] From 1f1703754c7446074d626586620fe64c42dcec91 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:14:34 +0100 Subject: [PATCH 06/79] sess: Add diagnostics handler --- src/cli.rs | 5 +---- src/sess.rs | 4 ++++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 1db5fb7c..8385c248 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -130,10 +130,6 @@ pub fn main() -> Result<()> { miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); let diagnostics = Diagnostics::new(suppressed); - diagnostics.emit(Warnings::Warning1); - diagnostics.emit(Warnings::Warning1); // should not be emitted again - diagnostics.emit(Warnings::Warning2); - return Ok(()); let mut suppressed_warnings: IndexSet = matches @@ -202,6 +198,7 @@ pub fn main() -> Result<()> { cli.local, force_fetch, git_throttle, + diagnostics, suppressed_warnings, ); diff --git a/src/sess.rs b/src/sess.rs index 64f6cf73..ca364b1f 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -77,6 +77,8 @@ pub struct Session<'ctx> { pub git_throttle: Arc, /// A toggle to disable remote fetches & clones pub local_only: bool, + /// A diagnostics handler. + pub diagnostics: Diagnostics, /// A list of warnings to suppress. pub suppress_warnings: IndexSet, } @@ -92,6 +94,7 @@ impl<'ctx> Session<'ctx> { local_only: bool, force_fetch: bool, git_throttle: usize, + diagnostics: Diagnostics, suppress_warnings: IndexSet, ) -> Session<'ctx> { Session { @@ -118,6 +121,7 @@ impl<'ctx> Session<'ctx> { cache: Default::default(), git_throttle: Arc::new(Semaphore::new(git_throttle)), local_only, + diagnostics, suppress_warnings, } } From e6c1ce4c6dfcf571dfdb7d40146ad5cf088a369f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:17:29 +0100 Subject: [PATCH 07/79] error: Add first warning --- src/cli.rs | 15 +++++---------- src/error.rs | 32 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8385c248..c4e75682 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -130,9 +130,7 @@ pub fn main() -> Result<()> { miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); let diagnostics = Diagnostics::new(suppressed); - return Ok(()); - - let mut suppressed_warnings: IndexSet = matches + let suppressed_warnings: IndexSet = matches .get_many::("suppress") .unwrap_or_default() .map(|s| s.to_owned()) @@ -262,13 +260,10 @@ pub fn main() -> Result<()> { ) })?; if !meta.file_type().is_symlink() { - if !sess.suppress_warnings.contains("W01") { - warnln!( - "[W01] Skipping link to package {} at {:?} since there is something there", - pkg_name, - path - ); - } + sess.diagnostics.emit(Warnings::SkippingPackageLink( + pkg_name.clone(), + path.clone(), + )); continue; } if path.read_link().map(|d| d != pkg_path).unwrap_or(true) { diff --git a/src/error.rs b/src/error.rs index 453d6f63..d959c6fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -162,6 +162,7 @@ pub fn println_stage(stage: &str, message: &str) { use std::cell::RefCell; use std::collections::HashSet; +use std::path::PathBuf; use miette::{Diagnostic, ReportHandler}; use owo_colors::OwoColorize; @@ -254,20 +255,31 @@ impl ReportHandler for DiagnosticRenderer { } } +/// Bold a package name in diagnostic messages. +macro_rules! pkg { + ($pkg:expr) => { + $pkg.bold() + }; +} + +/// Underline a path in diagnostic messages. +macro_rules! path { + ($pkg:expr) => { + $pkg.display().underline() + }; +} + #[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] pub enum Warnings { - #[error("This is warning 1")] - #[diagnostic( - severity(Warning), - code(W01), - help("Consider checking the configuration.") + #[error( + "Skipping link to package {} at {} since there is something there", + pkg!(.0), + path!(.1) )] - Warning1, - #[error("This is warning 2")] #[diagnostic( severity(Warning), - code(W02), - help("Consider updating your dependencies.") + code(W01), + help("Check the existing file or directory that is preventing the link.") )] - Warning2, + SkippingPackageLink(String, PathBuf), } From 4c72646cf4e48fe030b611d83dacb260ff2035ba Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:43:51 +0100 Subject: [PATCH 08/79] error: Force dimming again after styled messages --- src/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/error.rs b/src/error.rs index d959c6fa..b56bfd1f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -242,6 +242,9 @@ impl ReportHandler for DiagnosticRenderer { let corner = " ╰─›"; // Final corner with arrow if let Some(help) = diagnostic.help() { + // Styled messages (e.g. 'pkg.bold()') will reset the style afterwards, + // so we need to re-apply dimming after each reset. + let help = help.to_string().replace("\x1b[0m", "\x1b[0m\x1b[2m"); write!( f, "\n{} {} {}", From e9a76a1005ef7255543975d35ec23850124cda54 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 18:44:12 +0100 Subject: [PATCH 09/79] error: Add `emit_if` wrapper function --- src/error.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/error.rs b/src/error.rs index b56bfd1f..bc492115 100644 --- a/src/error.rs +++ b/src/error.rs @@ -213,6 +213,13 @@ impl Diagnostics { let report = miette::Report::new(warning); eprintln!("{:?}", report); } + + /// Emit a warning if the condition is true. + pub fn emit_if(&self, condition: bool, warning: Warnings) { + if condition { + self.emit(warning); + } + } } pub struct DiagnosticRenderer; From a57ed6e934eaf0f599174f16b341cbc65b598339 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 21:09:22 +0100 Subject: [PATCH 10/79] error: Make Diagnostic a global static variable --- src/cli.rs | 13 +++++------ src/error.rs | 61 ++++++++++++++++++++++++++++++++++++---------------- src/sess.rs | 4 ---- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index c4e75682..23cdb323 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -127,8 +127,8 @@ pub fn main() -> Result<()> { }) .collect(); - miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); - let diagnostics = Diagnostics::new(suppressed); + Diagnostics::init(suppressed); + let suppressed_warnings: IndexSet = matches .get_many::("suppress") @@ -196,7 +196,6 @@ pub fn main() -> Result<()> { cli.local, force_fetch, git_throttle, - diagnostics, suppressed_warnings, ); @@ -260,7 +259,7 @@ pub fn main() -> Result<()> { ) })?; if !meta.file_type().is_symlink() { - sess.diagnostics.emit(Warnings::SkippingPackageLink( + warn!(Warnings::SkippingPackageLink( pkg_name.clone(), path.clone(), )); @@ -520,8 +519,10 @@ fn maybe_load_config(path: &Path, warn_config_loaded: bool) -> Result12}\x1B[0m {}", stage, message); } -use std::cell::RefCell; use std::collections::HashSet; use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; use miette::{Diagnostic, ReportHandler}; use owo_colors::OwoColorize; use thiserror::Error; +static GLOBAL_DIAGNOSTICS: OnceLock = OnceLock::new(); + /// A diagnostics manager that handles warnings (and errors). +#[derive(Debug)] pub struct Diagnostics { /// A set of suppressed warnings. suppressed: HashSet, /// Whether all warnings are suppressed. all_suppressed: bool, /// A set of already emitted warnings. - /// Implemented as a RefCell to allow interior mutability. - emitted: RefCell>, + /// Requires synchronization as warnings may be emitted from multiple threads. + emitted: Mutex>, +} + +#[macro_export] +macro_rules! warn { + ($warning:expr) => { + $crate::error::Diagnostics::emit($warning) + }; } impl Diagnostics { /// Create a new diagnostics manager. - pub fn new(suppressed: HashSet) -> Diagnostics { - Diagnostics { + pub fn init(suppressed: HashSet) { + // Set up miette with our custom renderer + miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); + let diag = Diagnostics { all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), suppressed: suppressed, - emitted: RefCell::new(HashSet::new()), - } + emitted: Mutex::new(HashSet::new()), + }; + + GLOBAL_DIAGNOSTICS + .set(diag) + .expect("Diagnostics already initialized!"); + } + + /// Get the global diagnostics manager. + fn get() -> &'static Diagnostics { + GLOBAL_DIAGNOSTICS + .get() + .expect("Diagnostics not initialized!") } /// Emit a warning if it is not suppressed or already emitted. - pub fn emit(&self, warning: Warnings) { + pub fn emit(warning: Warnings) { + let diag = Diagnostics::get(); + // Check whether the command is suppressed if let Some(code) = warning.code() { - if self.all_suppressed || self.suppressed.contains(&code.to_string()) { + if diag.all_suppressed || diag.suppressed.contains(&code.to_string()) { return; } } // Check whether the warning was already emitted - // We scope the borrow so it drops immediately after the check - { - if self.emitted.borrow().contains(&warning) { - return; - } + let mut emitted = diag.emitted.lock().unwrap(); + if emitted.contains(&warning) { + return; } // Record the emitted warning - self.emitted.borrow_mut().insert(warning.clone()); + emitted.insert(warning.clone()); + drop(emitted); // Print the warning report - let report = miette::Report::new(warning); - eprintln!("{:?}", report); + eprintln!("{:?}", miette::Report::new(warning)); } /// Emit a warning if the condition is true. - pub fn emit_if(&self, condition: bool, warning: Warnings) { + pub fn emit_if(condition: bool, warning: Warnings) { if condition { - self.emit(warning); + Diagnostics::emit(warning); } } } diff --git a/src/sess.rs b/src/sess.rs index ca364b1f..64f6cf73 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -77,8 +77,6 @@ pub struct Session<'ctx> { pub git_throttle: Arc, /// A toggle to disable remote fetches & clones pub local_only: bool, - /// A diagnostics handler. - pub diagnostics: Diagnostics, /// A list of warnings to suppress. pub suppress_warnings: IndexSet, } @@ -94,7 +92,6 @@ impl<'ctx> Session<'ctx> { local_only: bool, force_fetch: bool, git_throttle: usize, - diagnostics: Diagnostics, suppress_warnings: IndexSet, ) -> Session<'ctx> { Session { @@ -121,7 +118,6 @@ impl<'ctx> Session<'ctx> { cache: Default::default(), git_throttle: Arc::new(Semaphore::new(git_throttle)), local_only, - diagnostics, suppress_warnings, } } From 113d1d121b6e9016636989886e44482e1923b4cf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 21:17:12 +0100 Subject: [PATCH 11/79] error: Make `emit` a function of `Warning` itself cli: Remove sample warning --- src/cli.rs | 12 +++++------- src/error.rs | 47 +++++++++++++++++++++-------------------------- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 23cdb323..d4b6452c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -259,10 +259,7 @@ pub fn main() -> Result<()> { ) })?; if !meta.file_type().is_symlink() { - warn!(Warnings::SkippingPackageLink( - pkg_name.clone(), - path.clone(), - )); + Warnings::SkippingPackageLink(pkg_name.clone(), path.clone()).emit(); continue; } if path.read_link().map(|d| d != pkg_path).unwrap_or(true) { @@ -519,9 +516,10 @@ fn maybe_load_config(path: &Path, warn_config_loaded: bool) -> Result>, } -#[macro_export] -macro_rules! warn { - ($warning:expr) => { - $crate::error::Diagnostics::emit($warning) - }; -} - impl Diagnostics { /// Create a new diagnostics manager. pub fn init(suppressed: HashSet) { @@ -211,37 +204,32 @@ impl Diagnostics { .get() .expect("Diagnostics not initialized!") } +} - /// Emit a warning if it is not suppressed or already emitted. - pub fn emit(warning: Warnings) { +impl Warnings { + /// Checks suppression, deduplicates, and emits the warning to stderr. + pub fn emit(self) { let diag = Diagnostics::get(); // Check whether the command is suppressed - if let Some(code) = warning.code() { + if let Some(code) = self.code() { if diag.all_suppressed || diag.suppressed.contains(&code.to_string()) { return; } } // Check whether the warning was already emitted - let mut emitted = diag.emitted.lock().unwrap(); - if emitted.contains(&warning) { - return; + // We scope the lock to keep the critical section short + { + let mut emitted = diag.emitted.lock().unwrap(); + if emitted.contains(&self) { + return; + } + emitted.insert(self.clone()); } - // Record the emitted warning - emitted.insert(warning.clone()); - drop(emitted); - - // Print the warning report - eprintln!("{:?}", miette::Report::new(warning)); - } - - /// Emit a warning if the condition is true. - pub fn emit_if(condition: bool, warning: Warnings) { - if condition { - Diagnostics::emit(warning); - } + // Print the warning report (consumes self i.e. the warning) + eprintln!("{:?}", miette::Report::new(self)); } } @@ -302,6 +290,13 @@ macro_rules! path { }; } +/// Italicize a field name in diagnostic messages. +macro_rules! field { + ($field:expr) => { + $field.italic() + }; +} + #[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] pub enum Warnings { #[error( From 4bd2481583b5b8115613a22994e9c7d314e2aa7f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 21:20:54 +0100 Subject: [PATCH 12/79] error: Drop `emitted` manually --- src/error.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index 40bb111b..e7ba7060 100644 --- a/src/error.rs +++ b/src/error.rs @@ -219,14 +219,12 @@ impl Warnings { } // Check whether the warning was already emitted - // We scope the lock to keep the critical section short - { - let mut emitted = diag.emitted.lock().unwrap(); - if emitted.contains(&self) { - return; - } - emitted.insert(self.clone()); + let mut emitted = diag.emitted.lock().unwrap(); + if emitted.contains(&self) { + return; } + emitted.insert(self.clone()); + drop(emitted); // Print the warning report (consumes self i.e. the warning) eprintln!("{:?}", miette::Report::new(self)); @@ -310,4 +308,16 @@ pub enum Warnings { help("Check the existing file or directory that is preventing the link.") )] SkippingPackageLink(String, PathBuf), + + #[error("Using config at {} for overrides.", path!(path))] + #[diagnostic(severity(Warning), code(W02))] + UsingConfigForOverride { path: PathBuf }, + + #[error("Ignoring unknown field {} in package {}.", field!(field), pkg!(pkg))] + #[diagnostic( + severity(Warning), + code(W03), + help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) + )] + IgnoreUnknownField { field: String, pkg: String }, } From c2c71ce070ebc6948a13231bad4f491156e15df3 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 21:23:27 +0100 Subject: [PATCH 13/79] error: Migrate some warnings error: Move W04 to `Warnings` error: Move W05 to `Warnings` error: Move W06 to `Warnings` error: Move W07 to `Warnings` error: Move W08 to `Warnings` error: Move W09 to `Warnings` error: Move W10 to `Warnings` --- src/cmd/vendor.rs | 4 +-- src/config.rs | 61 +++++++++++++++++------------------------- src/error.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++--- src/resolver.rs | 22 +++++----------- src/sess.rs | 63 ++++++++++++++++++++++---------------------- 5 files changed, 128 insertions(+), 89 deletions(-) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 5d271025..8dbdbae3 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -104,9 +104,9 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { git.clone().spawn_with(|c| c.arg("clone").arg(url).arg(".")) .map_err(move |cause| { if url.contains("git@") { - warnln!("[W07] Please ensure your public ssh key is added to the git server."); + Warnings::SshKeyMaybeMissing.emit(); } - warnln!("[W07] Please ensure the url is correct and you have access to the repository."); + Warnings::UrlMaybeIncorrect.emit(); Error::chain( format!("Failed to initialize git database in {:?}.", tmp_path), cause, diff --git a/src/config.rs b/src/config.rs index 908885c9..9b260236 100644 --- a/src/config.rs +++ b/src/config.rs @@ -454,13 +454,11 @@ impl Validate for PartialManifest { p.name = p.name.to_lowercase(); if !pre_output { p.extra.iter().for_each(|(k, _)| { - if !suppress_warnings.contains("W03") { - warnln!( - "[W03] Ignoring unknown field `{}` in manifest package for {}.", - k, - p.name - ); + Warnings::IgnoreUnknownField { + field: k.clone(), + pkg: p.name.clone(), } + .emit(); }); } p @@ -513,13 +511,11 @@ impl Validate for PartialManifest { }; if !pre_output { self.extra.iter().for_each(|(k, _)| { - if !suppress_warnings.contains("W03") { - warnln!( - "[W03] Ignoring unknown field `{}` in manifest for {}.", - k, - pkg.name - ); + Warnings::IgnoreUnknownField { + field: k.clone(), + pkg: pkg.name.clone(), } + .emit(); }); } Ok(Manifest { @@ -657,13 +653,11 @@ impl Validate for PartialDependency { } if !pre_output { self.extra.iter().for_each(|(k, _)| { - if !suppress_warnings.contains("W03") { - warnln!( - "[W03] Ignoring unknown field `{}` in a dependency in manifest for {}.", - k, - package_name - ); + Warnings::IgnoreUnknownField { + field: k.clone(), + pkg: package_name.to_string(), } + .emit(); }); } if let Some(path) = self.path { @@ -1044,21 +1038,16 @@ impl Validate for PartialSources { .collect(); let files: Vec = files?; let files: Vec = files.into_iter().collect(); - if files.is_empty() && !pre_output && !suppress_warnings.contains("W04") { - warnln!( - "[W04] No source files specified in a sourcegroup in manifest for {}.", - package_name - ); + if files.is_empty() && !pre_output { + Warnings::NoFilesInSourceGroup(package_name.to_string()).emit(); } if !pre_output { extra.iter().for_each(|(k, _)| { - if !suppress_warnings.contains("W03") { - warnln!( - "[W03] Ignoring unknown field `{}` in sources in manifest for {}.", - k, - package_name - ); + Warnings::IgnoreUnknownField { + field: k.clone(), + pkg: package_name.to_string(), } + .emit(); }); } Ok(SourceFile::Group(Box::new(Sources { @@ -1259,8 +1248,8 @@ impl GlobFile for PartialSourceFile { }) }) .collect::>>()?; - if out.is_empty() && !suppress_warnings.contains("W05") { - warnln!("[W05] No files found for glob pattern {:?}", path); + if out.is_empty() { + Warnings::NoFilesForGlobalPattern { path: path.clone() }.emit(); } Ok(out) } else { @@ -1323,13 +1312,11 @@ impl Validate for PartialWorkspace { .collect(); if !pre_output { self.extra.iter().for_each(|(k, _)| { - if !suppress_warnings.contains("W03") { - warnln!( - "[W03] Ignoring unknown field `{}` in workspace configuration in manifest for {}.", - k, - package_name - ); + Warnings::IgnoreUnknownField { + field: k.clone(), + pkg: package_name.to_string(), } + .emit(); }); } Ok(Workspace { diff --git a/src/error.rs b/src/error.rs index e7ba7060..63ade654 100644 --- a/src/error.rs +++ b/src/error.rs @@ -284,7 +284,7 @@ macro_rules! pkg { /// Underline a path in diagnostic messages. macro_rules! path { ($pkg:expr) => { - $pkg.display().underline() + $pkg.underline() }; } @@ -300,7 +300,7 @@ pub enum Warnings { #[error( "Skipping link to package {} at {} since there is something there", pkg!(.0), - path!(.1) + path!(.1.display()) )] #[diagnostic( severity(Warning), @@ -309,7 +309,7 @@ pub enum Warnings { )] SkippingPackageLink(String, PathBuf), - #[error("Using config at {} for overrides.", path!(path))] + #[error("Using config at {} for overrides.", path!(path.display()))] #[diagnostic(severity(Warning), code(W02))] UsingConfigForOverride { path: PathBuf }, @@ -320,4 +320,65 @@ pub enum Warnings { help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) )] IgnoreUnknownField { field: String, pkg: String }, + + #[error("Source group in package {} contains no source files.", pkg!(.0))] + #[diagnostic( + severity(Warning), + code(W04), + help("Add source files to the source group or remove it from the manifest.") + )] + NoFilesInSourceGroup(String), + + #[error("No files matched the global pattern {}.", path!(path))] + #[diagnostic(severity(Warning), code(W05))] + NoFilesForGlobalPattern { path: String }, + + #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", pkg!(.0), path!(.1.display()))] + #[diagnostic( + severity(Warning), + code(W06), + help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.") + )] + NotAGitDependency(String, PathBuf), + + // TODO(fischeti): Why are there two W07 variants? + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("SSH key might be missing.")] + #[diagnostic( + severity(Warning), + code(W07), + help("Please ensure your public ssh key is added to the git server.") + )] + SshKeyMaybeMissing, + + // TODO(fischeti): Why are there two W07 variants? + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("SSH key might be missing.")] + #[diagnostic( + severity(Warning), + code(W07), + help("Please ensure the url is correct and you have access to the repository.") + )] + UrlMaybeIncorrect, + + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("Revision {} not found in repository {}.", pkg!(.0), pkg!(.1))] + #[diagnostic( + severity(Warning), + code(W08), + help("Check that the revision exists in the remote repository or run `bender update`.") + )] + RevisionNotFound(String, String), + + #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", pkg!(pkg), pkg!(top_pkg))] + #[diagnostic(severity(Warning), code(W09))] + PathDepInGitDep { pkg: String, top_pkg: String }, + + #[error("There may be issues in the path for {}.", pkg!(.0))] + #[diagnostic( + severity(Warning), + code(W10), + help("Please check that {} is correct and accessible.", path!(.1.display())) + )] + MaybePathIssues(String, PathBuf), } diff --git a/src/resolver.rs b/src/resolver.rs index 0a89164e..c66d7f05 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -104,16 +104,11 @@ impl<'ctx> DependencyResolver<'ctx> { // - the dependency is not in a clean state (i.e., was modified) if !ignore_checkout { if !is_git_repo { - if !self.sess.suppress_warnings.contains("W06") { - warnln!("[W06] Dependency `{}` in checkout_dir `{}` is not a git repository. Setting as path dependency.\n\ - \tPlease use `bender clone` to work on git dependencies.\n\ - \tRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.", - dir.as_ref().unwrap().path().file_name().unwrap().to_str().unwrap(), - &checkout.display()); - } + Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); self.checked_out.insert( depname, - config::Dependency::Path(dir.unwrap().path(), Vec::new()), + config::Dependency::Path(dir.unwrap().path()), + vec![], ); } else if !(SysCommand::new(&self.sess.config.git) // If not in a clean state .arg("status") @@ -123,16 +118,11 @@ impl<'ctx> DependencyResolver<'ctx> { .stdout .is_empty()) { - if !self.sess.suppress_warnings.contains("W06") { - warnln!("[W06] Dependency `{}` in checkout_dir `{}` is not in a clean state. Setting as path dependency.\n\ - \tPlease use `bender clone` to work on git dependencies.\n\ - \tRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.", - dir.as_ref().unwrap().path().file_name().unwrap().to_str().unwrap(), - &checkout.display()); - } + Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); self.checked_out.insert( depname, - config::Dependency::Path(dir.unwrap().path(), Vec::new()), + config::Dependency::Path(dir.unwrap().path()), + vec![], ); } } diff --git a/src/sess.rs b/src/sess.rs index 64f6cf73..a5f6234b 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -577,14 +577,10 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { }) .await .map_err(move |cause| { - if url3.contains("git@") && !self.sess.suppress_warnings.contains("W07") { - warnln!("[W07] Please ensure your public ssh key is added to the git server."); - } - if !self.sess.suppress_warnings.contains("W07") { - warnln!( - "[W07] Please ensure the url is correct and you have access to the repository." - ); + if url3.contains("git@") { + Warnings::SshKeyMaybeMissing.emit(); } + Warnings::UrlMaybeIncorrect.emit(); Error::chain( format!("Failed to initialize git database in {:?}.", db_dir), cause, @@ -612,14 +608,10 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { }) .await .map_err(move |cause| { - if url3.contains("git@") && !self.sess.suppress_warnings.contains("W07") { - warnln!("[W07] Please ensure your public ssh key is added to the git server."); - } - if !self.sess.suppress_warnings.contains("W07") { - warnln!( - "[W07] Please ensure the url is correct and you have access to the repository." - ); + if url3.contains("git@") { + Warnings::SshKeyMaybeMissing.emit(); } + Warnings::UrlMaybeIncorrect.emit(); Error::chain( format!("Failed to update git database in {:?}.", db_dir), cause, @@ -975,18 +967,26 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { git.clone() .spawn_with(move |c| c.arg("fetch").arg("--all")) .await?; - git.clone().spawn_with(move |c| c.arg("tag").arg(tag_name_1).arg(revision).arg("--force").arg("--no-sign")).map_err(|cause| { - if !self.sess.suppress_warnings.contains("W08") { - warnln!("[W08] Please ensure the commits are available on the remote or run bender update"); - } - Error::chain( - format!( - "Failed to checkout commit {} for {} given in Bender.lock.\n", - revision, name - ), - cause, - ) - }).await + git.clone() + .spawn_with(move |c| { + c.arg("tag") + .arg(tag_name_1) + .arg(revision) + .arg("--force") + .arg("--no-sign") + }) + .map_err(|cause| { + Warnings::RevisionNotFound(revision.to_string(), name.to_string()) + .emit(); + Error::chain( + format!( + "Failed to checkout commit {} for {} given in Bender.lock.\n", + revision, name + ), + cause, + ) + }) + .await } }?; if clear == CheckoutState::ToClone { @@ -1036,10 +1036,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { for dep in (dep_iter_mut).iter_mut() { if let (_, config::Dependency::Path(ref path, _)) = dep { if !path.starts_with("/") { - if !self.sess.suppress_warnings.contains("W09") { - warnln!("[W09] Path dependencies ({:?}) in git dependencies ({:?}) currently not fully supported. \ - Your mileage may vary.", dep.0, top_package_name); + Warnings::PathDepInGitDep { + pkg: dep.0.clone(), + top_pkg: top_package_name.clone(), } + .emit(); let sub_entries = db .clone() @@ -1155,8 +1156,8 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { use self::DependencyVersion as DepVer; match (&dep.source, version) { (DepSrc::Path(path), DepVer::Path) => { - if !path.starts_with("/") && !self.sess.suppress_warnings.contains("W10") { - warnln!("[W10] There may be issues in the path for {:?}.", dep.name); + if !path.is_absolute() { + Warnings::MaybePathIssues(dep.name.clone(), path.to_path_buf()).emit(); } let manifest_path = path.join("Bender.yml"); if manifest_path.exists() { From 4ea3b4ec77912163b72d6b3257d674db5e87bc82 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 10 Jan 2026 22:24:54 +0100 Subject: [PATCH 14/79] error: Render multiple help messages error: Move W11 to `Warnings` error: Move W12 to `Warnings` error: Move W13 to `Warnings` error: Move W14 to `Warnings` error: Deduplicate W15/W01 warnings error: Move W15 to `Warnings` --- src/cmd/clone.rs | 10 ++----- src/cmd/snapshot.rs | 6 +--- src/cmd/update.rs | 8 ++---- src/cmd/vendor.rs | 13 ++++++--- src/error.rs | 66 +++++++++++++++++++++++++++++++++++-------- src/sess.rs | 69 ++++++++++++++++++++------------------------- 6 files changed, 101 insertions(+), 71 deletions(-) diff --git a/src/cmd/clone.rs b/src/cmd/clone.rs index 04ca26c1..1d4bb4b1 100644 --- a/src/cmd/clone.rs +++ b/src/cmd/clone.rs @@ -138,8 +138,8 @@ pub fn run(sess: &Session, path: &Path, args: &CloneArgs) -> Result<()> { { Err(Error::new("git fetch failed".to_string()))?; } - } else if !sess.suppress_warnings.contains("W14") { - warnln!("[W14] fetch not performed due to --local argument."); + } else { + Warnings::LocalNoFetch.emit(); } eprintln!( @@ -263,11 +263,7 @@ pub fn run(sess: &Session, path: &Path, args: &CloneArgs) -> Result<()> { ) })?; if !meta.file_type().is_symlink() { - warnln!( - "[W15] Skipping link to package {} at {:?} since there is something there", - pkg_name, - link_path - ); + Warnings::SkippingPackageLink(pkg_name.clone(), link_path.to_path_buf()).emit(); continue; } if link_path.read_link().map(|d| d != pkg_path).unwrap_or(true) { diff --git a/src/cmd/snapshot.rs b/src/cmd/snapshot.rs index 8a294b47..59eccccc 100644 --- a/src/cmd/snapshot.rs +++ b/src/cmd/snapshot.rs @@ -255,11 +255,7 @@ pub fn run(sess: &Session, args: &SnapshotArgs) -> Result<()> { ) })?; if !meta.file_type().is_symlink() { - warnln!( - "[W15] Skipping link to package {} at {:?} since there is something there", - pkg_name, - link_path - ); + Warnings::SkippingPackageLink(pkg_name.clone(), link_path.to_path_buf()).emit(); continue; } if link_path.read_link().map(|d| d != pkg_path).unwrap_or(true) { diff --git a/src/cmd/update.rs b/src/cmd/update.rs index 0b934d75..948ed3d5 100644 --- a/src/cmd/update.rs +++ b/src/cmd/update.rs @@ -42,11 +42,9 @@ pub struct UpdateArgs { } /// Execute the `update` subcommand. -pub fn setup(args: &UpdateArgs, local: bool, suppress_warnings: &IndexSet) -> Result { - if local && args.fetch && !suppress_warnings.contains("W14") { - warnln!( - "[W14] As --local argument is set for bender command, no fetching will be performed." - ); +pub fn setup(args: &UpdateArgs, local: bool) -> Result { + if local && args.fetch { + Warnings::LocalNoFetch.emit(); } Ok(args.fetch) } diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 8dbdbae3..dcef386b 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -269,7 +269,7 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { } // Generate patch - sorted_links.into_iter().try_for_each( |patch_link| { + sorted_links.into_iter().try_for_each(|patch_link| { match patch_link.patch_dir.clone() { Some(patch_dir) => { if *plain { @@ -283,11 +283,16 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { } else { gen_format_patch(&rt, sess, git.clone(), patch_link, vendor_package.target_dir.clone(), message.as_ref()) } - }, + } None => { - warnln!("[W15] No patch directory specified for package {}, mapping {} => {}. Skipping patch generation.", vendor_package.name.clone(), patch_link.from_prefix.to_str().unwrap(), patch_link.to_prefix.to_str().unwrap()); + Warnings::NoPatchDir { + vendor_pkg: vendor_package.name.clone(), + from_prefix: patch_link.from_prefix.clone(), + to_prefix: patch_link.to_prefix.clone(), + } + .emit(); Ok(()) - }, + } } }) } diff --git a/src/error.rs b/src/error.rs index 63ade654..ecdd9cd1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -254,20 +254,32 @@ impl ReportHandler for DiagnosticRenderer { write!(f, "{}", diagnostic)?; // Below the message, there might be an additional help message - let _branch = " ├─›"; // Branching with arrow + let branch = " ├─›"; // Branching with arrow let corner = " ╰─›"; // Final corner with arrow if let Some(help) = diagnostic.help() { - // Styled messages (e.g. 'pkg.bold()') will reset the style afterwards, - // so we need to re-apply dimming after each reset. - let help = help.to_string().replace("\x1b[0m", "\x1b[0m\x1b[2m"); - write!( - f, - "\n{} {} {}", - corner.dimmed(), - "help:".bold(), - help.dimmed() - )?; + // Convert to string and split by lines + let help_str = help.to_string(); + let lines: Vec<&str> = help_str.lines().collect(); + + // Print each line with the appropriate help prefix and branching + for (i, line) in lines.iter().enumerate() { + // Determine the tree character + let is_last = i == lines.len() - 1; + let prefix = if is_last { corner } else { branch }; + + // Styled messages (e.g. 'pkg.bold()') will reset the style afterwards, + // so we need to re-apply dimming after each reset. + let line = line.replace("\x1b[0m", "\x1b[0m\x1b[2m"); + + write!( + f, + "\n{} {} {}", + prefix.dimmed(), + "help:".bold(), + line.dimmed() + )?; + } } Ok(()) @@ -381,4 +393,36 @@ pub enum Warnings { help("Please check that {} is correct and accessible.", path!(.1.display())) )] MaybePathIssues(String, PathBuf), + + #[error("Dependency package name {} does not match the package name {} in its manifest.", pkg!(.0), pkg!(.1))] + #[diagnostic( + severity(Warning), + code(W11), + help("Check that the dependency name in your root manifest matches the name in the {} manifest.", pkg!(.0)) + )] + DepPkgNameNotMatching(String, String), + + #[error("Manifest for package {} not found at {}.", pkg!(pkg), path!(src))] + #[diagnostic(severity(Warning), code(W12))] + ManifestNotFound { pkg: String, src: String }, + + #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", pkg!(.0))] + #[diagnostic( + severity(Warning), + code(W13), + help("Could be related to name missmatch, check `bender update`.") + )] + ExportDirNameIssue(String), + + #[error("If `--local` is used, no fetching will be performed.")] + #[diagnostic(severity(Warning), code(W14))] + LocalNoFetch, + + #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", pkg!(vendor_pkg), path!(from_prefix.display()), path!(to_prefix.display()))] + #[diagnostic(severity(Warning), code(W15))] + NoPatchDir { + vendor_pkg: String, + from_prefix: PathBuf, + to_prefix: PathBuf, + }, } diff --git a/src/sess.rs b/src/sess.rs index a5f6234b..71bc10df 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -1163,11 +1163,12 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { if manifest_path.exists() { match read_manifest(&manifest_path, &self.sess.suppress_warnings) { Ok(m) => { - if dep.name != m.package.name - && !self.sess.suppress_warnings.contains("W11") - { - warnln!("[W11] Dependency name and package name do not match for {:?} / {:?}, this can cause unwanted behavior", - dep.name, m.package.name); // TODO: This should be an error + if dep.name != m.package.name { + Warnings::DepPkgNameNotMatching( + dep.name.clone(), + m.package.name.clone(), + ) + .emit(); } Ok(Some(self.sess.intern_manifest(m))) } @@ -1198,11 +1199,12 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { match partial.validate_ignore_sources("", true, &self.sess.suppress_warnings) { Ok(m) => { - if dep.name != m.package.name - && !self.sess.suppress_warnings.contains("W11") - { - warnln!("[W11] Dependency name and package name do not match for {:?} / {:?}, this can cause unwanted behavior", - dep.name, m.package.name); // TODO: This should be an error + if dep.name != m.package.name { + Warnings::DepPkgNameNotMatching( + dep.name.clone(), + m.package.name.clone(), + ) + .emit(); } Ok(Some(self.sess.intern_manifest(m))) } @@ -1229,13 +1231,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } } } - if !self.sess.suppress_warnings.contains("W12") { - warnln!( - "[W12] Manifest not found for {:?} at {:?}", - dep.name, - dep.source - ); + Warnings::ManifestNotFound { + pkg: dep.name.clone(), + src: manifest_path.display().to_string(), } + .emit(); Ok(None) } } @@ -1291,9 +1291,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { Ok(Some(self.sess.intern_manifest(full))) } None => { - if !self.sess.suppress_warnings.contains("W12") { - warnln!("[W12] Manifest not found for {:?}", dep.name); + Warnings::ManifestNotFound { + pkg: dep.name.clone(), + src: url.to_string(), } + .emit(); Ok(None) } }; @@ -1304,18 +1306,12 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .lock() .unwrap() .insert(cache_key, manifest); - if dep.name - != match manifest { - Some(x) => &x.package.name, - None => "dead", - } - && !self.sess.suppress_warnings.contains("W11") - { - warnln!("[W11] Dependency name and package name do not match for {:?} / {:?}, this can cause unwanted behavior", - dep.name, match manifest { - Some(x) => &x.package.name, - None => "dead" - }); // TODO (micprog): This should be an error + let pkg_name = match manifest { + Some(x) => x.package.name.clone(), + None => "dead".to_string(), + }; + if dep.name != pkg_name { + Warnings::DepPkgNameNotMatching(dep.name.clone(), pkg_name.clone()).emit(); } Ok(manifest) } @@ -1531,21 +1527,16 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { IndexMap::new(); export_include_dirs.insert( m.package.name.clone(), - m.export_include_dirs - .iter() - .map(PathBuf::as_path) - .collect(), + m.export_include_dirs.iter().map(PathBuf::as_path).collect(), ); if !m.dependencies.is_empty() { for i in m.dependencies.keys() { if !all_export_include_dirs.contains_key(i) { - if !self.sess.suppress_warnings.contains("W13") { - warnln!("[W13] Name issue with {:?}, `export_include_dirs` not handled\n\tCould relate to name mismatch, see `bender update`", i); - } - export_include_dirs.insert(i.clone(), IndexSet::new()); + Warnings::ExportDirNameIssue(i.clone()).emit(); + export_include_dirs.insert(i.to_string(), IndexSet::new()); } else { export_include_dirs.insert( - i.clone(), + i.to_string(), all_export_include_dirs[i].clone(), ); } From e61a777dcedf8967bfc83df4a8b52d6f1ee1c11e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 09:46:00 +0100 Subject: [PATCH 15/79] error: Specify severity globally for `Warning` --- src/error.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/error.rs b/src/error.rs index ecdd9cd1..a6afb425 100644 --- a/src/error.rs +++ b/src/error.rs @@ -308,6 +308,7 @@ macro_rules! field { } #[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] +#[diagnostic(severity(Warning))] pub enum Warnings { #[error( "Skipping link to package {} at {} since there is something there", @@ -315,19 +316,17 @@ pub enum Warnings { path!(.1.display()) )] #[diagnostic( - severity(Warning), code(W01), help("Check the existing file or directory that is preventing the link.") )] SkippingPackageLink(String, PathBuf), #[error("Using config at {} for overrides.", path!(path.display()))] - #[diagnostic(severity(Warning), code(W02))] + #[diagnostic(code(W02))] UsingConfigForOverride { path: PathBuf }, #[error("Ignoring unknown field {} in package {}.", field!(field), pkg!(pkg))] #[diagnostic( - severity(Warning), code(W03), help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) )] @@ -335,19 +334,17 @@ pub enum Warnings { #[error("Source group in package {} contains no source files.", pkg!(.0))] #[diagnostic( - severity(Warning), code(W04), help("Add source files to the source group or remove it from the manifest.") )] NoFilesInSourceGroup(String), #[error("No files matched the global pattern {}.", path!(path))] - #[diagnostic(severity(Warning), code(W05))] + #[diagnostic(code(W05))] NoFilesForGlobalPattern { path: String }, #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", pkg!(.0), path!(.1.display()))] #[diagnostic( - severity(Warning), code(W06), help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.") )] @@ -357,7 +354,6 @@ pub enum Warnings { // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? #[error("SSH key might be missing.")] #[diagnostic( - severity(Warning), code(W07), help("Please ensure your public ssh key is added to the git server.") )] @@ -367,7 +363,6 @@ pub enum Warnings { // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? #[error("SSH key might be missing.")] #[diagnostic( - severity(Warning), code(W07), help("Please ensure the url is correct and you have access to the repository.") )] @@ -376,19 +371,17 @@ pub enum Warnings { // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? #[error("Revision {} not found in repository {}.", pkg!(.0), pkg!(.1))] #[diagnostic( - severity(Warning), code(W08), help("Check that the revision exists in the remote repository or run `bender update`.") )] RevisionNotFound(String, String), #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", pkg!(pkg), pkg!(top_pkg))] - #[diagnostic(severity(Warning), code(W09))] + #[diagnostic(code(W09))] PathDepInGitDep { pkg: String, top_pkg: String }, #[error("There may be issues in the path for {}.", pkg!(.0))] #[diagnostic( - severity(Warning), code(W10), help("Please check that {} is correct and accessible.", path!(.1.display())) )] @@ -396,30 +389,28 @@ pub enum Warnings { #[error("Dependency package name {} does not match the package name {} in its manifest.", pkg!(.0), pkg!(.1))] #[diagnostic( - severity(Warning), code(W11), help("Check that the dependency name in your root manifest matches the name in the {} manifest.", pkg!(.0)) )] DepPkgNameNotMatching(String, String), #[error("Manifest for package {} not found at {}.", pkg!(pkg), path!(src))] - #[diagnostic(severity(Warning), code(W12))] + #[diagnostic(code(W12))] ManifestNotFound { pkg: String, src: String }, #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", pkg!(.0))] #[diagnostic( - severity(Warning), code(W13), help("Could be related to name missmatch, check `bender update`.") )] ExportDirNameIssue(String), #[error("If `--local` is used, no fetching will be performed.")] - #[diagnostic(severity(Warning), code(W14))] + #[diagnostic(code(W14))] LocalNoFetch, #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", pkg!(vendor_pkg), path!(from_prefix.display()), path!(to_prefix.display()))] - #[diagnostic(severity(Warning), code(W15))] + #[diagnostic(code(W15))] NoPatchDir { vendor_pkg: String, from_prefix: PathBuf, From e3c352dc77effa11f91002c6c758b6c068c42296 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 10:56:58 +0100 Subject: [PATCH 16/79] error: Add suppression annotation below warning --- src/error.rs | 63 ++++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/src/error.rs b/src/error.rs index a6afb425..fd6f4ebb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -242,46 +242,45 @@ impl ReportHandler for DiagnosticRenderer { miette::Severity::Advice => unimplemented!(), }; - // Write the severity prefix - write!(f, "{}", severity.style(style))?; + // Write the severity prefix and the diagnostic message + write!(f, "{}: {}", severity.style(style), diagnostic)?; - // Writ the code, if any - if let Some(code) = diagnostic.code() { - write!(f, "{}", format!("[{}]: ", code).style(style))?; - } - - // Then, we write the diagnostic message - write!(f, "{}", diagnostic)?; - - // Below the message, there might be an additional help message - let branch = " ├─›"; // Branching with arrow - let corner = " ╰─›"; // Final corner with arrow + // We collect all footer lines into a vector. + let mut annotations: Vec = Vec::new(); + // First, we write the help message(s) if any if let Some(help) = diagnostic.help() { - // Convert to string and split by lines let help_str = help.to_string(); - let lines: Vec<&str> = help_str.lines().collect(); - - // Print each line with the appropriate help prefix and branching - for (i, line) in lines.iter().enumerate() { - // Determine the tree character - let is_last = i == lines.len() - 1; - let prefix = if is_last { corner } else { branch }; - - // Styled messages (e.g. 'pkg.bold()') will reset the style afterwards, - // so we need to re-apply dimming after each reset. - let line = line.replace("\x1b[0m", "\x1b[0m\x1b[2m"); - - write!( - f, - "\n{} {} {}", - prefix.dimmed(), + for line in help_str.lines() { + annotations.push(format!( + "{} {}", "help:".bold(), - line.dimmed() - )?; + line.replace("\x1b[0m", "\x1b[0m\x1b[2m").dimmed() + )); } } + // Finally, we write the code/suppression message, if any + if let Some(code) = diagnostic.code() { + annotations.push(format!( + "{} {}", + "suppress:".cyan().bold(), // No variable, no lifetime issue + format!("Run `bender --suppress {}` to suppress this warning", code).dimmed() + )); + } + + // Prepare tree characters + let branch = " ├─›"; + let corner = " ╰─›"; + + // Iterate over the annotations and print them + for (i, note) in annotations.iter().enumerate() { + // The last item gets the corner, everyone else gets a branch + let is_last = i == annotations.len() - 1; + let prefix = if is_last { corner } else { branch }; + write!(f, "\n{} {}", prefix.dimmed(), note)?; + } + Ok(()) } } From 9f5f088b20c5edd17f96c28656c96f5c154dfc98 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 11:03:33 +0100 Subject: [PATCH 17/79] error: Move W16 to `Warnings` (duplicates!) error: Move W17 to `Warnings` --- src/cmd/fusesoc.rs | 4 ++-- src/cmd/vendor.rs | 10 +++++----- src/error.rs | 13 +++++++++++++ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/cmd/fusesoc.rs b/src/cmd/fusesoc.rs index 559f8594..ac32ecfa 100644 --- a/src/cmd/fusesoc.rs +++ b/src/cmd/fusesoc.rs @@ -132,8 +132,8 @@ pub fn run_single(sess: &Session, args: &FusesocArgs) -> Result<()> { Error::chain(format!("Unable to write corefile for {:?}.", &name), cause) })?; - if fuse_depend_string.len() > 1 && !sess.suppress_warnings.contains("W16") { - warnln!("[W16] Depend strings may be wrong for the included dependencies!"); + if fuse_depend_string.len() > 1 { + Warnings::DependStringMaybeWrong.emit(); } Ok(()) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index dcef386b..9c7be81d 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -341,7 +341,7 @@ pub fn init( if !PathBuf::from(extend_paths(std::slice::from_ref(&path), dep_path, true)?[0].clone()) .exists() { - warnln!("[W16] {} not found in upstream, continuing.", path); + Warnings::NotInUpstream { path: path }.emit(); } } @@ -371,10 +371,10 @@ pub fn init( ) })?; } else { - warnln!( - "[W16] {} not found in upstream, continuing.", - link_from.to_str().unwrap() - ); + Warnings::NotInUpstream { + path: link_from.to_str().unwrap().to_string(), + } + .emit(); } } }; diff --git a/src/error.rs b/src/error.rs index fd6f4ebb..91b2de86 100644 --- a/src/error.rs +++ b/src/error.rs @@ -415,4 +415,17 @@ pub enum Warnings { from_prefix: PathBuf, to_prefix: PathBuf, }, + + #[error("Dependency string for the included dependencies might be wrong.")] + #[diagnostic(code(W16))] + DependStringMaybeWrong, + + // TODO(fischeti): Why are there two W16 variants? + #[error("{} not found in upstream, continuing.", path!(path))] + #[diagnostic(code(W16))] + NotInUpstream { path: String }, + + #[error("Package {} is shown to include dependency, but manifest does not have this information.", pkg!(pkg))] + #[diagnostic(code(W17))] + IncludeDepManifestMismatch { pkg: String }, } From 7d80b3f011f29a2aa3be1859b1336bef0d18b27f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 11:22:35 +0100 Subject: [PATCH 18/79] error: Export format macros --- src/error.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/error.rs b/src/error.rs index 91b2de86..cdbe8c1e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -286,6 +286,7 @@ impl ReportHandler for DiagnosticRenderer { } /// Bold a package name in diagnostic messages. +#[macro_export] macro_rules! pkg { ($pkg:expr) => { $pkg.bold() @@ -293,6 +294,7 @@ macro_rules! pkg { } /// Underline a path in diagnostic messages. +#[macro_export] macro_rules! path { ($pkg:expr) => { $pkg.underline() @@ -300,6 +302,7 @@ macro_rules! path { } /// Italicize a field name in diagnostic messages. +#[macro_export] macro_rules! field { ($field:expr) => { $field.italic() From c2ae983a9615b4cce39d80fb400ed4d4511c28b5 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 11:22:46 +0100 Subject: [PATCH 19/79] error: Migrate more warnings error: Move W19 to `Warnings` error: Move W20 to `Warnings` error: Move W21 to `Warnings` error: Move W22 to `Warnings` error: Move W23 to `Warnings` error: Move W24 to `Warnings` error: Move W30 to `Warnings` error: Move W31 to `Warnings` error: Move W32 to `Warnings` error: Move uncoded Warning to `Warnings` --- src/cmd/parents.rs | 22 ++++++++++++----- src/cmd/snapshot.rs | 6 +---- src/config.rs | 36 ++++++++++----------------- src/error.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++- src/resolver.rs | 27 ++++++--------------- src/sess.rs | 59 +++++++++++++-------------------------------- src/src.rs | 18 +++++++------- 7 files changed, 122 insertions(+), 105 deletions(-) diff --git a/src/cmd/parents.rs b/src/cmd/parents.rs index 2445082d..ba235e3a 100644 --- a/src/cmd/parents.rs +++ b/src/cmd/parents.rs @@ -7,6 +7,7 @@ use std::io::Write; use clap::Args; use indexmap::IndexMap; +use owo_colors::OwoColorize; use tabwriter::TabWriter; use tokio::runtime::Runtime; @@ -96,12 +97,21 @@ pub fn run(sess: &Session, args: &ParentsArgs) -> Result<()> { } ); - if sess.config.overrides.contains_key(dep) && !sess.suppress_warnings.contains("W18") { - warnln!( - "[W18] An override is configured for {} to {:?}", - dep, - sess.config.overrides[dep] - ) + if sess.config.overrides.contains_key(dep) { + Warnings::DepOverride { + pkg: dep.to_string(), + pkg_override: match sess.config.overrides[dep] { + Dependency::Version(ref v) => format!("version {}", pkg!(v)), + Dependency::Path(ref path) => format!("path {}", path!(path.display())), + Dependency::GitRevision(ref url, ref rev) => { + format!("git {} at revision {}", path!(url), pkg!(rev)) + } + Dependency::GitVersion(ref url, ref version) => { + format!("git {} with version {}", path!(url), pkg!(version)) + } + }, + } + .emit(); } Ok(()) diff --git a/src/cmd/snapshot.rs b/src/cmd/snapshot.rs index 59eccccc..748f9ba4 100644 --- a/src/cmd/snapshot.rs +++ b/src/cmd/snapshot.rs @@ -57,11 +57,7 @@ pub fn run(sess: &Session, args: &SnapshotArgs) -> Result<()> { .is_empty() && !args.no_skip { - warnln!( - "Skipping dirty dependency {}\ - \t use `--no-skip` to still snapshot.", - name - ); + Warnings::SkippingDirtyDep { pkg: name.clone() }.emit(); continue; } diff --git a/src/config.rs b/src/config.rs index 9b260236..24ecd542 100644 --- a/src/config.rs +++ b/src/config.rs @@ -538,22 +538,18 @@ impl Validate for PartialManifest { .iter() .filter_map(|path| match env_path_from_string(path.to_string()) { Ok(parsed_path) => { - if !(suppress_warnings.contains("W24") - || pre_output - || parsed_path.exists() && parsed_path.is_dir()) - { - warnln!( - "[W24] Include directory {} doesn't exist.", - &parsed_path.display() - ); + if !(pre_output || parsed_path.exists() && parsed_path.is_dir()) { + Warnings::IncludeDirMissing(parsed_path.clone()).emit(); } + Some(Ok(parsed_path)) } Err(cause) => { - if suppress_warnings.contains("E30") { - if !suppress_warnings.contains("W30") { - warnln!("[W30] File not added, ignoring: {}", cause); + if Diagnostics::is_suppressed("E30") { + Warnings::IgnoredPath { + cause: cause.to_string(), } + .emit(); None } else { Some(Err(Error::chain("[E30]", cause))) @@ -832,10 +828,8 @@ impl Validate for PartialSources { .filter_map(|path| match env_path_from_string(path.to_string()) { Ok(p) => Some(Ok(p)), Err(cause) => { - if suppress_warnings.contains("E30") { - if !suppress_warnings.contains("W30") { - warnln!("[W30] File not added, ignoring: {}", cause); - } + if Diagnostics::is_suppressed("E30") { + Warnings::IgnoredPath {cause: cause.to_string()}.emit(); None } else { Some(Err(Error::chain("[E30]", cause))) @@ -974,10 +968,8 @@ impl Validate for PartialSources { _ => unreachable!(), }, Err(cause) => { - if suppress_warnings.contains("E30") { - if !suppress_warnings.contains("W30") { - warnln!("[W30] File not added, ignoring: {}", cause); - } + if Diagnostics::is_suppressed("E30") { + Warnings::IgnoredPath {cause: cause.to_string()}.emit(); None } else { Some(Err(Error::chain("[E30]", cause))) @@ -1019,10 +1011,8 @@ impl Validate for PartialSources { .filter_map(|path| match env_path_from_string(path.to_string()) { Ok(p) => Some(Ok(p)), Err(cause) => { - if suppress_warnings.contains("E30") { - if !suppress_warnings.contains("W30") { - warnln!("[W30] File not added, ignoring: {}", cause); - } + if Diagnostics::is_suppressed("E30") { + Warnings::IgnoredPath {cause: cause.to_string()}.emit(); None } else { Some(Err(Error::chain("[E30]", cause))) diff --git a/src/error.rs b/src/error.rs index cdbe8c1e..eb02e737 100644 --- a/src/error.rs +++ b/src/error.rs @@ -204,6 +204,12 @@ impl Diagnostics { .get() .expect("Diagnostics not initialized!") } + + /// Check whether a warning/error code is suppressed. + pub fn is_suppressed(code: &str) -> bool { + let diag = Diagnostics::get(); + diag.all_suppressed || diag.suppressed.contains(&code.to_string()) + } } impl Warnings { @@ -239,7 +245,7 @@ impl ReportHandler for DiagnosticRenderer { let (severity, style) = match diagnostic.severity().unwrap_or_default() { miette::Severity::Error => ("error", owo_colors::Style::new().red().bold()), miette::Severity::Warning => ("warning", owo_colors::Style::new().yellow().bold()), - miette::Severity::Advice => unimplemented!(), + miette::Severity::Advice => ("advice", owo_colors::Style::new().cyan().bold()), }; // Write the severity prefix and the diagnostic message @@ -431,4 +437,55 @@ pub enum Warnings { #[error("Package {} is shown to include dependency, but manifest does not have this information.", pkg!(pkg))] #[diagnostic(code(W17))] IncludeDepManifestMismatch { pkg: String }, + + #[error("An override is specified for dependency {} to {}.", pkg!(pkg), pkg!(pkg_override))] + #[diagnostic(code(W18))] + DepOverride { pkg: String, pkg_override: String }, + + #[error("Workspace checkout directory set and has uncommitted changes, not updating {} at {}.", pkg!(.0), path!(.1.display()))] + #[diagnostic( + code(W19), + help("Run `bender checkout --force` to overwrite the dependency at your own risk.") + )] + CheckoutDirDirty(String, PathBuf), + + // TODO(fischeti): Should this be an error instead of a warning? + #[error("Ignoring error for {} at {}: {}", pkg!(.0), path!(.1), .2)] + #[diagnostic(code(W20))] + IgnoringError(String, String, String), + + #[error("No revision found in lock file for git dependency {}.", pkg!(pkg))] + #[diagnostic(code(W21))] + NoRevisionInLockFile { pkg: String }, + + #[error("Dependency {} has source path {} which does not exist.", pkg!(.0), path!(.1.display()))] + #[diagnostic(code(W22), help("Please check that the path exists and is correct."))] + DepSourcePathMissing(String, PathBuf), + + #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", pkg!(rev), pkg!(pkg))] + #[diagnostic(code(W23))] + LockedRevisionNotFound { pkg: String, rev: String }, + + #[error("Include directory {} doesn't exist.", path!(.0.display()))] + #[diagnostic( + code(W24), + help("Please check that the include directory exists and is correct.") + )] + IncludeDirMissing(PathBuf), + + #[error("Skipping dirty dependency {}", pkg!(pkg))] + #[diagnostic(help("Use `--no-skip` to still snapshot {}.", pkg!(pkg)))] + SkippingDirtyDep { pkg: String }, + + #[error("File not added, ignoring: {cause}")] + #[diagnostic(code(W30))] + IgnoredPath { cause: String }, + + #[error("File {} doesn't exist.", path!(path.display()))] + #[diagnostic(code(W31))] + FileMissing { path: PathBuf }, + + #[error("Path {} for dependency {} does not exist.", path!(path.display()), pkg!(pkg))] + #[diagnostic(code(W32))] + DepPathMissing { pkg: String, path: PathBuf }, } diff --git a/src/resolver.rs b/src/resolver.rs index c66d7f05..5aaadc55 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -375,12 +375,10 @@ impl<'ctx> DependencyResolver<'ctx> { match &locked_package.revision { Some(r) => r.clone(), None => { - if !io.sess.suppress_warnings.contains("W21") { - warnln!( - "[W21] No revision found in lock file for git dependency `{}`", - name - ); + Warnings::NoRevisionInLockFile { + pkg: name.to_string(), } + .emit(); return None; } }, @@ -463,13 +461,11 @@ impl<'ctx> DependencyResolver<'ctx> { match gv.revs.iter().position(|rev| *rev == hash.unwrap()) { Some(index) => index, None => { - if !self.sess.suppress_warnings.contains("W23") { - warnln!( - "[W23] Locked revision `{:?}` for dependency `{}` not found in available revisions, allowing update.", - hash.unwrap(), - dep - ); + Warnings::LockedRevisionNotFound { + rev: hash.unwrap().to_string(), + pkg: dep.to_string(), } + .emit(); self.locked.get_mut(dep.as_str()).unwrap().3 = false; continue; } @@ -643,14 +639,7 @@ impl<'ctx> DependencyResolver<'ctx> { if id == con_src { return Err(e); } - if !self.sess.suppress_warnings.contains("W20") { - warnln!( - "[W20] Ignoring error for `{}` at `{}`: {}", - name, - self.sess.dependency_source(*con_src), - e - ); - } + Warnings::IgnoringError(name.to_string(), self.sess.dependency_source(*con_src).to_string(), e.to_string()).emit(); Ok((*id, IndexSet::new())) } } diff --git a/src/sess.rs b/src/sess.rs index 71bc10df..f5f7f55a 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -140,15 +140,15 @@ impl<'ctx> Session<'ctx> { calling_package ); let src = DependencySource::from(cfg); - self.deps.lock().unwrap().add( - self.intern_dependency_entry(DependencyEntry { + self.deps + .lock() + .unwrap() + .add(self.intern_dependency_entry(DependencyEntry { name: name.into(), source: src, revision: None, version: None, - }), - &self.suppress_warnings, - ) + })) } /// Load a lock file. @@ -175,7 +175,6 @@ impl<'ctx> Session<'ctx> { .as_ref() .map(|s| semver::Version::parse(s).unwrap()), }), - &self.suppress_warnings, ); graph_names.insert(id, &pkg.dependencies); names.insert(name.clone(), id); @@ -895,25 +894,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { { CheckoutState::ToCheckout } else { - if !self.sess.suppress_warnings.contains("W19") { - warnln!( - "[W19] Workspace checkout directory set and has uncommitted changes, not updating {} at {}.\n\ - \tRun `bender checkout --force` to overwrite the dependency at your own risk.", - name, - path.display() - ); - } + Warnings::CheckoutDirDirty(name.to_string(), path.to_path_buf()).emit(); CheckoutState::Clean } } else { - if !self.sess.suppress_warnings.contains("W19") { - warnln!( - "[W19] Workspace checkout directory set and remote url doesn't match, not updating {} at {}.\n\ - \tRun `bender checkout --force` to overwrite the dependency at your own risk.", - name, - path.display() - ); - } + Warnings::CheckoutDirDirty(name.to_string(), path.to_path_buf()).emit(); CheckoutState::Clean } } else { @@ -1211,17 +1196,15 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { Err(e) => Err(e), } } else { - if !(self.sess.suppress_warnings.contains("E32") - && self.sess.suppress_warnings.contains("W32")) - { + if !(Diagnostics::is_suppressed("E32")) { if let DepSrc::Path(ref path) = dep.source { if !path.exists() { - if self.sess.suppress_warnings.contains("E32") { - warnln!( - "[W32] Path {:?} for dependency {:?} does not exist.", - path, - dep.name - ); + if Diagnostics::is_suppressed("E32") { + Warnings::DepPathMissing { + pkg: dep.name.clone(), + path: path.to_path_buf(), + } + .emit(); } else { return Err(Error::new(format!( "[E32] Path {:?} for dependency {:?} does not exist.", @@ -1821,22 +1804,14 @@ impl<'ctx> DependencyTable<'ctx> { /// /// The reference with which the information can later be retrieved is /// returned. - pub fn add( - &mut self, - entry: &'ctx DependencyEntry, - suppress_warnings: &IndexSet, - ) -> DependencyRef { + pub fn add(&mut self, entry: &'ctx DependencyEntry) -> DependencyRef { if let Some(&id) = self.ids.get(&entry) { debugln!("sess: reusing {:?}", id); id } else { if let DependencySource::Path(path) = &entry.source { - if !path.exists() && !suppress_warnings.contains("W22") { - warnln!( - "[W22] Dependency `{}` has source path `{}` which does not exist", - entry.name, - path.display() - ); + if !path.exists() { + Warnings::DepSourcePathMissing(entry.name.clone(), path.clone()).emit(); } } let id = DependencyRef(self.list.len()); diff --git a/src/src.rs b/src/src.rs index 5c2dad83..dd32f4e7 100644 --- a/src/src.rs +++ b/src/src.rs @@ -15,7 +15,7 @@ use indexmap::{IndexMap, IndexSet}; use serde::ser::{Serialize, Serializer}; use crate::config::Validate; -use crate::error::Error; +use crate::error::{Diagnostics, Error, Warnings}; use crate::sess::Session; use crate::target::{TargetSet, TargetSpec}; use semver; @@ -64,8 +64,8 @@ impl<'ctx> Validate for SourceGroup<'ctx> { .include_dirs .into_iter() .map(|p| { - if !(suppress_warnings.contains("W24") || p.exists() && p.is_dir()) { - warnln!("[W24] Include directory {} doesn't exist.", p.display()); + if !p.exists() || !p.is_dir() { + Warnings::IncludeDirMissing(p.to_path_buf()).emit(); } Ok(p) }) @@ -480,12 +480,12 @@ impl<'ctx> Validate for SourceFile<'ctx> { let env_path_buf = crate::config::env_path_from_string(path.to_string_lossy().to_string())?; let exists = env_path_buf.exists() && env_path_buf.is_file(); - if exists || suppress_warnings.contains("E31") { - if !(exists || suppress_warnings.contains("W31")) { - warnln!( - "[W31] File {} doesn't exist.", - env_path_buf.to_string_lossy() - ); + if exists || Diagnostics::is_suppressed("E31") { + if !exists { + Warnings::FileMissing { + path: env_path_buf.clone(), + } + .emit(); } Ok(SourceFile::File(path, ty)) } else { From 4cea9ab9af6f9dc4b8d0bb5d81d61dc6d56de7df Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 12:30:58 +0100 Subject: [PATCH 20/79] error: Replace deprecated `ATOMIC_BOOL_INIT` --- src/error.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index eb02e737..9974ebc6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,12 +5,10 @@ use std; use std::fmt; -#[allow(deprecated)] -use std::sync::atomic::{AtomicBool, ATOMIC_BOOL_INIT}; +use std::sync::atomic::AtomicBool; use std::sync::Arc; -#[allow(deprecated)] -pub static ENABLE_DEBUG: AtomicBool = ATOMIC_BOOL_INIT; +pub static ENABLE_DEBUG: AtomicBool = AtomicBool::new(false); /// Print an error. #[macro_export] From b9a80d734092729fca7d4eed1ce85894b2dd4043 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 13:10:58 +0100 Subject: [PATCH 21/79] error: Remove `warnln` macro --- src/error.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/error.rs b/src/error.rs index 9974ebc6..2a20b7ae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,12 +16,6 @@ macro_rules! errorln { ($($arg:tt)*) => { diagnostic!($crate::error::Severity::Error; $($arg)*); } } -/// Print a warning. -#[macro_export] -macro_rules! warnln { - ($($arg:tt)*) => { diagnostic!($crate::error::Severity::Warning; $($arg)*) } -} - /// Print an informational note. #[macro_export] macro_rules! noteln { From cedb691dc73f5840716e8a0ce856ccde18a39d21 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 13:11:47 +0100 Subject: [PATCH 22/79] sess: Don't pass `suppress_warnings` anymore --- src/cli.rs | 15 ++---- src/cmd/script.rs | 2 +- src/cmd/sources.rs | 2 +- src/config.rs | 122 +++++++++++++-------------------------------- src/sess.rs | 20 +++----- src/src.rs | 18 ++----- 6 files changed, 55 insertions(+), 124 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index d4b6452c..e2bd370c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -172,7 +172,7 @@ pub fn main() -> Result<()> { // Parse the manifest file of the package. let manifest_path = root_dir.join("Bender.yml"); - let manifest = read_manifest(&manifest_path, &suppressed_warnings)?; + let manifest = read_manifest(&manifest_path)?; debugln!("main: {:#?}", manifest); // Gather and parse the tool configuration. @@ -196,7 +196,6 @@ pub fn main() -> Result<()> { cli.local, force_fetch, git_throttle, - suppressed_warnings, ); if let Commands::Clean(args) = cli.command { @@ -399,7 +398,7 @@ fn find_package_root(from: &Path) -> Result { } /// Read a package manifest from a file. -pub fn read_manifest(path: &Path, suppress_warnings: &IndexSet) -> Result { +pub fn read_manifest(path: &Path) -> Result { use crate::config::PartialManifest; use std::fs::File; debugln!("read_manifest: {:?}", path); @@ -410,16 +409,12 @@ pub fn read_manifest(path: &Path, suppress_warnings: &IndexSet) -> Resul partial .prefix_paths(path.parent().unwrap()) .map_err(|cause| Error::chain(format!("Error in manifest prefixing {:?}.", path), cause))? - .validate("", false, suppress_warnings) + .validate("", false) .map_err(|cause| Error::chain(format!("Error in manifest {:?}.", path), cause)) } /// Load a configuration by traversing a directory hierarchy upwards. -fn load_config( - from: &Path, - warn_config_loaded: bool, - suppress_warnings: &IndexSet, -) -> Result { +fn load_config(from: &Path, warn_config_loaded: bool) -> Result { #[cfg(unix)] use std::os::unix::fs::MetadataExt; @@ -492,7 +487,7 @@ fn load_config( // Validate the configuration. let mut out = out - .validate("", false, suppress_warnings) + .validate("", false) .map_err(|cause| Error::chain("Invalid configuration:", cause))?; out.overrides = out diff --git a/src/cmd/script.rs b/src/cmd/script.rs index e58c00ae..b84ab371 100644 --- a/src/cmd/script.rs +++ b/src/cmd/script.rs @@ -283,7 +283,7 @@ pub fn run(sess: &Session, args: &ScriptArgs) -> Result<()> { let srcs = srcs .flatten() .into_iter() - .map(|f| f.validate("", false, &sess.suppress_warnings)) + .map(|f| f.validate("", false)) .collect::>>()?; let mut tera_context = Context::new(); diff --git a/src/cmd/sources.rs b/src/cmd/sources.rs index 91a94675..6c61615e 100644 --- a/src/cmd/sources.rs +++ b/src/cmd/sources.rs @@ -99,7 +99,7 @@ pub fn run(sess: &Session, args: &SourcesArgs) -> Result<()> { srcs = srcs.filter_packages(packages).unwrap_or_default(); } - srcs = srcs.validate("", false, &sess.suppress_warnings)?; + srcs = srcs.validate("", false)?; let result = { let stdout = std::io::stdout(); diff --git a/src/config.rs b/src/config.rs index 24ecd542..5c7098cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,7 +18,7 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use glob::glob; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexMap; use semver; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; @@ -266,7 +266,6 @@ pub trait Validate { self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> std::result::Result; } @@ -282,15 +281,12 @@ where self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> std::result::Result { self.into_iter() - .map( - |(k, v)| match v.validate(package_name, pre_output, suppress_warnings) { - Ok(v) => Ok((k, v)), - Err(e) => Err((k, e)), - }, - ) + .map(|(k, v)| match v.validate(package_name, pre_output) { + Ok(v) => Ok((k, v)), + Err(e) => Err((k, e)), + }) .collect() } } @@ -305,15 +301,12 @@ where self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> std::result::Result { self.into_iter() - .map( - |v| match v.validate(package_name, pre_output, suppress_warnings) { - Ok(v) => Ok(v), - Err(e) => Err(e), - }, - ) + .map(|v| match v.validate(package_name, pre_output) { + Ok(v) => Ok(v), + Err(e) => Err(e), + }) .collect() } } @@ -329,9 +322,8 @@ where self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> std::result::Result { - self.0.validate(package_name, pre_output, suppress_warnings) + self.0.validate(package_name, pre_output) } } @@ -346,9 +338,8 @@ where self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> std::result::Result { - self.0.validate(package_name, pre_output, suppress_warnings) + self.0.validate(package_name, pre_output) } } @@ -406,10 +397,9 @@ impl PartialManifest { mut self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> Result { self.sources = Some(SeqOrStruct::new(PartialSources::new_empty())); - self.validate(package_name, pre_output, suppress_warnings) + self.validate(package_name, pre_output) } } @@ -443,12 +433,7 @@ impl PrefixPaths for PartialManifest { impl Validate for PartialManifest { type Output = Manifest; type Error = Error; - fn validate( - self, - _package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, _package_name: &str, pre_output: bool) -> Result { let pkg = match self.package { Some(mut p) => { p.name = p.name.to_lowercase(); @@ -470,7 +455,7 @@ impl Validate for PartialManifest { .into_iter() .map(|(k, v)| (k.to_lowercase(), v)) .collect::>() - .validate(&pkg.name, pre_output, suppress_warnings) + .validate(&pkg.name, pre_output) .map_err(|(key, cause)| { Error::chain( format!("In dependency `{}` of package `{}`:", key, pkg.name), @@ -480,12 +465,9 @@ impl Validate for PartialManifest { None => IndexMap::new(), }; let srcs = match self.sources { - Some(s) => Some( - s.validate(&pkg.name, pre_output, suppress_warnings) - .map_err(|cause| { - Error::chain(format!("In source list of package `{}`:", pkg.name), cause) - })?, - ), + Some(s) => Some(s.validate(&pkg.name, pre_output).map_err(|cause| { + Error::chain(format!("In source list of package `{}`:", pkg.name), cause) + })?), None => None, }; let exp_inc_dirs = self.export_include_dirs.unwrap_or_default(); @@ -499,13 +481,13 @@ impl Validate for PartialManifest { let frozen = self.frozen.unwrap_or(false); let workspace = match self.workspace { Some(w) => w - .validate(&pkg.name, pre_output, suppress_warnings) + .validate(&pkg.name, pre_output) .map_err(|cause| Error::chain("In workspace configuration:", cause))?, None => Workspace::default(), }; let vendor_package = match self.vendor_package { Some(vend) => vend - .validate(&pkg.name, pre_output, suppress_warnings) + .validate(&pkg.name, pre_output) .map_err(|cause| Error::chain("Unable to parse vendor_package", cause))?, None => Vec::new(), }; @@ -621,12 +603,7 @@ impl PrefixPaths for PartialDependency { impl Validate for PartialDependency { type Output = Dependency; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { let pass_targets = self .pass_targets .unwrap_or_default() @@ -770,12 +747,7 @@ impl From> for PartialSources { impl Validate for PartialSources { type Output = SourceFile; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { match self { PartialSources { target: None, @@ -787,7 +759,7 @@ impl Validate for PartialSources { vhd: None, external_flists: None, extra: _, - } => PartialSourceFile::SvFile(sv).validate(package_name, pre_output, suppress_warnings), + } => PartialSourceFile::SvFile(sv).validate(package_name, pre_output), PartialSources { target: None, include_dirs: None, @@ -798,7 +770,7 @@ impl Validate for PartialSources { vhd: None, external_flists: None, extra: _, - } => PartialSourceFile::VerilogFile(v).validate(package_name, pre_output, suppress_warnings), + } => PartialSourceFile::VerilogFile(v).validate(package_name, pre_output), PartialSources { target: None, include_dirs: None, @@ -809,7 +781,7 @@ impl Validate for PartialSources { vhd: Some(vhd), external_flists: None, extra: _, - } => PartialSourceFile::VhdlFile(vhd).validate(package_name, pre_output, suppress_warnings), + } => PartialSourceFile::VhdlFile(vhd).validate(package_name, pre_output), PartialSources { target, include_dirs, @@ -991,7 +963,7 @@ impl Validate for PartialSources { | PartialSourceFile::VerilogFile(_) | PartialSourceFile::VhdlFile(_) => { // PartialSources .files item is pointing to PartialSourceFiles::file so do glob extension - pre_glob_file.glob_file(suppress_warnings) + pre_glob_file.glob_file() } _ => { // PartialSources .files item is pointing to PartialSourceFiles::group so pass on for recursion @@ -1024,7 +996,7 @@ impl Validate for PartialSources { let defines = defines.unwrap_or_default(); let files: Result> = post_glob_files .into_iter() - .map(|f| f.validate(package_name, pre_output, suppress_warnings)) + .map(|f| f.validate(package_name, pre_output)) .collect(); let files: Vec = files?; let files: Vec = files.into_iter().collect(); @@ -1163,20 +1135,13 @@ impl<'de> Deserialize<'de> for PartialSourceFile { impl Validate for PartialSourceFile { type Output = SourceFile; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { match self { PartialSourceFile::File(path) => Ok(SourceFile::File(PathBuf::from(path))), // PartialSourceFile::Group(srcs) => Ok(Some(SourceFile::Group(Box::new( // srcs.validate(package_name, pre_output, suppress_warnings)?, // )))), - PartialSourceFile::Group(srcs) => { - Ok(srcs.validate(package_name, pre_output, suppress_warnings)?) - } + PartialSourceFile::Group(srcs) => Ok(srcs.validate(package_name, pre_output)?), PartialSourceFile::SvFile(path) => Ok(SourceFile::SvFile(env_path_from_string(path)?)), PartialSourceFile::VerilogFile(path) => { Ok(SourceFile::VerilogFile(env_path_from_string(path)?)) @@ -1195,14 +1160,14 @@ pub trait GlobFile { /// The error type produced by validation. type Error; /// Validate self and convert to a full list of paths that exist - fn glob_file(self, suppress_warnings: &IndexSet) -> Result; + fn glob_file(self) -> Result; } impl GlobFile for PartialSourceFile { type Output = Vec; type Error = Error; - fn glob_file(self, suppress_warnings: &IndexSet) -> Result> { + fn glob_file(self) -> Result> { // let mut partial_source_files_vec: Vec = Vec::new(); // Only operate on files, not groups @@ -1288,12 +1253,7 @@ impl PrefixPaths for PartialWorkspace { impl Validate for PartialWorkspace { type Output = Workspace; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { let package_links: Result> = self .package_links .unwrap_or_default() @@ -1470,12 +1430,7 @@ impl Merge for PartialConfig { impl Validate for PartialConfig { type Output = Config; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { Ok(Config { database: match self.database { Some(db) => env_path_from_string(db)?, @@ -1487,7 +1442,7 @@ impl Validate for PartialConfig { }, overrides: match self.overrides { Some(d) => d - .validate(package_name, pre_output, suppress_warnings) + .validate(package_name, pre_output) .map_err(|(key, cause)| { Error::chain(format!("In override `{}`:", key), cause) })?, @@ -1495,7 +1450,7 @@ impl Validate for PartialConfig { }, plugins: match self.plugins { Some(d) => d - .validate(package_name, pre_output, suppress_warnings) + .validate(package_name, pre_output) .map_err(|(key, cause)| Error::chain(format!("In plugin `{}`:", key), cause))?, None => IndexMap::new(), }, @@ -1617,12 +1572,7 @@ impl PrefixPaths for PartialVendorPackage { impl Validate for PartialVendorPackage { type Output = VendorPackage; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result { + fn validate(self, package_name: &str, pre_output: bool) -> Result { Ok(VendorPackage { name: match self.name { Some(name) => name, @@ -1634,7 +1584,7 @@ impl Validate for PartialVendorPackage { }, upstream: match self.upstream { Some(upstream) => upstream - .validate(package_name, pre_output, suppress_warnings) + .validate(package_name, pre_output) .map_err(|cause| { Error::chain("Unable to parse external import upstream", cause) })?, diff --git a/src/sess.rs b/src/sess.rs index f5f7f55a..c59292ac 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -77,8 +77,6 @@ pub struct Session<'ctx> { pub git_throttle: Arc, /// A toggle to disable remote fetches & clones pub local_only: bool, - /// A list of warnings to suppress. - pub suppress_warnings: IndexSet, } impl<'ctx> Session<'ctx> { @@ -92,7 +90,6 @@ impl<'ctx> Session<'ctx> { local_only: bool, force_fetch: bool, git_throttle: usize, - suppress_warnings: IndexSet, ) -> Session<'ctx> { Session { root, @@ -118,7 +115,6 @@ impl<'ctx> Session<'ctx> { cache: Default::default(), git_throttle: Arc::new(Semaphore::new(git_throttle)), local_only, - suppress_warnings, } } @@ -1078,9 +1074,8 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { cause, ) })?; - let mut full = partial - .validate_ignore_sources("", true, &self.sess.suppress_warnings) - .map_err(|cause| { + let mut full = + partial.validate_ignore_sources("", true).map_err(|cause| { Error::chain( format!( "Error in manifest of dependency `{}` at revision \ @@ -1146,7 +1141,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } let manifest_path = path.join("Bender.yml"); if manifest_path.exists() { - match read_manifest(&manifest_path, &self.sess.suppress_warnings) { + match read_manifest(&manifest_path) { Ok(m) => { if dep.name != m.package.name { Warnings::DepPkgNameNotMatching( @@ -1182,7 +1177,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { Error::chain(format!("Syntax error in manifest {:?}.", path), cause) })?; - match partial.validate_ignore_sources("", true, &self.sess.suppress_warnings) { + match partial.validate_ignore_sources("", true) { Ok(m) => { if dep.name != m.package.name { Warnings::DepPkgNameNotMatching( @@ -1247,9 +1242,8 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { cause, ) })?; - let mut full = partial - .validate_ignore_sources("", true, &self.sess.suppress_warnings) - .map_err(|cause| { + let mut full = + partial.validate_ignore_sources("", true).map_err(|cause| { Error::chain( format!( "Error in manifest of dependency `{}` at revision \ @@ -1334,7 +1328,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .and_then(move |path| { let manifest_path = path.join("Bender.yml"); if manifest_path.exists() { - match read_manifest(&manifest_path, &self.sess.suppress_warnings) { + match read_manifest(&manifest_path) { Ok(m) => Ok(Some(self.sess.intern_manifest(m))), Err(e) => Err(e), } diff --git a/src/src.rs b/src/src.rs index dd32f4e7..791a7f3c 100644 --- a/src/src.rs +++ b/src/src.rs @@ -52,13 +52,12 @@ impl<'ctx> Validate for SourceGroup<'ctx> { self, package_name: &str, pre_output: bool, - suppress_warnings: &IndexSet, ) -> crate::error::Result> { Ok(SourceGroup { files: self .files .into_iter() - .map(|f| f.validate(package_name, pre_output, suppress_warnings)) + .map(|f| f.validate(package_name, pre_output)) .collect::, Error>>()?, include_dirs: self .include_dirs @@ -469,12 +468,7 @@ impl<'ctx> From<&'ctx Path> for SourceFile<'ctx> { impl<'ctx> Validate for SourceFile<'ctx> { type Output = SourceFile<'ctx>; type Error = Error; - fn validate( - self, - package_name: &str, - pre_output: bool, - suppress_warnings: &IndexSet, - ) -> Result, Error> { + fn validate(self, package_name: &str, pre_output: bool) -> Result, Error> { match self { SourceFile::File(path, ty) => { let env_path_buf = @@ -495,11 +489,9 @@ impl<'ctx> Validate for SourceFile<'ctx> { ))) } } - SourceFile::Group(srcs) => Ok(SourceFile::Group(Box::new(srcs.validate( - package_name, - pre_output, - suppress_warnings, - )?))), + SourceFile::Group(srcs) => Ok(SourceFile::Group(Box::new( + srcs.validate(package_name, pre_output)?, + ))), } } } From 8d528b316a9ce82ffeeb854bb394e2243ec2e075 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 13:53:24 +0100 Subject: [PATCH 23/79] error: Add unit tests --- src/error.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/error.rs b/src/error.rs index 2a20b7ae..cfaf5345 100644 --- a/src/error.rs +++ b/src/error.rs @@ -481,3 +481,176 @@ pub enum Warnings { #[diagnostic(code(W32))] DepPathMissing { pkg: String, path: PathBuf }, } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Once; + + static TEST_INIT: Once = Once::new(); + + /// Helper to initialize diagnostics once for the entire test run. + fn setup_diagnostics() { + TEST_INIT.call_once(|| { + // We use an empty set for the global init in tests + // or a specific set if needed. + Diagnostics::init(HashSet::from(["W02".to_string()])); + }); + } + + #[test] + fn test_is_suppressed() { + setup_diagnostics(); + assert!(Diagnostics::is_suppressed("W02")); + assert!(!Diagnostics::is_suppressed("W01")); + } + + #[test] + fn test_suppression_works() { + setup_diagnostics(); // Assumes this suppresses W02 + let diag = Diagnostics::get(); + + let warn = Warnings::UsingConfigForOverride { + path: PathBuf::from("/example/path"), + }; + + // Clear state + diag.emitted.lock().unwrap().clear(); + + // Call emit (The Gatekeeper) + warn.clone().emit(); + + let emitted = diag.emitted.lock().unwrap(); + assert!(!emitted.contains(&warn)); + } + + #[test] + fn test_all_suppressed() { + // Since we can't re-init the GLOBAL_DIAGNOSTICS with different values + // in the same process, we test the logic via a local instance. + let diag = Diagnostics { + suppressed: HashSet::new(), + all_suppressed: true, + emitted: Mutex::new(HashSet::new()), + }; + + // Manual check of the logic inside emit() + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + assert!(diag.all_suppressed || diag.suppressed.contains(&code)); + } + + #[test] + fn test_deduplication_logic() { + setup_diagnostics(); + let diag = Diagnostics::get(); + let warn1 = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".into(), + }; + let warn2 = Warnings::NoRevisionInLockFile { + pkg: "other_pkg".into(), + }; + + // Clear state + diag.emitted.lock().unwrap().clear(); + + // Emit first warning + warn1.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + assert!(emitted.contains(&warn1)); + assert_eq!(emitted.len(), 1); + } + + // Emit second warning (different data) + warn2.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + assert!(emitted.contains(&warn2)); + assert_eq!(emitted.len(), 2); + } + + // Emit first warning again + warn1.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + // The length should STILL be 2, because warn1 was already there + assert_eq!(emitted.len(), 2); + } + } + + #[test] + fn test_contains_code() { + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + assert_eq!(code, "W14".to_string()); + } + + #[test] + fn test_contains_no_code() { + let warn = Warnings::SkippingDirtyDep { + pkg: "example_pkg".to_string(), + }; + let code = warn.code(); + assert!(code.is_none()); + } + + #[test] + fn test_contains_help() { + let warn = Warnings::SkippingPackageLink( + "example_pkg".to_string(), + PathBuf::from("/example/path"), + ); + let help = warn.help().unwrap().to_string(); + assert!(help.contains("Check the existing file or directory")); + } + + #[test] + fn test_contains_no_help() { + let warn = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".to_string(), + }; + let help = warn.help(); + assert!(help.is_none()); + } + + #[test] + fn test_stderr_contains_code() { + setup_diagnostics(); + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + let report = format!("{:?}", miette::Report::new(warn)); + assert!(report.contains(&code)); + } + + #[test] + fn test_stderr_contains_help() { + setup_diagnostics(); + let warn = Warnings::SkippingPackageLink( + "example_pkg".to_string(), + PathBuf::from("/example/path"), + ); + let report = format!("{:?}", miette::Report::new(warn)); + assert!(report.contains("Check the existing file or directory")); + } + + #[test] + fn test_stderr_contains_no_help() { + setup_diagnostics(); + let warn = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".to_string(), + }; + let report = format!("{:?}", miette::Report::new(warn)); + assert!(!report.contains("help:")); + } + + #[test] + fn test_stderr_contains_two_help() { + setup_diagnostics(); + let warn = + Warnings::NotAGitDependency("example_dep".to_string(), PathBuf::from("/example/path")); + let report = format!("{:?}", miette::Report::new(warn)); + let help_count = report.matches("help:").count(); + assert_eq!(help_count, 2); + } +} From 240b9ac54e9cca226e5fa7ac7ae6eebc6c45c6d5 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sun, 11 Jan 2026 14:15:27 +0100 Subject: [PATCH 24/79] error: Add note on use of struct/tuple in enum variants --- src/error.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/error.rs b/src/error.rs index cfaf5345..a1c938b9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -307,6 +307,15 @@ macro_rules! field { }; } +// Note(fischeti): The enum variants should preferably use struct style +// variants for better readability, but this is not possible due to a current +// issue in `miette` that causes `unused` warnings when the help message does not +// use all fields of a struct variant. This is new since Rust 1.92.0, and a fix +// is pending in `miette`. See also: +// Issue: https://github.com/zkat/miette/issues/458 +// PR: https://github.com/zkat/miette/pull/459 +// The workaround for the moment is to use tuple style variants +// for variants where the help message does not use all fields. #[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] #[diagnostic(severity(Warning))] pub enum Warnings { From 4facd231c2d035fcf2df5a59b3da39af97ab4ac8 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 12 Jan 2026 23:08:15 +0100 Subject: [PATCH 25/79] error: Fix merge conflicts --- Cargo.lock | 155 ++++++++++++++++++++++++--------------------- src/cmd/parents.rs | 21 +++--- src/resolver.rs | 6 +- 3 files changed, 96 insertions(+), 86 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9b87dc8a..9bf920ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" @@ -224,9 +224,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.47" +version = "1.2.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" dependencies = [ "find-msvc-tools", "shlex", @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.61" +version = "4.5.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992" +checksum = "430b4dc2b5e3861848de79627b2bedc9f3342c7da5173a14eaa5d0f8dc18ae5d" dependencies = [ "clap", ] @@ -316,9 +316,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" @@ -462,9 +462,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" [[package]] name = "futures" @@ -567,9 +567,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -693,9 +693,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", @@ -737,15 +737,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -759,9 +759,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -771,9 +771,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags", "libc", @@ -796,9 +796,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" @@ -847,9 +847,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -944,9 +944,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "2c9eb05c21a464ea704b53158d358a31e6425db2f63a1a7312268b05fe2b75f7" dependencies = [ "memchr", "ucd-trie", @@ -954,9 +954,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "68f9dbced329c441fa79d80472764b1a2c7e57123553b8519b36663a2fb234ed" dependencies = [ "pest", "pest_generator", @@ -964,9 +964,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "3bb96d5051a78f44f43c8f712d8e810adb0ebf923fc9ed2655a7f66f63ba8ee5" dependencies = [ "pest", "pest_meta", @@ -977,9 +977,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "602113b5b5e8621770cfd490cfd90b9f84ab29bd2b0e49ad83eb6d186cef2365" dependencies = [ "pest", "sha2", @@ -1083,18 +1083,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -1132,7 +1132,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -1150,7 +1150,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror", ] @@ -1192,9 +1192,9 @@ checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", @@ -1211,9 +1211,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -1272,15 +1272,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -1315,10 +1315,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -1405,9 +1406,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.111" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -1425,9 +1426,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom 0.3.4", @@ -1459,10 +1460,6 @@ dependencies = [ ] [[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1472,6 +1469,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "textwrap" version = "0.16.2" @@ -1504,9 +1507,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -1632,9 +1635,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -1645,9 +1648,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1655,9 +1658,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -1668,9 +1671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -1840,20 +1843,26 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.8.28" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.28" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" diff --git a/src/cmd/parents.rs b/src/cmd/parents.rs index ba235e3a..2237f5ab 100644 --- a/src/cmd/parents.rs +++ b/src/cmd/parents.rs @@ -101,12 +101,12 @@ pub fn run(sess: &Session, args: &ParentsArgs) -> Result<()> { Warnings::DepOverride { pkg: dep.to_string(), pkg_override: match sess.config.overrides[dep] { - Dependency::Version(ref v) => format!("version {}", pkg!(v)), - Dependency::Path(ref path) => format!("path {}", path!(path.display())), - Dependency::GitRevision(ref url, ref rev) => { + Dependency::Version(ref v, _) => format!("version {}", pkg!(v)), + Dependency::Path(ref path, _) => format!("path {}", path!(path.display())), + Dependency::GitRevision(ref url, ref rev, _) => { format!("git {} at revision {}", path!(url), pkg!(rev)) } - Dependency::GitVersion(ref url, ref version) => { + Dependency::GitVersion(ref url, ref version, _) => { format!("git {} with version {}", path!(url), pkg!(version)) } }, @@ -160,9 +160,10 @@ pub fn get_parent_array( let dep_manifest = rt.block_on(io.dependency_manifest(pkg, false, &[]))?; // Filter out dependencies without a manifest if dep_manifest.is_none() { - if !sess.suppress_warnings.contains("W17") { - warnln!("[W17] {} is shown to include dependency, but manifest does not have this information.", pkg_name.to_string()); + Warnings::IncludeDepManifestMismatch { + pkg: pkg_name.to_string(), } + .emit(); continue; } let dep_manifest = dep_manifest.unwrap(); @@ -192,9 +193,11 @@ pub fn get_parent_array( ], ); } - } else if !sess.suppress_warnings.contains("W17") { - // Filter out dependencies with mismatching manifest - warnln!("[W17] {} is shown to include dependency, but manifest does not have this information.", pkg_name.to_string()); + } else { + Warnings::IncludeDepManifestMismatch { + pkg: pkg_name.to_string(), + } + .emit(); } } } diff --git a/src/resolver.rs b/src/resolver.rs index 5aaadc55..e40e5edc 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -107,8 +107,7 @@ impl<'ctx> DependencyResolver<'ctx> { Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); self.checked_out.insert( depname, - config::Dependency::Path(dir.unwrap().path()), - vec![], + config::Dependency::Path(dir.unwrap().path(), vec![]), ); } else if !(SysCommand::new(&self.sess.config.git) // If not in a clean state .arg("status") @@ -121,8 +120,7 @@ impl<'ctx> DependencyResolver<'ctx> { Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); self.checked_out.insert( depname, - config::Dependency::Path(dir.unwrap().path()), - vec![], + config::Dependency::Path(dir.unwrap().path(), vec![]), ); } } From ce302b9cb41f21b7e00873adb6e3e319258ffdf5 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 14:46:57 +0100 Subject: [PATCH 26/79] cli: Fix some merge conflicts --- src/cli.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index e2bd370c..b25ad212 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -5,7 +5,6 @@ use std; use std::collections::HashSet; -use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command as SysCommand; @@ -115,8 +114,9 @@ pub fn main() -> Result<()> { // Parse command line arguments. let cli = Cli::parse(); - let mut suppressed_warnings: IndexSet = + let mut suppressed_warnings: HashSet = cli.suppress.into_iter().map(|s| s.to_owned()).collect(); + // split suppress strings on commas and spaces suppressed_warnings = suppressed_warnings .into_iter() @@ -127,14 +127,9 @@ pub fn main() -> Result<()> { }) .collect(); - Diagnostics::init(suppressed); - + let warn_config_loaded = !suppressed_warnings.contains("W02"); - let suppressed_warnings: IndexSet = matches - .get_many::("suppress") - .unwrap_or_default() - .map(|s| s.to_owned()) - .collect(); + Diagnostics::init(suppressed_warnings); #[cfg(debug_assertions)] if cli.debug { @@ -154,7 +149,7 @@ pub fn main() -> Result<()> { } let force_fetch = match cli.command { - Commands::Update(ref args) => cmd::update::setup(args, cli.local, &suppressed_warnings)?, + Commands::Update(ref args) => cmd::update::setup(args, cli.local)?, _ => false, }; @@ -178,8 +173,7 @@ pub fn main() -> Result<()> { // Gather and parse the tool configuration. let config = load_config( &root_dir, - matches!(cli.command, Commands::Update(_)) && !suppressed_warnings.contains("W02"), - &suppressed_warnings, + matches!(cli.command, Commands::Update(_)) && warn_config_loaded, )?; debugln!("main: {:#?}", config); From 05e719c5553226882645ac3860e1670f8dbd3017 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 14:48:17 +0100 Subject: [PATCH 27/79] Fix clippy warnings --- src/cmd/vendor.rs | 25 +++++++++++++++++-------- src/error.rs | 4 ++-- src/resolver.rs | 36 +++++++++++++++--------------------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 9c7be81d..0d289778 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -273,15 +273,24 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { match patch_link.patch_dir.clone() { Some(patch_dir) => { if *plain { - let get_diff = diff(&rt, - git.clone(), - vendor_package, - patch_link, - dep_path.clone()) - .map_err(|cause| Error::chain("Failed to get diff.", cause))?; + let get_diff = diff( + &rt, + git.clone(), + vendor_package, + patch_link, + dep_path.clone(), + ) + .map_err(|cause| Error::chain("Failed to get diff.", cause))?; gen_plain_patch(get_diff, patch_dir, false) } else { - gen_format_patch(&rt, sess, git.clone(), patch_link, vendor_package.target_dir.clone(), message.as_ref()) + gen_format_patch( + &rt, + sess, + git.clone(), + patch_link, + vendor_package.target_dir.clone(), + message.as_ref(), + ) } } None => { @@ -341,7 +350,7 @@ pub fn init( if !PathBuf::from(extend_paths(std::slice::from_ref(&path), dep_path, true)?[0].clone()) .exists() { - Warnings::NotInUpstream { path: path }.emit(); + Warnings::NotInUpstream { path }.emit(); } } diff --git a/src/error.rs b/src/error.rs index a1c938b9..edbc66db 100644 --- a/src/error.rs +++ b/src/error.rs @@ -181,7 +181,7 @@ impl Diagnostics { miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); let diag = Diagnostics { all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), - suppressed: suppressed, + suppressed, emitted: Mutex::new(HashSet::new()), }; @@ -200,7 +200,7 @@ impl Diagnostics { /// Check whether a warning/error code is suppressed. pub fn is_suppressed(code: &str) -> bool { let diag = Diagnostics::get(); - diag.all_suppressed || diag.suppressed.contains(&code.to_string()) + diag.all_suppressed || diag.suppressed.contains(code) } } diff --git a/src/resolver.rs b/src/resolver.rs index e40e5edc..adcffc0a 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -102,27 +102,21 @@ impl<'ctx> DependencyResolver<'ctx> { // Only act if the avoiding flag is not set and any of the following match // - the dependency is not a git repo // - the dependency is not in a clean state (i.e., was modified) - if !ignore_checkout { - if !is_git_repo { - Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); - self.checked_out.insert( - depname, - config::Dependency::Path(dir.unwrap().path(), vec![]), - ); - } else if !(SysCommand::new(&self.sess.config.git) // If not in a clean state - .arg("status") - .arg("--porcelain") - .current_dir(dir.as_ref().unwrap().path()) - .output()? - .stdout - .is_empty()) - { - Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); - self.checked_out.insert( - depname, - config::Dependency::Path(dir.unwrap().path(), vec![]), - ); - } + if !ignore_checkout + && (!is_git_repo + || !(SysCommand::new(&self.sess.config.git) // If not in a clean state + .arg("status") + .arg("--porcelain") + .current_dir(dir.as_ref().unwrap().path()) + .output()? + .stdout + .is_empty())) + { + Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); + self.checked_out.insert( + depname, + config::Dependency::Path(dir.unwrap().path(), vec![]), + ); } } } From 9c30dbda467327d1ca6c72a359ddac7a33af1aaf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 15:16:10 +0100 Subject: [PATCH 28/79] Move Warnings into `diagnostics` --- src/cli.rs | 1 + src/cmd/clone.rs | 1 + src/cmd/fusesoc.rs | 1 + src/cmd/parents.rs | 1 + src/cmd/snapshot.rs | 1 + src/cmd/update.rs | 1 + src/cmd/vendor.rs | 1 + src/config.rs | 1 + src/diagnostic.rs | 491 ++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 488 ------------------------------------------- src/main.rs | 1 + src/resolver.rs | 1 + src/sess.rs | 1 + src/src.rs | 3 +- 14 files changed, 504 insertions(+), 489 deletions(-) create mode 100644 src/diagnostic.rs diff --git a/src/cli.rs b/src/cli.rs index b25ad212..8e07ee5f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -23,6 +23,7 @@ use tokio::runtime::Runtime; use crate::cmd; use crate::cmd::fusesoc::FusesocArgs; use crate::config::{Config, Manifest, Merge, PartialConfig, PrefixPaths, Validate}; +use crate::diagnostic::{Diagnostics, Warnings}; use crate::error::*; use crate::lockfile::*; use crate::sess::{Session, SessionArenas, SessionIo}; diff --git a/src/cmd/clone.rs b/src/cmd/clone.rs index 1d4bb4b1..45a232d2 100644 --- a/src/cmd/clone.rs +++ b/src/cmd/clone.rs @@ -12,6 +12,7 @@ use tokio::runtime::Runtime; use crate::config; use crate::config::{Locked, LockedSource}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{DependencyRef, DependencySource, Session, SessionIo}; diff --git a/src/cmd/fusesoc.rs b/src/cmd/fusesoc.rs index ac32ecfa..634c70a3 100644 --- a/src/cmd/fusesoc.rs +++ b/src/cmd/fusesoc.rs @@ -18,6 +18,7 @@ use itertools::Itertools; use tokio::runtime::Runtime; use walkdir::{DirEntry, WalkDir}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{Session, SessionIo}; use crate::src::{SourceFile, SourceGroup}; diff --git a/src/cmd/parents.rs b/src/cmd/parents.rs index 2237f5ab..de03599c 100644 --- a/src/cmd/parents.rs +++ b/src/cmd/parents.rs @@ -5,6 +5,7 @@ use std::io::Write; +use crate::diagnostic::Warnings; use clap::Args; use indexmap::IndexMap; use owo_colors::OwoColorize; diff --git a/src/cmd/snapshot.rs b/src/cmd/snapshot.rs index 748f9ba4..ce47e105 100644 --- a/src/cmd/snapshot.rs +++ b/src/cmd/snapshot.rs @@ -12,6 +12,7 @@ use tokio::runtime::Runtime; use crate::cmd::clone::{get_path_subdeps, symlink_dir}; use crate::config::{Dependency, Locked, LockedSource}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{DependencySource, Session, SessionIo}; diff --git a/src/cmd/update.rs b/src/cmd/update.rs index 948ed3d5..3d8999e1 100644 --- a/src/cmd/update.rs +++ b/src/cmd/update.rs @@ -12,6 +12,7 @@ use tabwriter::TabWriter; use crate::cmd; use crate::config::{Locked, LockedPackage}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::lockfile::*; use crate::resolver::DependencyResolver; diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 0d289778..6e4dc8d5 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -17,6 +17,7 @@ use tokio::runtime::Runtime; use crate::config; use crate::config::PrefixPaths; +use crate::diagnostic::Warnings; use crate::error::*; use crate::futures::TryFutureExt; use crate::git::Git; diff --git a/src/config.rs b/src/config.rs index 5c7098cc..05f1d98e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -26,6 +26,7 @@ use serde_yaml_ng::Value; #[cfg(unix)] use subst; +use crate::diagnostic::{Diagnostics, Warnings}; use crate::error::*; use crate::target::TargetSpec; use crate::util::*; diff --git a/src/diagnostic.rs b/src/diagnostic.rs new file mode 100644 index 00000000..31d1c157 --- /dev/null +++ b/src/diagnostic.rs @@ -0,0 +1,491 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +use std::collections::HashSet; +use std::fmt; +use std::path::PathBuf; +use std::sync::{Mutex, OnceLock}; + +use miette::{Diagnostic, ReportHandler}; +use owo_colors::OwoColorize; +use thiserror::Error; + +static GLOBAL_DIAGNOSTICS: OnceLock = OnceLock::new(); + +/// A diagnostics manager that handles warnings (and errors). +#[derive(Debug)] +pub struct Diagnostics { + /// A set of suppressed warnings. + suppressed: HashSet, + /// Whether all warnings are suppressed. + all_suppressed: bool, + /// A set of already emitted warnings. + /// Requires synchronization as warnings may be emitted from multiple threads. + emitted: Mutex>, +} + +impl Diagnostics { + /// Create a new diagnostics manager. + pub fn init(suppressed: HashSet) { + // Set up miette with our custom renderer + miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); + let diag = Diagnostics { + all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), + suppressed, + emitted: Mutex::new(HashSet::new()), + }; + + GLOBAL_DIAGNOSTICS + .set(diag) + .expect("Diagnostics already initialized!"); + } + + /// Get the global diagnostics manager. + fn get() -> &'static Diagnostics { + GLOBAL_DIAGNOSTICS + .get() + .expect("Diagnostics not initialized!") + } + + /// Check whether a warning/error code is suppressed. + pub fn is_suppressed(code: &str) -> bool { + let diag = Diagnostics::get(); + diag.all_suppressed || diag.suppressed.contains(code) + } +} + +impl Warnings { + /// Checks suppression, deduplicates, and emits the warning to stderr. + pub fn emit(self) { + let diag = Diagnostics::get(); + + // Check whether the command is suppressed + if let Some(code) = self.code() { + if diag.all_suppressed || diag.suppressed.contains(&code.to_string()) { + return; + } + } + + // Check whether the warning was already emitted + let mut emitted = diag.emitted.lock().unwrap(); + if emitted.contains(&self) { + return; + } + emitted.insert(self.clone()); + drop(emitted); + + // Print the warning report (consumes self i.e. the warning) + eprintln!("{:?}", miette::Report::new(self)); + } +} + +pub struct DiagnosticRenderer; + +impl ReportHandler for DiagnosticRenderer { + fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Determine severity and the resulting style + let (severity, style) = match diagnostic.severity().unwrap_or_default() { + miette::Severity::Error => ("error", owo_colors::Style::new().red().bold()), + miette::Severity::Warning => ("warning", owo_colors::Style::new().yellow().bold()), + miette::Severity::Advice => ("advice", owo_colors::Style::new().cyan().bold()), + }; + + // Write the severity prefix and the diagnostic message + write!(f, "{}: {}", severity.style(style), diagnostic)?; + + // We collect all footer lines into a vector. + let mut annotations: Vec = Vec::new(); + + // First, we write the help message(s) if any + if let Some(help) = diagnostic.help() { + let help_str = help.to_string(); + for line in help_str.lines() { + annotations.push(format!( + "{} {}", + "help:".bold(), + line.replace("\x1b[0m", "\x1b[0m\x1b[2m").dimmed() + )); + } + } + + // Finally, we write the code/suppression message, if any + if let Some(code) = diagnostic.code() { + annotations.push(format!( + "{} {}", + "suppress:".cyan().bold(), // No variable, no lifetime issue + format!("Run `bender --suppress {}` to suppress this warning", code).dimmed() + )); + } + + // Prepare tree characters + let branch = " ├─›"; + let corner = " ╰─›"; + + // Iterate over the annotations and print them + for (i, note) in annotations.iter().enumerate() { + // The last item gets the corner, everyone else gets a branch + let is_last = i == annotations.len() - 1; + let prefix = if is_last { corner } else { branch }; + write!(f, "\n{} {}", prefix.dimmed(), note)?; + } + + Ok(()) + } +} + +// Note(fischeti): The enum variants should preferably use struct style +// variants for better readability, but this is not possible due to a current +// issue in `miette` that causes `unused` warnings when the help message does not +// use all fields of a struct variant. This is new since Rust 1.92.0, and a fix +// is pending in `miette`. See also: +// Issue: https://github.com/zkat/miette/issues/458 +// PR: https://github.com/zkat/miette/pull/459 +// The workaround for the moment is to use tuple style variants +// for variants where the help message does not use all fields. +#[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] +#[diagnostic(severity(Warning))] +pub enum Warnings { + #[error( + "Skipping link to package {} at {} since there is something there", + pkg!(.0), + path!(.1.display()) + )] + #[diagnostic( + code(W01), + help("Check the existing file or directory that is preventing the link.") + )] + SkippingPackageLink(String, PathBuf), + + #[error("Using config at {} for overrides.", path!(path.display()))] + #[diagnostic(code(W02))] + UsingConfigForOverride { path: PathBuf }, + + #[error("Ignoring unknown field {} in package {}.", field!(field), pkg!(pkg))] + #[diagnostic( + code(W03), + help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) + )] + IgnoreUnknownField { field: String, pkg: String }, + + #[error("Source group in package {} contains no source files.", pkg!(.0))] + #[diagnostic( + code(W04), + help("Add source files to the source group or remove it from the manifest.") + )] + NoFilesInSourceGroup(String), + + #[error("No files matched the global pattern {}.", path!(path))] + #[diagnostic(code(W05))] + NoFilesForGlobalPattern { path: String }, + + #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", pkg!(.0), path!(.1.display()))] + #[diagnostic( + code(W06), + help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.") + )] + NotAGitDependency(String, PathBuf), + + // TODO(fischeti): Why are there two W07 variants? + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("SSH key might be missing.")] + #[diagnostic( + code(W07), + help("Please ensure your public ssh key is added to the git server.") + )] + SshKeyMaybeMissing, + + // TODO(fischeti): Why are there two W07 variants? + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("SSH key might be missing.")] + #[diagnostic( + code(W07), + help("Please ensure the url is correct and you have access to the repository.") + )] + UrlMaybeIncorrect, + + // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + #[error("Revision {} not found in repository {}.", pkg!(.0), pkg!(.1))] + #[diagnostic( + code(W08), + help("Check that the revision exists in the remote repository or run `bender update`.") + )] + RevisionNotFound(String, String), + + #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", pkg!(pkg), pkg!(top_pkg))] + #[diagnostic(code(W09))] + PathDepInGitDep { pkg: String, top_pkg: String }, + + #[error("There may be issues in the path for {}.", pkg!(.0))] + #[diagnostic( + code(W10), + help("Please check that {} is correct and accessible.", path!(.1.display())) + )] + MaybePathIssues(String, PathBuf), + + #[error("Dependency package name {} does not match the package name {} in its manifest.", pkg!(.0), pkg!(.1))] + #[diagnostic( + code(W11), + help("Check that the dependency name in your root manifest matches the name in the {} manifest.", pkg!(.0)) + )] + DepPkgNameNotMatching(String, String), + + #[error("Manifest for package {} not found at {}.", pkg!(pkg), path!(src))] + #[diagnostic(code(W12))] + ManifestNotFound { pkg: String, src: String }, + + #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", pkg!(.0))] + #[diagnostic( + code(W13), + help("Could be related to name missmatch, check `bender update`.") + )] + ExportDirNameIssue(String), + + #[error("If `--local` is used, no fetching will be performed.")] + #[diagnostic(code(W14))] + LocalNoFetch, + + #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", pkg!(vendor_pkg), path!(from_prefix.display()), path!(to_prefix.display()))] + #[diagnostic(code(W15))] + NoPatchDir { + vendor_pkg: String, + from_prefix: PathBuf, + to_prefix: PathBuf, + }, + + #[error("Dependency string for the included dependencies might be wrong.")] + #[diagnostic(code(W16))] + DependStringMaybeWrong, + + // TODO(fischeti): Why are there two W16 variants? + #[error("{} not found in upstream, continuing.", path!(path))] + #[diagnostic(code(W16))] + NotInUpstream { path: String }, + + #[error("Package {} is shown to include dependency, but manifest does not have this information.", pkg!(pkg))] + #[diagnostic(code(W17))] + IncludeDepManifestMismatch { pkg: String }, + + #[error("An override is specified for dependency {} to {}.", pkg!(pkg), pkg!(pkg_override))] + #[diagnostic(code(W18))] + DepOverride { pkg: String, pkg_override: String }, + + #[error("Workspace checkout directory set and has uncommitted changes, not updating {} at {}.", pkg!(.0), path!(.1.display()))] + #[diagnostic( + code(W19), + help("Run `bender checkout --force` to overwrite the dependency at your own risk.") + )] + CheckoutDirDirty(String, PathBuf), + + // TODO(fischeti): Should this be an error instead of a warning? + #[error("Ignoring error for {} at {}: {}", pkg!(.0), path!(.1), .2)] + #[diagnostic(code(W20))] + IgnoringError(String, String, String), + + #[error("No revision found in lock file for git dependency {}.", pkg!(pkg))] + #[diagnostic(code(W21))] + NoRevisionInLockFile { pkg: String }, + + #[error("Dependency {} has source path {} which does not exist.", pkg!(.0), path!(.1.display()))] + #[diagnostic(code(W22), help("Please check that the path exists and is correct."))] + DepSourcePathMissing(String, PathBuf), + + #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", pkg!(rev), pkg!(pkg))] + #[diagnostic(code(W23))] + LockedRevisionNotFound { pkg: String, rev: String }, + + #[error("Include directory {} doesn't exist.", path!(.0.display()))] + #[diagnostic( + code(W24), + help("Please check that the include directory exists and is correct.") + )] + IncludeDirMissing(PathBuf), + + #[error("Skipping dirty dependency {}", pkg!(pkg))] + #[diagnostic(help("Use `--no-skip` to still snapshot {}.", pkg!(pkg)))] + SkippingDirtyDep { pkg: String }, + + #[error("File not added, ignoring: {cause}")] + #[diagnostic(code(W30))] + IgnoredPath { cause: String }, + + #[error("File {} doesn't exist.", path!(path.display()))] + #[diagnostic(code(W31))] + FileMissing { path: PathBuf }, + + #[error("Path {} for dependency {} does not exist.", path!(path.display()), pkg!(pkg))] + #[diagnostic(code(W32))] + DepPathMissing { pkg: String, path: PathBuf }, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Once; + + static TEST_INIT: Once = Once::new(); + + /// Helper to initialize diagnostics once for the entire test run. + fn setup_diagnostics() { + TEST_INIT.call_once(|| { + // We use an empty set for the global init in tests + // or a specific set if needed. + Diagnostics::init(HashSet::from(["W02".to_string()])); + }); + } + + #[test] + fn test_is_suppressed() { + setup_diagnostics(); + assert!(Diagnostics::is_suppressed("W02")); + assert!(!Diagnostics::is_suppressed("W01")); + } + + #[test] + fn test_suppression_works() { + setup_diagnostics(); // Assumes this suppresses W02 + let diag = Diagnostics::get(); + + let warn = Warnings::UsingConfigForOverride { + path: PathBuf::from("/example/path"), + }; + + // Clear state + diag.emitted.lock().unwrap().clear(); + + // Call emit (The Gatekeeper) + warn.clone().emit(); + + let emitted = diag.emitted.lock().unwrap(); + assert!(!emitted.contains(&warn)); + } + + #[test] + fn test_all_suppressed() { + // Since we can't re-init the GLOBAL_DIAGNOSTICS with different values + // in the same process, we test the logic via a local instance. + let diag = Diagnostics { + suppressed: HashSet::new(), + all_suppressed: true, + emitted: Mutex::new(HashSet::new()), + }; + + // Manual check of the logic inside emit() + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + assert!(diag.all_suppressed || diag.suppressed.contains(&code)); + } + + #[test] + fn test_deduplication_logic() { + setup_diagnostics(); + let diag = Diagnostics::get(); + let warn1 = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".into(), + }; + let warn2 = Warnings::NoRevisionInLockFile { + pkg: "other_pkg".into(), + }; + + // Clear state + diag.emitted.lock().unwrap().clear(); + + // Emit first warning + warn1.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + assert!(emitted.contains(&warn1)); + assert_eq!(emitted.len(), 1); + } + + // Emit second warning (different data) + warn2.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + assert!(emitted.contains(&warn2)); + assert_eq!(emitted.len(), 2); + } + + // Emit first warning again + warn1.clone().emit(); + { + let emitted = diag.emitted.lock().unwrap(); + // The length should STILL be 2, because warn1 was already there + assert_eq!(emitted.len(), 2); + } + } + + #[test] + fn test_contains_code() { + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + assert_eq!(code, "W14".to_string()); + } + + #[test] + fn test_contains_no_code() { + let warn = Warnings::SkippingDirtyDep { + pkg: "example_pkg".to_string(), + }; + let code = warn.code(); + assert!(code.is_none()); + } + + #[test] + fn test_contains_help() { + let warn = Warnings::SkippingPackageLink( + "example_pkg".to_string(), + PathBuf::from("/example/path"), + ); + let help = warn.help().unwrap().to_string(); + assert!(help.contains("Check the existing file or directory")); + } + + #[test] + fn test_contains_no_help() { + let warn = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".to_string(), + }; + let help = warn.help(); + assert!(help.is_none()); + } + + #[test] + fn test_stderr_contains_code() { + setup_diagnostics(); + let warn = Warnings::LocalNoFetch; + let code = warn.code().unwrap().to_string(); + let report = format!("{:?}", miette::Report::new(warn)); + assert!(report.contains(&code)); + } + + #[test] + fn test_stderr_contains_help() { + setup_diagnostics(); + let warn = Warnings::SkippingPackageLink( + "example_pkg".to_string(), + PathBuf::from("/example/path"), + ); + let report = format!("{:?}", miette::Report::new(warn)); + assert!(report.contains("Check the existing file or directory")); + } + + #[test] + fn test_stderr_contains_no_help() { + setup_diagnostics(); + let warn = Warnings::NoRevisionInLockFile { + pkg: "example_pkg".to_string(), + }; + let report = format!("{:?}", miette::Report::new(warn)); + assert!(!report.contains("help:")); + } + + #[test] + fn test_stderr_contains_two_help() { + setup_diagnostics(); + let warn = + Warnings::NotAGitDependency("example_dep".to_string(), PathBuf::from("/example/path")); + let report = format!("{:?}", miette::Report::new(warn)); + let help_count = report.matches("help:").count(); + assert_eq!(help_count, 2); + } +} diff --git a/src/error.rs b/src/error.rs index edbc66db..cda680f7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -152,137 +152,6 @@ pub fn println_stage(stage: &str, message: &str) { eprintln!("\x1B[32;1m{:>12}\x1B[0m {}", stage, message); } -use std::collections::HashSet; -use std::path::PathBuf; -use std::sync::{Mutex, OnceLock}; - -use miette::{Diagnostic, ReportHandler}; -use owo_colors::OwoColorize; -use thiserror::Error; - -static GLOBAL_DIAGNOSTICS: OnceLock = OnceLock::new(); - -/// A diagnostics manager that handles warnings (and errors). -#[derive(Debug)] -pub struct Diagnostics { - /// A set of suppressed warnings. - suppressed: HashSet, - /// Whether all warnings are suppressed. - all_suppressed: bool, - /// A set of already emitted warnings. - /// Requires synchronization as warnings may be emitted from multiple threads. - emitted: Mutex>, -} - -impl Diagnostics { - /// Create a new diagnostics manager. - pub fn init(suppressed: HashSet) { - // Set up miette with our custom renderer - miette::set_hook(Box::new(|_| Box::new(DiagnosticRenderer))).unwrap(); - let diag = Diagnostics { - all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), - suppressed, - emitted: Mutex::new(HashSet::new()), - }; - - GLOBAL_DIAGNOSTICS - .set(diag) - .expect("Diagnostics already initialized!"); - } - - /// Get the global diagnostics manager. - fn get() -> &'static Diagnostics { - GLOBAL_DIAGNOSTICS - .get() - .expect("Diagnostics not initialized!") - } - - /// Check whether a warning/error code is suppressed. - pub fn is_suppressed(code: &str) -> bool { - let diag = Diagnostics::get(); - diag.all_suppressed || diag.suppressed.contains(code) - } -} - -impl Warnings { - /// Checks suppression, deduplicates, and emits the warning to stderr. - pub fn emit(self) { - let diag = Diagnostics::get(); - - // Check whether the command is suppressed - if let Some(code) = self.code() { - if diag.all_suppressed || diag.suppressed.contains(&code.to_string()) { - return; - } - } - - // Check whether the warning was already emitted - let mut emitted = diag.emitted.lock().unwrap(); - if emitted.contains(&self) { - return; - } - emitted.insert(self.clone()); - drop(emitted); - - // Print the warning report (consumes self i.e. the warning) - eprintln!("{:?}", miette::Report::new(self)); - } -} - -pub struct DiagnosticRenderer; - -impl ReportHandler for DiagnosticRenderer { - fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Determine severity and the resulting style - let (severity, style) = match diagnostic.severity().unwrap_or_default() { - miette::Severity::Error => ("error", owo_colors::Style::new().red().bold()), - miette::Severity::Warning => ("warning", owo_colors::Style::new().yellow().bold()), - miette::Severity::Advice => ("advice", owo_colors::Style::new().cyan().bold()), - }; - - // Write the severity prefix and the diagnostic message - write!(f, "{}: {}", severity.style(style), diagnostic)?; - - // We collect all footer lines into a vector. - let mut annotations: Vec = Vec::new(); - - // First, we write the help message(s) if any - if let Some(help) = diagnostic.help() { - let help_str = help.to_string(); - for line in help_str.lines() { - annotations.push(format!( - "{} {}", - "help:".bold(), - line.replace("\x1b[0m", "\x1b[0m\x1b[2m").dimmed() - )); - } - } - - // Finally, we write the code/suppression message, if any - if let Some(code) = diagnostic.code() { - annotations.push(format!( - "{} {}", - "suppress:".cyan().bold(), // No variable, no lifetime issue - format!("Run `bender --suppress {}` to suppress this warning", code).dimmed() - )); - } - - // Prepare tree characters - let branch = " ├─›"; - let corner = " ╰─›"; - - // Iterate over the annotations and print them - for (i, note) in annotations.iter().enumerate() { - // The last item gets the corner, everyone else gets a branch - let is_last = i == annotations.len() - 1; - let prefix = if is_last { corner } else { branch }; - write!(f, "\n{} {}", prefix.dimmed(), note)?; - } - - Ok(()) - } -} - /// Bold a package name in diagnostic messages. #[macro_export] macro_rules! pkg { @@ -306,360 +175,3 @@ macro_rules! field { $field.italic() }; } - -// Note(fischeti): The enum variants should preferably use struct style -// variants for better readability, but this is not possible due to a current -// issue in `miette` that causes `unused` warnings when the help message does not -// use all fields of a struct variant. This is new since Rust 1.92.0, and a fix -// is pending in `miette`. See also: -// Issue: https://github.com/zkat/miette/issues/458 -// PR: https://github.com/zkat/miette/pull/459 -// The workaround for the moment is to use tuple style variants -// for variants where the help message does not use all fields. -#[derive(Error, Diagnostic, Hash, Eq, PartialEq, Debug, Clone)] -#[diagnostic(severity(Warning))] -pub enum Warnings { - #[error( - "Skipping link to package {} at {} since there is something there", - pkg!(.0), - path!(.1.display()) - )] - #[diagnostic( - code(W01), - help("Check the existing file or directory that is preventing the link.") - )] - SkippingPackageLink(String, PathBuf), - - #[error("Using config at {} for overrides.", path!(path.display()))] - #[diagnostic(code(W02))] - UsingConfigForOverride { path: PathBuf }, - - #[error("Ignoring unknown field {} in package {}.", field!(field), pkg!(pkg))] - #[diagnostic( - code(W03), - help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) - )] - IgnoreUnknownField { field: String, pkg: String }, - - #[error("Source group in package {} contains no source files.", pkg!(.0))] - #[diagnostic( - code(W04), - help("Add source files to the source group or remove it from the manifest.") - )] - NoFilesInSourceGroup(String), - - #[error("No files matched the global pattern {}.", path!(path))] - #[diagnostic(code(W05))] - NoFilesForGlobalPattern { path: String }, - - #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", pkg!(.0), path!(.1.display()))] - #[diagnostic( - code(W06), - help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.") - )] - NotAGitDependency(String, PathBuf), - - // TODO(fischeti): Why are there two W07 variants? - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? - #[error("SSH key might be missing.")] - #[diagnostic( - code(W07), - help("Please ensure your public ssh key is added to the git server.") - )] - SshKeyMaybeMissing, - - // TODO(fischeti): Why are there two W07 variants? - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? - #[error("SSH key might be missing.")] - #[diagnostic( - code(W07), - help("Please ensure the url is correct and you have access to the repository.") - )] - UrlMaybeIncorrect, - - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? - #[error("Revision {} not found in repository {}.", pkg!(.0), pkg!(.1))] - #[diagnostic( - code(W08), - help("Check that the revision exists in the remote repository or run `bender update`.") - )] - RevisionNotFound(String, String), - - #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", pkg!(pkg), pkg!(top_pkg))] - #[diagnostic(code(W09))] - PathDepInGitDep { pkg: String, top_pkg: String }, - - #[error("There may be issues in the path for {}.", pkg!(.0))] - #[diagnostic( - code(W10), - help("Please check that {} is correct and accessible.", path!(.1.display())) - )] - MaybePathIssues(String, PathBuf), - - #[error("Dependency package name {} does not match the package name {} in its manifest.", pkg!(.0), pkg!(.1))] - #[diagnostic( - code(W11), - help("Check that the dependency name in your root manifest matches the name in the {} manifest.", pkg!(.0)) - )] - DepPkgNameNotMatching(String, String), - - #[error("Manifest for package {} not found at {}.", pkg!(pkg), path!(src))] - #[diagnostic(code(W12))] - ManifestNotFound { pkg: String, src: String }, - - #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", pkg!(.0))] - #[diagnostic( - code(W13), - help("Could be related to name missmatch, check `bender update`.") - )] - ExportDirNameIssue(String), - - #[error("If `--local` is used, no fetching will be performed.")] - #[diagnostic(code(W14))] - LocalNoFetch, - - #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", pkg!(vendor_pkg), path!(from_prefix.display()), path!(to_prefix.display()))] - #[diagnostic(code(W15))] - NoPatchDir { - vendor_pkg: String, - from_prefix: PathBuf, - to_prefix: PathBuf, - }, - - #[error("Dependency string for the included dependencies might be wrong.")] - #[diagnostic(code(W16))] - DependStringMaybeWrong, - - // TODO(fischeti): Why are there two W16 variants? - #[error("{} not found in upstream, continuing.", path!(path))] - #[diagnostic(code(W16))] - NotInUpstream { path: String }, - - #[error("Package {} is shown to include dependency, but manifest does not have this information.", pkg!(pkg))] - #[diagnostic(code(W17))] - IncludeDepManifestMismatch { pkg: String }, - - #[error("An override is specified for dependency {} to {}.", pkg!(pkg), pkg!(pkg_override))] - #[diagnostic(code(W18))] - DepOverride { pkg: String, pkg_override: String }, - - #[error("Workspace checkout directory set and has uncommitted changes, not updating {} at {}.", pkg!(.0), path!(.1.display()))] - #[diagnostic( - code(W19), - help("Run `bender checkout --force` to overwrite the dependency at your own risk.") - )] - CheckoutDirDirty(String, PathBuf), - - // TODO(fischeti): Should this be an error instead of a warning? - #[error("Ignoring error for {} at {}: {}", pkg!(.0), path!(.1), .2)] - #[diagnostic(code(W20))] - IgnoringError(String, String, String), - - #[error("No revision found in lock file for git dependency {}.", pkg!(pkg))] - #[diagnostic(code(W21))] - NoRevisionInLockFile { pkg: String }, - - #[error("Dependency {} has source path {} which does not exist.", pkg!(.0), path!(.1.display()))] - #[diagnostic(code(W22), help("Please check that the path exists and is correct."))] - DepSourcePathMissing(String, PathBuf), - - #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", pkg!(rev), pkg!(pkg))] - #[diagnostic(code(W23))] - LockedRevisionNotFound { pkg: String, rev: String }, - - #[error("Include directory {} doesn't exist.", path!(.0.display()))] - #[diagnostic( - code(W24), - help("Please check that the include directory exists and is correct.") - )] - IncludeDirMissing(PathBuf), - - #[error("Skipping dirty dependency {}", pkg!(pkg))] - #[diagnostic(help("Use `--no-skip` to still snapshot {}.", pkg!(pkg)))] - SkippingDirtyDep { pkg: String }, - - #[error("File not added, ignoring: {cause}")] - #[diagnostic(code(W30))] - IgnoredPath { cause: String }, - - #[error("File {} doesn't exist.", path!(path.display()))] - #[diagnostic(code(W31))] - FileMissing { path: PathBuf }, - - #[error("Path {} for dependency {} does not exist.", path!(path.display()), pkg!(pkg))] - #[diagnostic(code(W32))] - DepPathMissing { pkg: String, path: PathBuf }, -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::Once; - - static TEST_INIT: Once = Once::new(); - - /// Helper to initialize diagnostics once for the entire test run. - fn setup_diagnostics() { - TEST_INIT.call_once(|| { - // We use an empty set for the global init in tests - // or a specific set if needed. - Diagnostics::init(HashSet::from(["W02".to_string()])); - }); - } - - #[test] - fn test_is_suppressed() { - setup_diagnostics(); - assert!(Diagnostics::is_suppressed("W02")); - assert!(!Diagnostics::is_suppressed("W01")); - } - - #[test] - fn test_suppression_works() { - setup_diagnostics(); // Assumes this suppresses W02 - let diag = Diagnostics::get(); - - let warn = Warnings::UsingConfigForOverride { - path: PathBuf::from("/example/path"), - }; - - // Clear state - diag.emitted.lock().unwrap().clear(); - - // Call emit (The Gatekeeper) - warn.clone().emit(); - - let emitted = diag.emitted.lock().unwrap(); - assert!(!emitted.contains(&warn)); - } - - #[test] - fn test_all_suppressed() { - // Since we can't re-init the GLOBAL_DIAGNOSTICS with different values - // in the same process, we test the logic via a local instance. - let diag = Diagnostics { - suppressed: HashSet::new(), - all_suppressed: true, - emitted: Mutex::new(HashSet::new()), - }; - - // Manual check of the logic inside emit() - let warn = Warnings::LocalNoFetch; - let code = warn.code().unwrap().to_string(); - assert!(diag.all_suppressed || diag.suppressed.contains(&code)); - } - - #[test] - fn test_deduplication_logic() { - setup_diagnostics(); - let diag = Diagnostics::get(); - let warn1 = Warnings::NoRevisionInLockFile { - pkg: "example_pkg".into(), - }; - let warn2 = Warnings::NoRevisionInLockFile { - pkg: "other_pkg".into(), - }; - - // Clear state - diag.emitted.lock().unwrap().clear(); - - // Emit first warning - warn1.clone().emit(); - { - let emitted = diag.emitted.lock().unwrap(); - assert!(emitted.contains(&warn1)); - assert_eq!(emitted.len(), 1); - } - - // Emit second warning (different data) - warn2.clone().emit(); - { - let emitted = diag.emitted.lock().unwrap(); - assert!(emitted.contains(&warn2)); - assert_eq!(emitted.len(), 2); - } - - // Emit first warning again - warn1.clone().emit(); - { - let emitted = diag.emitted.lock().unwrap(); - // The length should STILL be 2, because warn1 was already there - assert_eq!(emitted.len(), 2); - } - } - - #[test] - fn test_contains_code() { - let warn = Warnings::LocalNoFetch; - let code = warn.code().unwrap().to_string(); - assert_eq!(code, "W14".to_string()); - } - - #[test] - fn test_contains_no_code() { - let warn = Warnings::SkippingDirtyDep { - pkg: "example_pkg".to_string(), - }; - let code = warn.code(); - assert!(code.is_none()); - } - - #[test] - fn test_contains_help() { - let warn = Warnings::SkippingPackageLink( - "example_pkg".to_string(), - PathBuf::from("/example/path"), - ); - let help = warn.help().unwrap().to_string(); - assert!(help.contains("Check the existing file or directory")); - } - - #[test] - fn test_contains_no_help() { - let warn = Warnings::NoRevisionInLockFile { - pkg: "example_pkg".to_string(), - }; - let help = warn.help(); - assert!(help.is_none()); - } - - #[test] - fn test_stderr_contains_code() { - setup_diagnostics(); - let warn = Warnings::LocalNoFetch; - let code = warn.code().unwrap().to_string(); - let report = format!("{:?}", miette::Report::new(warn)); - assert!(report.contains(&code)); - } - - #[test] - fn test_stderr_contains_help() { - setup_diagnostics(); - let warn = Warnings::SkippingPackageLink( - "example_pkg".to_string(), - PathBuf::from("/example/path"), - ); - let report = format!("{:?}", miette::Report::new(warn)); - assert!(report.contains("Check the existing file or directory")); - } - - #[test] - fn test_stderr_contains_no_help() { - setup_diagnostics(); - let warn = Warnings::NoRevisionInLockFile { - pkg: "example_pkg".to_string(), - }; - let report = format!("{:?}", miette::Report::new(warn)); - assert!(!report.contains("help:")); - } - - #[test] - fn test_stderr_contains_two_help() { - setup_diagnostics(); - let warn = - Warnings::NotAGitDependency("example_dep".to_string(), PathBuf::from("/example/path")); - let report = format!("{:?}", miette::Report::new(warn)); - let help_count = report.matches("help:").count(); - assert_eq!(help_count, 2); - } -} diff --git a/src/main.rs b/src/main.rs index 311990c2..343d0c9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,7 @@ pub mod error; pub mod cli; pub mod cmd; pub mod config; +pub mod diagnostic; pub mod git; pub mod lockfile; pub mod resolver; diff --git a/src/resolver.rs b/src/resolver.rs index adcffc0a..b6e651fb 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -24,6 +24,7 @@ use tabwriter::TabWriter; use tokio::runtime::Runtime; use crate::config::{self, Locked, LockedPackage, LockedSource, Manifest}; +use crate::diagnostic::Warnings; use crate::error::*; use crate::sess::{ DependencyConstraint, DependencyRef, DependencySource, DependencyVersion, DependencyVersions, diff --git a/src/sess.rs b/src/sess.rs index c59292ac..831460b9 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -31,6 +31,7 @@ use typed_arena::Arena; use crate::cli::read_manifest; use crate::config::{self, Config, Manifest, PartialManifest}; +use crate::diagnostic::{Diagnostics, Warnings}; use crate::error::*; use crate::git::Git; use crate::src::SourceGroup; diff --git a/src/src.rs b/src/src.rs index 791a7f3c..7bf4d3ee 100644 --- a/src/src.rs +++ b/src/src.rs @@ -15,7 +15,8 @@ use indexmap::{IndexMap, IndexSet}; use serde::ser::{Serialize, Serializer}; use crate::config::Validate; -use crate::error::{Diagnostics, Error, Warnings}; +use crate::diagnostic::{Diagnostics, Warnings}; +use crate::error::Error; use crate::sess::Session; use crate::target::{TargetSet, TargetSpec}; use semver; From 6c0a7c5b106adf25ef8ac9f32c0de0ace90a7908 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 17:20:52 +0100 Subject: [PATCH 29/79] error: Rename formatting macros and move to `util` --- src/cmd/parents.rs | 10 ++++---- src/diagnostic.rs | 62 ++++++++++++++++++++++++---------------------- src/error.rs | 24 ------------------ src/main.rs | 1 + src/util.rs | 35 ++++++++++++++++++++++++++ 5 files changed, 73 insertions(+), 59 deletions(-) diff --git a/src/cmd/parents.rs b/src/cmd/parents.rs index de03599c..adef05e9 100644 --- a/src/cmd/parents.rs +++ b/src/cmd/parents.rs @@ -8,7 +8,6 @@ use std::io::Write; use crate::diagnostic::Warnings; use clap::Args; use indexmap::IndexMap; -use owo_colors::OwoColorize; use tabwriter::TabWriter; use tokio::runtime::Runtime; @@ -16,6 +15,7 @@ use crate::config::Dependency; use crate::error::*; use crate::sess::{DependencyConstraint, DependencySource}; use crate::sess::{Session, SessionIo}; +use crate::{fmt_path, fmt_pkg}; /// List packages calling this dependency #[derive(Args, Debug)] @@ -102,13 +102,13 @@ pub fn run(sess: &Session, args: &ParentsArgs) -> Result<()> { Warnings::DepOverride { pkg: dep.to_string(), pkg_override: match sess.config.overrides[dep] { - Dependency::Version(ref v, _) => format!("version {}", pkg!(v)), - Dependency::Path(ref path, _) => format!("path {}", path!(path.display())), + Dependency::Version(ref v, _) => format!("version {}", fmt_pkg!(v)), + Dependency::Path(ref path, _) => format!("path {}", fmt_path!(path.display())), Dependency::GitRevision(ref url, ref rev, _) => { - format!("git {} at revision {}", path!(url), pkg!(rev)) + format!("git {} at revision {}", fmt_path!(url), fmt_pkg!(rev)) } Dependency::GitVersion(ref url, ref version, _) => { - format!("git {} with version {}", path!(url), pkg!(version)) + format!("git {} with version {}", fmt_path!(url), fmt_pkg!(version)) } }, } diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 31d1c157..f504d4a0 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -10,6 +10,8 @@ use miette::{Diagnostic, ReportHandler}; use owo_colors::OwoColorize; use thiserror::Error; +use crate::{fmt_field, fmt_path, fmt_pkg}; + static GLOBAL_DIAGNOSTICS: OnceLock = OnceLock::new(); /// A diagnostics manager that handles warnings (and errors). @@ -147,8 +149,8 @@ impl ReportHandler for DiagnosticRenderer { pub enum Warnings { #[error( "Skipping link to package {} at {} since there is something there", - pkg!(.0), - path!(.1.display()) + fmt_pkg!(.0), + fmt_path!(.1.display()) )] #[diagnostic( code(W01), @@ -156,29 +158,29 @@ pub enum Warnings { )] SkippingPackageLink(String, PathBuf), - #[error("Using config at {} for overrides.", path!(path.display()))] + #[error("Using config at {} for overrides.", fmt_path!(path.display()))] #[diagnostic(code(W02))] UsingConfigForOverride { path: PathBuf }, - #[error("Ignoring unknown field {} in package {}.", field!(field), pkg!(pkg))] + #[error("Ignoring unknown field {} in package {}.", fmt_field!(field), fmt_pkg!(pkg))] #[diagnostic( code(W03), - help("Check for typos in {} or remove it from the {} manifest.", field!(field), pkg!(pkg)) + help("Check for typos in {} or remove it from the {} manifest.", fmt_field!(field), fmt_pkg!(pkg)) )] IgnoreUnknownField { field: String, pkg: String }, - #[error("Source group in package {} contains no source files.", pkg!(.0))] + #[error("Source group in package {} contains no source files.", fmt_pkg!(.0))] #[diagnostic( code(W04), help("Add source files to the source group or remove it from the manifest.") )] NoFilesInSourceGroup(String), - #[error("No files matched the global pattern {}.", path!(path))] + #[error("No files matched the global pattern {}.", fmt_path!(path))] #[diagnostic(code(W05))] NoFilesForGlobalPattern { path: String }, - #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", pkg!(.0), path!(.1.display()))] + #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", fmt_pkg!(.0), fmt_path!(.1.display()))] #[diagnostic( code(W06), help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk.") @@ -204,36 +206,36 @@ pub enum Warnings { UrlMaybeIncorrect, // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? - #[error("Revision {} not found in repository {}.", pkg!(.0), pkg!(.1))] + #[error("Revision {} not found in repository {}.", fmt_pkg!(.0), fmt_pkg!(.1))] #[diagnostic( code(W08), help("Check that the revision exists in the remote repository or run `bender update`.") )] RevisionNotFound(String, String), - #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", pkg!(pkg), pkg!(top_pkg))] + #[error("Path dependency {} inside git dependency {} detected. This is currently not fully suppored and your milage may vary.", fmt_pkg!(pkg), fmt_pkg!(top_pkg))] #[diagnostic(code(W09))] PathDepInGitDep { pkg: String, top_pkg: String }, - #[error("There may be issues in the path for {}.", pkg!(.0))] + #[error("There may be issues in the path for {}.", fmt_pkg!(.0))] #[diagnostic( code(W10), - help("Please check that {} is correct and accessible.", path!(.1.display())) + help("Please check that {} is correct and accessible.", fmt_path!(.1.display())) )] MaybePathIssues(String, PathBuf), - #[error("Dependency package name {} does not match the package name {} in its manifest.", pkg!(.0), pkg!(.1))] + #[error("Dependency package name {} does not match the package name {} in its manifest.", fmt_pkg!(.0), fmt_pkg!(.1))] #[diagnostic( code(W11), - help("Check that the dependency name in your root manifest matches the name in the {} manifest.", pkg!(.0)) + help("Check that the dependency name in your root manifest matches the name in the {} manifest.", fmt_pkg!(.0)) )] DepPkgNameNotMatching(String, String), - #[error("Manifest for package {} not found at {}.", pkg!(pkg), path!(src))] + #[error("Manifest for package {} not found at {}.", fmt_pkg!(pkg), fmt_path!(src))] #[diagnostic(code(W12))] ManifestNotFound { pkg: String, src: String }, - #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", pkg!(.0))] + #[error("Name issue with package {}. `export_include_dirs` cannot be handled.", fmt_pkg!(.0))] #[diagnostic( code(W13), help("Could be related to name missmatch, check `bender update`.") @@ -244,7 +246,7 @@ pub enum Warnings { #[diagnostic(code(W14))] LocalNoFetch, - #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", pkg!(vendor_pkg), path!(from_prefix.display()), path!(to_prefix.display()))] + #[error("No patch directory found for package {} when trying to apply patches from {} to {}. Skipping patch generation.", fmt_pkg!(vendor_pkg), fmt_path!(from_prefix.display()), fmt_path!(to_prefix.display()))] #[diagnostic(code(W15))] NoPatchDir { vendor_pkg: String, @@ -257,19 +259,19 @@ pub enum Warnings { DependStringMaybeWrong, // TODO(fischeti): Why are there two W16 variants? - #[error("{} not found in upstream, continuing.", path!(path))] + #[error("{} not found in upstream, continuing.", fmt_path!(path))] #[diagnostic(code(W16))] NotInUpstream { path: String }, - #[error("Package {} is shown to include dependency, but manifest does not have this information.", pkg!(pkg))] + #[error("Package {} is shown to include dependency, but manifest does not have this information.", fmt_pkg!(pkg))] #[diagnostic(code(W17))] IncludeDepManifestMismatch { pkg: String }, - #[error("An override is specified for dependency {} to {}.", pkg!(pkg), pkg!(pkg_override))] + #[error("An override is specified for dependency {} to {}.", fmt_pkg!(pkg), fmt_pkg!(pkg_override))] #[diagnostic(code(W18))] DepOverride { pkg: String, pkg_override: String }, - #[error("Workspace checkout directory set and has uncommitted changes, not updating {} at {}.", pkg!(.0), path!(.1.display()))] + #[error("Workspace checkout directory set and has uncommitted changes, not updating {} at {}.", fmt_pkg!(.0), fmt_path!(.1.display()))] #[diagnostic( code(W19), help("Run `bender checkout --force` to overwrite the dependency at your own risk.") @@ -277,42 +279,42 @@ pub enum Warnings { CheckoutDirDirty(String, PathBuf), // TODO(fischeti): Should this be an error instead of a warning? - #[error("Ignoring error for {} at {}: {}", pkg!(.0), path!(.1), .2)] + #[error("Ignoring error for {} at {}: {}", fmt_pkg!(.0), fmt_path!(.1), .2)] #[diagnostic(code(W20))] IgnoringError(String, String, String), - #[error("No revision found in lock file for git dependency {}.", pkg!(pkg))] + #[error("No revision found in lock file for git dependency {}.", fmt_pkg!(pkg))] #[diagnostic(code(W21))] NoRevisionInLockFile { pkg: String }, - #[error("Dependency {} has source path {} which does not exist.", pkg!(.0), path!(.1.display()))] + #[error("Dependency {} has source path {} which does not exist.", fmt_pkg!(.0), fmt_path!(.1.display()))] #[diagnostic(code(W22), help("Please check that the path exists and is correct."))] DepSourcePathMissing(String, PathBuf), - #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", pkg!(rev), pkg!(pkg))] + #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", fmt_pkg!(rev), fmt_pkg!(pkg))] #[diagnostic(code(W23))] LockedRevisionNotFound { pkg: String, rev: String }, - #[error("Include directory {} doesn't exist.", path!(.0.display()))] + #[error("Include directory {} doesn't exist.", fmt_path!(.0.display()))] #[diagnostic( code(W24), help("Please check that the include directory exists and is correct.") )] IncludeDirMissing(PathBuf), - #[error("Skipping dirty dependency {}", pkg!(pkg))] - #[diagnostic(help("Use `--no-skip` to still snapshot {}.", pkg!(pkg)))] + #[error("Skipping dirty dependency {}", fmt_pkg!(pkg))] + #[diagnostic(help("Use `--no-skip` to still snapshot {}.", fmt_pkg!(pkg)))] SkippingDirtyDep { pkg: String }, #[error("File not added, ignoring: {cause}")] #[diagnostic(code(W30))] IgnoredPath { cause: String }, - #[error("File {} doesn't exist.", path!(path.display()))] + #[error("File {} doesn't exist.", fmt_path!(path.display()))] #[diagnostic(code(W31))] FileMissing { path: PathBuf }, - #[error("Path {} for dependency {} does not exist.", path!(path.display()), pkg!(pkg))] + #[error("Path {} for dependency {} does not exist.", fmt_path!(path.display()), fmt_pkg!(pkg))] #[diagnostic(code(W32))] DepPathMissing { pkg: String, path: PathBuf }, } diff --git a/src/error.rs b/src/error.rs index cda680f7..0bda72dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -151,27 +151,3 @@ macro_rules! stageln { pub fn println_stage(stage: &str, message: &str) { eprintln!("\x1B[32;1m{:>12}\x1B[0m {}", stage, message); } - -/// Bold a package name in diagnostic messages. -#[macro_export] -macro_rules! pkg { - ($pkg:expr) => { - $pkg.bold() - }; -} - -/// Underline a path in diagnostic messages. -#[macro_export] -macro_rules! path { - ($pkg:expr) => { - $pkg.underline() - }; -} - -/// Italicize a field name in diagnostic messages. -#[macro_export] -macro_rules! field { - ($field:expr) => { - $field.italic() - }; -} diff --git a/src/main.rs b/src/main.rs index 343d0c9d..314967bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,7 @@ pub mod resolver; pub mod sess; pub mod src; pub mod target; +#[macro_use] pub mod util; fn main() { diff --git a/src/util.rs b/src/util.rs index f332c638..1a1dae91 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,6 +18,9 @@ use semver::{Version, VersionReq}; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; +/// Re-export owo_colors for use in macros. +pub use owo_colors::OwoColorize; + use crate::error::*; /// A type that cannot be materialized. @@ -427,3 +430,35 @@ pub fn version_req_bottom_bound(req: &VersionReq) -> Result> { Ok(None) } } + +/// Bold a package name in diagnostic messages. +#[macro_export] +macro_rules! fmt_pkg { + ($pkg:expr) => { + $crate::util::OwoColorize::bold(&$pkg) + }; +} + +/// Underline a path in diagnostic messages. +#[macro_export] +macro_rules! fmt_path { + ($pkg:expr) => { + $crate::util::OwoColorize::underline(&$pkg) + }; +} + +/// Italicize a field name in diagnostic messages. +#[macro_export] +macro_rules! fmt_field { + ($field:expr) => { + $crate::util::OwoColorize::italic(&$field) + }; +} + +/// Bold a version in diagnostic messages. +#[macro_export] +macro_rules! fmt_version { + ($ver:expr) => { + $crate::util::OwoColorize::bold(&$ver) + }; +} From 02efe006ab8f781f91fc1929aa9e4fae584d846e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 18:54:25 +0100 Subject: [PATCH 30/79] diagnostic: Differentiate W06 warnings --- src/diagnostic.rs | 6 ++++++ src/resolver.rs | 36 +++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index f504d4a0..ed9f50fb 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -180,6 +180,7 @@ pub enum Warnings { #[diagnostic(code(W05))] NoFilesForGlobalPattern { path: String }, + // TODO(fischeti): Why are there two W06 variants? #[error("Dependency {} in checkout_dir {} is not a git repository. Setting as path dependency.", fmt_pkg!(.0), fmt_path!(.1.display()))] #[diagnostic( code(W06), @@ -187,6 +188,11 @@ pub enum Warnings { )] NotAGitDependency(String, PathBuf), + // TODO(fischeti): Why are there two W06 variants? + #[error("Dependency {} in checkout_dir {} is not in a clean state. Setting as path dependency.", fmt_pkg!(.0), fmt_path!(.1.display()))] + #[diagnostic(code(W06), help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk."))] + DirtyGitDependency(String, PathBuf), + // TODO(fischeti): Why are there two W07 variants? // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? #[error("SSH key might be missing.")] diff --git a/src/resolver.rs b/src/resolver.rs index b6e651fb..3af95c15 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -103,21 +103,27 @@ impl<'ctx> DependencyResolver<'ctx> { // Only act if the avoiding flag is not set and any of the following match // - the dependency is not a git repo // - the dependency is not in a clean state (i.e., was modified) - if !ignore_checkout - && (!is_git_repo - || !(SysCommand::new(&self.sess.config.git) // If not in a clean state - .arg("status") - .arg("--porcelain") - .current_dir(dir.as_ref().unwrap().path()) - .output()? - .stdout - .is_empty())) - { - Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); - self.checked_out.insert( - depname, - config::Dependency::Path(dir.unwrap().path(), vec![]), - ); + if !ignore_checkout { + if !is_git_repo { + Warnings::NotAGitDependency(depname.clone(), checkout.clone()).emit(); + self.checked_out.insert( + depname, + config::Dependency::Path(dir.unwrap().path(), vec![]), + ); + } else if !(SysCommand::new(&self.sess.config.git) // If not in a clean state + .arg("status") + .arg("--porcelain") + .current_dir(dir.as_ref().unwrap().path()) + .output()? + .stdout + .is_empty()) + { + Warnings::DirtyGitDependency(depname.clone(), checkout.clone()).emit(); + self.checked_out.insert( + depname, + config::Dependency::Path(dir.unwrap().path(), vec![]), + ); + } } } } From bbffaf9238c28d5ce8cfaeb0dc3d5f8b0b2605bf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 19:11:33 +0100 Subject: [PATCH 31/79] diagnostics: Remove suppression hint, show code again --- src/diagnostic.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index ed9f50fb..dc1e5325 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -92,8 +92,16 @@ impl ReportHandler for DiagnosticRenderer { miette::Severity::Advice => ("advice", owo_colors::Style::new().cyan().bold()), }; - // Write the severity prefix and the diagnostic message - write!(f, "{}: {}", severity.style(style), diagnostic)?; + // Write the severity prefix + write!(f, "{}", severity.style(style))?; + + // Write the code, if any + if let Some(code) = diagnostic.code() { + write!(f, "{}", format!("[{}]", code).style(style))?; + } + + // Write the main diagnostic message + write!(f, ": {}", diagnostic)?; // We collect all footer lines into a vector. let mut annotations: Vec = Vec::new(); @@ -110,15 +118,6 @@ impl ReportHandler for DiagnosticRenderer { } } - // Finally, we write the code/suppression message, if any - if let Some(code) = diagnostic.code() { - annotations.push(format!( - "{} {}", - "suppress:".cyan().bold(), // No variable, no lifetime issue - format!("Run `bender --suppress {}` to suppress this warning", code).dimmed() - )); - } - // Prepare tree characters let branch = " ├─›"; let corner = " ╰─›"; From ddb5ba3b6222d79ac3a5ec79c63272f1fdedb9af Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 20:05:51 +0100 Subject: [PATCH 32/79] diagnostic: Consistently format `version` and `revision` --- src/cmd/parents.rs | 6 +++--- src/diagnostic.rs | 6 +++--- src/util.rs | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/cmd/parents.rs b/src/cmd/parents.rs index adef05e9..abfa133b 100644 --- a/src/cmd/parents.rs +++ b/src/cmd/parents.rs @@ -15,7 +15,7 @@ use crate::config::Dependency; use crate::error::*; use crate::sess::{DependencyConstraint, DependencySource}; use crate::sess::{Session, SessionIo}; -use crate::{fmt_path, fmt_pkg}; +use crate::{fmt_path, fmt_pkg, fmt_version}; /// List packages calling this dependency #[derive(Args, Debug)] @@ -102,10 +102,10 @@ pub fn run(sess: &Session, args: &ParentsArgs) -> Result<()> { Warnings::DepOverride { pkg: dep.to_string(), pkg_override: match sess.config.overrides[dep] { - Dependency::Version(ref v, _) => format!("version {}", fmt_pkg!(v)), + Dependency::Version(ref v, _) => format!("version {}", fmt_version!(v)), Dependency::Path(ref path, _) => format!("path {}", fmt_path!(path.display())), Dependency::GitRevision(ref url, ref rev, _) => { - format!("git {} at revision {}", fmt_path!(url), fmt_pkg!(rev)) + format!("git {} at revision {}", fmt_path!(url), fmt_version!(rev)) } Dependency::GitVersion(ref url, ref version, _) => { format!("git {} with version {}", fmt_path!(url), fmt_pkg!(version)) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index dc1e5325..ddef9db9 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -10,7 +10,7 @@ use miette::{Diagnostic, ReportHandler}; use owo_colors::OwoColorize; use thiserror::Error; -use crate::{fmt_field, fmt_path, fmt_pkg}; +use crate::{fmt_field, fmt_path, fmt_pkg, fmt_version}; static GLOBAL_DIAGNOSTICS: OnceLock = OnceLock::new(); @@ -211,7 +211,7 @@ pub enum Warnings { UrlMaybeIncorrect, // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? - #[error("Revision {} not found in repository {}.", fmt_pkg!(.0), fmt_pkg!(.1))] + #[error("Revision {} not found in repository {}.", fmt_version!(.0), fmt_pkg!(.1))] #[diagnostic( code(W08), help("Check that the revision exists in the remote repository or run `bender update`.") @@ -296,7 +296,7 @@ pub enum Warnings { #[diagnostic(code(W22), help("Please check that the path exists and is correct."))] DepSourcePathMissing(String, PathBuf), - #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", fmt_pkg!(rev), fmt_pkg!(pkg))] + #[error("Locked revision {} for dependency {} not found in available revisions, allowing update.", fmt_version!(rev), fmt_pkg!(pkg))] #[diagnostic(code(W23))] LockedRevisionNotFound { pkg: String, rev: String }, diff --git a/src/util.rs b/src/util.rs index 1a1dae91..df840984 100644 --- a/src/util.rs +++ b/src/util.rs @@ -431,7 +431,7 @@ pub fn version_req_bottom_bound(req: &VersionReq) -> Result> { } } -/// Bold a package name in diagnostic messages. +/// Format for `package` names in diagnostic messages. #[macro_export] macro_rules! fmt_pkg { ($pkg:expr) => { @@ -439,7 +439,7 @@ macro_rules! fmt_pkg { }; } -/// Underline a path in diagnostic messages. +/// Format for `path` and `url` fields in diagnostic messages. #[macro_export] macro_rules! fmt_path { ($pkg:expr) => { @@ -447,7 +447,7 @@ macro_rules! fmt_path { }; } -/// Italicize a field name in diagnostic messages. +/// Format for `field` names in diagnostic messages. #[macro_export] macro_rules! fmt_field { ($field:expr) => { @@ -455,7 +455,7 @@ macro_rules! fmt_field { }; } -/// Bold a version in diagnostic messages. +/// Format for `version` and `revision` fields in diagnostic messages. #[macro_export] macro_rules! fmt_version { ($ver:expr) => { From a02fb4d376961f348138407a84bf6c519012933c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 20:59:20 +0100 Subject: [PATCH 33/79] diagnostic: Fix/duplicate W19 warning --- src/diagnostic.rs | 7 +++++++ src/sess.rs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index ddef9db9..e0184b4c 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -283,6 +283,13 @@ pub enum Warnings { )] CheckoutDirDirty(String, PathBuf), + #[error("Workspace checkout directory set and remote url doesn't match, not updating {} at {}.", fmt_pkg!(.0), fmt_path!(.1.display()))] + #[diagnostic( + code(W19), + help("Run `bender checkout --force` to overwrite the dependency at your own risk.") + )] + CheckoutDirUrlMismatch(String, PathBuf), + // TODO(fischeti): Should this be an error instead of a warning? #[error("Ignoring error for {} at {}: {}", fmt_pkg!(.0), fmt_path!(.1), .2)] #[diagnostic(code(W20))] diff --git a/src/sess.rs b/src/sess.rs index 831460b9..002b1d59 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -895,7 +895,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { CheckoutState::Clean } } else { - Warnings::CheckoutDirDirty(name.to_string(), path.to_path_buf()).emit(); + Warnings::CheckoutDirUrlMismatch(name.to_string(), path.to_path_buf()).emit(); CheckoutState::Clean } } else { From b7601c1f025b9eef12c446390f4ca2b85002d31f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 20:59:53 +0100 Subject: [PATCH 34/79] diagnostic: Warning todo comments --- src/diagnostic.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index e0184b4c..a017a77f 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -192,8 +192,7 @@ pub enum Warnings { #[diagnostic(code(W06), help("Use `bender clone` to work on git dependencies.\nRun `bender update --ignore-checkout-dir` to overwrite this at your own risk."))] DirtyGitDependency(String, PathBuf), - // TODO(fischeti): Why are there two W07 variants? - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + // TODO(fischeti): This is part of an error, not a warning. Should be converted to an Error. #[error("SSH key might be missing.")] #[diagnostic( code(W07), @@ -201,8 +200,7 @@ pub enum Warnings { )] SshKeyMaybeMissing, - // TODO(fischeti): Why are there two W07 variants? - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + // TODO(fischeti): This is part of an error, not a warning. Should be converted to an Error. #[error("SSH key might be missing.")] #[diagnostic( code(W07), @@ -210,7 +208,7 @@ pub enum Warnings { )] UrlMaybeIncorrect, - // TODO(fischeti): This is part of an error, not a warning. Move to Error enum later? + // TODO(fischeti): This is part of an error, not a warning. Should be converted to an Error. #[error("Revision {} not found in repository {}.", fmt_version!(.0), fmt_pkg!(.1))] #[diagnostic( code(W08), From 35b4c0cf059346cdbb86888e0cadc9a814b53fb3 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 30 Dec 2025 16:14:29 +0100 Subject: [PATCH 35/79] cargo: Add new dependencies --- Cargo.lock | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ 2 files changed, 60 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9bf920ee..e145e8a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,17 +150,20 @@ dependencies = [ "blake2", "clap", "clap_complete", + "console", "dirs", "dunce", "futures", "glob", "indexmap", + "indicatif", "is-terminal", "itertools", "miette", "owo-colors", "pathdiff", "pretty_assertions", + "regex", "semver", "serde", "serde_json", @@ -326,6 +329,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -438,6 +454,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -703,6 +725,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + [[package]] name = "is-terminal" version = "0.4.17" @@ -1035,6 +1070,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1581,6 +1622,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -1678,6 +1725,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi-util" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index d42a104c..7327b736 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,9 @@ tera = "1.19" miette = { version = "7.6.0", features = ["fancy"] } thiserror = "2.0.17" owo-colors = "4.2.3" +indicatif = "0.18.3" +console = "0.16.2" +regex = "1.12.2" [target.'cfg(windows)'.dependencies] dunce = "1.0.4" From 41c6afbf6fec84c7f57b30c0cb59a78592009503 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 31 Dec 2025 13:01:09 +0100 Subject: [PATCH 36/79] progress: Implementation and initial integration of progress bars First compiling version --- src/cmd/vendor.rs | 130 +++++++++++--------- src/git.rs | 213 +++++++++++++++++++++----------- src/main.rs | 1 + src/progress.rs | 303 ++++++++++++++++++++++++++++++++++++++++++++++ src/sess.rs | 137 +++++++++++++++------ 5 files changed, 619 insertions(+), 165 deletions(-) create mode 100644 src/progress.rs diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 6e4dc8d5..a47d66bc 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -21,6 +21,7 @@ use crate::diagnostic::Warnings; use crate::error::*; use crate::futures::TryFutureExt; use crate::git::Git; +use crate::progress::{GitProgressOps, ProgressHandler}; use crate::sess::{DependencySource, Session}; /// A patch linkage @@ -101,8 +102,13 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { DependencySource::Git(ref url) => { let git = Git::new(tmp_path, &sess.config.git, sess.git_throttle.clone()); rt.block_on(async { - stageln!("Cloning", "{} ({})", vendor_package.name, url); - git.clone().spawn_with(|c| c.arg("clone").arg(url).arg(".")) + // stageln!("Cloning", "{} ({})", vendor_package.name, url); + let pb = ProgressHandler::new( + sess.progress.clone(), + GitProgressOps::Clone, + vendor_package.name.as_str(), + ); + git.clone().spawn_with(|c| c.arg("clone").arg(url).arg("."), Some(pb)) .map_err(move |cause| { if url.contains("git@") { Warnings::SshKeyMaybeMissing.emit(); @@ -117,8 +123,13 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { config::Dependency::GitRevision(_, ref rev, _) => Ok(rev), _ => Err(Error::new("Please ensure your vendor reference is a commit hash to avoid upstream changes impacting your checkout")), }?; - git.clone().spawn_with(|c| c.arg("checkout").arg(rev_hash)).await?; - if *rev_hash != git.spawn_with(|c| c.arg("rev-parse").arg("--verify").arg(format!("{}^{{commit}}", rev_hash))).await?.trim_end_matches('\n') { + let pb = ProgressHandler::new( + sess.progress.clone(), + GitProgressOps::Checkout, + vendor_package.name.as_str(), + ); + git.clone().spawn_with(|c| c.arg("checkout").arg(rev_hash), Some(pb)).await?; + if *rev_hash != git.spawn_with(|c| c.arg("rev-parse").arg("--verify").arg(format!("{}^{{commit}}", rev_hash)), None).await?.trim_end_matches('\n') { Err(Error::new("Please ensure your vendor reference is a commit hash to avoid upstream changes impacting your checkout")) } else { Ok(()) @@ -427,34 +438,37 @@ pub fn apply_patches( Ok(()) }) .and_then(|_| { - git.clone().spawn_with(|c| { - let is_file = patch_link - .from_prefix - .clone() - .prefix_paths(git.path) - .unwrap() - .is_file(); - - let current_patch_target = if is_file { - patch_link.from_prefix.parent().unwrap().to_str().unwrap() - } else { - patch_link.from_prefix.as_path().to_str().unwrap() - }; - - c.arg("apply") - .arg("--directory") - .arg(current_patch_target) - .arg("-p1") - .arg(&patch); - - // limit to specific file for file links - if is_file { - let file_path = patch_link.from_prefix.to_str().unwrap(); - c.arg("--include").arg(file_path); - } + git.clone().spawn_with( + |c| { + let is_file = patch_link + .from_prefix + .clone() + .prefix_paths(git.path) + .unwrap() + .is_file(); - c - }) + let current_patch_target = if is_file { + patch_link.from_prefix.parent().unwrap().to_str().unwrap() + } else { + patch_link.from_prefix.as_path().to_str().unwrap() + }; + + c.arg("apply") + .arg("--directory") + .arg(current_patch_target) + .arg("-p1") + .arg(&patch); + + // limit to specific file for file links + if is_file { + let file_path = patch_link.from_prefix.to_str().unwrap(); + c.arg("--include").arg(file_path); + } + + c + }, + None, + ) }) .await .map_err(move |cause| { @@ -524,15 +538,18 @@ pub fn diff( }; // Get diff rt.block_on(async { - git.spawn_with(|c| { - c.arg("diff").arg(format!( - "--relative={}", - patch_link - .from_prefix - .to_str() - .expect("Failed to convert from_prefix to string.") - )) - }) + git.spawn_with( + |c| { + c.arg("diff").arg(format!( + "--relative={}", + patch_link + .from_prefix + .to_str() + .expect("Failed to convert from_prefix to string.") + )) + }, + None, + ) .await }) } @@ -667,7 +684,7 @@ pub fn gen_format_patch( // Get staged changes in dependency let get_diff_cached = rt - .block_on(async { git_parent.spawn_with(|c| c.args(&diff_args)).await }) + .block_on(async { git_parent.spawn_with(|c| c.args(&diff_args), None).await }) .map_err(|cause| Error::chain("Failed to generate diff", cause))?; if !get_diff_cached.is_empty() { @@ -685,8 +702,8 @@ pub fn gen_format_patch( .arg(&from_path_relative) .arg("-p1") .arg(&diff_cached_path) - }) - .and_then(|_| git.clone().spawn_with(|c| c.arg("add").arg("--all"))) + }, None) + .and_then(|_| git.clone().spawn_with(|c| c.arg("add").arg("--all"), None)) .await }).map_err(|cause| Error::chain("Could not apply staged changes on top of patched upstream repository. Did you commit all previously patched modifications?", cause))?; @@ -735,18 +752,21 @@ pub fn gen_format_patch( // Generate format-patch rt.block_on(async { - git.spawn_with(|c| { - c.arg("format-patch") - .arg("-o") - .arg(patch_dir.to_str().unwrap()) - .arg("-1") - .arg(format!("--start-number={}", max_number + 1)) - .arg(format!( - "--relative={}", - from_path_relative.to_str().unwrap() - )) - .arg("HEAD") - }) + git.spawn_with( + |c| { + c.arg("format-patch") + .arg("-o") + .arg(patch_dir.to_str().unwrap()) + .arg("-1") + .arg(format!("--start-number={}", max_number + 1)) + .arg(format!( + "--relative={}", + from_path_relative.to_str().unwrap() + )) + .arg("HEAD") + }, + None, + ) .await })?; } diff --git a/src/git.rs b/src/git.rs index b11e5af0..6425d8f3 100644 --- a/src/git.rs +++ b/src/git.rs @@ -7,12 +7,16 @@ use std::ffi::OsStr; use std::path::Path; +use std::process::Stdio; use std::sync::Arc; use futures::TryFutureExt; +use tokio::io::AsyncReadExt; use tokio::process::Command; use tokio::sync::Semaphore; +use crate::progress::{monitor_stderr, ProgressHandler}; + use crate::error::*; /// A git repository. @@ -61,56 +65,94 @@ impl<'ctx> Git<'ctx> { /// If `check` is false, the stdout will be returned regardless of the /// command's exit code. #[allow(clippy::format_push_string)] - pub async fn spawn(self, mut cmd: Command, check: bool) -> Result { - // acquire throttle + pub async fn spawn( + self, + mut cmd: Command, + check: bool, + pb: Option, + ) -> Result { + // Acquire the throttle semaphore let permit = self.throttle.clone().acquire_owned().await.unwrap(); - let output = cmd.output().map_err(|cause| { + + // Configure pipes for streaming + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + // Spawn the child process + let mut child = cmd.spawn().map_err(|cause| { if cause .to_string() .to_lowercase() .contains("too many open files") { - eprintln!( - "Please consider increasing your `ulimit -n`, e.g. by running `ulimit -n 4096`" - ); - eprintln!("This is a known issue (#52)."); + eprintln!("Please consider increasing your `ulimit -n`..."); Error::chain("Failed to spawn child process.", cause) } else { Error::chain("Failed to spawn child process.", cause) } + })?; + + debugln!("git: {:?} in {:?}", cmd, self.path); + + // Setup Streaming for Stderr (Progress + Error Collection) + // We need to capture stderr in case the command fails, so we collect it while parsing. + let stderr = child.stderr.take().unwrap(); + + // Spawn a background task to handle stderr so it doesn't block + let stderr_handle = tokio::spawn(async move { + // We pass the handler clone into the async task + monitor_stderr(stderr, pb).await }); - let result = output.and_then(|output| async move { - debugln!("git: {:?} in {:?}", cmd, self.path); - if output.status.success() || !check { - String::from_utf8(output.stdout).map_err(|cause| { - Error::chain( - format!( - "Output of git command ({:?}) in directory {:?} is not valid UTF-8.", - cmd, self.path - ), - cause, - ) - }) - } else { - let mut msg = format!("Git command ({:?}) in directory {:?}", cmd, self.path); - match output.status.code() { - Some(code) => msg.push_str(&format!(" failed with exit code {}", code)), - None => msg.push_str(" failed"), - }; - match String::from_utf8(output.stderr) { - Ok(txt) => { - msg.push_str(":\n\n"); - msg.push_str(&txt); - } - Err(err) => msg.push_str(&format!(". Stderr is not valid UTF-8, {}.", err)), - }; - Err(Error::new(msg)) + + // Read Stdout (for the success return value) + let mut stdout_buffer = Vec::new(); + if let Some(mut stdout) = child.stdout.take() { + // We just read all of stdout. + if let Err(e) = stdout.read_to_end(&mut stdout_buffer).await { + return Err(Error::chain("Failed to read stdout", e)); } - }); - let result = result.await; - // release throttle + } + + // Wait for child process to finish + let status = child + .wait() + .await + .map_err(|e| Error::chain("Failed to wait on child", e))?; + + // Join the stderr task to get the error log + let collected_stderr = stderr_handle + .await + .unwrap_or_else(|_| String::from("")); + + // We can release the throttle here since we're done with the process drop(permit); - result + + // Process the output based on success and check flag + if status.success() || !check { + String::from_utf8(stdout_buffer).map_err(|cause| { + Error::chain( + format!( + "Output of git command ({:?}) in directory {:?} is not valid UTF-8.", + cmd, self.path + ), + cause, + ) + }) + } else { + let mut msg = format!("Git command ({:?}) in directory {:?}", cmd, self.path); + match status.code() { + Some(code) => msg.push_str(&format!(" failed with exit code {}", code)), + None => msg.push_str(" failed"), + }; + + // Use the stderr we collected in the background task + if !collected_stderr.is_empty() { + msg.push_str(":\n\n"); + msg.push_str(&collected_stderr); + } + + Err(Error::new(msg)) + } } /// Assemble a command and schedule it for execution. @@ -118,28 +160,28 @@ impl<'ctx> Git<'ctx> { /// This is a convenience function that creates a command, passes it to the /// closure `f` for configuration, then passes it to the `spawn` function /// and returns the future. - pub async fn spawn_with(self, f: F) -> Result + pub async fn spawn_with(self, f: F, pb: Option) -> Result where F: FnOnce(&mut Command) -> &mut Command, { let mut cmd = Command::new(self.git); cmd.current_dir(self.path); f(&mut cmd); - self.spawn(cmd, true).await + self.spawn(cmd, true, pb).await } /// Assemble a command and schedule it for execution. /// /// This is the same as `spawn_with()`, but returns the stdout regardless of /// whether the command failed or not. - pub async fn spawn_unchecked_with(self, f: F) -> Result + pub async fn spawn_unchecked_with(self, f: F, pb: Option) -> Result where F: FnOnce(&mut Command) -> &mut Command, { let mut cmd = Command::new(self.git); cmd.current_dir(self.path); f(&mut cmd); - self.spawn(cmd, false).await + self.spawn(cmd, false, pb).await } /// Assemble a command and execute it interactively. @@ -158,26 +200,48 @@ impl<'ctx> Git<'ctx> { } /// Fetch the tags and refs of a remote. - pub async fn fetch(self, remote: &str) -> Result<()> { + pub async fn fetch(self, remote: &str, pb: Option) -> Result<()> { let r1 = String::from(remote); let r2 = String::from(remote); self.clone() - .spawn_with(|c| c.arg("fetch").arg("--prune").arg(r1)) - .and_then(|_| self.spawn_with(|c| c.arg("fetch").arg("--tags").arg("--prune").arg(r2))) + .spawn_with( + |c| c.arg("fetch").arg("--prune").arg(r1).arg("--progress"), + pb.clone(), + ) + .and_then(|_| { + self.spawn_with( + |c| { + c.arg("fetch") + .arg("--tags") + .arg("--prune") + .arg(r2) + .arg("--progress") + }, + pb, + ) + }) .await .map(|_| ()) } /// Fetch the specified ref of a remote. - pub async fn fetch_ref(self, remote: &str, reference: &str) -> Result<()> { - self.spawn_with(|c| c.arg("fetch").arg(remote).arg(reference)) - .await - .map(|_| ()) + pub async fn fetch_ref( + self, + remote: &str, + reference: &str, + pb: Option, + ) -> Result<()> { + self.spawn_with( + |c| c.arg("fetch").arg(remote).arg(reference).arg("--progress"), + pb, + ) + .await + .map(|_| ()) } /// Stage all local changes. pub async fn add_all(self) -> Result<()> { - self.spawn_with(|c| c.arg("add").arg("--all")) + self.spawn_with(|c| c.arg("add").arg("--all"), None) .await .map(|_| ()) } @@ -188,13 +252,16 @@ impl<'ctx> Git<'ctx> { pub async fn commit(self, message: Option<&String>) -> Result<()> { match message { Some(msg) => self - .spawn_with(|c| { - c.arg("-c") - .arg("commit.gpgsign=false") - .arg("commit") - .arg("-m") - .arg(msg) - }) + .spawn_with( + |c| { + c.arg("-c") + .arg("commit.gpgsign=false") + .arg("commit") + .arg("-m") + .arg(msg) + }, + None, + ) .await .map(|_| ()), @@ -207,7 +274,7 @@ impl<'ctx> Git<'ctx> { /// List all refs and their hashes. pub async fn list_refs(self) -> Result> { - self.spawn_unchecked_with(|c| c.arg("show-ref").arg("--dereference")) + self.spawn_unchecked_with(|c| c.arg("show-ref").arg("--dereference"), None) .and_then(|raw| async move { let mut all_revs = raw .lines() @@ -242,21 +309,24 @@ impl<'ctx> Git<'ctx> { /// List all revisions. pub async fn list_revs(self) -> Result> { - self.spawn_with(|c| c.arg("rev-list").arg("--all").arg("--date-order")) + self.spawn_with(|c| c.arg("rev-list").arg("--all").arg("--date-order"), None) .await .map(|raw| raw.lines().map(String::from).collect()) } /// Determine the currently checked out revision. pub async fn current_checkout(self) -> Result> { - self.spawn_with(|c| c.arg("rev-parse").arg("--revs-only").arg("HEAD^{commit}")) - .await - .map(|raw| raw.lines().take(1).map(String::from).next()) + self.spawn_with( + |c| c.arg("rev-parse").arg("--revs-only").arg("HEAD^{commit}"), + None, + ) + .await + .map(|raw| raw.lines().take(1).map(String::from).next()) } /// Determine the url of a remote. pub async fn remote_url(self, remote: &str) -> Result { - self.spawn_with(|c| c.arg("remote").arg("get-url").arg(remote)) + self.spawn_with(|c| c.arg("remote").arg("get-url").arg(remote), None) .await .map(|raw| raw.lines().take(1).map(String::from).next().unwrap()) } @@ -269,20 +339,23 @@ impl<'ctx> Git<'ctx> { rev: R, path: Option

, ) -> Result> { - self.spawn_with(|c| { - c.arg("ls-tree").arg(rev); - if let Some(p) = path { - c.arg(p); - } - c - }) + self.spawn_with( + |c| { + c.arg("ls-tree").arg(rev); + if let Some(p) = path { + c.arg(p); + } + c + }, + None, + ) .await .map(|raw| raw.lines().map(TreeEntry::parse).collect()) } /// Read the content of a file. pub async fn cat_file>(self, hash: O) -> Result { - self.spawn_with(|c| c.arg("cat-file").arg("blob").arg(hash)) + self.spawn_with(|c| c.arg("cat-file").arg("blob").arg(hash), None) .await } } diff --git a/src/main.rs b/src/main.rs index 314967bf..b96bf92a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ pub mod config; pub mod diagnostic; pub mod git; pub mod lockfile; +pub mod progress; pub mod resolver; #[allow(clippy::bind_instead_of_map)] pub mod sess; diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 00000000..7a848dd0 --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,303 @@ +// Copyright (c) 2025 ETH Zurich +// Tim Fischer + +use std::sync::OnceLock; +use std::time::Duration; + +use console::style; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use regex::Regex; +use tokio::io::{AsyncReadExt, BufReader}; + +/// Parses a line of git output. +/// (Put your `GitProgress` enum and `parse_git_line` function here) +#[derive(Debug, PartialEq, Clone)] +pub enum GitProgress { + CloningInto { + path: String, + }, + SubmoduleEnd { + path: String, + }, + Receiving { + percent: u8, + current: usize, + total: usize, + }, + Resolving { + percent: u8, + current: usize, + total: usize, + }, + Checkout { + percent: u8, + current: usize, + total: usize, + }, + Other, +} + +/// The git operation types that currently support progress reporting. +#[derive(Debug, PartialEq, Clone)] +pub enum GitProgressOps { + Checkout, + Clone, + Fetch, +} + +static RE_GIT: OnceLock = OnceLock::new(); + +pub fn parse_git_line(line: &str) -> GitProgress { + let line = line.trim(); + let re = RE_GIT.get_or_init(|| { + Regex::new(r"(?x) + ^ # Start + (?: + Cloning\ into\ '(?P[^']+)'\.\.\. | + Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | + (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% + (?: \s+ \( (?P\d+) / (?P\d+) \) )? + ) + ").expect("Invalid Regex") + }); + + if let Some(caps) = re.captures(line) { + // Case 1: Cloning into... + if let Some(path) = caps.name("clone_path") { + return GitProgress::CloningInto { + path: path.as_str().to_string(), + }; + } + + // Case 2: Submodule finished + if let Some(path) = caps.name("sub_end_path") { + return GitProgress::SubmoduleEnd { + path: path.as_str().to_string(), + }; + } + + // Case 3: Progress + if let Some(phase) = caps.name("phase") { + let percent = caps.name("percent").unwrap().as_str().parse().unwrap_or(0); + let current = caps + .name("current") + .map(|m| m.as_str().parse().unwrap_or(0)) + .unwrap_or(0); + let total = caps + .name("total") + .map(|m| m.as_str().parse().unwrap_or(0)) + .unwrap_or(0); + + return match phase.as_str() { + "Receiving objects" => GitProgress::Receiving { + percent, + current, + total, + }, + "Resolving deltas" => GitProgress::Resolving { + percent, + current, + total, + }, + "Checking out files" => GitProgress::Checkout { + percent, + current, + total, + }, + _ => GitProgress::Other, + }; + } + } + // Otherwise, we don't care + GitProgress::Other +} + +/// This struct captures (dynamic) state information for a git operation's progress. +/// for instance, the actuall progress bars to update. +pub struct ProgressState { + /// The progress bar of the current package. + pb: ProgressBar, + /// The progress bar for submodules, if any. + sub_pb: Option, + /// Whether the main progress bar is done. + /// This is used to determine when to start submodule progress bars. + main_done: bool, +} + +/// This struct captures (static) information neeed to handle progress updates for a git operation. +#[derive(Clone)] +pub struct ProgressHandler { + /// Reference to the multi-progress bar, which can manage multiple progress bars. + mpb: MultiProgress, + /// The style used for progress bars. + style: ProgressStyle, + /// The type of git operation being performed. + git_op: GitProgressOps, + /// The name of the repository being processed. + name: String, +} + +impl ProgressHandler { + /// Create a new progress handler for a git operation. + pub fn new(mpb: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { + // Set the style for progress bars + let style = ProgressStyle::with_template( + "{spinner:.green} {prefix:<24!} {bar:40.cyan/blue} {percent:>3}% {msg}", + ) + .unwrap() + .progress_chars("-- ") + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + + Self { + mpb, + git_op, + name: name.to_string(), + style, + } + } + + pub fn start(&self) -> ProgressState { + // Add a new progress bar to the multi-progress (with a length of 100) + let pb = self.mpb.add(ProgressBar::new(100)); + pb.set_style(self.style.clone()); + + let prefix = match self.git_op { + GitProgressOps::Clone => "Cloning", + GitProgressOps::Fetch => "Fetching", + GitProgressOps::Checkout => "Checkout", + }; + let prefix = format!( + "{} {}", + console::style(prefix).bold().green(), + console::style(&self.name).bright() + ); + pb.set_prefix(prefix); + // Configure the spinners to automatically tick every 100ms + pb.enable_steady_tick(Duration::from_millis(100)); + + ProgressState { + pb, + sub_pb: None, + main_done: false, + } + } + + pub fn handle_line(&self, line: &str, state: &mut ProgressState) { + let progress = parse_git_line(line); + let target_pb = state.sub_pb.as_ref().unwrap_or(&state.pb); + + match progress { + GitProgress::CloningInto { path } => { + if state.main_done { + state.pb.set_position(100); + state.pb.set_message(style("Done.").dim().to_string()); + + let sub_pb = self.mpb.insert_after(&state.pb, ProgressBar::new(100)); + sub_pb.set_style(self.style.clone()); + + let sub_name = path.split('/').last().unwrap_or(&path); + let sub_prefix = + format!(" {} {}", style("└─ Sub").dim(), style(sub_name).dim()); + sub_pb.set_prefix(sub_prefix); + state.sub_pb = Some(sub_pb); + } + state.main_done = true; + } + GitProgress::SubmoduleEnd { .. } => { + if let Some(sub) = state.sub_pb.take() { + sub.finish_and_clear(); + } + } + GitProgress::Receiving { current, total, .. } => { + target_pb.set_message(style("Receiving objects").dim().to_string()); + target_pb.set_length(total as u64); + target_pb.set_position(current as u64); + } + GitProgress::Resolving { percent, .. } => { + target_pb.set_message(style("Resolving deltas").dim().to_string()); + target_pb.set_length(100); + target_pb.set_position(percent as u64); + } + GitProgress::Checkout { percent, .. } => { + target_pb.set_message(style("Checking out").dim().to_string()); + target_pb.set_length(100); + target_pb.set_position(percent as u64); + } + _ => {} + } + } + + pub fn finish(self, state: &mut ProgressState) { + if let Some(sub) = state.sub_pb.take() { + sub.finish_and_clear(); + } + state.pb.finish_and_clear(); + } +} + +pub async fn monitor_stderr( + stream: impl tokio::io::AsyncRead + Unpin, + handler: Option, +) -> String { + let mut reader = BufReader::new(stream); + let mut buffer = Vec::new(); + let mut collected_stderr = String::new(); + + // Add a new progress bar and state if we have a handler + let mut state = match &handler { + Some(h) => h.start(), + None => ProgressState { + pb: ProgressBar::hidden(), + sub_pb: None, + main_done: false, + }, + }; + + loop { + match reader.read_u8().await { + Ok(byte) => { + // Collect raw error output (simplified for brevity) + if byte.is_ascii() { + collected_stderr.push(byte as char); + } + + if byte == b'\r' || byte == b'\n' { + if !buffer.is_empty() { + if let Ok(line) = std::str::from_utf8(&buffer) { + // Update UI if we have a handler + if let Some(h) = &handler { + h.handle_line(line, &mut state); + } + } + buffer.clear(); + } + } else { + buffer.push(byte); + } + } + Err(_) => break, + } + } + + // Cleanup any lingering sub-bars from this command + if let Some(sub) = state.sub_pb.take() { + sub.finish_and_clear(); + } + + collected_stderr +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parsing_logic() { + // Copy your existing unit tests here + let p = parse_git_line("Receiving objects: 34% (123/456)"); + match p { + GitProgress::Receiving { percent, .. } => assert_eq!(percent, 34), + _ => panic!("Failed to parse receiving"), + } + } +} diff --git a/src/sess.rs b/src/sess.rs index 002b1d59..baa00155 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -25,6 +25,7 @@ use async_recursion::async_recursion; use futures::future::join_all; use futures::TryFutureExt; use indexmap::{IndexMap, IndexSet}; +use indicatif::MultiProgress; use semver::Version; use tokio::sync::Semaphore; use typed_arena::Arena; @@ -34,6 +35,7 @@ use crate::config::{self, Config, Manifest, PartialManifest}; use crate::diagnostic::{Diagnostics, Warnings}; use crate::error::*; use crate::git::Git; +use crate::progress::{GitProgressOps, ProgressHandler}; use crate::src::SourceGroup; use crate::target::TargetSet; use crate::util::try_modification_time; @@ -78,6 +80,8 @@ pub struct Session<'ctx> { pub git_throttle: Arc, /// A toggle to disable remote fetches & clones pub local_only: bool, + /// The global progress bar manager. + pub progress: MultiProgress, } impl<'ctx> Session<'ctx> { @@ -116,6 +120,7 @@ impl<'ctx> Session<'ctx> { cache: Default::default(), git_throttle: Arc::new(Semaphore::new(git_throttle)), local_only, + progress: MultiProgress::new(), } } @@ -555,18 +560,20 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // Initialize. self.sess.stats.num_database_init.increment(); // TODO MICHAERO: May need throttle - stageln!("Cloning", "{} ({})", name2, url2); + // stageln!("Cloning", "{} ({})", name2, url2); + // TODO(fischeti): Is this actually the cloning stage? git.clone() - .spawn_with(|c| c.arg("init").arg("--bare")) + .spawn_with(|c| c.arg("init").arg("--bare"), None) .await?; git.clone() - .spawn_with(|c| c.arg("remote").arg("add").arg("origin").arg(url)) + .spawn_with(|c| c.arg("remote").arg("add").arg("origin").arg(url), None) .await?; + let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); git.clone() - .fetch("origin") + .fetch("origin", Some(pb.clone())) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference).await + git.clone().fetch_ref("origin", reference, Some(pb)).await } else { Ok(()) } @@ -592,12 +599,14 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } self.sess.stats.num_database_fetch.increment(); // TODO MICHAERO: May need throttle - stageln!("Fetching", "{} ({})", name2, url2); + // stageln!("Fetching", "{} ({})", name2, url2); + // TODO(fischeti): Is this actually the fetching stage? + let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Fetch, name); git.clone() - .fetch("origin") + .fetch("origin", Some(pb.clone())) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference).await + git.clone().fetch_ref("origin", reference, Some(pb)).await } else { Ok(()) } @@ -885,7 +894,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { if checkout_already_good == CheckoutState::ToCheckout { if local_git .clone() - .spawn_with(|c| c.arg("status").arg("--porcelain")) + .spawn_with(|c| c.arg("status").arg("--porcelain"), None) .await .is_ok() { @@ -919,7 +928,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // Perform the checkout if necessary. if clear != CheckoutState::Clean { - stageln!("Checkout", "{} ({})", name, url); + // stageln!("Checkout", "{} ({})", name, url); // First generate a tag to be cloned in the database. This is // necessary since `git clone` does not accept commits, but only @@ -930,13 +939,16 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { let git = self.git_database(name, url, false, Some(revision)).await?; match git .clone() - .spawn_with(move |c| { - c.arg("tag") - .arg(tag_name_0) - .arg(revision) - .arg("--force") - .arg("--no-sign") - }) + .spawn_with( + move |c| { + c.arg("tag") + .arg(tag_name_0) + .arg(revision) + .arg("--force") + .arg("--no-sign") + }, + None, + ) .await { Ok(r) => Ok(r), @@ -945,18 +957,29 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { "checkout_git: failed to tag commit {:?}, attempting fetch.", cause ); + let pb = ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Checkout, + name, + ); // Attempt to fetch from remote and retry, as commits seem unavailable. git.clone() - .spawn_with(move |c| c.arg("fetch").arg("--all")) + .spawn_with( + move |c| c.arg("fetch").arg("--all").arg("--progress"), + Some(pb), + ) .await?; git.clone() - .spawn_with(move |c| { - c.arg("tag") - .arg(tag_name_1) - .arg(revision) - .arg("--force") - .arg("--no-sign") - }) + .spawn_with( + move |c| { + c.arg("tag") + .arg(tag_name_1) + .arg(revision) + .arg("--force") + .arg("--no-sign") + }, + None, + ) .map_err(|cause| { Warnings::RevisionNotFound(revision.to_string(), name.to_string()) .emit(); @@ -972,33 +995,67 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } }?; if clear == CheckoutState::ToClone { + let pb = + ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); git.clone() - .spawn_with(move |c| { - c.arg("clone") - .arg(git.path) - .arg(path) - .arg("--branch") - .arg(tag_name_2) - }) + .spawn_with( + move |c| { + c.arg("clone") + .arg(git.path) + .arg(path) + .arg("--branch") + .arg(tag_name_2) + }, + Some(pb), + ) .await?; } else if clear == CheckoutState::ToCheckout { + let pb = + ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Fetch, name); local_git .clone() - .spawn_with(move |c| c.arg("fetch").arg("--all").arg("--tags").arg("--prune")) + .spawn_with( + move |c| { + c.arg("fetch") + .arg("--all") + .arg("--tags") + .arg("--prune") + .arg("--progress") + }, + Some(pb), + ) .await?; + let pb = ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Checkout, + name, + ); local_git .clone() - .spawn_with(move |c| c.arg("checkout").arg(tag_name_2).arg("--force")) + .spawn_with( + move |c| { + c.arg("checkout") + .arg(tag_name_2) + .arg("--force") + .arg("--progress") + }, + Some(pb), + ) .await?; } + let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); local_git .clone() - .spawn_with(move |c| { - c.arg("submodule") - .arg("update") - .arg("--init") - .arg("--recursive") - }) + .spawn_with( + move |c| { + c.arg("submodule") + .arg("update") + .arg("--init") + .arg("--recursive") + .arg("--progress") + }, + Some(pb), + ) .await?; } Ok(path) From 777c4e8d303341bfe75a6ca3cbcef1327975221b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 31 Dec 2025 13:08:04 +0100 Subject: [PATCH 37/79] sess: Fix compiler warnings --- src/sess.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sess.rs b/src/sess.rs index baa00155..5132e2f6 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -544,10 +544,8 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { &self.sess.config.git, self.sess.git_throttle.clone(), ); - let name2 = String::from(name); let url = String::from(url); let url2 = url.clone(); - let url3 = url.clone(); // Either initialize the repository or update it if needed. if !db_dir.join("config").exists() { From 4e00733c75f0e53c8353c481c9936f9c1dbaaf2b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 31 Dec 2025 14:25:41 +0100 Subject: [PATCH 38/79] progress: Get rid of the `Sub` prefix for submodules --- src/progress.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/progress.rs b/src/progress.rs index 7a848dd0..c3fad7e1 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -197,7 +197,7 @@ impl ProgressHandler { let sub_name = path.split('/').last().unwrap_or(&path); let sub_prefix = - format!(" {} {}", style("└─ Sub").dim(), style(sub_name).dim()); + format!(" {} {}", style("└─ ").dim(), style(sub_name).dim()); sub_pb.set_prefix(sub_prefix); state.sub_pb = Some(sub_pb); } From 1e092d983c794a14bace723f3323f4f366a8424d Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 31 Dec 2025 14:44:48 +0100 Subject: [PATCH 39/79] progress: Clean up a bit --- src/progress.rs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index c3fad7e1..d7765686 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -244,14 +244,7 @@ pub async fn monitor_stderr( let mut collected_stderr = String::new(); // Add a new progress bar and state if we have a handler - let mut state = match &handler { - Some(h) => h.start(), - None => ProgressState { - pb: ProgressBar::hidden(), - sub_pb: None, - main_done: false, - }, - }; + let mut state = handler.as_ref().map(|h| h.start()); loop { match reader.read_u8().await { @@ -266,7 +259,7 @@ pub async fn monitor_stderr( if let Ok(line) = std::str::from_utf8(&buffer) { // Update UI if we have a handler if let Some(h) = &handler { - h.handle_line(line, &mut state); + h.handle_line(line, &mut state.as_mut().unwrap()); } } buffer.clear(); @@ -279,10 +272,7 @@ pub async fn monitor_stderr( } } - // Cleanup any lingering sub-bars from this command - if let Some(sub) = state.sub_pb.take() { - sub.finish_and_clear(); - } + handler.map(|h| h.finish(&mut state.unwrap())); collected_stderr } From 3c6a6c3033c05ee9de9a4a1943521a39fa1e4ccf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 31 Dec 2025 16:36:42 +0100 Subject: [PATCH 40/79] sess: Wrap Progress handlers in `Some` --- src/sess.rs | 61 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/sess.rs b/src/sess.rs index 5132e2f6..01ef65eb 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -566,12 +566,16 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { git.clone() .spawn_with(|c| c.arg("remote").arg("add").arg("origin").arg(url), None) .await?; - let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Clone, + name, + )); git.clone() - .fetch("origin", Some(pb.clone())) + .fetch("origin", pb.clone()) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, Some(pb)).await + git.clone().fetch_ref("origin", reference, pb.clone()).await } else { Ok(()) } @@ -599,12 +603,16 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // TODO MICHAERO: May need throttle // stageln!("Fetching", "{} ({})", name2, url2); // TODO(fischeti): Is this actually the fetching stage? - let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Fetch, name); + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Fetch, + name, + )); git.clone() - .fetch("origin", Some(pb.clone())) + .fetch("origin", pb.clone()) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, Some(pb)).await + git.clone().fetch_ref("origin", reference, pb.clone()).await } else { Ok(()) } @@ -955,17 +963,14 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { "checkout_git: failed to tag commit {:?}, attempting fetch.", cause ); - let pb = ProgressHandler::new( + let pb = Some(ProgressHandler::new( self.sess.progress.clone(), GitProgressOps::Checkout, name, - ); + )); // Attempt to fetch from remote and retry, as commits seem unavailable. git.clone() - .spawn_with( - move |c| c.arg("fetch").arg("--all").arg("--progress"), - Some(pb), - ) + .spawn_with(move |c| c.arg("fetch").arg("--all").arg("--progress"), pb) .await?; git.clone() .spawn_with( @@ -993,8 +998,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } }?; if clear == CheckoutState::ToClone { - let pb = - ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Clone, + name, + )); git.clone() .spawn_with( move |c| { @@ -1004,12 +1012,15 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--branch") .arg(tag_name_2) }, - Some(pb), + pb, ) .await?; } else if clear == CheckoutState::ToCheckout { - let pb = - ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Fetch, name); + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Fetch, + name, + )); local_git .clone() .spawn_with( @@ -1020,14 +1031,14 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--prune") .arg("--progress") }, - Some(pb), + pb, ) .await?; - let pb = ProgressHandler::new( + let pb = Some(ProgressHandler::new( self.sess.progress.clone(), GitProgressOps::Checkout, name, - ); + )); local_git .clone() .spawn_with( @@ -1037,11 +1048,15 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--force") .arg("--progress") }, - Some(pb), + pb, ) .await?; } - let pb = ProgressHandler::new(self.sess.progress.clone(), GitProgressOps::Clone, name); + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Clone, + name, + )); local_git .clone() .spawn_with( @@ -1052,7 +1067,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--recursive") .arg("--progress") }, - Some(pb), + pb, ) .await?; } From 8ba15b8b5b1b47b56f94b10e905f74f1bbe10533 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 20:49:39 +0100 Subject: [PATCH 41/79] git: Don't report progress when fetching tags --- src/git.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/git.rs b/src/git.rs index 6425d8f3..e1113cd8 100644 --- a/src/git.rs +++ b/src/git.rs @@ -215,9 +215,8 @@ impl<'ctx> Git<'ctx> { .arg("--tags") .arg("--prune") .arg(r2) - .arg("--progress") }, - pb, + None, ) }) .await From 4a2c0482f10e78480bc6bde35f89c4f7d5d558f7 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 20:49:57 +0100 Subject: [PATCH 42/79] progress: Don't set length multiple times --- src/progress.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index d7765686..efe0fbe7 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -208,19 +208,16 @@ impl ProgressHandler { sub.finish_and_clear(); } } - GitProgress::Receiving { current, total, .. } => { + GitProgress::Receiving { current, .. } => { target_pb.set_message(style("Receiving objects").dim().to_string()); - target_pb.set_length(total as u64); target_pb.set_position(current as u64); } GitProgress::Resolving { percent, .. } => { target_pb.set_message(style("Resolving deltas").dim().to_string()); - target_pb.set_length(100); target_pb.set_position(percent as u64); } GitProgress::Checkout { percent, .. } => { target_pb.set_message(style("Checking out").dim().to_string()); - target_pb.set_length(100); target_pb.set_position(percent as u64); } _ => {} From 6daa218b19e16b4ce8e4c0ac91240ffe9b2b792a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 20:59:37 +0100 Subject: [PATCH 43/79] progress: Refactor --- src/progress.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index efe0fbe7..57e73e3f 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -182,7 +182,7 @@ impl ProgressHandler { } } - pub fn handle_line(&self, line: &str, state: &mut ProgressState) { + pub fn update_pb(&self, line: &str, state: &mut ProgressState) { let progress = parse_git_line(line); let target_pb = state.sub_pb.as_ref().unwrap_or(&state.pb); @@ -256,7 +256,7 @@ pub async fn monitor_stderr( if let Ok(line) = std::str::from_utf8(&buffer) { // Update UI if we have a handler if let Some(h) = &handler { - h.handle_line(line, &mut state.as_mut().unwrap()); + h.update_pb(line, &mut state.as_mut().unwrap()); } } buffer.clear(); From 8c43f57629ad0dab271e632fa11bf677d568d73f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 21:00:25 +0100 Subject: [PATCH 44/79] progress: Don't allow clones of Progresshandlers Clones of progress bars don't really work well after they were finished git: Don't clone progress handler --- src/git.rs | 2 +- src/progress.rs | 1 - src/sess.rs | 16 +++++----------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/git.rs b/src/git.rs index e1113cd8..5f456ba7 100644 --- a/src/git.rs +++ b/src/git.rs @@ -206,7 +206,7 @@ impl<'ctx> Git<'ctx> { self.clone() .spawn_with( |c| c.arg("fetch").arg("--prune").arg(r1).arg("--progress"), - pb.clone(), + pb, ) .and_then(|_| { self.spawn_with( diff --git a/src/progress.rs b/src/progress.rs index 57e73e3f..c20bb21f 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -125,7 +125,6 @@ pub struct ProgressState { } /// This struct captures (static) information neeed to handle progress updates for a git operation. -#[derive(Clone)] pub struct ProgressHandler { /// Reference to the multi-progress bar, which can manage multiple progress bars. mpb: MultiProgress, diff --git a/src/sess.rs b/src/sess.rs index 01ef65eb..7368d6f9 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -557,9 +557,6 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } // Initialize. self.sess.stats.num_database_init.increment(); - // TODO MICHAERO: May need throttle - // stageln!("Cloning", "{} ({})", name2, url2); - // TODO(fischeti): Is this actually the cloning stage? git.clone() .spawn_with(|c| c.arg("init").arg("--bare"), None) .await?; @@ -572,10 +569,10 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { name, )); git.clone() - .fetch("origin", pb.clone()) + .fetch("origin", pb) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, pb.clone()).await + git.clone().fetch_ref("origin", reference, None).await } else { Ok(()) } @@ -600,19 +597,16 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { return Ok(git); } self.sess.stats.num_database_fetch.increment(); - // TODO MICHAERO: May need throttle - // stageln!("Fetching", "{} ({})", name2, url2); - // TODO(fischeti): Is this actually the fetching stage? let pb = Some(ProgressHandler::new( self.sess.progress.clone(), GitProgressOps::Fetch, name, )); git.clone() - .fetch("origin", pb.clone()) + .fetch("origin", pb) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, pb.clone()).await + git.clone().fetch_ref("origin", reference, None).await } else { Ok(()) } @@ -934,7 +928,6 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // Perform the checkout if necessary. if clear != CheckoutState::Clean { - // stageln!("Checkout", "{} ({})", name, url); // First generate a tag to be cloned in the database. This is // necessary since `git clone` does not accept commits, but only @@ -1011,6 +1004,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg(path) .arg("--branch") .arg(tag_name_2) + .arg("--progress") }, pb, ) From 2648fd1c56f4145034b33952d2b5a6fbc1ed6f5e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 21:53:44 +0100 Subject: [PATCH 45/79] Format sources --- src/git.rs | 7 +------ src/progress.rs | 3 +-- src/sess.rs | 1 - 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/git.rs b/src/git.rs index 5f456ba7..d61f6bc9 100644 --- a/src/git.rs +++ b/src/git.rs @@ -210,12 +210,7 @@ impl<'ctx> Git<'ctx> { ) .and_then(|_| { self.spawn_with( - |c| { - c.arg("fetch") - .arg("--tags") - .arg("--prune") - .arg(r2) - }, + |c| c.arg("fetch").arg("--tags").arg("--prune").arg(r2), None, ) }) diff --git a/src/progress.rs b/src/progress.rs index c20bb21f..f10a6deb 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -195,8 +195,7 @@ impl ProgressHandler { sub_pb.set_style(self.style.clone()); let sub_name = path.split('/').last().unwrap_or(&path); - let sub_prefix = - format!(" {} {}", style("└─ ").dim(), style(sub_name).dim()); + let sub_prefix = format!(" {} {}", style("└─ ").dim(), style(sub_name).dim()); sub_pb.set_prefix(sub_prefix); state.sub_pb = Some(sub_pb); } diff --git a/src/sess.rs b/src/sess.rs index 7368d6f9..17a554f9 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -928,7 +928,6 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // Perform the checkout if necessary. if clear != CheckoutState::Clean { - // First generate a tag to be cloned in the database. This is // necessary since `git clone` does not accept commits, but only // branches or tags for shallow clones. From c8ff0826383f07c4d41cfb97aa243e9391f73134 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 22:35:25 +0100 Subject: [PATCH 46/79] error: Suspend progress bars while printing errors and warnings --- src/error.rs | 27 ++++++++++++++++++++++++++- src/sess.rs | 8 ++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index 0bda72dd..7cdc32a3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -7,9 +7,34 @@ use std; use std::fmt; use std::sync::atomic::AtomicBool; use std::sync::Arc; +use std::sync::{Arc, RwLock}; + +use indicatif::MultiProgress; pub static ENABLE_DEBUG: AtomicBool = AtomicBool::new(false); +/// A global hook for the progress bar +pub static GLOBAL_MULTI_PROGRESS: RwLock> = RwLock::new(None); + +/// Helper function to print diagnostics safely without messing up progress bars. +pub fn print_diagnostic(severity: Severity, msg: &str) { + let text = format!("{} {}", severity, msg); + + // Try to acquire read access to the global progress bar + if let Ok(guard) = GLOBAL_MULTI_PROGRESS.read() { + if let Some(mp) = &*guard { + // SUSPEND: Hides progress bars, prints the message, then redraws bars. + mp.suspend(|| { + eprintln!("{}", text); + }); + return; + } + } + + // Fallback: Just print if no bar is registered or lock is poisoned + eprintln!("{}", text); +} + /// Print an error. #[macro_export] macro_rules! errorln { @@ -45,7 +70,7 @@ macro_rules! debugln { /// Emit a diagnostic message. macro_rules! diagnostic { ($severity:expr; $($arg:tt)*) => { - eprintln!("{} {}", $severity, format!($($arg)*)) + $crate::error::print_diagnostic($severity, &format!($($arg)*)) } } diff --git a/src/sess.rs b/src/sess.rs index 17a554f9..2e3059c3 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -96,6 +96,14 @@ impl<'ctx> Session<'ctx> { force_fetch: bool, git_throttle: usize, ) -> Session<'ctx> { + + // Initialize the global multi-progress bar + // to handle warning and error messages correctly. + let mpb = MultiProgress::new(); + if let Ok(mut global_mpb) = GLOBAL_MULTI_PROGRESS.write() { + *global_mpb = Some(mpb.clone()); + } + Session { root, manifest, From 8293d3c967f9fb031a39f061153d3131fae5e92f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 1 Jan 2026 22:40:13 +0100 Subject: [PATCH 47/79] sess: Format sources --- src/sess.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sess.rs b/src/sess.rs index 2e3059c3..6bf87739 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -96,7 +96,6 @@ impl<'ctx> Session<'ctx> { force_fetch: bool, git_throttle: usize, ) -> Session<'ctx> { - // Initialize the global multi-progress bar // to handle warning and error messages correctly. let mpb = MultiProgress::new(); From b0483842d4567901e4ecbcc740f6292a8821ddbf Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 12:55:37 +0100 Subject: [PATCH 48/79] progress: Print completion message --- src/progress.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/progress.rs b/src/progress.rs index f10a6deb..6944cec6 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -227,6 +227,20 @@ impl ProgressHandler { sub.finish_and_clear(); } state.pb.finish_and_clear(); + + // Print a final message indicating completion + let op_str = match self.git_op { + GitProgressOps::Clone => "Cloned", + GitProgressOps::Fetch => "Fetched", + GitProgressOps::Checkout => "Checked out", + }; + self.mpb + .println(format!( + " {} {}", + style(op_str).green().bold(), + style(&self.name).bright() + )) + .unwrap(); } } From bda0008db3ef66a19e6eae89f5d03a9d29113f55 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 14:37:07 +0100 Subject: [PATCH 49/79] progress: Print duration of git operation --- src/progress.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/progress.rs b/src/progress.rs index 6944cec6..fdcb1da4 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -122,6 +122,8 @@ pub struct ProgressState { /// Whether the main progress bar is done. /// This is used to determine when to start submodule progress bars. main_done: bool, + /// The start time of the operation. + start_time: std::time::Instant, } /// This struct captures (static) information neeed to handle progress updates for a git operation. @@ -178,6 +180,7 @@ impl ProgressHandler { pb, sub_pb: None, main_done: false, + start_time: std::time::Instant::now(), } } @@ -234,11 +237,18 @@ impl ProgressHandler { GitProgressOps::Fetch => "Fetched", GitProgressOps::Checkout => "Checked out", }; + let duration = state.start_time.elapsed(); + let duration_str = if duration.as_secs() > 0 { + format!("in {}s", duration.as_secs()) + } else { + format!("in {}ms", duration.as_millis()) + }; self.mpb .println(format!( - " {} {}", + " {} {} {}", style(op_str).green().bold(), style(&self.name).bright() + style(duration_str).dim() )) .unwrap(); } From 84c61fef8f7f383e63e3313ee80578abc127d45f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 14:37:20 +0100 Subject: [PATCH 50/79] progress: Stylistic changes --- src/progress.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index fdcb1da4..4a3a17a9 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -170,7 +170,7 @@ impl ProgressHandler { let prefix = format!( "{} {}", console::style(prefix).bold().green(), - console::style(&self.name).bright() + console::style(&self.name).bold() ); pb.set_prefix(prefix); // Configure the spinners to automatically tick every 100ms @@ -192,10 +192,13 @@ impl ProgressHandler { GitProgress::CloningInto { path } => { if state.main_done { state.pb.set_position(100); - state.pb.set_message(style("Done.").dim().to_string()); + state + .pb + .finish_with_message(style("Submodules").dim().to_string()); let sub_pb = self.mpb.insert_after(&state.pb, ProgressBar::new(100)); - sub_pb.set_style(self.style.clone()); + // We don't want any spinner for submodules since the main one already has a spinner + sub_pb.set_style(self.style.clone().tick_strings(&[" ", " "])); let sub_name = path.split('/').last().unwrap_or(&path); let sub_prefix = format!(" {} {}", style("└─ ").dim(), style(sub_name).dim()); @@ -247,7 +250,7 @@ impl ProgressHandler { .println(format!( " {} {} {}", style(op_str).green().bold(), - style(&self.name).bright() + style(&self.name).bold(), style(duration_str).dim() )) .unwrap(); From 2c84fdf4333f946a12f7d7063d56399dd6f554cc Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 15:53:54 +0100 Subject: [PATCH 51/79] progress: Add `Submodule` operations --- src/progress.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/progress.rs b/src/progress.rs index 4a3a17a9..88eac9a4 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -43,6 +43,7 @@ pub enum GitProgressOps { Checkout, Clone, Fetch, + Submodule, } static RE_GIT: OnceLock = OnceLock::new(); @@ -166,6 +167,7 @@ impl ProgressHandler { GitProgressOps::Clone => "Cloning", GitProgressOps::Fetch => "Fetching", GitProgressOps::Checkout => "Checkout", + GitProgressOps::Submodule => "Update", }; let prefix = format!( "{} {}", @@ -239,6 +241,7 @@ impl ProgressHandler { GitProgressOps::Clone => "Cloned", GitProgressOps::Fetch => "Fetched", GitProgressOps::Checkout => "Checked out", + GitProgressOps::Submodule => "Updated", }; let duration = state.start_time.elapsed(); let duration_str = if duration.as_secs() > 0 { From b9593092369e195298c6cc554fc1f02267c491ae Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 15:54:12 +0100 Subject: [PATCH 52/79] sess: Remove progress bars for disk operations --- src/sess.rs | 60 ++++++++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/sess.rs b/src/sess.rs index 6bf87739..77963fc7 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -564,17 +564,19 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } // Initialize. self.sess.stats.num_database_init.increment(); + // The progress bar object for cloning. We only use it for the + // last fetch operation, which is the only network operation here. + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Clone, + name, + )); git.clone() .spawn_with(|c| c.arg("init").arg("--bare"), None) .await?; git.clone() .spawn_with(|c| c.arg("remote").arg("add").arg("origin").arg(url), None) .await?; - let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), - GitProgressOps::Clone, - name, - )); git.clone() .fetch("origin", pb) .and_then(|_| async { @@ -604,6 +606,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { return Ok(git); } self.sess.stats.num_database_fetch.increment(); + // The progress bar object for fetching. let pb = Some(ProgressHandler::new( self.sess.progress.clone(), GitProgressOps::Fetch, @@ -999,7 +1002,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { if clear == CheckoutState::ToClone { let pb = Some(ProgressHandler::new( self.sess.progress.clone(), - GitProgressOps::Clone, + GitProgressOps::Checkout, name, )); git.clone() @@ -1016,11 +1019,6 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { ) .await?; } else if clear == CheckoutState::ToCheckout { - let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), - GitProgressOps::Fetch, - name, - )); local_git .clone() .spawn_with( @@ -1031,7 +1029,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--prune") .arg("--progress") }, - pb, + None, ) .await?; let pb = Some(ProgressHandler::new( @@ -1052,24 +1050,26 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { ) .await?; } - let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), - GitProgressOps::Clone, - name, - )); - local_git - .clone() - .spawn_with( - move |c| { - c.arg("submodule") - .arg("update") - .arg("--init") - .arg("--recursive") - .arg("--progress") - }, - pb, - ) - .await?; + if path.join(".gitmodules").exists() { + let pb = Some(ProgressHandler::new( + self.sess.progress.clone(), + GitProgressOps::Submodule, + name, + )); + local_git + .clone() + .spawn_with( + move |c| { + c.arg("submodule") + .arg("update") + .arg("--init") + .arg("--recursive") + .arg("--progress") + }, + pb, + ) + .await?; + } } Ok(path) } From 18a2c700ae904ee820c62d40efe8fcf836b4157b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 16:10:43 +0100 Subject: [PATCH 53/79] progress: Improve time formatting --- src/progress.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 88eac9a4..753485ff 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -243,11 +243,11 @@ impl ProgressHandler { GitProgressOps::Checkout => "Checked out", GitProgressOps::Submodule => "Updated", }; - let duration = state.start_time.elapsed(); - let duration_str = if duration.as_secs() > 0 { - format!("in {}s", duration.as_secs()) - } else { - format!("in {}ms", duration.as_millis()) + + // Format the duration nicely based on its length + let duration_str = match state.start_time.elapsed().as_millis() { + ms if ms < 1000 => format!("in {}ms", ms), + ms => format!("in {:.1}s", ms as f64 / 1000.0), }; self.mpb .println(format!( From 1d1a5b93e75f81dcd88a576d56d577ab7f3562fd Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 16:19:50 +0100 Subject: [PATCH 54/79] progress: Mention submodules in completed message --- src/progress.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/progress.rs b/src/progress.rs index 753485ff..e7d86a09 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -244,6 +244,11 @@ impl ProgressHandler { GitProgressOps::Submodule => "Updated", }; + let op_suffix_str = match self.git_op { + GitProgressOps::Submodule => " submodules", + _ => "", + }; + // Format the duration nicely based on its length let duration_str = match state.start_time.elapsed().as_millis() { ms if ms < 1000 => format!("in {}ms", ms), @@ -251,9 +256,10 @@ impl ProgressHandler { }; self.mpb .println(format!( - " {} {} {}", + " {} {}{} {}", style(op_str).green().bold(), style(&self.name).bold(), + style(op_suffix_str).dim(), style(duration_str).dim() )) .unwrap(); From 5c09b3ae6a3ed7b520f5cff902e877d8af82ed91 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 16:28:23 +0100 Subject: [PATCH 55/79] progress: Improve display for submodules --- src/progress.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index e7d86a09..3779d260 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -144,7 +144,7 @@ impl ProgressHandler { pub fn new(mpb: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { // Set the style for progress bars let style = ProgressStyle::with_template( - "{spinner:.green} {prefix:<24!} {bar:40.cyan/blue} {percent:>3}% {msg}", + "{spinner:.green} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", ) .unwrap() .progress_chars("-- ") @@ -167,7 +167,7 @@ impl ProgressHandler { GitProgressOps::Clone => "Cloning", GitProgressOps::Fetch => "Fetching", GitProgressOps::Checkout => "Checkout", - GitProgressOps::Submodule => "Update", + GitProgressOps::Submodule => "Update Submodules", }; let prefix = format!( "{} {}", @@ -241,12 +241,7 @@ impl ProgressHandler { GitProgressOps::Clone => "Cloned", GitProgressOps::Fetch => "Fetched", GitProgressOps::Checkout => "Checked out", - GitProgressOps::Submodule => "Updated", - }; - - let op_suffix_str = match self.git_op { - GitProgressOps::Submodule => " submodules", - _ => "", + GitProgressOps::Submodule => "Updated Submodules", }; // Format the duration nicely based on its length @@ -256,10 +251,9 @@ impl ProgressHandler { }; self.mpb .println(format!( - " {} {}{} {}", + " {} {} {}", style(op_str).green().bold(), style(&self.name).bold(), - style(op_suffix_str).dim(), style(duration_str).dim() )) .unwrap(); From 0fd7016ff8c04e17a102138ae767cb38e435ba82 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 17:05:31 +0100 Subject: [PATCH 56/79] progress: Stylistic changes for submodules --- src/progress.rs | 65 ++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 3779d260..135f3b03 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -120,9 +120,6 @@ pub struct ProgressState { pb: ProgressBar, /// The progress bar for submodules, if any. sub_pb: Option, - /// Whether the main progress bar is done. - /// This is used to determine when to start submodule progress bars. - main_done: bool, /// The start time of the operation. start_time: std::time::Instant, } @@ -131,8 +128,6 @@ pub struct ProgressState { pub struct ProgressHandler { /// Reference to the multi-progress bar, which can manage multiple progress bars. mpb: MultiProgress, - /// The style used for progress bars. - style: ProgressStyle, /// The type of git operation being performed. git_op: GitProgressOps, /// The name of the repository being processed. @@ -142,26 +137,23 @@ pub struct ProgressHandler { impl ProgressHandler { /// Create a new progress handler for a git operation. pub fn new(mpb: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { - // Set the style for progress bars - let style = ProgressStyle::with_template( - "{spinner:.green} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", - ) - .unwrap() - .progress_chars("-- ") - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); - Self { mpb, git_op, name: name.to_string(), - style, } } pub fn start(&self) -> ProgressState { - // Add a new progress bar to the multi-progress (with a length of 100) - let pb = self.mpb.add(ProgressBar::new(100)); - pb.set_style(self.style.clone()); + // Create and configure the main progress bar + let pb_style = ProgressStyle::with_template( + "{spinner:.green} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", + ) + .unwrap() + .progress_chars("-- ") + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + + let pb = self.mpb.add(ProgressBar::new(100).with_style(pb_style)); let prefix = match self.git_op { GitProgressOps::Clone => "Cloning", @@ -181,7 +173,6 @@ impl ProgressHandler { ProgressState { pb, sub_pb: None, - main_done: false, start_time: std::time::Instant::now(), } } @@ -192,22 +183,36 @@ impl ProgressHandler { match progress { GitProgress::CloningInto { path } => { - if state.main_done { - state.pb.set_position(100); - state - .pb - .finish_with_message(style("Submodules").dim().to_string()); - - let sub_pb = self.mpb.insert_after(&state.pb, ProgressBar::new(100)); - // We don't want any spinner for submodules since the main one already has a spinner - sub_pb.set_style(self.style.clone().tick_strings(&[" ", " "])); - + // Only spawn a sub-bar if we are explicitly running the 'Submodule' op. + // For normal Clone/Checkout, 'Cloning into' is just the main repo header, which we ignore. + if self.git_op == GitProgressOps::Submodule { + if let Some(sub) = state.sub_pb.take() { + sub.finish_and_clear(); + } + // The main simply becomes a spinner since the sub-bar will show progress + // on the subsequent line. + state.pb.set_style( + ProgressStyle::with_template("{spinner:.green} {prefix:<32!}").unwrap(), + ); + + // The submodule style is similar to the main bar, but indented and without spinner + let sub_pb_style = ProgressStyle::with_template( + " {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", + ) + .unwrap() + .progress_chars("-- "); + + // Create the submodule progress bar below the main one + let sub_pb = self + .mpb + .insert_after(&state.pb, ProgressBar::new(100).with_style(sub_pb_style)); + + // Set the submodule prefix to the submodule name let sub_name = path.split('/').last().unwrap_or(&path); - let sub_prefix = format!(" {} {}", style("└─ ").dim(), style(sub_name).dim()); + let sub_prefix = format!("{} {}", style("└─ ").dim(), style(sub_name).dim()); sub_pb.set_prefix(sub_prefix); state.sub_pb = Some(sub_pb); } - state.main_done = true; } GitProgress::SubmoduleEnd { .. } => { if let Some(sub) = state.sub_pb.take() { From d2b6b7e87e27a730d0377bddbaebd3cc35328c26 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 17:53:32 +0100 Subject: [PATCH 57/79] error: Align styling with progress bar --- src/error.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/error.rs b/src/error.rs index 7cdc32a3..fa751738 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::sync::{Arc, RwLock}; +use console::style; use indicatif::MultiProgress; pub static ENABLE_DEBUG: AtomicBool = AtomicBool::new(false); @@ -85,13 +86,13 @@ pub enum Severity { impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let (color, prefix) = match *self { - Severity::Error => ("\x1B[31;1m", "error"), - Severity::Warning => ("\x1B[33;1m", "warning"), - Severity::Note => ("\x1B[;1m", "note"), - Severity::Debug => ("\x1B[34;1m", "debug"), + let styled_str = match *self { + Severity::Error => style("Error:").red().bold(), + Severity::Warning => style("Warning:").yellow().bold(), + Severity::Note => style("Note:").white().bold(), + Severity::Debug => style("Debug:").blue().bold(), }; - write!(f, "{}{}:\x1B[m", color, prefix) + write!(f, " {}", styled_str) } } @@ -174,5 +175,9 @@ macro_rules! stageln { /// Print stage progress. pub fn println_stage(stage: &str, message: &str) { - eprintln!("\x1B[32;1m{:>12}\x1B[0m {}", stage, message); + eprintln!( + " {} {}", + style(format!("{:>12}", stage)).green().bold(), + message + ); } From df1bc3a1c93b62b94f69fc1703de0062ac5b8901 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 18:08:15 +0100 Subject: [PATCH 58/79] error: Add formatting macros --- src/error.rs | 24 ++++++++++++++++++++++++ src/progress.rs | 29 ++++++++++++----------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/error.rs b/src/error.rs index fa751738..45912ecc 100644 --- a/src/error.rs +++ b/src/error.rs @@ -84,6 +84,30 @@ pub enum Severity { Error, } +/// Style a message in green bold. +#[macro_export] +macro_rules! green_bold { + ($arg:expr) => { + console::style($arg).green().bold() + }; +} + +/// Style a message in dimmed text. +#[macro_export] +macro_rules! dim { + ($arg:expr) => { + console::style($arg).dim() + }; +} + +/// Style a message in bold text. +#[macro_export] +macro_rules! bold { + ($arg:expr) => { + console::style($arg).bold() + }; +} + impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let styled_str = match *self { diff --git a/src/progress.rs b/src/progress.rs index 135f3b03..40a53510 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -4,7 +4,6 @@ use std::sync::OnceLock; use std::time::Duration; -use console::style; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use regex::Regex; use tokio::io::{AsyncReadExt, BufReader}; @@ -146,14 +145,14 @@ impl ProgressHandler { pub fn start(&self) -> ProgressState { // Create and configure the main progress bar - let pb_style = ProgressStyle::with_template( + let style = ProgressStyle::with_template( "{spinner:.green} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", ) .unwrap() .progress_chars("-- ") .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); - let pb = self.mpb.add(ProgressBar::new(100).with_style(pb_style)); + let pb = self.mpb.add(ProgressBar::new(100).with_style(style)); let prefix = match self.git_op { GitProgressOps::Clone => "Cloning", @@ -161,11 +160,7 @@ impl ProgressHandler { GitProgressOps::Checkout => "Checkout", GitProgressOps::Submodule => "Update Submodules", }; - let prefix = format!( - "{} {}", - console::style(prefix).bold().green(), - console::style(&self.name).bold() - ); + let prefix = format!("{} {}", green_bold!(prefix), bold!(&self.name)); pb.set_prefix(prefix); // Configure the spinners to automatically tick every 100ms pb.enable_steady_tick(Duration::from_millis(100)); @@ -196,7 +191,7 @@ impl ProgressHandler { ); // The submodule style is similar to the main bar, but indented and without spinner - let sub_pb_style = ProgressStyle::with_template( + let style = ProgressStyle::with_template( " {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", ) .unwrap() @@ -205,11 +200,11 @@ impl ProgressHandler { // Create the submodule progress bar below the main one let sub_pb = self .mpb - .insert_after(&state.pb, ProgressBar::new(100).with_style(sub_pb_style)); + .insert_after(&state.pb, ProgressBar::new(100).with_style(style)); // Set the submodule prefix to the submodule name let sub_name = path.split('/').last().unwrap_or(&path); - let sub_prefix = format!("{} {}", style("└─ ").dim(), style(sub_name).dim()); + let sub_prefix = format!("{} {}", dim!("└─ "), dim!(sub_name)); sub_pb.set_prefix(sub_prefix); state.sub_pb = Some(sub_pb); } @@ -220,15 +215,15 @@ impl ProgressHandler { } } GitProgress::Receiving { current, .. } => { - target_pb.set_message(style("Receiving objects").dim().to_string()); + target_pb.set_message(dim!("Receiving objects").to_string()); target_pb.set_position(current as u64); } GitProgress::Resolving { percent, .. } => { - target_pb.set_message(style("Resolving deltas").dim().to_string()); + target_pb.set_message(dim!("Resolving deltas").to_string()); target_pb.set_position(percent as u64); } GitProgress::Checkout { percent, .. } => { - target_pb.set_message(style("Checking out").dim().to_string()); + target_pb.set_message(dim!("Checking out").to_string()); target_pb.set_position(percent as u64); } _ => {} @@ -257,9 +252,9 @@ impl ProgressHandler { self.mpb .println(format!( " {} {} {}", - style(op_str).green().bold(), - style(&self.name).bold(), - style(duration_str).dim() + green_bold!(op_str), + bold!(&self.name), + dim!(duration_str) )) .unwrap(); } From e88547dabef914519e5398bc4659bb6549ef2c1c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 18:22:40 +0100 Subject: [PATCH 59/79] error: Align `stageln` to other macros --- src/cmd/vendor.rs | 1 - src/error.rs | 25 ++++++++----------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index a47d66bc..b20b99c0 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -102,7 +102,6 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { DependencySource::Git(ref url) => { let git = Git::new(tmp_path, &sess.config.git, sess.git_throttle.clone()); rt.block_on(async { - // stageln!("Cloning", "{} ({})", vendor_package.name, url); let pb = ProgressHandler::new( sess.progress.clone(), GitProgressOps::Clone, diff --git a/src/error.rs b/src/error.rs index 45912ecc..e51cbd49 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,12 @@ macro_rules! debugln { } } +/// Format and print stage progress. +#[macro_export] +macro_rules! stageln { + ($stage_name:expr, $($arg:tt)*) => { diagnostic!($crate::error::Severity::Stage($stage_name); $($arg)*); } +} + /// Print debug information. Omitted in release builds. #[macro_export] #[cfg(not(debug_assertions))] @@ -82,6 +88,7 @@ pub enum Severity { Note, Warning, Error, + Stage(&'static str), } /// Style a message in green bold. @@ -115,6 +122,7 @@ impl fmt::Display for Severity { Severity::Warning => style("Warning:").yellow().bold(), Severity::Note => style("Note:").white().bold(), Severity::Debug => style("Debug:").blue().bold(), + Severity::Stage(name) => style(name).green().bold(), }; write!(f, " {}", styled_str) } @@ -188,20 +196,3 @@ impl From for Error { Error::chain("Cannot startup runtime.".to_string(), err) } } - -/// Format and print stage progress. -#[macro_export] -macro_rules! stageln { - ($stage:expr, $($arg:tt)*) => { - $crate::error::println_stage($stage, &format!($($arg)*)) - } -} - -/// Print stage progress. -pub fn println_stage(stage: &str, message: &str) { - eprintln!( - " {} {}", - style(format!("{:>12}", stage)).green().bold(), - message - ); -} From 79bae9532633f7a98fea17602d4f248ba41a7137 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 18:29:19 +0100 Subject: [PATCH 60/79] error: Rename `Note` to `Info` --- src/error.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index e51cbd49..593bde8e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -44,8 +44,8 @@ macro_rules! errorln { /// Print an informational note. #[macro_export] -macro_rules! noteln { - ($($arg:tt)*) => { diagnostic!($crate::error::Severity::Note; $($arg)*); } +macro_rules! infoln { + ($($arg:tt)*) => { diagnostic!($crate::error::Severity::Info; $($arg)*); } } /// Print debug information. Omitted in release builds. @@ -85,7 +85,7 @@ macro_rules! diagnostic { #[derive(PartialEq, Eq)] pub enum Severity { Debug, - Note, + Info, Warning, Error, Stage(&'static str), @@ -120,7 +120,7 @@ impl fmt::Display for Severity { let styled_str = match *self { Severity::Error => style("Error:").red().bold(), Severity::Warning => style("Warning:").yellow().bold(), - Severity::Note => style("Note:").white().bold(), + Severity::Info => style("Info:").white().bold(), Severity::Debug => style("Debug:").blue().bold(), Severity::Stage(name) => style(name).green().bold(), }; From 22f20883e170195d2391ac4ffd2003e44dbf7d1c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 18:39:32 +0100 Subject: [PATCH 61/79] util: Move formatting duration to `util` --- src/progress.rs | 9 +++------ src/util.rs | 6 ++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 40a53510..efd28ecc 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -1,6 +1,8 @@ // Copyright (c) 2025 ETH Zurich // Tim Fischer +use crate::util::fmt_duration; + use std::sync::OnceLock; use std::time::Duration; @@ -244,17 +246,12 @@ impl ProgressHandler { GitProgressOps::Submodule => "Updated Submodules", }; - // Format the duration nicely based on its length - let duration_str = match state.start_time.elapsed().as_millis() { - ms if ms < 1000 => format!("in {}ms", ms), - ms => format!("in {:.1}s", ms as f64 / 1000.0), - }; self.mpb .println(format!( " {} {} {}", green_bold!(op_str), bold!(&self.name), - dim!(duration_str) + dim!(fmt_duration(state.start_time.elapsed())) )) .unwrap(); } diff --git a/src/util.rs b/src/util.rs index df840984..c8292a53 100644 --- a/src/util.rs +++ b/src/util.rs @@ -428,6 +428,12 @@ pub fn version_req_bottom_bound(req: &VersionReq) -> Result> { Ok(Some(bottom_bound)) } else { Ok(None) +/// Format time duration with proper units. +pub fn fmt_duration(duration: std::time::Duration) -> String { + match duration.as_millis() { + t if t < 1000 => format!("in {}ms", t), + t if t < 60_000 => format!("in {:.1}s", t as f64 / 1000.0), + t => format!("in {:.1}min", t as f64 / 60000.0), } } From 1352dbb03dbc0552862662948d18b14c50694e40 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 18:40:03 +0100 Subject: [PATCH 62/79] checkout: Print out total elapsed time for checkout --- src/cmd/checkout.rs | 11 ++++++++++- src/sess.rs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cmd/checkout.rs b/src/cmd/checkout.rs index 3fa7a144..c176da6d 100644 --- a/src/cmd/checkout.rs +++ b/src/cmd/checkout.rs @@ -8,6 +8,7 @@ use tokio::runtime::Runtime; use crate::error::*; use crate::sess::{Session, SessionIo}; +use crate::util::fmt_duration; /// Checkout all dependencies referenced in the Lock file #[derive(Args, Debug)] @@ -26,7 +27,15 @@ pub fn run(sess: &Session, args: &CheckoutArgs) -> Result<()> { pub fn run_plain(sess: &Session, force: bool, update_list: &[String]) -> Result<()> { let rt = Runtime::new()?; let io = SessionIo::new(sess); - let _srcs = rt.block_on(io.sources(force, update_list))?; + let start_time = std::time::Instant::now(); + let _srcs = rt.block_on(io.sources(forcibly, update_list))?; + let num_dependencies = io.sess.names.lock().unwrap().len(); + infoln!( + "{} {} dependencies {}", + dim!("Checked out"), + num_dependencies, + dim!(fmt_duration(start_time.elapsed())) + ); Ok(()) } diff --git a/src/sess.rs b/src/sess.rs index 77963fc7..c875afaa 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -65,7 +65,7 @@ pub struct Session<'ctx> { /// The internalized strings. strings: Mutex>, /// The package name table. - names: Mutex>, + pub names: Mutex>, /// The dependency graph. graph: Mutex>>>, /// The topologically sorted list of packages. From 96efd58edceb3ee12e7207ba55fc7a52f42dde27 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 20:22:43 +0100 Subject: [PATCH 63/79] progress: Print out git error messages --- src/error.rs | 8 ++++++++ src/progress.rs | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/error.rs b/src/error.rs index 593bde8e..e37532f9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -99,6 +99,14 @@ macro_rules! green_bold { }; } +/// Style a message in green bold. +#[macro_export] +macro_rules! red_bold { + ($arg:expr) => { + console::style($arg).red().bold() + }; +} + /// Style a message in dimmed text. #[macro_export] macro_rules! dim { diff --git a/src/progress.rs b/src/progress.rs index efd28ecc..0f96510b 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -35,6 +35,7 @@ pub enum GitProgress { current: usize, total: usize, }, + Error(String), Other, } @@ -57,8 +58,8 @@ pub fn parse_git_line(line: &str) -> GitProgress { (?: Cloning\ into\ '(?P[^']+)'\.\.\. | Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | - (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% - (?: \s+ \( (?P\d+) / (?P\d+) \) )? + (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% | + (?Pfatal:.*|error:.*|remote:\ aborting.*) # <--- Capture errors ) ").expect("Invalid Regex") }); @@ -109,6 +110,9 @@ pub fn parse_git_line(line: &str) -> GitProgress { _ => GitProgress::Other, }; } + if let Some(err) = caps.name("error") { + return GitProgress::Error(err.as_str().to_string()); + } } // Otherwise, we don't care GitProgress::Other @@ -228,6 +232,16 @@ impl ProgressHandler { target_pb.set_message(dim!("Checking out").to_string()); target_pb.set_position(percent as u64); } + GitProgress::Error(err_msg) => { + target_pb.finish_and_clear(); + // TODO(fischeti): Consider enumerating error + errorln!( + "{} {}: {}", + "Error during git operation of", + bold!(&self.name), + err_msg + ); + } _ => {} } } From e22b228994abff7557c86dfbb4c7593c8cabdba6 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 21:24:03 +0100 Subject: [PATCH 64/79] progress: Show all submodules as tree structure --- src/progress.rs | 185 +++++++++++++++++++++++++++--------------------- 1 file changed, 104 insertions(+), 81 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 0f96510b..eb20f978 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -3,6 +3,7 @@ use crate::util::fmt_duration; +use indexmap::IndexMap; use std::sync::OnceLock; use std::time::Duration; @@ -14,27 +15,12 @@ use tokio::io::{AsyncReadExt, BufReader}; /// (Put your `GitProgress` enum and `parse_git_line` function here) #[derive(Debug, PartialEq, Clone)] pub enum GitProgress { - CloningInto { - path: String, - }, - SubmoduleEnd { - path: String, - }, - Receiving { - percent: u8, - current: usize, - total: usize, - }, - Resolving { - percent: u8, - current: usize, - total: usize, - }, - Checkout { - percent: u8, - current: usize, - total: usize, - }, + SubmoduleRegistered { name: String }, + CloningInto { name: String }, + SubmoduleEnd { name: String }, + Receiving { percent: u8 }, + Resolving { percent: u8 }, + Checkout { percent: u8 }, Error(String), Other, } @@ -50,69 +36,67 @@ pub enum GitProgressOps { static RE_GIT: OnceLock = OnceLock::new(); +/// Helper to extract the name from a git path. +fn path_to_name(path: &str) -> String { + path.trim_end_matches('/') + .split('/') + .last() + .unwrap_or(path) + .to_string() +} + pub fn parse_git_line(line: &str) -> GitProgress { let line = line.trim(); let re = RE_GIT.get_or_init(|| { Regex::new(r"(?x) ^ # Start (?: + # 1. Registration: Capture the path, ignore the descriptive name + Submodule\ '[^']+'\ .*\ registered\ for\ path\ '(?P[^']+)' | + + # 2. Cloning: Capture the path Cloning\ into\ '(?P[^']+)'\.\.\. | - Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | + + # 3. Completion: Capture the name + Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | + + # 4. Progress (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% | - (?Pfatal:.*|error:.*|remote:\ aborting.*) # <--- Capture errors + + # 5. Errors + (?Pfatal:.*|error:.*|remote:\ aborting.*) ) ").expect("Invalid Regex") }); if let Some(caps) = re.captures(line) { - // Case 1: Cloning into... + if let Some(path) = caps.name("reg_path") { + return GitProgress::SubmoduleRegistered { + name: path_to_name(path.as_str()), + }; + } if let Some(path) = caps.name("clone_path") { return GitProgress::CloningInto { - path: path.as_str().to_string(), + name: path_to_name(path.as_str()), }; } - - // Case 2: Submodule finished - if let Some(path) = caps.name("sub_end_path") { + if let Some(path) = caps.name("sub_end_name") { return GitProgress::SubmoduleEnd { - path: path.as_str().to_string(), + name: path_to_name(path.as_str()), }; } - - // Case 3: Progress + if let Some(err) = caps.name("error") { + return GitProgress::Error(err.as_str().to_string()); + } if let Some(phase) = caps.name("phase") { let percent = caps.name("percent").unwrap().as_str().parse().unwrap_or(0); - let current = caps - .name("current") - .map(|m| m.as_str().parse().unwrap_or(0)) - .unwrap_or(0); - let total = caps - .name("total") - .map(|m| m.as_str().parse().unwrap_or(0)) - .unwrap_or(0); - return match phase.as_str() { - "Receiving objects" => GitProgress::Receiving { - percent, - current, - total, - }, - "Resolving deltas" => GitProgress::Resolving { - percent, - current, - total, - }, - "Checking out files" => GitProgress::Checkout { - percent, - current, - total, - }, + "Receiving objects" => GitProgress::Receiving { percent }, + "Resolving deltas" => GitProgress::Resolving { percent }, + "Checking out files" => GitProgress::Checkout { percent }, _ => GitProgress::Other, }; } - if let Some(err) = caps.name("error") { - return GitProgress::Error(err.as_str().to_string()); - } } // Otherwise, we don't care GitProgress::Other @@ -123,8 +107,10 @@ pub fn parse_git_line(line: &str) -> GitProgress { pub struct ProgressState { /// The progress bar of the current package. pb: ProgressBar, - /// The progress bar for submodules, if any. - sub_pb: Option, + /// The sub-progress bar (for submodules), if any. + pub sub_bars: IndexMap, + // The currently active submodule, if any. + pub active_sub: Option, /// The start time of the operation. start_time: std::time::Instant, } @@ -173,23 +159,25 @@ impl ProgressHandler { ProgressState { pb, - sub_pb: None, + sub_bars: IndexMap::new(), + active_sub: None, start_time: std::time::Instant::now(), } } pub fn update_pb(&self, line: &str, state: &mut ProgressState) { let progress = parse_git_line(line); - let target_pb = state.sub_pb.as_ref().unwrap_or(&state.pb); + + // Target the active submodule if one exists, otherwise the main bar + let target_pb = if let Some(name) = &state.active_sub { + state.sub_bars.get(name).unwrap_or(&state.pb) + } else { + &state.pb + }; match progress { - GitProgress::CloningInto { path } => { - // Only spawn a sub-bar if we are explicitly running the 'Submodule' op. - // For normal Clone/Checkout, 'Cloning into' is just the main repo header, which we ignore. + GitProgress::SubmoduleRegistered { name } => { if self.git_op == GitProgressOps::Submodule { - if let Some(sub) = state.sub_pb.take() { - sub.finish_and_clear(); - } // The main simply becomes a spinner since the sub-bar will show progress // on the subsequent line. state.pb.set_style( @@ -203,26 +191,60 @@ impl ProgressHandler { .unwrap() .progress_chars("-- "); - // Create the submodule progress bar below the main one + // Tree Logic + let ref_bar = match state.sub_bars.last() { + Some((last_name, last_pb)) => { + // Update the previous last bar to have a "T" connector (├─) + // because it is no longer the last one. + let prev_prefix = format!("{} {}", dim!("├─ "), dim!(last_name)); + last_pb.set_prefix(prev_prefix); + last_pb // Insert the new one after this one + } + None => &state.pb, // Insert the first one after the main bar + }; + + // Create bar immediately let sub_pb = self .mpb - .insert_after(&state.pb, ProgressBar::new(100).with_style(style)); + .insert_after(ref_bar, ProgressBar::new(100).with_style(style)); - // Set the submodule prefix to the submodule name - let sub_name = path.split('/').last().unwrap_or(&path); - let sub_prefix = format!("{} {}", dim!("└─ "), dim!(sub_name)); + let sub_prefix = format!("{} {}", dim!("└─ "), dim!(&name)); sub_pb.set_prefix(sub_prefix); - state.sub_pb = Some(sub_pb); + sub_pb.set_message(dim!("Waiting...").to_string()); + + state.sub_bars.insert(name, sub_pb); + } + } + GitProgress::CloningInto { name } => { + if self.git_op == GitProgressOps::Submodule { + // Logic to handle missing 'checked out' lines: + // If we are activating 'bar', but 'foo' was active, assume 'foo' is done. + if let Some(prev) = &state.active_sub { + if prev != &name { + if let Some(b) = state.sub_bars.get(prev) { + b.finish_and_clear(); + } + } + } + // Activate the new bar + if let Some(bar) = state.sub_bars.get(&name) { + // Switch style to the active progress bar style + bar.set_message(dim!("Cloning...").to_string()); + } + state.active_sub = Some(name); } } - GitProgress::SubmoduleEnd { .. } => { - if let Some(sub) = state.sub_pb.take() { - sub.finish_and_clear(); + GitProgress::SubmoduleEnd { name } => { + if let Some(bar) = state.sub_bars.get(&name) { + bar.finish_and_clear(); + } + if state.active_sub.as_ref() == Some(&name) { + state.active_sub = None; } } - GitProgress::Receiving { current, .. } => { + GitProgress::Receiving { percent, .. } => { target_pb.set_message(dim!("Receiving objects").to_string()); - target_pb.set_position(current as u64); + target_pb.set_position(percent as u64); } GitProgress::Resolving { percent, .. } => { target_pb.set_message(dim!("Resolving deltas").to_string()); @@ -247,8 +269,9 @@ impl ProgressHandler { } pub fn finish(self, state: &mut ProgressState) { - if let Some(sub) = state.sub_pb.take() { - sub.finish_and_clear(); + // Clear all sub bars that might be lingering + for pb in state.sub_bars.values() { + pb.finish_and_clear(); } state.pb.finish_and_clear(); From 131afca271b9f287f073bfb8a7d8957cefc292cc Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 21:35:03 +0100 Subject: [PATCH 65/79] progress: Add more unit tests --- src/progress.rs | 62 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/src/progress.rs b/src/progress.rs index eb20f978..07deecc3 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -341,7 +341,7 @@ mod tests { use super::*; #[test] - fn test_parsing_logic() { + fn test_parsing_receiving() { // Copy your existing unit tests here let p = parse_git_line("Receiving objects: 34% (123/456)"); match p { @@ -349,4 +349,64 @@ mod tests { _ => panic!("Failed to parse receiving"), } } + #[test] + fn test_parsing_receiving_done() { + // Copy your existing unit tests here + let p = + parse_git_line("Receiving objects: 100% (1955/1955), 1.51 MiB | 45.53 MiB/s, done."); + match p { + GitProgress::Receiving { percent, .. } => assert_eq!(percent, 100), + _ => panic!("Failed to parse receiving"), + } + } + #[test] + fn test_parsing_resolving() { + // Copy your existing unit tests here + let p = parse_git_line("Resolving deltas: 56% (789/1400)"); + match p { + GitProgress::Resolving { percent, .. } => assert_eq!(percent, 56), + _ => panic!("Failed to parse receiving"), + } + } + #[test] + fn test_parsing_resolving_deltas_done() { + // Copy your existing unit tests here + let p = parse_git_line("Resolving deltas: 100% (1122/1122), done."); + match p { + GitProgress::Resolving { percent, .. } => assert_eq!(percent, 100), + _ => panic!("Failed to parse receiving"), + } + } + #[test] + fn test_parsing_cloning_into() { + let p = parse_git_line("Cloning into 'myrepo'..."); + match p { + GitProgress::CloningInto { name } => assert_eq!(name, "myrepo"), + _ => panic!("Failed to parse cloning into"), + } + } + #[test] + fn test_parsing_submodule_registered() { + let p = parse_git_line("Submodule 'libs/mylib' ... registered for path 'libs/mylib'"); + match p { + GitProgress::SubmoduleRegistered { name } => assert_eq!(name, "mylib"), + _ => panic!("Failed to parse submodule registered"), + } + } + #[test] + fn test_parsing_submodule_end() { + let p = parse_git_line("Submodule path 'libs/mylib': checked out 'abc1234'"); + match p { + GitProgress::SubmoduleEnd { name } => assert_eq!(name, "mylib"), + _ => panic!("Failed to parse submodule end"), + } + } + #[test] + fn test_parsing_error() { + let p = parse_git_line("fatal: unable to access 'https://example.com/repo.git/': Could not resolve host: example.com"); + match p { + GitProgress::Error(msg) => assert!(msg.contains("fatal: unable to access")), + _ => panic!("Failed to parse error"), + } + } } From 371bc19b56700c0ad5ef5e968438e0ce4205aec7 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 21:42:55 +0100 Subject: [PATCH 66/79] progress: Prefer `format` over `to_string()` --- src/progress.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 07deecc3..2bc8279f 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -210,7 +210,7 @@ impl ProgressHandler { let sub_prefix = format!("{} {}", dim!("└─ "), dim!(&name)); sub_pb.set_prefix(sub_prefix); - sub_pb.set_message(dim!("Waiting...").to_string()); + sub_pb.set_message(format!("{}", dim!("Waiting..."))); state.sub_bars.insert(name, sub_pb); } @@ -229,7 +229,7 @@ impl ProgressHandler { // Activate the new bar if let Some(bar) = state.sub_bars.get(&name) { // Switch style to the active progress bar style - bar.set_message(dim!("Cloning...").to_string()); + bar.set_message(format!("{}", dim!("Cloning..."))); } state.active_sub = Some(name); } @@ -243,15 +243,15 @@ impl ProgressHandler { } } GitProgress::Receiving { percent, .. } => { - target_pb.set_message(dim!("Receiving objects").to_string()); + target_pb.set_message(format!("{}", dim!("Receiving objects"))); target_pb.set_position(percent as u64); } GitProgress::Resolving { percent, .. } => { - target_pb.set_message(dim!("Resolving deltas").to_string()); + target_pb.set_message(format!("{}", dim!("Resolving deltas"))); target_pb.set_position(percent as u64); } GitProgress::Checkout { percent, .. } => { - target_pb.set_message(dim!("Checking out").to_string()); + target_pb.set_message(format!("{}", dim!("Checking out"))); target_pb.set_position(percent as u64); } GitProgress::Error(err_msg) => { From 773264c82c2a8230aa0a085c8740cb9d28a69266 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 21:55:58 +0100 Subject: [PATCH 67/79] progress: Don't check for ASCII characters --- src/progress.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 2bc8279f..acc4146e 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -300,7 +300,7 @@ pub async fn monitor_stderr( ) -> String { let mut reader = BufReader::new(stream); let mut buffer = Vec::new(); - let mut collected_stderr = String::new(); + let mut raw_log = Vec::new(); // Add a new progress bar and state if we have a handler let mut state = handler.as_ref().map(|h| h.start()); @@ -308,10 +308,7 @@ pub async fn monitor_stderr( loop { match reader.read_u8().await { Ok(byte) => { - // Collect raw error output (simplified for brevity) - if byte.is_ascii() { - collected_stderr.push(byte as char); - } + raw_log.push(byte); if byte == b'\r' || byte == b'\n' { if !buffer.is_empty() { @@ -333,7 +330,7 @@ pub async fn monitor_stderr( handler.map(|h| h.finish(&mut state.unwrap())); - collected_stderr + String::from_utf8_lossy(&raw_log).to_string() } #[cfg(test)] From f003b8cb2dd72c2c834bcda2fb931460ba698f63 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 22:00:04 +0100 Subject: [PATCH 68/79] progress: Change the order of functions, enums and impl blocks --- src/progress.rs | 216 ++++++++++++++++++++++++------------------------ 1 file changed, 108 insertions(+), 108 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index acc4146e..346e787d 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -11,6 +11,8 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use regex::Regex; use tokio::io::{AsyncReadExt, BufReader}; +static RE_GIT: OnceLock = OnceLock::new(); + /// Parses a line of git output. /// (Put your `GitProgress` enum and `parse_git_line` function here) #[derive(Debug, PartialEq, Clone)] @@ -25,83 +27,6 @@ pub enum GitProgress { Other, } -/// The git operation types that currently support progress reporting. -#[derive(Debug, PartialEq, Clone)] -pub enum GitProgressOps { - Checkout, - Clone, - Fetch, - Submodule, -} - -static RE_GIT: OnceLock = OnceLock::new(); - -/// Helper to extract the name from a git path. -fn path_to_name(path: &str) -> String { - path.trim_end_matches('/') - .split('/') - .last() - .unwrap_or(path) - .to_string() -} - -pub fn parse_git_line(line: &str) -> GitProgress { - let line = line.trim(); - let re = RE_GIT.get_or_init(|| { - Regex::new(r"(?x) - ^ # Start - (?: - # 1. Registration: Capture the path, ignore the descriptive name - Submodule\ '[^']+'\ .*\ registered\ for\ path\ '(?P[^']+)' | - - # 2. Cloning: Capture the path - Cloning\ into\ '(?P[^']+)'\.\.\. | - - # 3. Completion: Capture the name - Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | - - # 4. Progress - (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% | - - # 5. Errors - (?Pfatal:.*|error:.*|remote:\ aborting.*) - ) - ").expect("Invalid Regex") - }); - - if let Some(caps) = re.captures(line) { - if let Some(path) = caps.name("reg_path") { - return GitProgress::SubmoduleRegistered { - name: path_to_name(path.as_str()), - }; - } - if let Some(path) = caps.name("clone_path") { - return GitProgress::CloningInto { - name: path_to_name(path.as_str()), - }; - } - if let Some(path) = caps.name("sub_end_name") { - return GitProgress::SubmoduleEnd { - name: path_to_name(path.as_str()), - }; - } - if let Some(err) = caps.name("error") { - return GitProgress::Error(err.as_str().to_string()); - } - if let Some(phase) = caps.name("phase") { - let percent = caps.name("percent").unwrap().as_str().parse().unwrap_or(0); - return match phase.as_str() { - "Receiving objects" => GitProgress::Receiving { percent }, - "Resolving deltas" => GitProgress::Resolving { percent }, - "Checking out files" => GitProgress::Checkout { percent }, - _ => GitProgress::Other, - }; - } - } - // Otherwise, we don't care - GitProgress::Other -} - /// This struct captures (dynamic) state information for a git operation's progress. /// for instance, the actuall progress bars to update. pub struct ProgressState { @@ -125,6 +50,54 @@ pub struct ProgressHandler { name: String, } +/// The git operation types that currently support progress reporting. +#[derive(PartialEq)] +pub enum GitProgressOps { + Checkout, + Clone, + Fetch, + Submodule, +} + +pub async fn monitor_stderr( + stream: impl tokio::io::AsyncRead + Unpin, + handler: Option, +) -> String { + let mut reader = BufReader::new(stream); + let mut buffer = Vec::new(); + let mut raw_log = Vec::new(); + + // Add a new progress bar and state if we have a handler + let mut state = handler.as_ref().map(|h| h.start()); + + loop { + match reader.read_u8().await { + Ok(byte) => { + raw_log.push(byte); + + if byte == b'\r' || byte == b'\n' { + if !buffer.is_empty() { + if let Ok(line) = std::str::from_utf8(&buffer) { + // Update UI if we have a handler + if let Some(h) = &handler { + h.update_pb(line, &mut state.as_mut().unwrap()); + } + } + buffer.clear(); + } + } else { + buffer.push(byte); + } + } + Err(_) => break, + } + } + + handler.map(|h| h.finish(&mut state.unwrap())); + + String::from_utf8_lossy(&raw_log).to_string() +} + impl ProgressHandler { /// Create a new progress handler for a git operation. pub fn new(mpb: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { @@ -294,43 +267,70 @@ impl ProgressHandler { } } -pub async fn monitor_stderr( - stream: impl tokio::io::AsyncRead + Unpin, - handler: Option, -) -> String { - let mut reader = BufReader::new(stream); - let mut buffer = Vec::new(); - let mut raw_log = Vec::new(); +pub fn parse_git_line(line: &str) -> GitProgress { + let line = line.trim(); + let re = RE_GIT.get_or_init(|| { + Regex::new(r"(?x) + ^ # Start + (?: + # 1. Registration: Capture the path, ignore the descriptive name + Submodule\ '[^']+'\ .*\ registered\ for\ path\ '(?P[^']+)' | - // Add a new progress bar and state if we have a handler - let mut state = handler.as_ref().map(|h| h.start()); + # 2. Cloning: Capture the path + Cloning\ into\ '(?P[^']+)'\.\.\. | - loop { - match reader.read_u8().await { - Ok(byte) => { - raw_log.push(byte); + # 3. Completion: Capture the name + Submodule\ path\ '(?P[^']+)':\ checked\ out\ '.* | - if byte == b'\r' || byte == b'\n' { - if !buffer.is_empty() { - if let Ok(line) = std::str::from_utf8(&buffer) { - // Update UI if we have a handler - if let Some(h) = &handler { - h.update_pb(line, &mut state.as_mut().unwrap()); - } - } - buffer.clear(); - } - } else { - buffer.push(byte); - } - } - Err(_) => break, + # 4. Progress + (?PReceiving\ objects|Resolving\ deltas|Checking\ out\ files):\s+(?P\d+)% | + + # 5. Errors + (?Pfatal:.*|error:.*|remote:\ aborting.*) + ) + ").expect("Invalid Regex") + }); + + if let Some(caps) = re.captures(line) { + if let Some(path) = caps.name("reg_path") { + return GitProgress::SubmoduleRegistered { + name: path_to_name(path.as_str()), + }; + } + if let Some(path) = caps.name("clone_path") { + return GitProgress::CloningInto { + name: path_to_name(path.as_str()), + }; + } + if let Some(path) = caps.name("sub_end_name") { + return GitProgress::SubmoduleEnd { + name: path_to_name(path.as_str()), + }; + } + if let Some(err) = caps.name("error") { + return GitProgress::Error(err.as_str().to_string()); + } + if let Some(phase) = caps.name("phase") { + let percent = caps.name("percent").unwrap().as_str().parse().unwrap_or(0); + return match phase.as_str() { + "Receiving objects" => GitProgress::Receiving { percent }, + "Resolving deltas" => GitProgress::Resolving { percent }, + "Checking out files" => GitProgress::Checkout { percent }, + _ => GitProgress::Other, + }; } } + // Otherwise, we don't care + GitProgress::Other +} - handler.map(|h| h.finish(&mut state.unwrap())); - - String::from_utf8_lossy(&raw_log).to_string() +/// Helper to extract the name from a git path. +fn path_to_name(path: &str) -> String { + path.trim_end_matches('/') + .split('/') + .last() + .unwrap_or(path) + .to_string() } #[cfg(test)] From b4002871ae186f83cc4dd4554e6691510b53528b Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 2 Jan 2026 22:15:13 +0100 Subject: [PATCH 69/79] progress: Clean up and document --- src/progress.rs | 64 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index 346e787d..f434fe3e 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -13,9 +13,7 @@ use tokio::io::{AsyncReadExt, BufReader}; static RE_GIT: OnceLock = OnceLock::new(); -/// Parses a line of git output. -/// (Put your `GitProgress` enum and `parse_git_line` function here) -#[derive(Debug, PartialEq, Clone)] +/// The result of parsing a git progress line. pub enum GitProgress { SubmoduleRegistered { name: String }, CloningInto { name: String }, @@ -27,7 +25,7 @@ pub enum GitProgress { Other, } -/// This struct captures (dynamic) state information for a git operation's progress. +/// Captures (dynamic) state information for a git operation's progress. /// for instance, the actuall progress bars to update. pub struct ProgressState { /// The progress bar of the current package. @@ -40,7 +38,7 @@ pub struct ProgressState { start_time: std::time::Instant, } -/// This struct captures (static) information neeed to handle progress updates for a git operation. +/// Captures (static) information neeed to handle progress updates for a git operation. pub struct ProgressHandler { /// Reference to the multi-progress bar, which can manage multiple progress bars. mpb: MultiProgress, @@ -59,22 +57,30 @@ pub enum GitProgressOps { Submodule, } +/// Monitor the stderr stream of a git process and update progress bars +/// of a given handler accordingly. pub async fn monitor_stderr( stream: impl tokio::io::AsyncRead + Unpin, handler: Option, ) -> String { let mut reader = BufReader::new(stream); - let mut buffer = Vec::new(); - let mut raw_log = Vec::new(); + let mut buffer = Vec::new(); // Buffer for accumulating bytes of a line + let mut raw_log = Vec::new(); // The full raw log output // Add a new progress bar and state if we have a handler let mut state = handler.as_ref().map(|h| h.start()); + // We loop over the stream reading byte by byte + // and process lines as they are completed. loop { match reader.read_u8().await { Ok(byte) => { raw_log.push(byte); + // Git output lines end with either \n or \r + // Every time we encounter one, we process the line. + // Note: \r is used for progress updates, meaning the line + // is overwritten in place. if byte == b'\r' || byte == b'\n' { if !buffer.is_empty() { if let Ok(line) = std::str::from_utf8(&buffer) { @@ -83,18 +89,22 @@ pub async fn monitor_stderr( h.update_pb(line, &mut state.as_mut().unwrap()); } } + // Clear the buffer for the next line buffer.clear(); } } else { buffer.push(byte); } } + // We break the loop on EOF or error Err(_) => break, } } + // Finalize the progress bar if we have a handler handler.map(|h| h.finish(&mut state.unwrap())); + // Return the full raw log as a string String::from_utf8_lossy(&raw_log).to_string() } @@ -108,6 +118,8 @@ impl ProgressHandler { } } + /// Adds a new progress bar to the multi-progress and returns the initial state + /// that is needed to track progress updates. pub fn start(&self) -> ProgressState { // Create and configure the main progress bar let style = ProgressStyle::with_template( @@ -117,8 +129,10 @@ impl ProgressHandler { .progress_chars("-- ") .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + // Create and attach the progress bar to the multi-progress bar. let pb = self.mpb.add(ProgressBar::new(100).with_style(style)); + // Set the prefix based on the git operation let prefix = match self.git_op { GitProgressOps::Clone => "Cloning", GitProgressOps::Fetch => "Fetching", @@ -127,6 +141,7 @@ impl ProgressHandler { }; let prefix = format!("{} {}", green_bold!(prefix), bold!(&self.name)); pb.set_prefix(prefix); + // Configure the spinners to automatically tick every 100ms pb.enable_steady_tick(Duration::from_millis(100)); @@ -138,7 +153,9 @@ impl ProgressHandler { } } + /// Update the progress bar(s) based on a parsed git progress line. pub fn update_pb(&self, line: &str, state: &mut ProgressState) { + // Parse the line to determine the type of progress update let progress = parse_git_line(line); // Target the active submodule if one exists, otherwise the main bar @@ -149,9 +166,11 @@ impl ProgressHandler { }; match progress { + // This case is only relevant for submodule operations i.e. `git submodule update` + // It indicates that a new submodule has been registered, and we create a new progress bar for it. GitProgress::SubmoduleRegistered { name } => { if self.git_op == GitProgressOps::Submodule { - // The main simply becomes a spinner since the sub-bar will show progress + // The main bar simply becomes a spinner since the sub-bar will show progress // on the subsequent line. state.pb.set_style( ProgressStyle::with_template("{spinner:.green} {prefix:<32!}").unwrap(), @@ -164,11 +183,11 @@ impl ProgressHandler { .unwrap() .progress_chars("-- "); - // Tree Logic - let ref_bar = match state.sub_bars.last() { + // We can have multiple sub-bars, and we insert them after the last one. + // In order to maintain proper tree-like structure, we need to update the previous last bar + // to have a "T" connector (├─) instead of an "L" + let prev_bar = match state.sub_bars.last() { Some((last_name, last_pb)) => { - // Update the previous last bar to have a "T" connector (├─) - // because it is no longer the last one. let prev_prefix = format!("{} {}", dim!("├─ "), dim!(last_name)); last_pb.set_prefix(prev_prefix); last_pb // Insert the new one after this one @@ -176,18 +195,23 @@ impl ProgressHandler { None => &state.pb, // Insert the first one after the main bar }; - // Create bar immediately + // Create the new sub-bar and insert it in the multi-progress *after* the previous sub-bar let sub_pb = self .mpb - .insert_after(ref_bar, ProgressBar::new(100).with_style(style)); + .insert_after(prev_bar, ProgressBar::new(100).with_style(style)); + // Set the prefix and initial message let sub_prefix = format!("{} {}", dim!("└─ "), dim!(&name)); sub_pb.set_prefix(sub_prefix); sub_pb.set_message(format!("{}", dim!("Waiting..."))); + // Store the sub-bar in the state for later updates state.sub_bars.insert(name, sub_pb); } } + // This indicates that we are starting to clone a submodule. + // Again, it is only relevant for submodule operations. For normal + // clones, we just update the main bar. GitProgress::CloningInto { name } => { if self.git_op == GitProgressOps::Submodule { // Logic to handle missing 'checked out' lines: @@ -199,7 +223,7 @@ impl ProgressHandler { } } } - // Activate the new bar + // Set the new bar to active if let Some(bar) = state.sub_bars.get(&name) { // Switch style to the active progress bar style bar.set_message(format!("{}", dim!("Cloning..."))); @@ -207,26 +231,33 @@ impl ProgressHandler { state.active_sub = Some(name); } } + // Indicates that we have finished processing a submodule. GitProgress::SubmoduleEnd { name } => { + // We finish and clear the sub-bar if let Some(bar) = state.sub_bars.get(&name) { bar.finish_and_clear(); } + // If this was the active submodule, we clear the active state if state.active_sub.as_ref() == Some(&name) { state.active_sub = None; } } + // Update the progress percentage for receiving objects GitProgress::Receiving { percent, .. } => { target_pb.set_message(format!("{}", dim!("Receiving objects"))); target_pb.set_position(percent as u64); } + // Update the progress percentage for resolving deltas GitProgress::Resolving { percent, .. } => { target_pb.set_message(format!("{}", dim!("Resolving deltas"))); target_pb.set_position(percent as u64); } + // Update the progress percentage for checking out files GitProgress::Checkout { percent, .. } => { target_pb.set_message(format!("{}", dim!("Checking out"))); target_pb.set_position(percent as u64); } + // Handle errors by finishing and clearing the target bar, then logging the error GitProgress::Error(err_msg) => { target_pb.finish_and_clear(); // TODO(fischeti): Consider enumerating error @@ -241,6 +272,7 @@ impl ProgressHandler { } } + // Finalize the progress bars and print a completion message. pub fn finish(self, state: &mut ProgressState) { // Clear all sub bars that might be lingering for pb in state.sub_bars.values() { @@ -256,6 +288,7 @@ impl ProgressHandler { GitProgressOps::Submodule => "Updated Submodules", }; + // Print a completion message on top of active progress bars self.mpb .println(format!( " {} {} {}", @@ -267,6 +300,7 @@ impl ProgressHandler { } } +/// Parse a git progress line and return the corresponding `GitProgress` enum. pub fn parse_git_line(line: &str) -> GitProgress { let line = line.trim(); let re = RE_GIT.get_or_init(|| { From c827f66d006de9a89870ede2ce5e328e1f38a834 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Sat, 3 Jan 2026 11:12:37 +0100 Subject: [PATCH 70/79] git: Selectively throttle git spawns --- src/cmd/vendor.rs | 24 ++++++++++------- src/git.rs | 67 ++++++++++++++++++++++++++++++++--------------- src/sess.rs | 52 ++++++++++++++++++++++++++---------- 3 files changed, 99 insertions(+), 44 deletions(-) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index b20b99c0..936ceefe 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -100,14 +100,14 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { let dep_path = match dep_src { DependencySource::Path(path) => path, DependencySource::Git(ref url) => { - let git = Git::new(tmp_path, &sess.config.git, sess.git_throttle.clone()); + let git = Git::new(tmp_path, &sess.config.git); rt.block_on(async { let pb = ProgressHandler::new( sess.progress.clone(), GitProgressOps::Clone, vendor_package.name.as_str(), ); - git.clone().spawn_with(|c| c.arg("clone").arg(url).arg("."), Some(pb)) + git.clone().spawn_with(|c| c.arg("clone").arg(url).arg("."), Some(sess.git_throttle.clone()), Some(pb)) .map_err(move |cause| { if url.contains("git@") { Warnings::SshKeyMaybeMissing.emit(); @@ -127,8 +127,8 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { GitProgressOps::Checkout, vendor_package.name.as_str(), ); - git.clone().spawn_with(|c| c.arg("checkout").arg(rev_hash), Some(pb)).await?; - if *rev_hash != git.spawn_with(|c| c.arg("rev-parse").arg("--verify").arg(format!("{}^{{commit}}", rev_hash)), None).await?.trim_end_matches('\n') { + git.clone().spawn_with(|c| c.arg("checkout").arg(rev_hash), None ,Some(pb)).await?; + if *rev_hash != git.spawn_with(|c| c.arg("rev-parse").arg("--verify").arg(format!("{}^{{commit}}", rev_hash)), None, None).await?.trim_end_matches('\n') { Err(Error::new("Please ensure your vendor reference is a commit hash to avoid upstream changes impacting your checkout")) } else { Ok(()) @@ -194,7 +194,7 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { seen_paths.insert(patch_link.to_prefix.clone()); } - let git = Git::new(tmp_path, &sess.config.git, sess.git_throttle.clone()); + let git = Git::new(tmp_path, &sess.config.git); match &args.vendor_subcommand { VendorSubcommand::Diff { err_on_diff } => { @@ -467,6 +467,7 @@ pub fn apply_patches( c }, None, + None, ) }) .await @@ -548,6 +549,7 @@ pub fn diff( )) }, None, + None, ) .await }) @@ -636,7 +638,6 @@ pub fn gen_format_patch( to_path.parent().unwrap() }, &sess.config.git, - sess.git_throttle.clone(), ); // If the patch link maps a file, use the parent directory for the following git operations. @@ -683,7 +684,11 @@ pub fn gen_format_patch( // Get staged changes in dependency let get_diff_cached = rt - .block_on(async { git_parent.spawn_with(|c| c.args(&diff_args), None).await }) + .block_on(async { + git_parent + .spawn_with(|c| c.args(&diff_args), None, None) + .await + }) .map_err(|cause| Error::chain("Failed to generate diff", cause))?; if !get_diff_cached.is_empty() { @@ -701,8 +706,8 @@ pub fn gen_format_patch( .arg(&from_path_relative) .arg("-p1") .arg(&diff_cached_path) - }, None) - .and_then(|_| git.clone().spawn_with(|c| c.arg("add").arg("--all"), None)) + }, None, None) + .and_then(|_| git.clone().spawn_with(|c| c.arg("add").arg("--all"), None, None)) .await }).map_err(|cause| Error::chain("Could not apply staged changes on top of patched upstream repository. Did you commit all previously patched modifications?", cause))?; @@ -765,6 +770,7 @@ pub fn gen_format_patch( .arg("HEAD") }, None, + None, ) .await })?; diff --git a/src/git.rs b/src/git.rs index d61f6bc9..30b6cba4 100644 --- a/src/git.rs +++ b/src/git.rs @@ -29,18 +29,12 @@ pub struct Git<'ctx> { pub path: &'ctx Path, /// The session within which commands will be executed. pub git: &'ctx String, - /// Reference to the throttle object. - pub throttle: Arc, } impl<'ctx> Git<'ctx> { /// Create a new git context. - pub fn new(path: &'ctx Path, git: &'ctx String, throttle: Arc) -> Git<'ctx> { - Git { - path, - git, - throttle, - } + pub fn new(path: &'ctx Path, git: &'ctx String) -> Git<'ctx> { + Git { path, git } } /// Create a new git command. @@ -69,10 +63,15 @@ impl<'ctx> Git<'ctx> { self, mut cmd: Command, check: bool, + throttle: Option>, pb: Option, ) -> Result { // Acquire the throttle semaphore - let permit = self.throttle.clone().acquire_owned().await.unwrap(); + // let permit = self.throttle.clone().acquire_owned().await.unwrap(); + let permit = match throttle { + Some(sem) => Some(sem.acquire_owned().await.unwrap()), + None => None, + }; // Configure pipes for streaming cmd.stdout(Stdio::piped()); @@ -160,28 +159,38 @@ impl<'ctx> Git<'ctx> { /// This is a convenience function that creates a command, passes it to the /// closure `f` for configuration, then passes it to the `spawn` function /// and returns the future. - pub async fn spawn_with(self, f: F, pb: Option) -> Result + pub async fn spawn_with( + self, + f: F, + throttle: Option>, + pb: Option, + ) -> Result where F: FnOnce(&mut Command) -> &mut Command, { let mut cmd = Command::new(self.git); cmd.current_dir(self.path); f(&mut cmd); - self.spawn(cmd, true, pb).await + self.spawn(cmd, true, throttle, pb).await } /// Assemble a command and schedule it for execution. /// /// This is the same as `spawn_with()`, but returns the stdout regardless of /// whether the command failed or not. - pub async fn spawn_unchecked_with(self, f: F, pb: Option) -> Result + pub async fn spawn_unchecked_with( + self, + f: F, + throttle: Option>, + pb: Option, + ) -> Result where F: FnOnce(&mut Command) -> &mut Command, { let mut cmd = Command::new(self.git); cmd.current_dir(self.path); f(&mut cmd); - self.spawn(cmd, false, pb).await + self.spawn(cmd, false, throttle, pb).await } /// Assemble a command and execute it interactively. @@ -200,17 +209,24 @@ impl<'ctx> Git<'ctx> { } /// Fetch the tags and refs of a remote. - pub async fn fetch(self, remote: &str, pb: Option) -> Result<()> { + pub async fn fetch( + self, + remote: &str, + throttle: Option>, + pb: Option, + ) -> Result<()> { let r1 = String::from(remote); let r2 = String::from(remote); self.clone() .spawn_with( |c| c.arg("fetch").arg("--prune").arg(r1).arg("--progress"), + throttle.clone(), pb, ) .and_then(|_| { self.spawn_with( |c| c.arg("fetch").arg("--tags").arg("--prune").arg(r2), + throttle, None, ) }) @@ -223,10 +239,12 @@ impl<'ctx> Git<'ctx> { self, remote: &str, reference: &str, + throttle: Option>, pb: Option, ) -> Result<()> { self.spawn_with( |c| c.arg("fetch").arg(remote).arg(reference).arg("--progress"), + throttle, pb, ) .await @@ -235,7 +253,7 @@ impl<'ctx> Git<'ctx> { /// Stage all local changes. pub async fn add_all(self) -> Result<()> { - self.spawn_with(|c| c.arg("add").arg("--all"), None) + self.spawn_with(|c| c.arg("add").arg("--all"), None, None) .await .map(|_| ()) } @@ -255,6 +273,7 @@ impl<'ctx> Git<'ctx> { .arg(msg) }, None, + None, ) .await .map(|_| ()), @@ -268,7 +287,7 @@ impl<'ctx> Git<'ctx> { /// List all refs and their hashes. pub async fn list_refs(self) -> Result> { - self.spawn_unchecked_with(|c| c.arg("show-ref").arg("--dereference"), None) + self.spawn_unchecked_with(|c| c.arg("show-ref").arg("--dereference"), None, None) .and_then(|raw| async move { let mut all_revs = raw .lines() @@ -303,9 +322,13 @@ impl<'ctx> Git<'ctx> { /// List all revisions. pub async fn list_revs(self) -> Result> { - self.spawn_with(|c| c.arg("rev-list").arg("--all").arg("--date-order"), None) - .await - .map(|raw| raw.lines().map(String::from).collect()) + self.spawn_with( + |c| c.arg("rev-list").arg("--all").arg("--date-order"), + None, + None, + ) + .await + .map(|raw| raw.lines().map(String::from).collect()) } /// Determine the currently checked out revision. @@ -313,6 +336,7 @@ impl<'ctx> Git<'ctx> { self.spawn_with( |c| c.arg("rev-parse").arg("--revs-only").arg("HEAD^{commit}"), None, + None, ) .await .map(|raw| raw.lines().take(1).map(String::from).next()) @@ -320,7 +344,7 @@ impl<'ctx> Git<'ctx> { /// Determine the url of a remote. pub async fn remote_url(self, remote: &str) -> Result { - self.spawn_with(|c| c.arg("remote").arg("get-url").arg(remote), None) + self.spawn_with(|c| c.arg("remote").arg("get-url").arg(remote), None, None) .await .map(|raw| raw.lines().take(1).map(String::from).next().unwrap()) } @@ -342,6 +366,7 @@ impl<'ctx> Git<'ctx> { c }, None, + None, ) .await .map(|raw| raw.lines().map(TreeEntry::parse).collect()) @@ -349,7 +374,7 @@ impl<'ctx> Git<'ctx> { /// Read the content of a file. pub async fn cat_file>(self, hash: O) -> Result { - self.spawn_with(|c| c.arg("cat-file").arg("blob").arg(hash), None) + self.spawn_with(|c| c.arg("cat-file").arg("blob").arg(hash), None, None) .await } } diff --git a/src/sess.rs b/src/sess.rs index c875afaa..c140b808 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -546,11 +546,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { )) } }; - let git = Git::new( - db_dir, - &self.sess.config.git, - self.sess.git_throttle.clone(), - ); + let git = Git::new(db_dir, &self.sess.config.git); let url = String::from(url); let url2 = url.clone(); @@ -572,16 +568,27 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { name, )); git.clone() - .spawn_with(|c| c.arg("init").arg("--bare"), None) + .spawn_with(|c| c.arg("init").arg("--bare"), None, None) .await?; git.clone() - .spawn_with(|c| c.arg("remote").arg("add").arg("origin").arg(url), None) + .spawn_with( + |c| c.arg("remote").arg("add").arg("origin").arg(url), + None, + None, + ) .await?; git.clone() - .fetch("origin", pb) + .fetch("origin", Some(self.sess.git_throttle.clone()), pb) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, None).await + git.clone() + .fetch_ref( + "origin", + reference, + Some(self.sess.git_throttle.clone()), + None, + ) + .await } else { Ok(()) } @@ -613,10 +620,17 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { name, )); git.clone() - .fetch("origin", pb) + .fetch("origin", Some(self.sess.git_throttle.clone()), pb) .and_then(|_| async { if let Some(reference) = fetch_ref { - git.clone().fetch_ref("origin", reference, None).await + git.clone() + .fetch_ref( + "origin", + reference, + Some(self.sess.git_throttle.clone()), + None, + ) + .await } else { Ok(()) } @@ -863,7 +877,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { ToCheckout, ToClone, } - let local_git = Git::new(path, &self.sess.config.git, self.sess.git_throttle.clone()); + let local_git = Git::new(path, &self.sess.config.git); let clear = if path.exists() { // Scrap checkouts with the wrong tag. let current_checkout = local_git.clone().current_checkout().await; @@ -904,7 +918,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { if checkout_already_good == CheckoutState::ToCheckout { if local_git .clone() - .spawn_with(|c| c.arg("status").arg("--porcelain"), None) + .spawn_with(|c| c.arg("status").arg("--porcelain"), None, None) .await .is_ok() { @@ -956,6 +970,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--no-sign") }, None, + None, ) .await { @@ -972,7 +987,11 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { )); // Attempt to fetch from remote and retry, as commits seem unavailable. git.clone() - .spawn_with(move |c| c.arg("fetch").arg("--all").arg("--progress"), pb) + .spawn_with( + move |c| c.arg("fetch").arg("--all").arg("--progress"), + Some(self.sess.git_throttle.clone()), + pb, + ) .await?; git.clone() .spawn_with( @@ -984,6 +1003,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--no-sign") }, None, + None, ) .map_err(|cause| { Warnings::RevisionNotFound(revision.to_string(), name.to_string()) @@ -1015,6 +1035,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg(tag_name_2) .arg("--progress") }, + None, pb, ) .await?; @@ -1030,6 +1051,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--progress") }, None, + None, ) .await?; let pb = Some(ProgressHandler::new( @@ -1046,6 +1068,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--force") .arg("--progress") }, + Some(self.sess.git_throttle.clone()), pb, ) .await?; @@ -1066,6 +1089,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { .arg("--recursive") .arg("--progress") }, + Some(self.sess.git_throttle.clone()), pb, ) .await?; From b53ebfc498c90ea65732bc38ebc3167728b1b33c Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 6 Jan 2026 19:34:40 +0100 Subject: [PATCH 71/79] cmd(checkout): Use public function to determine number of packages --- src/cmd/checkout.rs | 2 +- src/sess.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmd/checkout.rs b/src/cmd/checkout.rs index c176da6d..f03b32be 100644 --- a/src/cmd/checkout.rs +++ b/src/cmd/checkout.rs @@ -29,7 +29,7 @@ pub fn run_plain(sess: &Session, force: bool, update_list: &[String]) -> Result< let io = SessionIo::new(sess); let start_time = std::time::Instant::now(); let _srcs = rt.block_on(io.sources(forcibly, update_list))?; - let num_dependencies = io.sess.names.lock().unwrap().len(); + let num_dependencies = io.sess.packages().iter().flatten().count(); infoln!( "{} {} dependencies {}", dim!("Checked out"), diff --git a/src/sess.rs b/src/sess.rs index c140b808..b9329a12 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -65,7 +65,7 @@ pub struct Session<'ctx> { /// The internalized strings. strings: Mutex>, /// The package name table. - pub names: Mutex>, + names: Mutex>, /// The dependency graph. graph: Mutex>>>, /// The topologically sorted list of packages. From c02662145b8a57a1157752d3ba738385c5105a4e Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Tue, 6 Jan 2026 19:37:55 +0100 Subject: [PATCH 72/79] util: Fix merge conflict mistake --- src/util.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util.rs b/src/util.rs index c8292a53..c450b801 100644 --- a/src/util.rs +++ b/src/util.rs @@ -428,6 +428,9 @@ pub fn version_req_bottom_bound(req: &VersionReq) -> Result> { Ok(Some(bottom_bound)) } else { Ok(None) + } +} + /// Format time duration with proper units. pub fn fmt_duration(duration: std::time::Duration) -> String { match duration.as_millis() { From cc3383a5597b8a7d25f8d86b1f44fa8890382680 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Fri, 9 Jan 2026 00:08:16 +0100 Subject: [PATCH 73/79] progress: Small style changes --- src/progress.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index f434fe3e..4d9f707a 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -188,7 +188,7 @@ impl ProgressHandler { // to have a "T" connector (├─) instead of an "L" let prev_bar = match state.sub_bars.last() { Some((last_name, last_pb)) => { - let prev_prefix = format!("{} {}", dim!("├─ "), dim!(last_name)); + let prev_prefix = format!("{} {}", dim!("├─"), last_name); last_pb.set_prefix(prev_prefix); last_pb // Insert the new one after this one } @@ -199,9 +199,8 @@ impl ProgressHandler { let sub_pb = self .mpb .insert_after(prev_bar, ProgressBar::new(100).with_style(style)); - // Set the prefix and initial message - let sub_prefix = format!("{} {}", dim!("└─ "), dim!(&name)); + let sub_prefix = format!("{} {}", dim!("╰─"), &name); sub_pb.set_prefix(sub_prefix); sub_pb.set_message(format!("{}", dim!("Waiting..."))); From 9fe1448818e5726f50f67c3af1cd351e57117d23 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 12 Jan 2026 13:44:53 +0100 Subject: [PATCH 74/79] progress: Color in progress operations in cyan --- src/error.rs | 8 ++++++++ src/progress.rs | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index e37532f9..4cc6ee35 100644 --- a/src/error.rs +++ b/src/error.rs @@ -99,6 +99,14 @@ macro_rules! green_bold { }; } +/// Style a message in cyan bold. +#[macro_export] +macro_rules! cyan_bold { + ($arg:expr) => { + console::style($arg).cyan().bold() + }; +} + /// Style a message in green bold. #[macro_export] macro_rules! red_bold { diff --git a/src/progress.rs b/src/progress.rs index 4d9f707a..ee794076 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -123,7 +123,7 @@ impl ProgressHandler { pub fn start(&self) -> ProgressState { // Create and configure the main progress bar let style = ProgressStyle::with_template( - "{spinner:.green} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", + "{spinner:.cyan} {prefix:<32!} {bar:40.cyan/blue} {percent:>3}% {msg}", ) .unwrap() .progress_chars("-- ") @@ -139,7 +139,7 @@ impl ProgressHandler { GitProgressOps::Checkout => "Checkout", GitProgressOps::Submodule => "Update Submodules", }; - let prefix = format!("{} {}", green_bold!(prefix), bold!(&self.name)); + let prefix = format!("{} {}", cyan_bold!(prefix), bold!(&self.name)); pb.set_prefix(prefix); // Configure the spinners to automatically tick every 100ms @@ -173,7 +173,7 @@ impl ProgressHandler { // The main bar simply becomes a spinner since the sub-bar will show progress // on the subsequent line. state.pb.set_style( - ProgressStyle::with_template("{spinner:.green} {prefix:<32!}").unwrap(), + ProgressStyle::with_template("{spinner:.cyan} {prefix:<32!}").unwrap(), ); // The submodule style is similar to the main bar, but indented and without spinner From ae8cc60496f2203e38666e7e712410962e14c78f Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Mon, 12 Jan 2026 13:45:21 +0100 Subject: [PATCH 75/79] progress: Reword in progress messages --- src/progress.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index ee794076..de4cec5b 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -136,8 +136,8 @@ impl ProgressHandler { let prefix = match self.git_op { GitProgressOps::Clone => "Cloning", GitProgressOps::Fetch => "Fetching", - GitProgressOps::Checkout => "Checkout", - GitProgressOps::Submodule => "Update Submodules", + GitProgressOps::Checkout => "Checking out", + GitProgressOps::Submodule => "Updating Submodules", }; let prefix = format!("{} {}", cyan_bold!(prefix), bold!(&self.name)); pb.set_prefix(prefix); @@ -173,7 +173,7 @@ impl ProgressHandler { // The main bar simply becomes a spinner since the sub-bar will show progress // on the subsequent line. state.pb.set_style( - ProgressStyle::with_template("{spinner:.cyan} {prefix:<32!}").unwrap(), + ProgressStyle::with_template("{spinner:.cyan} {prefix:<40!}").unwrap(), ); // The submodule style is similar to the main bar, but indented and without spinner From 5a06e75ebd37823feda350baec61384e4a933e18 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Wed, 14 Jan 2026 23:56:56 +0100 Subject: [PATCH 76/79] Fix merge conflicts --- src/cmd/checkout.rs | 2 +- src/error.rs | 1 - src/sess.rs | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cmd/checkout.rs b/src/cmd/checkout.rs index f03b32be..29264ec2 100644 --- a/src/cmd/checkout.rs +++ b/src/cmd/checkout.rs @@ -28,7 +28,7 @@ pub fn run_plain(sess: &Session, force: bool, update_list: &[String]) -> Result< let rt = Runtime::new()?; let io = SessionIo::new(sess); let start_time = std::time::Instant::now(); - let _srcs = rt.block_on(io.sources(forcibly, update_list))?; + let _srcs = rt.block_on(io.sources(force, update_list))?; let num_dependencies = io.sess.packages().iter().flatten().count(); infoln!( "{} {} dependencies {}", diff --git a/src/error.rs b/src/error.rs index 4cc6ee35..1d8a4f7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,7 +6,6 @@ use std; use std::fmt; use std::sync::atomic::AtomicBool; -use std::sync::Arc; use std::sync::{Arc, RwLock}; use console::style; diff --git a/src/sess.rs b/src/sess.rs index b9329a12..b50a7a18 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -595,7 +595,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { }) .await .map_err(move |cause| { - if url3.contains("git@") { + if url2.contains("git@") { Warnings::SshKeyMaybeMissing.emit(); } Warnings::UrlMaybeIncorrect.emit(); @@ -637,7 +637,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { }) .await .map_err(move |cause| { - if url3.contains("git@") { + if url2.contains("git@") { Warnings::SshKeyMaybeMissing.emit(); } Warnings::UrlMaybeIncorrect.emit(); From 82924a66cb7208217d0c86e9b53f4abf8f60dd76 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 15 Jan 2026 21:09:20 +0100 Subject: [PATCH 77/79] diagnostic: Register multiprogressbar in `Diagnostic` --- src/cmd/vendor.rs | 4 ++-- src/diagnostic.rs | 29 +++++++++++++++++++++++++++-- src/error.rs | 27 ++------------------------- src/progress.rs | 14 ++++++++------ src/sess.rs | 27 +++++++++++++-------------- 5 files changed, 52 insertions(+), 49 deletions(-) diff --git a/src/cmd/vendor.rs b/src/cmd/vendor.rs index 936ceefe..8dc91a3b 100644 --- a/src/cmd/vendor.rs +++ b/src/cmd/vendor.rs @@ -103,7 +103,7 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { let git = Git::new(tmp_path, &sess.config.git); rt.block_on(async { let pb = ProgressHandler::new( - sess.progress.clone(), + sess.multiprogress.clone(), GitProgressOps::Clone, vendor_package.name.as_str(), ); @@ -123,7 +123,7 @@ pub fn run(sess: &Session, args: &VendorArgs) -> Result<()> { _ => Err(Error::new("Please ensure your vendor reference is a commit hash to avoid upstream changes impacting your checkout")), }?; let pb = ProgressHandler::new( - sess.progress.clone(), + sess.multiprogress.clone(), GitProgressOps::Checkout, vendor_package.name.as_str(), ); diff --git a/src/diagnostic.rs b/src/diagnostic.rs index a017a77f..7b94f609 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -6,6 +6,7 @@ use std::fmt; use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; +use indicatif::MultiProgress; use miette::{Diagnostic, ReportHandler}; use owo_colors::OwoColorize; use thiserror::Error; @@ -24,6 +25,8 @@ pub struct Diagnostics { /// A set of already emitted warnings. /// Requires synchronization as warnings may be emitted from multiple threads. emitted: Mutex>, + /// The active multi-progress bar (if any). + multiprogress: Mutex>, } impl Diagnostics { @@ -35,6 +38,7 @@ impl Diagnostics { all_suppressed: suppressed.contains("all") || suppressed.contains("Wall"), suppressed, emitted: Mutex::new(HashSet::new()), + multiprogress: Mutex::new(None), }; GLOBAL_DIAGNOSTICS @@ -42,6 +46,12 @@ impl Diagnostics { .expect("Diagnostics already initialized!"); } + pub fn set_multiprogress(multiprogress: Option) { + let diag = Diagnostics::get(); + let mut guard = diag.multiprogress.lock().unwrap(); + *guard = multiprogress; + } + /// Get the global diagnostics manager. fn get() -> &'static Diagnostics { GLOBAL_DIAGNOSTICS @@ -76,8 +86,22 @@ impl Warnings { emitted.insert(self.clone()); drop(emitted); - // Print the warning report (consumes self i.e. the warning) - eprintln!("{:?}", miette::Report::new(self)); + // Prepare the report + let report = miette::Report::new(self.clone()); + + // Print cleanly (using suspend if a bar exists) + let mp_guard = diag.multiprogress.lock().unwrap(); + + if let Some(mp) = &*mp_guard { + // If we have progress bars, hide them momentarily + mp.suspend(|| { + eprintln!("{:?}", report); + }); + } else { + eprintln!("No multiprogress bar available."); + // Otherwise just print + eprintln!("{:?}", report); + } } } @@ -379,6 +403,7 @@ mod tests { suppressed: HashSet::new(), all_suppressed: true, emitted: Mutex::new(HashSet::new()), + multiprogress: Mutex::new(None), }; // Manual check of the logic inside emit() diff --git a/src/error.rs b/src/error.rs index 1d8a4f7b..7600b372 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,35 +6,12 @@ use std; use std::fmt; use std::sync::atomic::AtomicBool; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use console::style; -use indicatif::MultiProgress; pub static ENABLE_DEBUG: AtomicBool = AtomicBool::new(false); -/// A global hook for the progress bar -pub static GLOBAL_MULTI_PROGRESS: RwLock> = RwLock::new(None); - -/// Helper function to print diagnostics safely without messing up progress bars. -pub fn print_diagnostic(severity: Severity, msg: &str) { - let text = format!("{} {}", severity, msg); - - // Try to acquire read access to the global progress bar - if let Ok(guard) = GLOBAL_MULTI_PROGRESS.read() { - if let Some(mp) = &*guard { - // SUSPEND: Hides progress bars, prints the message, then redraws bars. - mp.suspend(|| { - eprintln!("{}", text); - }); - return; - } - } - - // Fallback: Just print if no bar is registered or lock is poisoned - eprintln!("{}", text); -} - /// Print an error. #[macro_export] macro_rules! errorln { @@ -76,7 +53,7 @@ macro_rules! debugln { /// Emit a diagnostic message. macro_rules! diagnostic { ($severity:expr; $($arg:tt)*) => { - $crate::error::print_diagnostic($severity, &format!($($arg)*)) + eprintln!("{} {}", $severity, format!($($arg)*)) } } diff --git a/src/progress.rs b/src/progress.rs index de4cec5b..bfdaf4cf 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -41,7 +41,7 @@ pub struct ProgressState { /// Captures (static) information neeed to handle progress updates for a git operation. pub struct ProgressHandler { /// Reference to the multi-progress bar, which can manage multiple progress bars. - mpb: MultiProgress, + multiprogress: MultiProgress, /// The type of git operation being performed. git_op: GitProgressOps, /// The name of the repository being processed. @@ -110,9 +110,9 @@ pub async fn monitor_stderr( impl ProgressHandler { /// Create a new progress handler for a git operation. - pub fn new(mpb: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { + pub fn new(multiprogress: MultiProgress, git_op: GitProgressOps, name: &str) -> Self { Self { - mpb, + multiprogress, git_op, name: name.to_string(), } @@ -130,7 +130,9 @@ impl ProgressHandler { .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); // Create and attach the progress bar to the multi-progress bar. - let pb = self.mpb.add(ProgressBar::new(100).with_style(style)); + let pb = self + .multiprogress + .add(ProgressBar::new(100).with_style(style)); // Set the prefix based on the git operation let prefix = match self.git_op { @@ -197,7 +199,7 @@ impl ProgressHandler { // Create the new sub-bar and insert it in the multi-progress *after* the previous sub-bar let sub_pb = self - .mpb + .multiprogress .insert_after(prev_bar, ProgressBar::new(100).with_style(style)); // Set the prefix and initial message let sub_prefix = format!("{} {}", dim!("╰─"), &name); @@ -288,7 +290,7 @@ impl ProgressHandler { }; // Print a completion message on top of active progress bars - self.mpb + self.multiprogress .println(format!( " {} {} {}", green_bold!(op_str), diff --git a/src/sess.rs b/src/sess.rs index b50a7a18..73302cf3 100644 --- a/src/sess.rs +++ b/src/sess.rs @@ -81,7 +81,7 @@ pub struct Session<'ctx> { /// A toggle to disable remote fetches & clones pub local_only: bool, /// The global progress bar manager. - pub progress: MultiProgress, + pub multiprogress: MultiProgress, } impl<'ctx> Session<'ctx> { @@ -96,12 +96,11 @@ impl<'ctx> Session<'ctx> { force_fetch: bool, git_throttle: usize, ) -> Session<'ctx> { - // Initialize the global multi-progress bar - // to handle warning and error messages correctly. - let mpb = MultiProgress::new(); - if let Ok(mut global_mpb) = GLOBAL_MULTI_PROGRESS.write() { - *global_mpb = Some(mpb.clone()); - } + // Create the global multi-progress bar manager. + let multiprogress = MultiProgress::new(); + + // Register it with the global diagnostics system + Diagnostics::set_multiprogress(Some(multiprogress.clone())); Session { root, @@ -127,7 +126,7 @@ impl<'ctx> Session<'ctx> { cache: Default::default(), git_throttle: Arc::new(Semaphore::new(git_throttle)), local_only, - progress: MultiProgress::new(), + multiprogress, } } @@ -563,7 +562,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { // The progress bar object for cloning. We only use it for the // last fetch operation, which is the only network operation here. let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Clone, name, )); @@ -615,7 +614,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { self.sess.stats.num_database_fetch.increment(); // The progress bar object for fetching. let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Fetch, name, )); @@ -981,7 +980,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { cause ); let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Checkout, name, )); @@ -1021,7 +1020,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { }?; if clear == CheckoutState::ToClone { let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Checkout, name, )); @@ -1055,7 +1054,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { ) .await?; let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Checkout, name, )); @@ -1075,7 +1074,7 @@ impl<'io, 'sess: 'io, 'ctx: 'sess> SessionIo<'sess, 'ctx> { } if path.join(".gitmodules").exists() { let pb = Some(ProgressHandler::new( - self.sess.progress.clone(), + self.sess.multiprogress.clone(), GitProgressOps::Submodule, name, )); From e5c6cbbe136333dcebb5c46fea38d12c3762051a Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 15 Jan 2026 21:33:01 +0100 Subject: [PATCH 78/79] progress: Replace `console` with `owo_colors` --- Cargo.lock | 1 - Cargo.toml | 1 - src/cmd/checkout.rs | 5 ++-- src/diagnostic.rs | 8 +++---- src/error.rs | 56 ++++++--------------------------------------- src/progress.rs | 28 ++++++++++++----------- src/util.rs | 16 +++++++++++++ 7 files changed, 45 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e145e8a5..507169a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,6 @@ dependencies = [ "blake2", "clap", "clap_complete", - "console", "dirs", "dunce", "futures", diff --git a/Cargo.toml b/Cargo.toml index 7327b736..1b86ab81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,6 @@ miette = { version = "7.6.0", features = ["fancy"] } thiserror = "2.0.17" owo-colors = "4.2.3" indicatif = "0.18.3" -console = "0.16.2" regex = "1.12.2" [target.'cfg(windows)'.dependencies] diff --git a/src/cmd/checkout.rs b/src/cmd/checkout.rs index 29264ec2..fa6c570f 100644 --- a/src/cmd/checkout.rs +++ b/src/cmd/checkout.rs @@ -4,6 +4,7 @@ //! The `checkout` subcommand. use clap::Args; +use owo_colors::OwoColorize; use tokio::runtime::Runtime; use crate::error::*; @@ -32,9 +33,9 @@ pub fn run_plain(sess: &Session, force: bool, update_list: &[String]) -> Result< let num_dependencies = io.sess.packages().iter().flatten().count(); infoln!( "{} {} dependencies {}", - dim!("Checked out"), + "Checked out".dimmed(), num_dependencies, - dim!(fmt_duration(start_time.elapsed())) + fmt_duration(start_time.elapsed()).dimmed() ); Ok(()) diff --git a/src/diagnostic.rs b/src/diagnostic.rs index 7b94f609..8ea5b77a 100644 --- a/src/diagnostic.rs +++ b/src/diagnostic.rs @@ -8,7 +8,7 @@ use std::sync::{Mutex, OnceLock}; use indicatif::MultiProgress; use miette::{Diagnostic, ReportHandler}; -use owo_colors::OwoColorize; +use owo_colors::{OwoColorize, Style}; use thiserror::Error; use crate::{fmt_field, fmt_path, fmt_pkg, fmt_version}; @@ -111,9 +111,9 @@ impl ReportHandler for DiagnosticRenderer { fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Determine severity and the resulting style let (severity, style) = match diagnostic.severity().unwrap_or_default() { - miette::Severity::Error => ("error", owo_colors::Style::new().red().bold()), - miette::Severity::Warning => ("warning", owo_colors::Style::new().yellow().bold()), - miette::Severity::Advice => ("advice", owo_colors::Style::new().cyan().bold()), + miette::Severity::Error => ("error", Style::new().red().bold()), + miette::Severity::Warning => ("warning", Style::new().yellow().bold()), + miette::Severity::Advice => ("advice", Style::new().cyan().bold()), }; // Write the severity prefix diff --git a/src/error.rs b/src/error.rs index 7600b372..0980c1c5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,7 +8,7 @@ use std::fmt; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use console::style; +use owo_colors::{OwoColorize, Style}; pub static ENABLE_DEBUG: AtomicBool = AtomicBool::new(false); @@ -62,61 +62,19 @@ macro_rules! diagnostic { pub enum Severity { Debug, Info, - Warning, Error, Stage(&'static str), } -/// Style a message in green bold. -#[macro_export] -macro_rules! green_bold { - ($arg:expr) => { - console::style($arg).green().bold() - }; -} - -/// Style a message in cyan bold. -#[macro_export] -macro_rules! cyan_bold { - ($arg:expr) => { - console::style($arg).cyan().bold() - }; -} - -/// Style a message in green bold. -#[macro_export] -macro_rules! red_bold { - ($arg:expr) => { - console::style($arg).red().bold() - }; -} - -/// Style a message in dimmed text. -#[macro_export] -macro_rules! dim { - ($arg:expr) => { - console::style($arg).dim() - }; -} - -/// Style a message in bold text. -#[macro_export] -macro_rules! bold { - ($arg:expr) => { - console::style($arg).bold() - }; -} - impl fmt::Display for Severity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let styled_str = match *self { - Severity::Error => style("Error:").red().bold(), - Severity::Warning => style("Warning:").yellow().bold(), - Severity::Info => style("Info:").white().bold(), - Severity::Debug => style("Debug:").blue().bold(), - Severity::Stage(name) => style(name).green().bold(), + let (severity, style) = match *self { + Severity::Error => ("Error:", Style::new().red().bold()), + Severity::Info => ("Info:", Style::new().white().bold()), + Severity::Debug => ("Debug:", Style::new().blue().bold()), + Severity::Stage(name) => (name, Style::new().green().bold()), }; - write!(f, " {}", styled_str) + write!(f, " {}", severity.style(style)) } } diff --git a/src/progress.rs b/src/progress.rs index bfdaf4cf..b97b65b2 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -4,6 +4,7 @@ use crate::util::fmt_duration; use indexmap::IndexMap; +use owo_colors::OwoColorize; use std::sync::OnceLock; use std::time::Duration; @@ -11,6 +12,8 @@ use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use regex::Regex; use tokio::io::{AsyncReadExt, BufReader}; +use crate::{fmt_completed, fmt_pkg, fmt_stage}; + static RE_GIT: OnceLock = OnceLock::new(); /// The result of parsing a git progress line. @@ -141,7 +144,7 @@ impl ProgressHandler { GitProgressOps::Checkout => "Checking out", GitProgressOps::Submodule => "Updating Submodules", }; - let prefix = format!("{} {}", cyan_bold!(prefix), bold!(&self.name)); + let prefix = format!("{} {}", fmt_stage!(prefix), fmt_pkg!(&self.name)); pb.set_prefix(prefix); // Configure the spinners to automatically tick every 100ms @@ -190,7 +193,7 @@ impl ProgressHandler { // to have a "T" connector (├─) instead of an "L" let prev_bar = match state.sub_bars.last() { Some((last_name, last_pb)) => { - let prev_prefix = format!("{} {}", dim!("├─"), last_name); + let prev_prefix = format!("{} {}", "├─".dimmed(), last_name); last_pb.set_prefix(prev_prefix); last_pb // Insert the new one after this one } @@ -202,9 +205,9 @@ impl ProgressHandler { .multiprogress .insert_after(prev_bar, ProgressBar::new(100).with_style(style)); // Set the prefix and initial message - let sub_prefix = format!("{} {}", dim!("╰─"), &name); + let sub_prefix = format!("{} {}", "╰─".dimmed(), &name); sub_pb.set_prefix(sub_prefix); - sub_pb.set_message(format!("{}", dim!("Waiting..."))); + sub_pb.set_message(format!("{}", "Waiting...".dimmed())); // Store the sub-bar in the state for later updates state.sub_bars.insert(name, sub_pb); @@ -227,7 +230,7 @@ impl ProgressHandler { // Set the new bar to active if let Some(bar) = state.sub_bars.get(&name) { // Switch style to the active progress bar style - bar.set_message(format!("{}", dim!("Cloning..."))); + bar.set_message(format!("{}", "Cloning...".dimmed())); } state.active_sub = Some(name); } @@ -245,27 +248,26 @@ impl ProgressHandler { } // Update the progress percentage for receiving objects GitProgress::Receiving { percent, .. } => { - target_pb.set_message(format!("{}", dim!("Receiving objects"))); + target_pb.set_message(format!("{}", "Receiving objects".dimmed())); target_pb.set_position(percent as u64); } // Update the progress percentage for resolving deltas GitProgress::Resolving { percent, .. } => { - target_pb.set_message(format!("{}", dim!("Resolving deltas"))); + target_pb.set_message(format!("{}", "Resolving deltas".dimmed())); target_pb.set_position(percent as u64); } // Update the progress percentage for checking out files GitProgress::Checkout { percent, .. } => { - target_pb.set_message(format!("{}", dim!("Checking out"))); + target_pb.set_message(format!("{}", "Checking out".dimmed())); target_pb.set_position(percent as u64); } // Handle errors by finishing and clearing the target bar, then logging the error GitProgress::Error(err_msg) => { target_pb.finish_and_clear(); - // TODO(fischeti): Consider enumerating error errorln!( "{} {}: {}", "Error during git operation of", - bold!(&self.name), + fmt_pkg!(&self.name), err_msg ); } @@ -293,9 +295,9 @@ impl ProgressHandler { self.multiprogress .println(format!( " {} {} {}", - green_bold!(op_str), - bold!(&self.name), - dim!(fmt_duration(state.start_time.elapsed())) + fmt_completed!(op_str), + fmt_pkg!(&self.name), + fmt_duration(state.start_time.elapsed()).dimmed() )) .unwrap(); } diff --git a/src/util.rs b/src/util.rs index c450b801..70389037 100644 --- a/src/util.rs +++ b/src/util.rs @@ -471,3 +471,19 @@ macro_rules! fmt_version { $crate::util::OwoColorize::bold(&$ver) }; } + +/// Format for an ongoing progress stage in diagnostic messages. +#[macro_export] +macro_rules! fmt_stage { + ($stage:expr) => { + $crate::util::OwoColorize::cyan(&$stage).bold() + }; +} + +/// Format a completed progress stage in diagnostic messages. +#[macro_export] +macro_rules! fmt_completed { + ($stage:expr) => { + $crate::util::OwoColorize::green(&$stage).bold() + }; +} From 856474147ad7691bf99fd7134ccd550442fbd017 Mon Sep 17 00:00:00 2001 From: Tim Fischer Date: Thu, 15 Jan 2026 21:43:48 +0100 Subject: [PATCH 79/79] progress: Fix clippy warnings and clean up --- src/progress.rs | 54 ++++++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/src/progress.rs b/src/progress.rs index b97b65b2..62bdec78 100644 --- a/src/progress.rs +++ b/src/progress.rs @@ -75,37 +75,30 @@ pub async fn monitor_stderr( // We loop over the stream reading byte by byte // and process lines as they are completed. - loop { - match reader.read_u8().await { - Ok(byte) => { - raw_log.push(byte); - - // Git output lines end with either \n or \r - // Every time we encounter one, we process the line. - // Note: \r is used for progress updates, meaning the line - // is overwritten in place. - if byte == b'\r' || byte == b'\n' { - if !buffer.is_empty() { - if let Ok(line) = std::str::from_utf8(&buffer) { - // Update UI if we have a handler - if let Some(h) = &handler { - h.update_pb(line, &mut state.as_mut().unwrap()); - } - } - // Clear the buffer for the next line - buffer.clear(); - } - } else { - buffer.push(byte); - } - } - // We break the loop on EOF or error - Err(_) => break, + while let Ok(byte) = reader.read_u8().await { + raw_log.push(byte); + + // We push bytes into the buffer until we hit a delimiter + if byte != b'\r' && byte != b'\n' { + buffer.push(byte); + continue; } + + // Process the line, if we can parse it and have a handler + if let (Ok(line), Some(h)) = (std::str::from_utf8(&buffer), &handler) { + // Parse the line and update the progress bar accordingly + let progress = parse_git_line(line); + h.update_pb(progress, state.as_mut().unwrap()); + } + + // Always clear buffer after a delimiter + buffer.clear(); } // Finalize the progress bar if we have a handler - handler.map(|h| h.finish(&mut state.unwrap())); + if let Some(handler) = handler { + handler.finish(&mut state.unwrap()); + } // Return the full raw log as a string String::from_utf8_lossy(&raw_log).to_string() @@ -159,10 +152,7 @@ impl ProgressHandler { } /// Update the progress bar(s) based on a parsed git progress line. - pub fn update_pb(&self, line: &str, state: &mut ProgressState) { - // Parse the line to determine the type of progress update - let progress = parse_git_line(line); - + fn update_pb(&self, progress: GitProgress, state: &mut ProgressState) { // Target the active submodule if one exists, otherwise the main bar let target_pb = if let Some(name) = &state.active_sub { state.sub_bars.get(name).unwrap_or(&state.pb) @@ -365,7 +355,7 @@ pub fn parse_git_line(line: &str) -> GitProgress { fn path_to_name(path: &str) -> String { path.trim_end_matches('/') .split('/') - .last() + .next_back() .unwrap_or(path) .to_string() }