Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 10 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ members = ["cli", "fmt"]
resolver = "2"

[workspace.package]
version = "6.4.2"
version = "6.5.0"
edition = "2021"
authors = ["tison <wander4096@gmail.com>"]
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" }
Expand Down
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
16 changes: 9 additions & 7 deletions cli/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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(());
}
Expand All @@ -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)
}
}
Expand Down
2 changes: 1 addition & 1 deletion fmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 11 additions & 8 deletions fmt/src/document/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -54,7 +55,7 @@ impl DocumentFactory {
}
}

pub fn create_document(&self, filepath: &Path) -> anyhow::Result<Option<Document>> {
pub fn create_document(&self, filepath: &Path) -> Result<Option<Document>, Error> {
let lower_file_name = filepath
.file_name()
.map(|n| n.to_string_lossy().to_lowercase())
Expand All @@ -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();

Expand Down
49 changes: 30 additions & 19 deletions fmt/src/document/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,7 +63,7 @@ impl Document {
keywords: &[String],
props: HashMap<String, String>,
attrs: Attributes,
) -> anyhow::Result<Option<Self>> {
) -> Result<Option<Self>, Error> {
match FileContent::new(&filepath) {
Ok(content) => Ok(Some(Self {
parser: parse_header(content, &header_def, keywords),
Expand All @@ -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()
))))
}
}
}
Expand All @@ -95,7 +100,7 @@ impl Document {
&self,
header: &HeaderMatcher,
strict_check: bool,
) -> anyhow::Result<bool> {
) -> Result<bool, Error> {
if strict_check {
let file_header = {
let mut lines = self.read_file_first_lines(header)?.join("\n");
Expand All @@ -115,15 +120,19 @@ impl Document {
}
}

fn read_file_first_lines(&self, header: &HeaderMatcher) -> std::io::Result<Vec<String>> {
let file = File::open(&self.filepath)?;
#[track_caller]
fn read_file_first_lines(&self, header: &HeaderMatcher) -> Result<Vec<String>, 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::<std::io::Result<Vec<_>>>()
.or_raise(make_error)
}

fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> std::io::Result<String> {
#[track_caller]
fn read_file_header_on_one_line(&self, header: &HeaderMatcher) -> Result<String, Error> {
let first_lines = self.read_file_first_lines(header)?;
let file_header = first_lines
.join("")
Expand All @@ -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;
Expand All @@ -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<String> {
pub(crate) fn merge_properties(&self, s: &str) -> Result<String, Error> {
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)
}
Expand Down
34 changes: 34 additions & 0 deletions fmt/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2024 tison <wander4096@gmail.com>
//
// 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,
}

impl Error {
pub fn new(message: impl Into<String>) -> 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 {}
Loading