From fc143cbc3549d8254aed9bc7acffae1f82f0cc5b Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 9 Feb 2026 18:20:49 +0800 Subject: [PATCH 1/2] chore: better error reporting Signed-off-by: tison --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 14 ++++++ Cargo.lock | 14 ++++-- Cargo.toml | 8 ++-- cli/Cargo.toml | 2 +- cli/src/subcommand.rs | 16 ++++--- fmt/Cargo.toml | 2 +- fmt/src/document/factory.rs | 19 +++++---- fmt/src/document/mod.rs | 49 ++++++++++++--------- fmt/src/error.rs | 20 +++++++++ fmt/src/git.rs | 80 ++++++++++++++++++++-------------- fmt/src/header/model.rs | 52 +++++++++++++++-------- fmt/src/lib.rs | 1 + fmt/src/processor.rs | 75 ++++++++++++++++---------------- fmt/src/selection.rs | 85 +++++++++++++++++++++---------------- 15 files changed, 271 insertions(+), 168 deletions(-) create mode 100644 fmt/src/error.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fe8d4cb..0cedabb6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: test: strategy: matrix: - rust-version: ["1.85.0", "stable"] + rust-version: ["1.90.0", "stable"] name: Build and test runs-on: ubuntu-24.04 steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 38e5317f..523dc89e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. ## Unreleased +## [6.5.0] 2026-02-09 + +### Notable changes + +* Minimal Supported Rust Version (MSRV) is now 1.90.0. + +### Bug fixes + +* `hawkeye` CLI now uses hawkeye-fmt of exactly the same version to format headers, instead of using the latest version of `hawkeye-fmt` that may not be compatible with the current version of `hawkeye`. + +### Improvements + +* Replace `anyhow` with `exn` for more informative error messages. + ## [6.4.2] 2026-02-07 ## Bug fixes diff --git a/Cargo.lock b/Cargo.lock index 105b3259..93ebc2e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,6 +394,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "exn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca6badeb8a07b76663c7d7cb0903c1cc40c38d6fc06d3a2206780c63e8b9496" + [[package]] name = "faster-hex" version = "0.10.0" @@ -1259,12 +1265,12 @@ dependencies = [ [[package]] name = "hawkeye" -version = "6.4.2" +version = "6.5.0" dependencies = [ - "anyhow", "build-data", "clap", "const_format", + "exn", "gix-discover", "hawkeye-fmt", "log", @@ -1276,9 +1282,9 @@ dependencies = [ [[package]] name = "hawkeye-fmt" -version = "6.4.2" +version = "6.5.0" dependencies = [ - "anyhow", + "exn", "gix", "ignore", "jiff", diff --git a/Cargo.toml b/Cargo.toml index 2d847191..abbe5e43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,21 +17,21 @@ members = ["cli", "fmt"] resolver = "2" [workspace.package] -version = "6.4.2" +version = "6.5.0" edition = "2021" authors = ["tison "] readme = "README.md" license = "Apache-2.0" repository = "https://github.com/korandoru/hawkeye/" -rust-version = "1.85.0" +rust-version = "1.90.0" [workspace.dependencies] -hawkeye-fmt = { version = "6.4.2", path = "fmt" } +hawkeye-fmt = { version = "=6.5.0", path = "fmt" } -anyhow = { version = "1.0.94" } build-data = { version = "0.3.0" } clap = { version = "4.5.23", features = ["derive"] } const_format = { version = "0.2.34" } +exn = { version = "0.3.0" } log = { version = "0.4.22", features = ["kv_serde", "serde"] } shadow-rs = { version = "1.7.0", default-features = false } toml = { version = "0.9.5" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 4ae01b77..b052145f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -23,9 +23,9 @@ license.workspace = true repository.workspace = true [dependencies] -anyhow = { workspace = true } clap = { workspace = true } const_format = { workspace = true } +exn = { workspace = true } hawkeye-fmt = { workspace = true } log = { workspace = true } logforth = { version = "0.29.1", features = ["starter-log"] } diff --git a/cli/src/subcommand.rs b/cli/src/subcommand.rs index 4fdadca1..99d4d803 100644 --- a/cli/src/subcommand.rs +++ b/cli/src/subcommand.rs @@ -18,7 +18,9 @@ use std::path::PathBuf; use clap::Args; use clap::Parser; +use exn::Result; use hawkeye_fmt::document::Document; +use hawkeye_fmt::error::Error; use hawkeye_fmt::header::matcher::HeaderMatcher; use hawkeye_fmt::processor::check_license_header; use hawkeye_fmt::processor::Callback; @@ -95,11 +97,11 @@ impl Callback for CheckContext { self.unknown.push(path.display().to_string()); } - fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> anyhow::Result<()> { + fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> { Ok(()) } - fn on_not_matched(&mut self, _: &HeaderMatcher, document: Document) -> anyhow::Result<()> { + fn on_not_matched(&mut self, _: &HeaderMatcher, document: Document) -> Result<(), Error> { self.missing.push(document.filepath.display().to_string()); Ok(()) } @@ -149,11 +151,11 @@ impl Callback for FormatContext { self.unknown.push(path.display().to_string()); } - fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> anyhow::Result<()> { + fn on_matched(&mut self, _: &HeaderMatcher, _: Document) -> Result<(), Error> { Ok(()) } - fn on_not_matched(&mut self, header: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> { + fn on_not_matched(&mut self, header: &HeaderMatcher, mut doc: Document) -> Result<(), Error> { if doc.header_detected() { doc.remove_header(); doc.update_header(header)?; @@ -221,7 +223,7 @@ struct RemoveContext { } impl RemoveContext { - fn remove(&mut self, doc: &mut Document) -> anyhow::Result<()> { + fn remove(&mut self, doc: &mut Document) -> Result<(), Error> { if !doc.header_detected() { return Ok(()); } @@ -244,11 +246,11 @@ impl Callback for RemoveContext { self.unknown.push(path.display().to_string()); } - fn on_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> { + fn on_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> { self.remove(&mut doc) } - fn on_not_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> anyhow::Result<()> { + fn on_not_matched(&mut self, _: &HeaderMatcher, mut doc: Document) -> Result<(), Error> { self.remove(&mut doc) } } diff --git a/fmt/Cargo.toml b/fmt/Cargo.toml index abd24ad8..6cfd3952 100644 --- a/fmt/Cargo.toml +++ b/fmt/Cargo.toml @@ -23,7 +23,7 @@ license.workspace = true repository.workspace = true [dependencies] -anyhow = { workspace = true } +exn = { workspace = true } gix = { version = "0.78.0", default-features = false, features = [ "blob-diff", "excludes", diff --git a/fmt/src/document/factory.rs b/fmt/src/document/factory.rs index 95db9473..f5105d06 100644 --- a/fmt/src/document/factory.rs +++ b/fmt/src/document/factory.rs @@ -15,16 +15,17 @@ use std::collections::HashMap; use std::collections::HashSet; use std::fs; -use std::io; use std::path::Path; use std::path::PathBuf; use std::time::SystemTime; -use anyhow::Context; +use exn::OptionExt; +use exn::Result; use crate::config::Mapping; use crate::document::Attributes; use crate::document::Document; +use crate::error::Error; use crate::git::GitFileAttrs; use crate::header::model::HeaderDef; @@ -54,7 +55,7 @@ impl DocumentFactory { } } - pub fn create_document(&self, filepath: &Path) -> anyhow::Result> { + pub fn create_document(&self, filepath: &Path) -> Result, Error> { let lower_file_name = filepath .file_name() .map(|n| n.to_string_lossy().to_lowercase()) @@ -65,11 +66,13 @@ impl DocumentFactory { .find_map(|m| m.header_type(&lower_file_name)) .unwrap_or_else(|| "unknown".to_string()) .to_lowercase(); - let header_def = self - .definitions - .get(&header_type) - .ok_or_else(|| io::Error::other(format!("header type {header_type} not found"))) - .with_context(|| format!("cannot create document: {}", filepath.display()))?; + let header_def = self.definitions.get(&header_type).ok_or_raise(|| { + Error::new(format!( + "cannot create document: {}, header type {} not found", + filepath.display(), + header_type + )) + })?; let props = self.properties.clone(); diff --git a/fmt/src/document/mod.rs b/fmt/src/document/mod.rs index 8ce01a6d..93d0a790 100644 --- a/fmt/src/document/mod.rs +++ b/fmt/src/document/mod.rs @@ -19,12 +19,15 @@ use std::fs::File; use std::io::BufRead; use std::path::PathBuf; -use anyhow::Context; +use exn::ErrorExt; +use exn::Result; +use exn::ResultExt; use minijinja::context; use minijinja::Environment; use serde::Deserialize; use serde::Serialize; +use crate::error::Error; use crate::header::matcher::HeaderMatcher; use crate::header::model::HeaderDef; use crate::header::parser::parse_header; @@ -60,7 +63,7 @@ impl Document { keywords: &[String], props: HashMap, attrs: Attributes, - ) -> anyhow::Result> { + ) -> Result, Error> { match FileContent::new(&filepath) { Ok(content) => Ok(Some(Self { parser: parse_header(content, &header_def, keywords), @@ -69,13 +72,15 @@ impl Document { props, attrs, })), - Err(e) => { - if matches!(e.kind(), std::io::ErrorKind::InvalidData) { + Err(err) => { + if matches!(err.kind(), std::io::ErrorKind::InvalidData) { log::debug!("skip non-textual file: {}", filepath.display()); Ok(None) } else { - Err(e) - .with_context(|| format!("cannot create document: {}", filepath.display())) + Err(err.raise().raise(Error::new(format!( + "cannot create document: {}", + filepath.display() + )))) } } } @@ -95,7 +100,7 @@ impl Document { &self, header: &HeaderMatcher, strict_check: bool, - ) -> anyhow::Result { + ) -> Result { if strict_check { let file_header = { let mut lines = self.read_file_first_lines(header)?.join("\n"); @@ -115,15 +120,19 @@ impl Document { } } - fn read_file_first_lines(&self, header: &HeaderMatcher) -> std::io::Result> { - let file = File::open(&self.filepath)?; + #[track_caller] + fn read_file_first_lines(&self, header: &HeaderMatcher) -> Result, Error> { + let make_error = || Error::new("cannot read file first line"); + let file = File::open(&self.filepath).or_raise(make_error)?; std::io::BufReader::new(file) .lines() .take(header.header_content_lines_count() + 10) .collect::>>() + .or_raise(make_error) } - fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> std::io::Result { + #[track_caller] + fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> Result { let first_lines = self.read_file_first_lines(header)?; let file_header = first_lines .join("") @@ -137,7 +146,7 @@ impl Document { Ok(file_header) } - pub fn update_header(&mut self, header: &HeaderMatcher) -> anyhow::Result<()> { + pub fn update_header(&mut self, header: &HeaderMatcher) -> Result<(), Error> { let header_str = header.build_for_definition(&self.header_def); let header_str = self.merge_properties(&header_str)?; let begin_pos = self.parser.begin_pos; @@ -155,22 +164,24 @@ impl Document { } } - pub fn save(&mut self, filepath: Option<&PathBuf>) -> anyhow::Result<()> { + pub fn save(&mut self, filepath: Option<&PathBuf>) -> Result<(), Error> { let filepath = filepath.unwrap_or(&self.filepath); fs::write(filepath, self.parser.file_content.content()) - .context(format!("cannot save document {}", filepath.display())) + .or_raise(|| Error::new(format!("cannot save document {}", filepath.display()))) } - pub(crate) fn merge_properties(&self, s: &str) -> anyhow::Result { + pub(crate) fn merge_properties(&self, s: &str) -> Result { let mut env = Environment::new(); env.add_template("template", s) - .context("malformed template")?; + .or_raise(|| Error::new("malformed template"))?; let tmpl = env.get_template("template").expect("template must exist"); - let mut result = tmpl.render(context! { - props => &self.props, - attrs => &self.attrs, - })?; + let mut result = tmpl + .render(context! { + props => &self.props, + attrs => &self.attrs, + }) + .or_raise(|| Error::new("cannot render template"))?; result.push('\n'); Ok(result) } diff --git a/fmt/src/error.rs b/fmt/src/error.rs new file mode 100644 index 00000000..6c88e053 --- /dev/null +++ b/fmt/src/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug)] +pub struct Error { + message: String, +} + +impl Error { + pub fn new(message: impl Into) -> Self { + Self { + message: message.into(), + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for Error {} diff --git a/fmt/src/git.rs b/fmt/src/git.rs index 3ecb286e..67bc6127 100644 --- a/fmt/src/git.rs +++ b/fmt/src/git.rs @@ -18,8 +18,10 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -use anyhow::bail; -use anyhow::Context; +use exn::bail; +use exn::ErrorExt; +use exn::Result; +use exn::ResultExt; use gix::diff::tree_with_rewrites::Change; use gix::status::Item; use gix::Repository; @@ -27,6 +29,7 @@ use walkdir::WalkDir; use crate::config; use crate::config::FeatureGate; +use crate::error::Error; #[derive(Debug, Clone)] pub struct GitContext { @@ -34,7 +37,7 @@ pub struct GitContext { pub config: config::Git, } -pub fn discover(basedir: &Path, config: config::Git) -> anyhow::Result { +pub fn discover(basedir: &Path, config: config::Git) -> Result { let feature = resolve_features(&config); if feature.is_disable() { @@ -49,7 +52,7 @@ pub fn discover(basedir: &Path, config: config::Git) -> anyhow::Result { @@ -65,7 +68,9 @@ pub fn discover(basedir: &Path, config: config::Git) -> anyhow::Result anyhow::Result> { +) -> Result, Error> { let mut attrs = HashMap::new(); if git_context.config.attrs.is_disable() { @@ -114,14 +119,16 @@ pub fn resolve_file_attrs( let worktree = repo.worktree().expect("worktree cannot be absent"); let workdir = repo.workdir().expect("workdir cannot be absent"); - let workdir = workdir - .canonicalize() - .with_context(|| format!("cannot resolve absolute path: {}", workdir.display()))?; + let workdir = workdir.canonicalize().or_raise(|| { + Error::new(format!( + "cannot resolve absolute path: {}", + workdir.display() + )) + })?; let mut excludes = worktree .excludes(None) - .map_err(Box::new) - .context("cannot create gix exclude stack")?; + .or_raise(|| Error::new("cannot create gix exclude stack"))?; let mut update_attrs = |rela_path: &Path, time: gix::date::Time, author: &str| { let filepath = workdir.join(rela_path); @@ -164,38 +171,43 @@ pub fn resolve_file_attrs( option }; - let head = repo.head_commit()?; + let make_error = || Error::new("cannot resolve git file attributes"); + + let head = repo.head_commit().or_raise(make_error)?; let mut next_commit = head.clone(); - for info in head.ancestors().all()? { - let info = info?; - let this_commit = info.object()?; - let time = next_commit.time()?; - let author = next_commit.author()?.name.to_string(); + for info in head.ancestors().all().or_raise(make_error)? { + let info = info.or_raise(make_error)?; + let this_commit = info.object().or_raise(make_error)?; + let time = next_commit.time().or_raise(make_error)?; + let author = next_commit.author().or_raise(make_error)?.name.to_string(); - let this_tree = this_commit.tree()?; - let next_tree = next_commit.tree()?; + let this_tree = this_commit.tree().or_raise(make_error)?; + let next_tree = next_commit.tree().or_raise(make_error)?; - let changes = repo.diff_tree_to_tree(Some(&this_tree), Some(&next_tree), Some(option))?; + let changes = repo + .diff_tree_to_tree(Some(&this_tree), Some(&next_tree), Some(option)) + .or_raise(make_error)?; process_changes(changes, time, &author); next_commit = this_commit; } // process the root commit - let time = next_commit.time()?; - let author = next_commit.author()?.name.to_string(); - let next_tree = next_commit.tree()?; - let changes = repo.diff_tree_to_tree(None, Some(&next_tree), Some(option))?; + let time = next_commit.time().or_raise(make_error)?; + let author = next_commit.author().or_raise(make_error)?.name.to_string(); + let next_tree = next_commit.tree().or_raise(make_error)?; + let changes = repo + .diff_tree_to_tree(None, Some(&next_tree), Some(option)) + .or_raise(make_error)?; process_changes(changes, time, &author); // process dirty working tree - let status_platform = repo.status(gix::progress::Discard)?; - let status_iter = status_platform.into_iter(None)?; + let status_platform = repo.status(gix::progress::Discard).or_raise(make_error)?; + let status_iter = status_platform.into_iter(None).or_raise(make_error)?; let now = gix::date::Time::now_local_or_utc(); for item in status_iter { - let item = item.context("failed to check git status item")?; - match item { + match item.or_raise(|| Error::new("failed to check git status item"))? { Item::IndexWorktree(item) => match item { gix::status::index_worktree::Item::Modification { rela_path, .. } => { let rela_path = gix::path::from_bstring(rela_path); @@ -205,13 +217,17 @@ pub fn resolve_file_attrs( if entry.disk_kind.is_some_and(|k| k.is_dir()) { let dirpath = gix::path::from_bstr(&entry.rela_path) .canonicalize() - .with_context(|| { - format!("cannot resolve absolute path: {}", &entry.rela_path) + .or_raise(|| { + Error::new(format!( + "cannot resolve absolute path: {}", + &entry.rela_path + )) })?; let mut it = WalkDir::new(dirpath).follow_links(false).into_iter(); while let Some(entry) = it.next() { - let entry = entry.context("cannot traverse directory")?; + let entry = + entry.or_raise(|| Error::new("cannot traverse directory"))?; let path = entry.path(); let file_type = entry.file_type(); if !file_type.is_file() && !file_type.is_dir() { @@ -229,7 +245,7 @@ pub fn resolve_file_attrs( }); let platform = excludes .at_path(rela_path, mode) - .context("cannot check gix exclude")?; + .or_raise(|| Error::new("cannot check gix exclude"))?; if file_type.is_dir() { if platform.is_excluded() { diff --git a/fmt/src/header/model.rs b/fmt/src/header/model.rs index ebaf5924..1074d75d 100644 --- a/fmt/src/header/model.rs +++ b/fmt/src/header/model.rs @@ -14,12 +14,14 @@ use std::collections::HashMap; -use anyhow::Context; +use exn::Result; +use exn::ResultExt; use regex::Regex; use serde::Deserialize; use serde::Serialize; use crate::default_true; +use crate::error::Error; #[derive(Debug, Clone)] pub struct HeaderDef { @@ -34,8 +36,8 @@ pub struct HeaderDef { pub pad_lines: bool, pub skip_line_pattern: Option, - pub first_line_detection_pattern: Regex, - pub last_line_detection_pattern: Regex, + pub first_line_detection_pattern: Option, + pub last_line_detection_pattern: Option, } impl HeaderDef { @@ -51,13 +53,17 @@ impl HeaderDef { /// Tells if the given content line is the first line of a possible header of this definition /// kind. pub fn is_first_header_line(&self, line: &str) -> bool { - self.first_line_detection_pattern.is_match(line) + self.first_line_detection_pattern + .as_ref() + .is_some_and(|p| p.is_match(line)) } /// Tells if the given content line is the last line of a possible header of this definition /// kind. pub fn is_last_header_line(&self, line: &str) -> bool { - self.last_line_detection_pattern.is_match(line) + self.last_line_detection_pattern + .as_ref() + .is_some_and(|p| p.is_match(line)) } } @@ -66,8 +72,9 @@ pub fn default_headers() -> HashMap { deserialize_header_definitions(defaults.to_string()).unwrap() } -pub fn deserialize_header_definitions(value: String) -> anyhow::Result> { - let header_styles: HashMap = toml::from_str(&value).map_err(Box::new)?; +pub fn deserialize_header_definitions(value: String) -> Result, Error> { + let header_styles = toml::from_str::>(&value) + .or_raise(|| Error::new("failed to parse header definitions"))?; let headers = header_styles .into_iter() @@ -76,7 +83,7 @@ pub fn deserialize_header_definitions(value: String) -> anyhow::Result anyhow::Result>>()?; + .collect::, Error>>()?; Ok(headers) } diff --git a/fmt/src/lib.rs b/fmt/src/lib.rs index f267e69c..011e2d22 100644 --- a/fmt/src/lib.rs +++ b/fmt/src/lib.rs @@ -14,6 +14,7 @@ pub mod config; pub mod document; +pub mod error; pub mod git; pub mod header; pub mod license; diff --git a/fmt/src/processor.rs b/fmt/src/processor.rs index 6205763e..71890b7b 100644 --- a/fmt/src/processor.rs +++ b/fmt/src/processor.rs @@ -18,12 +18,17 @@ use std::fs; use std::path::Path; use std::path::PathBuf; -use anyhow::Context; +use exn::bail; +use exn::ensure; +use exn::OptionExt; +use exn::Result; +use exn::ResultExt; use crate::config::Config; use crate::document::factory::DocumentFactory; use crate::document::model::default_mapping; use crate::document::Document; +use crate::error::Error; use crate::git; use crate::header::matcher::HeaderMatcher; use crate::header::model::default_headers; @@ -39,36 +44,35 @@ pub trait Callback { fn on_unknown(&mut self, path: &Path); /// Called when the header is matched. - fn on_matched(&mut self, header: &HeaderMatcher, document: Document) -> anyhow::Result<()>; + fn on_matched(&mut self, header: &HeaderMatcher, document: Document) -> Result<(), Error>; /// Called when the header is not matched. - fn on_not_matched(&mut self, header: &HeaderMatcher, document: Document) -> anyhow::Result<()>; + fn on_not_matched(&mut self, header: &HeaderMatcher, document: Document) -> Result<(), Error>; } pub fn check_license_header( run_config: PathBuf, callback: &mut C, -) -> anyhow::Result<()> { +) -> Result<(), Error> { let config = { let name = run_config.display().to_string(); let config = fs::read_to_string(&run_config) - .with_context(|| format!("cannot load config: {name}"))?; + .or_raise(|| Error::new(format!("cannot load config: {name}")))?; toml::from_str::(&config) - .map_err(Box::new) - .with_context(|| format!("cannot parse config file: {name}"))? + .or_raise(|| Error::new(format!("cannot parse config file: {name}")))? }; let config_dir = run_config .parent() - .context("cannot get parent directory of config file")?; + .ok_or_raise(|| Error::new("cannot get parent directory of config file"))?; let basedir = config.base_dir.clone(); - anyhow::ensure!( + ensure!( basedir.is_dir(), - format!( - "{} does not exist or is not a directory.", + Error::new(format!( + "{} does not exist or is not a directory", basedir.display() - ) + )) ); let git_context = git::discover(&basedir, config.git)?; @@ -158,10 +162,7 @@ pub fn check_license_header( if document.is_unsupported() { callback.on_unknown(&file); - } else if document - .header_matched(&header_matcher, config.strict_check) - .context("failed to match header")? - { + } else if document.header_matched(&header_matcher, config.strict_check)? { callback.on_matched(&header_matcher, document)?; } else { callback.on_not_matched(&header_matcher, document)?; @@ -175,7 +176,11 @@ fn load_additional_headers( additional_header: impl AsRef, config: &Config, config_dir: &Path, -) -> anyhow::Result> { +) -> Result, Error> { + fn make_error(path: &Path) -> String { + format!("cannot load additional header {}", path.display()) + } + let additional_header = additional_header.as_ref(); // 1. Based on config directory. @@ -185,8 +190,7 @@ fn load_additional_headers( path }; if let Ok(content) = fs::read_to_string(&path) { - return deserialize_header_definitions(content) - .with_context(|| format!("cannot load header definitions: {}", path.display())); + return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(&path))); } // 2. Based on the base_dir. @@ -196,37 +200,31 @@ fn load_additional_headers( path }; if let Ok(content) = fs::read_to_string(&path) { - return deserialize_header_definitions(content) - .with_context(|| format!("cannot load header definitions: {}", path.display())); + return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(&path))); } // 3. Based on current working directory. - if let Ok(content) = fs::read_to_string(additional_header) { - return deserialize_header_definitions(content).with_context(|| { - format!( - "cannot load header definitions: {}", - additional_header.display() - ) - }); + let path = additional_header; + if let Ok(content) = fs::read_to_string(path) { + return deserialize_header_definitions(content).or_raise(|| Error::new(make_error(path))); } - Err(anyhow::anyhow!( + bail!(Error::new(format!( "cannot find header definitions: {}", additional_header.display() - )) + ))) } -fn load_header_sources(config: &Config, config_dir: &Path) -> anyhow::Result { +fn load_header_sources(config: &Config, config_dir: &Path) -> Result { // 1. inline_header takes priority. if let Some(content) = config.inline_header.as_ref().cloned() { return Ok(HeaderSource { content }); } // 2. Then, try to load from header_path. - let header_path = config - .header_path - .as_ref() - .context("no header source found (both inline_header and header_path are None)")?; + let header_path = config.header_path.as_ref().ok_or_else(|| { + Error::new("no header source found (both inline_header and header_path are None)") + })?; // 2.1 Based on config directory. let path = { @@ -254,6 +252,9 @@ fn load_header_sources(config: &Config, config_dir: &Path) -> anyhow::Result anyhow::Result> { + pub fn select(self) -> Result, Error> { log::debug!( "selecting files with baseDir: {}, included: {:?}, excluded: {:?}", self.basedir.display(), @@ -71,24 +74,25 @@ impl Selection { let (excludes, reverse_excludes) = { let mut excludes = self.excludes; - let mut reverse_excludes = vec![]; - // TODO(tisonkun): can be simplified and no clone when extract_if stable - excludes.retain_mut(|pat| { - if pat.starts_with('!') { - pat.remove(0); - reverse_excludes.push(pat.clone()); - false - } else { - true - } - }); + let reverse_excludes = excludes + .extract_if(.., |pat| { + if pat.starts_with('!') { + pat.remove(0); + true + } else { + false + } + }) + .collect::>(); (excludes, reverse_excludes) }; let includes = self.includes; - anyhow::ensure!( + ensure!( includes.iter().all(|pat| !pat.starts_with('!')), - "select files failed; reverse pattern is not allowed for includes: {includes:?}" + Error::new(format!( + "select files failed; reverse pattern is not allowed for includes: {includes:?}" + )) ); let ignore = self.git_context.config.ignore.is_auto(); @@ -116,7 +120,9 @@ fn select_files_with_ignore( excludes: &[String], reverse_excludes: &[String], turn_on_git_ignore: bool, -) -> anyhow::Result> { +) -> Result, Error> { + let make_error = || Error::new("failed to select files with ignore crate"); + log::debug!(turn_on_git_ignore; "Selecting files with ignore crate"); let mut result = vec![]; @@ -131,21 +137,21 @@ fn select_files_with_ignore( .overrides({ let mut builder = OverrideBuilder::new(basedir); for pat in includes.iter() { - builder.add(pat)?; + builder.add(pat).or_raise(make_error)?; } for pat in excludes.iter() { let pat = format!("!{pat}"); - builder.add(pat.as_str())?; + builder.add(pat.as_str()).or_raise(make_error)?; } for pat in reverse_excludes.iter() { - builder.add(pat)?; + builder.add(pat).or_raise(make_error)?; } - builder.build()? + builder.build().or_raise(make_error)? }) .build(); for mat in walker { - let mat = mat?; + let mat = mat.or_raise(make_error)?; if mat.file_type().map(|ft| ft.is_file()).unwrap_or(false) { result.push(mat.into_path()) } @@ -160,44 +166,51 @@ fn select_files_with_git( excludes: &[String], reverse_excludes: &[String], repo: gix::Repository, -) -> anyhow::Result> { +) -> Result, Error> { log::debug!("selecting files with git helper"); let mut result = vec![]; let matcher = { + let make_error = || Error::new("failed to select files with ignore crate"); + let mut builder = OverrideBuilder::new(basedir); for pat in includes.iter() { - builder.add(pat)?; + builder.add(pat).or_raise(make_error)?; } for pat in excludes.iter() { let pat = format!("!{pat}"); - builder.add(pat.as_str())?; + builder.add(pat.as_str()).or_raise(make_error)?; } for pat in reverse_excludes.iter() { - builder.add(pat)?; + builder.add(pat).or_raise(make_error)?; } - builder.build()? + builder.build().or_raise(make_error)? }; - let basedir = basedir - .canonicalize() - .with_context(|| format!("cannot resolve absolute path: {}", basedir.display()))?; + let basedir = basedir.canonicalize().or_raise(|| { + Error::new(format!( + "cannot resolve absolute path: {}", + basedir.display() + )) + })?; let mut it = WalkDir::new(basedir.clone()) .follow_links(false) .into_iter(); let workdir = repo.workdir().expect("workdir cannot be absent"); - let workdir = workdir - .canonicalize() - .with_context(|| format!("cannot resolve absolute path: {}", workdir.display()))?; + let workdir = workdir.canonicalize().or_raise(|| { + Error::new(format!( + "cannot resolve absolute path: {}", + workdir.display() + )) + })?; let worktree = repo.worktree().expect("worktree cannot be absent"); let mut excludes = worktree .excludes(None) - .map_err(Box::new) - .context("cannot create gix exclude stack")?; + .or_raise(|| Error::new("cannot create gix exclude stack"))?; while let Some(entry) = it.next() { - let entry = entry.context("cannot traverse directory")?; + let entry = entry.or_raise(|| Error::new("cannot traverse directory"))?; let path = entry.path(); let file_type = entry.file_type(); if !file_type.is_file() && !file_type.is_dir() { @@ -215,7 +228,7 @@ fn select_files_with_git( }); let platform = excludes .at_path(rela_path, mode) - .context("cannot check gix exclude")?; + .or_raise(|| Error::new("cannot check gix exclude"))?; if file_type.is_dir() { if platform.is_excluded() { From 02e61865f84766b95e81978ccacf8cd457fbed30 Mon Sep 17 00:00:00 2001 From: tison Date: Mon, 9 Feb 2026 18:22:02 +0800 Subject: [PATCH 2/2] fixup Signed-off-by: tison --- fmt/src/error.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fmt/src/error.rs b/fmt/src/error.rs index 6c88e053..1d053714 100644 --- a/fmt/src/error.rs +++ b/fmt/src/error.rs @@ -1,3 +1,17 @@ +// Copyright 2024 tison +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #[derive(Debug)] pub struct Error { message: String,