From 94d9a1b64713b33c8dda976e694155df2f0c8e86 Mon Sep 17 00:00:00 2001 From: Kaur Kuut Date: Mon, 2 Feb 2026 00:17:49 +0200 Subject: [PATCH] Add `copyright` command. --- Cargo.lock | 61 +++++++++++++++++++++++ Cargo.toml | 1 + prep/Cargo.toml | 1 + prep/src/cmd/copyright.rs | 61 +++++++++++++++++++++++ prep/src/cmd/mod.rs | 1 + prep/src/main.rs | 3 ++ prep/src/ui/help.rs | 101 ++++++++++++++++++++++---------------- prep/src/ui/mod.rs | 24 ++------- prep/src/ui/style.rs | 56 +++++++++++++++++++++ 9 files changed, 248 insertions(+), 61 deletions(-) create mode 100644 prep/src/cmd/copyright.rs create mode 100644 prep/src/ui/style.rs diff --git a/Cargo.lock b/Cargo.lock index 2ab99bc..1a4eb23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "heck" version = "0.5.0" @@ -116,18 +125,31 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prep" version = "0.1.0" dependencies = [ "anyhow", "clap", + "time", ] [[package]] @@ -148,6 +170,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "strsim" version = "0.11.1" @@ -165,6 +207,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "time" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index 661f467..c53a9c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,4 @@ rust.missing_docs = "warn" [workspace.dependencies] anyhow = "1.0.100" clap = "4.5.56" +time = "0.3.46" diff --git a/prep/Cargo.toml b/prep/Cargo.toml index d5ea5e6..c202f71 100644 --- a/prep/Cargo.toml +++ b/prep/Cargo.toml @@ -21,3 +21,4 @@ workspace = true [dependencies] anyhow.workspace = true clap = { workspace = true, features = ["derive"] } +time.workspace = true diff --git a/prep/src/cmd/copyright.rs b/prep/src/cmd/copyright.rs new file mode 100644 index 0000000..12a7452 --- /dev/null +++ b/prep/src/cmd/copyright.rs @@ -0,0 +1,61 @@ +// Copyright 2026 the Prep Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +use std::process::Command; + +use anyhow::{Context, bail, ensure}; +use time::UtcDateTime; + +use crate::ui; +use crate::ui::style::{ERROR, HEADER, LITERAL, NOTE}; + +// TODO: Allow configuring the regex +// TODO: Allow excluding files from the check +// TODO: ALlow configuring the project name +// TODO: Allow configuring the license (or fetch it from Cargo.toml per-package?) + +const REGEX: &str = r#"^// Copyright (19|20)[\d]{2} (.+ and )?the Prep Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0 OR MIT$\n\n"#; + +/// Verify copyright headers. +pub fn run() -> anyhow::Result<()> { + let mut cmd = Command::new("rg"); + let cmd = cmd + .arg(REGEX) + .arg("--files-without-match") + .arg("--multiline") + .args(["-g", "*.rs"]) + .arg("."); + + ui::print_cmd(cmd); + + let output = cmd.output().context("failed to run ripgrep")?; + + // ripgrep exits with code 1 in case of no matches, code 2 in case of error + ensure!( + output.status.success() || output.status.code().is_some_and(|code| code == 1), + "ripgrep failed: {}", + output.status + ); + + if !output.stdout.is_empty() { + print_missing(String::from_utf8(output.stdout).unwrap()); + bail!("failed copyright header verification"); + } + + let h = HEADER; + eprintln!(" {h}Verified{h:#} all source files have correct copyright headers."); + + Ok(()) +} + +fn print_missing(msg: String) { + let (e, l, n) = (ERROR, LITERAL, NOTE); + let year = UtcDateTime::now().year(); + + eprintln!("{e}The following files lack the correct copyright header:{e:#}"); + eprintln!("{l}{msg}{l:#}"); + eprintln!("{n}Please add the following header:{n:#}\n"); + eprintln!("// Copyright {year} the Prep Authors"); + eprintln!("// SPDX-License-Identifier: Apache-2.0 OR MIT"); + eprintln!("\n... rest of the file ...\n"); +} diff --git a/prep/src/cmd/mod.rs b/prep/src/cmd/mod.rs index 732aa17..d4f7ba8 100644 --- a/prep/src/cmd/mod.rs +++ b/prep/src/cmd/mod.rs @@ -5,6 +5,7 @@ use clap::ValueEnum; pub mod ci; pub mod clippy; +pub mod copyright; pub mod format; /// Cargo targets. diff --git a/prep/src/main.rs b/prep/src/main.rs index fd75aff..0f32388 100644 --- a/prep/src/main.rs +++ b/prep/src/main.rs @@ -35,6 +35,8 @@ enum Commands { #[arg(short, long)] strict: bool, }, + #[command()] + Copyright, #[command(alias = "fmt")] Format { #[arg(short, long)] @@ -58,6 +60,7 @@ fn main() -> anyhow::Result<()> { no_fail_fast, } => cmd::ci::run(extended, !no_fail_fast), Commands::Clippy { targets, strict } => cmd::clippy::run(targets, strict), + Commands::Copyright => cmd::copyright::run(), Commands::Format { check } => cmd::format::run(check), } } diff --git a/prep/src/ui/help.rs b/prep/src/ui/help.rs index dd08dc3..9a58764 100644 --- a/prep/src/ui/help.rs +++ b/prep/src/ui/help.rs @@ -4,7 +4,7 @@ use clap::Command; use clap::builder::StyledStr; -use crate::ui; +use crate::ui::style::{HEADER, LITERAL, PLACEHOLDER}; /// Sets our custom help messages. pub fn set(cmd: Command) -> Command { @@ -18,6 +18,8 @@ pub fn set(cmd: Command) -> Command { scmd.override_help(format_msg()) } else if name == "clippy" { scmd.override_help(clippy_msg()) + } else if name == "copyright" { + scmd.override_help(copyright_msg()) } else { panic!("Sub-command '{name}' help message is not implemented"); } @@ -26,22 +28,23 @@ pub fn set(cmd: Command) -> Command { /// Returns the main help message. pub fn root_msg() -> StyledStr { - let (gb, cb, bb) = ui::styles(); + let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER); let help = format!( "\ Prepare Rust projects for greatness. -{gb}Usage:{gb:#} {cb}prep{cb:#} {bb}[command] [options]{bb:#} +{h}Usage:{h:#} {l}prep{l:#} {p}[command] [options]{p:#} -{gb}Commands:{gb:#} - {cb} ci {cb:#}Verify for CI. - {cb}clp clippy {cb:#}Analyze with Clippy. - {cb}fmt format {cb:#}Format with rustfmt. - {cb} help {cb:#}Print help for the provided command. +{h}Commands:{h:#} + {l} ci {l:#}Verify for CI. + {l}clp clippy {l:#}Analyze with Clippy. + {l} copyright {l:#}Verify copyright headers. + {l}fmt format {l:#}Format with rustfmt. + {l} help {l:#}Print help for the provided command. -{gb}Options:{gb:#} - {cb}-h --help {cb:#}Print help for the provided command. - {cb}-V --version {cb:#}Print version information. +{h}Options:{h:#} + {l}-h --help {l:#}Print help for the provided command. + {l}-V --version {l:#}Print version information. " ); @@ -50,19 +53,18 @@ Prepare Rust projects for greatness. /// Returns the `ci` help message. fn ci_msg() -> StyledStr { - let (gb, cb, bb) = ui::styles(); - + let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER); let help = format!( "\ Verify the Rust workspace for CI. -{gb}Usage:{gb:#} {cb}prep ci{cb:#} {bb}[options]{bb:#} +{h}Usage:{h:#} {l}prep ci{l:#} {p}[options]{p:#} -{gb}Options:{gb:#} - {cb}-e --extended {cb:#}Run the extended verification suite. - ···· ······Good idea for actual CI, rarely useful for local prep. - {cb}-n --no-fail-fast {cb:#}Keep going when encountering an error. - {cb}-h --help {cb:#}Print this help message. +{h}Options:{h:#} + {l}-e --extended {l:#}Run the extended verification suite. + ··· ·····Good idea for actual CI, rarely useful for local prep. + {l}-n --no-fail-fast {l:#}Keep going when encountering an error. + {l}-h --help {l:#}Print this help message. " ) .replace("·", ""); @@ -70,20 +72,41 @@ Verify the Rust workspace for CI. StyledStr::from(help) } -/// Returns the `format` help message. -fn format_msg() -> StyledStr { - let (gb, cb, bb) = ui::styles(); +/// Returns the `clippy` help message. +fn clippy_msg() -> StyledStr { + let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER); + let help = format!( + "\ +Analyze the Rust workspace with Clippy. + +{h}Usage:{h:#} {l}prep clp{l:#} {p}[options]{p:#} +··· ····· {l}prep clippy{l:#} {p}[options]{p:#} +{h}Options:{h:#} + {l}-c --crates {l:#}Target specified crates. Possible values: + ··· ·····{p}main{p:#} -> Binaries and the main library. (default) + ··· ·····{p}aux{p:#} -> Examples, tests, and benches. + ··· ·····{p}all{p:#} -> All of the above. + {l}-s --strict {l:#}Treat warnings as errors. + {l}-h --help {l:#}Print this help message. +" + ) + .replace("·", ""); + + StyledStr::from(help) +} + +/// Returns the `copyright` help message. +fn copyright_msg() -> StyledStr { + let (h, l) = (HEADER, LITERAL); let help = format!( "\ -Format the Rust workspace with rustfmt. +Verify that all Rust source files have the correct copyright header. -{gb}Usage:{gb:#} {cb}prep fmt{cb:#} {bb}[options]{bb:#} -···· ······ {cb}prep format{cb:#} {bb}[options]{bb:#} +{h}Usage:{h:#} {l}prep copyright{l:#} -{gb}Options:{gb:#} - {cb}-c --check {cb:#}Verify that the workspace is already formatted. - {cb}-h --help {cb:#}Print this help message. +{h}Options:{h:#} + {l}-h --help {l:#}Print this help message. " ) .replace("·", ""); @@ -91,23 +114,19 @@ Format the Rust workspace with rustfmt. StyledStr::from(help) } -/// Returns the `clippy` help message. -fn clippy_msg() -> StyledStr { - let (gb, cb, bb) = ui::styles(); +/// Returns the `format` help message. +fn format_msg() -> StyledStr { + let (h, l, p) = (HEADER, LITERAL, PLACEHOLDER); let help = format!( "\ -Analyze the Rust workspace with Clippy. +Format the Rust workspace with rustfmt. -{gb}Usage:{gb:#} {cb}prep clp{cb:#} {bb}[options]{bb:#} -···· ······ {cb}prep clippy{cb:#} {bb}[options]{bb:#} +{h}Usage:{h:#} {l}prep fmt{l:#} {p}[options]{p:#} +··· ····· {l}prep format{l:#} {p}[options]{p:#} -{gb}Options:{gb:#} - {cb}-c --crates {cb:#}Target specified crates. Possible values: - ···· ······{bb}main{bb:#} -> Binaries and the main library. (default) - ···· ······{bb}aux{bb:#} -> Examples, tests, and benches. - ···· ······{bb}all{bb:#} -> All of the above. - {cb}-s --strict {cb:#}Treat warnings as errors. - {cb}-h --help {cb:#}Print this help message. +{h}Options:{h:#} + {l}-c --check {l:#}Verify that the workspace is already formatted. + {l}-h --help {l:#}Print this help message. " ) .replace("·", ""); diff --git a/prep/src/ui/mod.rs b/prep/src/ui/mod.rs index b799fbc..8eb099b 100644 --- a/prep/src/ui/mod.rs +++ b/prep/src/ui/mod.rs @@ -2,28 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 OR MIT pub mod help; +pub mod style; use std::ffi::OsStr; use std::process::Command; -use clap::builder::StyledStr; -use clap::builder::styling::{AnsiColor, Style}; - /// Prints the binary name and its arguments to stderr. pub fn print_cmd(cmd: &Command) { let bin = cmd.get_program(); let args = cmd.get_args().collect::>().join(OsStr::new(" ")); - let (g, _, _) = styles(); - - let msg = format!( - " {g}Running{g:#} `{} {}`", + let h = style::HEADER; + eprintln!( + " {h}Running{h:#} `{} {}`", bin.display(), args.display() ); - let msg = StyledStr::from(msg); - - eprintln!("{}", msg.ansi()); } /// Prints the main help message. @@ -31,13 +25,3 @@ pub fn print_help() { // TODO: Don't print ANSI codes when not supported by the environment. eprint!("{}", help::root_msg().ansi()); } - -/// Returns `(header, cmd_or_arg, optional)` styles. -/// -/// These correspond to `(green, cyan, blue)`. -pub fn styles() -> (Style, Style, Style) { - let g = AnsiColor::Green.on_default().bold(); // Green - let c = AnsiColor::BrightCyan.on_default().bold(); // Cyan - let b = AnsiColor::Cyan.on_default(); // Blue - (g, c, b) -} diff --git a/prep/src/ui/style.rs b/prep/src/ui/style.rs new file mode 100644 index 0000000..3e1bb6b --- /dev/null +++ b/prep/src/ui/style.rs @@ -0,0 +1,56 @@ +// Copyright 2026 the Prep Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Color styles based on Cargo color choices. + +#![allow(unused)] + +use clap::builder::styling::{AnsiColor, Style}; + +const USE_WINDOWS_COLORS: bool = cfg!(windows); +const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightCyan.on_default() +} else { + AnsiColor::BrightBlue.on_default() +}; +const YELLOW: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightYellow.on_default() +} else { + AnsiColor::Yellow.on_default() +}; +pub const EMPHASIS: Style = if USE_WINDOWS_COLORS { + AnsiColor::BrightWhite.on_default() +} else { + Style::new() +} +.bold(); + +pub const NOP: Style = Style::new(); +pub const INFO: Style = BRIGHT_BLUE.bold(); +pub const NOTE: Style = AnsiColor::BrightGreen.on_default().bold(); +pub const HELP: Style = AnsiColor::BrightCyan.on_default().bold(); +pub const WARN: Style = YELLOW.bold(); +pub const ERROR: Style = AnsiColor::BrightRed.on_default().bold(); +pub const GOOD: Style = AnsiColor::BrightGreen.on_default().bold(); +pub const VALID: Style = AnsiColor::BrightCyan.on_default().bold(); +pub const INVALID: Style = WARN; +pub const TRANSIENT: Style = HELP; +pub const HEADER: Style = AnsiColor::BrightGreen.on_default().bold(); +pub const USAGE: Style = AnsiColor::BrightGreen.on_default().bold(); +pub const LITERAL: Style = AnsiColor::BrightCyan.on_default().bold(); +pub const PLACEHOLDER: Style = AnsiColor::Cyan.on_default(); +pub const LINE_NUM: Style = BRIGHT_BLUE.bold(); +pub const CONTEXT: Style = BRIGHT_BLUE.bold(); +pub const ADDITION: Style = AnsiColor::BrightGreen.on_default(); +pub const REMOVAL: Style = AnsiColor::BrightRed.on_default(); + +pub const UPDATE_ADDED: Style = NOTE; +pub const UPDATE_REMOVED: Style = ERROR; +pub const UPDATE_UPGRADED: Style = GOOD; +pub const UPDATE_DOWNGRADED: Style = WARN; +pub const UPDATE_UNCHANGED: Style = Style::new().bold(); + +pub const DEP_NORMAL: Style = Style::new().dimmed(); +pub const DEP_BUILD: Style = AnsiColor::Blue.on_default().bold(); +pub const DEP_DEV: Style = AnsiColor::Cyan.on_default().bold(); +pub const DEP_FEATURE: Style = AnsiColor::Magenta.on_default().dimmed();