Compare commits

...

37 Commits
0.1.0 ... main

Author SHA1 Message Date
Bauke 87c53d5e09
Add the silent flag to the install command. 2024-01-24 18:29:38 +01:00
Bauke dff366f9a0
Update Makefile. 2024-01-24 18:25:42 +01:00
Bauke 595b8c25c1
Add typos for source code spellchecking. 2024-01-24 18:23:20 +01:00
Bauke caa158ce9f
Add additional packages. 2024-01-24 18:18:03 +01:00
Bauke b98426a993
Fix successful typo. 2024-01-24 17:54:19 +01:00
Bauke d12895184b
Install tools in a local output directory. 2024-01-19 15:33:01 +01:00
Bauke 9a4299e5a1
Add cargo-edit and a shell hook to install Hooked. 2024-01-19 14:16:15 +01:00
Bauke 9e7dd2e3dd
Add Rust and the toolchain via an overlay. 2024-01-19 14:00:02 +01:00
Bauke e7df35a765
Add the --noise-level option and implementation. 2024-01-18 14:37:02 +01:00
Bauke a9ee3c20fe
Derive Clone and implement From<String> for automatic Clap parsing. 2024-01-18 14:32:14 +01:00
Bauke 6b6c48c476
Update all the dependencies. 2024-01-18 14:21:43 +01:00
Bauke 0634e86b3d
Add a basic noise level implementation. 2024-01-18 13:52:15 +01:00
Bauke f0030fd57f
Add lazy_static. 2024-01-18 13:41:11 +01:00
Bauke df397b2cda
Make noise_level optional for hooks. 2024-01-18 12:27:27 +01:00
Bauke 260bcb7ca5
Add a noise level configuration option. 2024-01-17 18:43:17 +01:00
Bauke ba6d4fb0d1
Add missing documentation. 2024-01-17 18:32:11 +01:00
Bauke d031374360
Enable workspace lints. 2024-01-17 18:25:57 +01:00
Bauke 762051116e
Add mdbook-linkcheck. 2024-01-17 18:19:54 +01:00
Bauke 15dcca32b0
Add Nix flake and direnv files. 2024-01-17 18:15:36 +01:00
Bauke 3da03abe16
Move the lints to the Cargo workspace level and fix any issues. 2024-01-17 18:10:25 +01:00
Bauke 6512eaac5a
Add resolver to remove the warning from cargo. 2024-01-17 18:06:48 +01:00
Bauke 0d09e2e086
Factor creating a GlobSet from a Vec<String> out to a utility function. 2022-11-28 22:48:35 +01:00
Bauke 47b1b7ec51
Move utilities to a directory. 2022-11-28 22:43:26 +01:00
Bauke 5d26a72c8b
Rename git_staged to staged. 2022-11-27 17:29:44 +01:00
Bauke 4a4974fa35
Add git_staged documentation. 2022-11-20 13:07:32 +01:00
Bauke e4ed623e64
Skip hook if no globs are matched. 2022-11-20 12:57:58 +01:00
Bauke 651699c40a
Add git_staged CLI functionality. 2022-11-20 12:39:08 +01:00
Bauke da0e38e3bb
Add git_staged as a config option. 2022-11-20 12:20:59 +01:00
Bauke 4ba723d5cc
Make Clippy happy, use license symlinks. 2022-11-07 14:42:08 +01:00
Bauke a8f587f43a
Add extra metadata to Cargo.toml files. 2022-11-07 14:27:09 +01:00
Bauke 82cbc580dc
Use the generate CLI reference. 2022-11-06 18:46:28 +01:00
Bauke 5bb3a282c3
Generate the subcommands usage reference automatically. 2022-11-06 18:44:23 +01:00
Bauke a4d0431a32
Add the template option documentation. 2022-11-04 19:19:14 +01:00
Bauke 8d1baaf57f
Add the general.template option. 2022-11-04 19:13:04 +01:00
Bauke 1a79f9e051
Refactor subcommands into their own functions. 2022-11-04 18:09:15 +01:00
Bauke 9c11ae3db6
Switch to dedicated structs for subcommand arguments. 2022-11-04 17:54:05 +01:00
Bauke aed22fad90
Add the .deb installation section. 2022-11-03 14:16:40 +01:00
41 changed files with 1392 additions and 618 deletions

3
.envrc Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bash
use flake

12
.gitignore vendored
View File

@ -1,9 +1,7 @@
# Generated by Cargo
debug/
target/
# Code coverage results
.direnv/
.vscode/
coverage/
# mdBook output
debug/
hooked-book/book/
outputs/
target/

1007
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,3 +3,11 @@ members = [
"hooked-cli",
"hooked-config"
]
resolver = "2"
[workspace.lints.clippy]
missing_docs_in_private_items = "warn"
[workspace.lints.rust]
missing_docs = "warn"
unsafe_code = "forbid"

View File

@ -1,3 +1,7 @@
[[pre_commit]]
name = "Cargo Complete Check"
command = "cargo make complete-check"
[[pre_commit]]
name = "Typos"
command = "typos"

View File

@ -1,32 +1,18 @@
[env]
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
[tasks.fmt]
command = "cargo"
args = ["fmt", "${@}"]
[tasks.check]
command = "cargo"
args = ["check", "${@}"]
[tasks.clippy]
command = "cargo"
args = ["clippy", "${@}"]
[tasks.test]
command = "cargo"
args = ["test", "${@}"]
[tasks.doc]
command = "cargo"
args = ["doc", "${@}"]
[tasks.build]
command = "cargo"
args = ["build", "${@}"]
[tasks.complete-check]
dependencies = ["fmt", "check", "clippy", "test", "doc", "build"]
dependencies = [
"format",
"check",
"clippy",
"test",
"code-coverage",
"docs",
"build",
"audit-flow",
"outdated-flow",
]
[tasks.code-coverage]
workspace = false
@ -38,7 +24,7 @@ args = [
"--out=html",
"--output-dir=coverage",
"--skip-clean",
"--target-dir=target/tarpaulin"
"--target-dir=target/tarpaulin",
]
[tasks.book]

View File

@ -2,6 +2,10 @@
> **Git hooks manager.**
## Docs
See [hooked.holllo.org](https://hooked.holllo.org) for documentation.
## Feedback
Found a problem or want to request a new feature? Email [helllo@holllo.org](mailto:helllo@holllo.org) and I'll see what I can do for you.

128
flake.lock Normal file
View File

@ -0,0 +1,128 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1705635624,
"narHash": "sha256-DU0schxQOtBNO1c9hUsgYl+QMOXQMfRT7Qw/mg+ayno=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4471857c0a4a8a0ffc7bdbeaf1b998746ce12a82",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1681358109,
"narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1705630663,
"narHash": "sha256-f+kcR17ZtwMyCEtNAfpD0Mv6qObNKoJ41l+6deoaXi8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "47cac072a313d9cce884b9ea418d2bf712fa23dd",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

17
flake.nix Normal file
View File

@ -0,0 +1,17 @@
{
inputs = {
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
in
{
devShells.default = import ./shell.nix { inherit pkgs; };
}
);
}

View File

@ -0,0 +1,42 @@
This file is automatically generated using the cli-reference subcommand.
Use `cargo run -- cli-reference` to generate it.
// ANCHOR: install
$ hooked install --help
Install Hooked into ".git/hooks"
Usage: hooked install [OPTIONS]
Options:
--overwrite Overwrite existing files
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
// ANCHOR_END: install
// ANCHOR: run
$ hooked run --help
Manually run hooks
Usage: hooked run [OPTIONS] <HOOK_TYPE>
Arguments:
<HOOK_TYPE> The hook type to run [possible values: pre-commit]
Options:
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
// ANCHOR_END: run
// ANCHOR: uninstall
$ hooked uninstall --help
Remove installed hooks
Usage: hooked uninstall [OPTIONS]
Options:
--all Remove hooks not installed by Hooked
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
// ANCHOR_END: uninstall

View File

@ -3,16 +3,7 @@
The `install` command creates the scripts inside `.git/hooks`.
```sh
$ hooked install --help
Install Hooked into ".git/hooks"
Usage: hooked install [OPTIONS]
Options:
--overwrite Overwrite existing files
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
{{#include ../cli-reference.txt:install}}
```
Below is the default script template that Hooked uses, where `hook_type` is the type of hook to run (like `pre-commit`) and `config_path` is the `general.config` field from the parsed configuration.
@ -20,3 +11,7 @@ Below is the default script template that Hooked uses, where `hook_type` is the
```sh
{{#include ../../../hooked-cli/source/templates/default.sh}}
```
You can provide your own template by using the `general.template` configuration setting. If you do, make sure you include a line somewhere that says `# Installed by Hooked.` for the [uninstall CLI command][cli-uninstall].
[cli-uninstall]: ./uninstall.md

View File

@ -2,17 +2,6 @@
The `run` command manually runs configured hooks.
```
$ hooked run --help
Manually run hooks
Usage: hooked run [OPTIONS] <HOOK_TYPE>
Arguments:
<HOOK_TYPE> The hook type to run [possible values: pre-commit]
Options:
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
```sh
{{#include ../cli-reference.txt:run}}
```

View File

@ -3,16 +3,7 @@
The `uninstall` command removes script files inside `.git/hooks`.
```sh
hooked uninstall --help
Remove installed hooks
Usage: hooked uninstall [OPTIONS]
Options:
--all Remove hooks not installed by Hooked
-c, --config <CONFIG> Path to a Hooked configuration [default: Hooked.toml]
-h, --help Print help information
-V, --version Print version information
{{#include ../cli-reference.txt:uninstall}}
```
By default Hooked will only remove scripts that have a `# Installed by Hooked.` line in them, using `--all` however will remove all script files.

View File

@ -10,11 +10,13 @@ The `general` [table][toml-table] is for main Hooked configuration.
|-----|------|---------|-------------|
| config | String | Hooked.toml | The configuration file to use. If your configuration file isn't `Hooked.toml` you should set this accordingly. |
| directory | String | hooks | The directory Hooked looks in for anything related to files. For example: scripts, templates, etc. |
| template | Optional string | | Path to a custom template to be used when installing scripts. See the [install CLI command][cli-install] for more details. |
```toml
[general]
config = "Hooked.toml"
directory = "hooks"
template = "template.sh"
```
## Pre-commit
@ -27,6 +29,7 @@ Pre-commit hooks are defined using `pre_commit` [arrays of tables][toml-arrays-o
| command[^command-and-script] | String | | A command to run when the hook is called. |
| script[^command-and-script] | String | | A script to run when the hook is called. This script should be executable and be located inside the configured general directory. |
| on_failure | String | stop | What to do when the hook task returns a non-zero status code. Can be either "continue" or "stop". |
| staged | Optional list of strings | | A list of [globs][globset-docs] that will be checked against staged files. If none of the globs match the hook will be skipped. With no globs defined at all the hook will always run. |
```toml
[[pre_commit]]
@ -37,11 +40,14 @@ command = "echo \"Hey, $USER!\""
name = "Script Example"
script = "example.sh"
on_failure = "continue"
staged = ["*.txt"]
```
## Footnotes
[^command-and-script]: When both a command and script are defined in a hook, *only* the command will be run.
[cli-install]: ../cli/install.md
[globset-docs]: https://docs.rs/globset/0.4.9/globset/#syntax
[toml-table]: https://toml.io/en/v1.0.0#table
[toml-arrays-of-tables]: https://toml.io/en/v1.0.0#array-of-tables

View File

@ -15,4 +15,13 @@ cargo install hooked-cli
Precompiled `x86_64-unknown-linux-gnu` binaries are available on the [Releases page][releases].
## Debian & Derivatives
An amd64 `.deb` file is available on the [Releases page][releases].
```sh
# Don't forget to change <version>.
dpkg --install hooked-cli_<version>_amd64.deb
```
[releases]: https://git.bauke.xyz/Holllo/hooked/releases

View File

@ -1,7 +1,10 @@
[package]
name = "hooked-cli"
description = "Git hooks manager."
documentation = "https://hooked.holllo.org"
homepage = "https://hooked.holllo.org"
repository = "https://git.bauke.xyz/Holllo/hooked"
readme = "../README.md"
license = "AGPL-3.0-or-later"
version = "0.1.0"
authors = ["Holllo <helllo@holllo.org>"]
@ -11,22 +14,27 @@ edition = "2021"
name = "hooked"
path = "source/main.rs"
[lints]
workspace = true
[dependencies]
color-eyre = "0.6.2"
owo-colors = "3.5.0"
globset = "0.4.14"
lazy_static = "1.4.0"
owo-colors = "4.0.0"
subprocess = "0.2.9"
supports-color = "1.3.0"
tera = "1.17.1"
supports-color = "2.1.0"
tera = "1.19.1"
[dependencies.clap]
features = ["derive"]
version = "4.0.18"
version = "4.4.18"
[dependencies.hooked-config]
path = "../hooked-config"
version = "0.1.0"
[dev-dependencies]
assert_cmd = "2.0.5"
insta = "1.21.0"
test-case = "2.2.2"
assert_cmd = "2.0.13"
insta = "1.34.0"
test-case = "3.3.1"

1
hooked-cli/LICENSE Symbolic link
View File

@ -0,0 +1 @@
LICENSE

View File

@ -0,0 +1,52 @@
//! The `cli-reference` subcommand, only available in debug mode.
use std::{fs::write, process::Command, str};
use {
color_eyre::Result,
hooked_config::Config,
tera::{Context, Tera},
};
use crate::cli::CliReferenceArgs;
/// The CLI reference template.
const REFERENCE_TEMPLATE: &str = include_str!("../templates/cli-reference.txt");
/// The `cli-reference` subcommand.
pub fn hooked_cli_reference(
_config: Config,
args: CliReferenceArgs,
) -> Result<()> {
let out_path = if args.output.is_dir() {
args.output.join("cli-reference.txt")
} else {
args.output
};
let commands = {
let mut commands = vec![];
let commands_to_document = &["install", "run", "uninstall"];
for command_name in commands_to_document {
let output = Command::new("cargo")
.env("NO_COLOR", "1")
.args(["run", "-q", "--", command_name, "--help"])
.output()
.unwrap();
let usage = str::from_utf8(&output.stdout).unwrap().trim().to_string();
commands.push((command_name.to_string(), usage))
}
commands
};
let mut context = Context::new();
context.insert("commands", &commands);
write(
out_path,
Tera::one_off(REFERENCE_TEMPLATE, &context, false)?,
)?;
Ok(())
}

View File

@ -0,0 +1,53 @@
//! The `install` subcommand.
use std::{
fs::{read_to_string, set_permissions, write, Permissions},
os::unix::fs::PermissionsExt,
path::PathBuf,
};
use {
color_eyre::{eyre::eyre, Result},
hooked_config::Config,
tera::{Context, Tera},
};
use crate::{cli::InstallArgs, DEFAULT_TEMPLATE, HOOK_TYPES};
/// The `install` subcommand.
pub fn hooked_install(config: Config, args: InstallArgs) -> Result<()> {
let silent = args.silent;
let git_hooks_dir = PathBuf::from(".git/hooks/");
if !git_hooks_dir.exists() {
return Err(eyre!("The \".git/hooks/\" directory does not exist"));
}
let hooked_directory = config.general.directory;
for hook_type in HOOK_TYPES {
let mut context = Context::new();
context.insert("config_path", &config.general.config);
context.insert("hook_type", hook_type);
let hook_path = git_hooks_dir.join(hook_type);
if hook_path.exists() && !args.overwrite {
if !silent {
println!(
"{:?} exists, use --overwrite to replace the existing file",
hook_path
);
}
continue;
}
let template = match config.general.template.as_ref() {
Some(template_path) => {
read_to_string(hooked_directory.join(template_path))?
}
None => DEFAULT_TEMPLATE.to_string(),
};
write(&hook_path, Tera::one_off(&template, &context, false)?)?;
set_permissions(hook_path, Permissions::from_mode(0o775))?;
}
Ok(())
}

View File

@ -2,11 +2,22 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use {
clap::{Args as Arguments, Parser, Subcommand},
hooked_config::NoiseLevel,
};
#[cfg(debug_assertions)]
mod cli_reference;
mod install;
mod run;
mod uninstall;
#[cfg(debug_assertions)]
pub use cli_reference::hooked_cli_reference;
pub use install::hooked_install;
pub use run::hooked_run;
pub use uninstall::hooked_uninstall;
/// CLI arguments struct using [`clap::Parser`].
#[derive(Debug, Parser)]
@ -26,23 +37,55 @@ pub struct Args {
#[derive(Debug, Subcommand)]
pub enum MainSubcommands {
/// Install Hooked into ".git/hooks".
Install {
/// Overwrite existing files.
#[clap(long)]
overwrite: bool,
},
Install(InstallArgs),
/// Remove installed hooks.
Uninstall {
/// Remove hooks not installed by Hooked.
#[clap(long)]
all: bool,
},
Uninstall(UninstallArgs),
/// Manually run hooks.
Run {
/// The hook type to run.
#[clap(value_parser = crate::HOOK_TYPES)]
hook_type: String,
},
Run(RunArgs),
#[cfg(debug_assertions)]
/// Generate the CLI reference file for the mdBook.
CliReference(CliReferenceArgs),
}
/// The `install` subcommand arguments.
#[derive(Debug, Arguments)]
pub struct InstallArgs {
/// Overwrite existing files.
#[clap(long)]
pub overwrite: bool,
/// Don't output any information.
#[clap(long)]
pub silent: bool,
}
/// The `uninstall` subcommand arguments.
#[derive(Debug, Arguments)]
pub struct UninstallArgs {
/// Remove hooks not installed by Hooked.
#[clap(long)]
pub all: bool,
}
/// The `run` subcommand arguments.
#[derive(Debug, Arguments)]
pub struct RunArgs {
/// The hook type to run.
#[clap(value_parser = crate::HOOK_TYPES)]
pub hook_type: String,
/// The noise level to override for all hooks.
#[clap(long)]
pub noise_level: Option<NoiseLevel>,
}
/// The `cli-reference` subcommand arguments.
#[derive(Debug, Arguments)]
pub struct CliReferenceArgs {
/// Path where the CLI reference file should be generated.
#[clap(short, long, default_value = "hooked-book/source/")]
pub output: PathBuf,
}

View File

@ -5,38 +5,57 @@ use std::{io::Read, process::exit};
use {
color_eyre::{eyre::eyre, Result},
hooked_config::{Config, ExitAction},
owo_colors::{OwoColorize, Style},
owo_colors::OwoColorize,
subprocess::{Exec, Redirection},
supports_color::Stream,
};
use crate::utilities::plural;
use crate::{
cli::RunArgs,
printer::{print, PrintType, PRINT_STYLE},
utilities::{globset_from_strings, plural},
};
/// The `run` subcommand.
pub fn hooked_run(config: Config, hook_type: String) -> Result<()> {
let (success_style, warn_style, error_style) =
if let Some(_support) = supports_color::on(Stream::Stdout) {
let shared_style = Style::new().bold();
(
shared_style.green(),
shared_style.yellow(),
shared_style.red(),
)
} else {
(Style::new(), Style::new(), Style::new())
};
pub fn hooked_run(config: Config, args: RunArgs) -> Result<()> {
let cli_noise_level = args.noise_level.as_ref();
let global_noise_level = &config.general.noise_level;
if hook_type == "pre-commit" {
if args.hook_type == "pre-commit" {
let hook_count = config.pre_commit.len();
println!(
"Hooked: Running {} pre-commit {}.",
hook_count,
plural(hook_count, "hook", None)
print(
format!(
"Hooked: Running {} pre-commit {}.",
hook_count,
plural(hook_count, "hook", None)
),
cli_noise_level.unwrap_or(global_noise_level),
PrintType::Info,
);
for hook in config.pre_commit {
'hook_loop: for hook in config.pre_commit {
let hook_name = hook.name.unwrap_or_else(|| "Unnamed Hook".to_string());
if !hook.staged.is_empty() {
let globs = globset_from_strings(&hook.staged)?;
let staged_files = Exec::cmd("git")
.args(&["diff", "--name-only", "--cached"])
.capture()?
.stdout_str();
if !staged_files.lines().any(|line| globs.is_match(line)) {
print(
format!(
"\t{} {}",
"".style(PRINT_STYLE.skipped),
hook_name.style(PRINT_STYLE.skipped)
),
cli_noise_level.unwrap_or(global_noise_level),
PrintType::Info,
);
continue 'hook_loop;
}
}
let command = match (hook.task.command, hook.task.script) {
(Some(command), _) => Ok(Exec::shell(command)),
@ -66,16 +85,32 @@ pub fn hooked_run(config: Config, hook_type: String) -> Result<()> {
output
};
let (stop, print_output, prefix, style) =
let (stop, print_output, prefix, style, print_type) =
match (exit_status.success(), hook.on_failure) {
(true, _) => (false, false, "", success_style),
(false, ExitAction::Continue) => (false, true, "", warn_style),
(false, ExitAction::Stop) => (true, true, "", error_style),
(true, _) => {
(false, false, "", PRINT_STYLE.success, PrintType::Info)
}
(false, ExitAction::Continue) => {
(false, true, "", PRINT_STYLE.warn, PrintType::Warn)
}
(false, ExitAction::Stop) => {
(true, true, "", PRINT_STYLE.error, PrintType::Error)
}
};
println!("\t{} {}", prefix.style(style), hook_name.style(style));
let hook_noise_level =
hook.noise_level.as_ref().unwrap_or(global_noise_level);
print(
format!("\t{} {}", prefix.style(style), hook_name.style(style)),
cli_noise_level.unwrap_or(hook_noise_level),
print_type,
);
if !output.is_empty() && print_output {
println!("{}", output);
print(
output,
cli_noise_level.unwrap_or(hook_noise_level),
PrintType::Info,
);
}
if stop {

View File

@ -0,0 +1,40 @@
//! The `uninstall` subcommand.
use std::{
fs::{read_to_string, remove_file},
path::PathBuf,
};
use {
color_eyre::{eyre::eyre, Result},
hooked_config::Config,
};
use crate::{cli::UninstallArgs, HOOK_TYPES};
/// The `uninstall` subcommand.
pub fn hooked_uninstall(_config: Config, args: UninstallArgs) -> Result<()> {
let git_hooks_dir = PathBuf::from(".git/hooks/");
if !git_hooks_dir.exists() {
return Err(eyre!("The \".git/hooks/\" directory does not exist"));
}
for hook_type in HOOK_TYPES {
let hook_path = git_hooks_dir.join(hook_type);
if !hook_path.exists() {
continue;
}
let hook_contents = read_to_string(&hook_path)?;
if args.all || hook_contents.contains("# Installed by Hooked.") {
remove_file(hook_path)?;
} else {
println!(
"{:?} wasn't installed by Hooked, use --all to remove it",
hook_path
);
}
}
Ok(())
}

View File

@ -2,20 +2,10 @@
//!
//! > **Git hooks manager.**
#![forbid(unsafe_code)]
#![warn(missing_docs, clippy::missing_docs_in_private_items)]
use std::{
fs::{read_to_string, remove_file, set_permissions, write, Permissions},
os::unix::fs::PermissionsExt,
path::PathBuf,
};
use {
clap::Parser,
color_eyre::{eyre::eyre, install, Result},
color_eyre::{install, Result},
hooked_config::Config,
tera::{Context, Tera},
};
use crate::cli::{Args, MainSubcommands};
@ -27,6 +17,7 @@ pub const DEFAULT_TEMPLATE: &str = include_str!("templates/default.sh");
pub const HOOK_TYPES: [&str; 1] = ["pre-commit"];
mod cli;
mod printer;
mod utilities;
fn main() -> Result<()> {
@ -35,61 +26,22 @@ fn main() -> Result<()> {
let args = Args::parse();
let config = Config::from_toml_file(args.config)?;
let git_hooks_dir = PathBuf::from(".git/hooks/");
match args.command {
MainSubcommands::Install { overwrite } => {
if !git_hooks_dir.exists() {
return Err(eyre!("The \".git/hooks/\" directory does not exist"));
}
for hook_type in HOOK_TYPES {
let mut context = Context::new();
context.insert("config_path", &config.general.config);
context.insert("hook_type", hook_type);
let hook_path = git_hooks_dir.join(hook_type);
if hook_path.exists() && !overwrite {
println!(
"{:?} exists, use --overwrite to replace the existing file",
hook_path
);
continue;
}
write(
&hook_path,
Tera::one_off(DEFAULT_TEMPLATE, &context, false)?,
)?;
set_permissions(hook_path, Permissions::from_mode(0o775))?;
}
MainSubcommands::Install(sub_args) => {
cli::hooked_install(config, sub_args)?;
}
MainSubcommands::Uninstall { all } => {
if !git_hooks_dir.exists() {
return Err(eyre!("The \".git/hooks/\" directory does not exist"));
}
for hook_type in HOOK_TYPES {
let hook_path = git_hooks_dir.join(hook_type);
if !hook_path.exists() {
continue;
}
let hook_contents = read_to_string(&hook_path)?;
if all || hook_contents.contains("# Installed by Hooked.") {
remove_file(hook_path)?;
} else {
println!(
"{:?} wasn't installed by Hooked, use --all to remove it",
hook_path
);
}
}
MainSubcommands::Uninstall(sub_args) => {
cli::hooked_uninstall(config, sub_args)?;
}
MainSubcommands::Run { hook_type } => {
cli::hooked_run(config, hook_type)?;
MainSubcommands::Run(sub_args) => {
cli::hooked_run(config, sub_args)?;
}
#[cfg(debug_assertions)]
MainSubcommands::CliReference(sub_args) => {
cli::hooked_cli_reference(config, sub_args)?;
}
}

View File

@ -0,0 +1,76 @@
//! Shared logic for printing output to the terminal.
use {
hooked_config::NoiseLevel, lazy_static::lazy_static, owo_colors::Style,
std::fmt::Display, supports_color::Stream,
};
/// The available types to print output as.
#[derive(Debug, Eq, PartialEq)]
pub enum PrintType {
/// Print the output as an error line.
Error,
/// Print the output as a warning line.
Warn,
/// Print the output as an information line.
Info,
}
/// The available print styles for colorized output.
#[derive(Debug)]
pub struct PrintStyles {
/// The style for errored hooks output.
pub error: Style,
/// The style for skipped hooks output.
pub skipped: Style,
/// The style for successful hooks output.
pub success: Style,
/// The style for hooks with warnings.
pub warn: Style,
}
lazy_static! {
pub static ref PRINT_STYLE: PrintStyles = {
let (success_style, warn_style, error_style, skipped_style) =
if let Some(_support) = supports_color::on(Stream::Stdout) {
let shared_style = Style::new().bold();
(
shared_style.green(),
shared_style.yellow(),
shared_style.red(),
shared_style.blue(),
)
} else {
(Style::new(), Style::new(), Style::new(), Style::new())
};
PrintStyles {
error: error_style,
skipped: skipped_style,
success: success_style,
warn: warn_style,
}
};
}
/// Print something to the terminal according to a given [`NoiseLevel`] and
/// [`PrintType`].
pub fn print<D: Display>(
something: D,
noise_level: &NoiseLevel,
print_type: PrintType,
) {
let should_print = match (noise_level, print_type) {
// Only output errors under the quiet noise level.
(NoiseLevel::Quiet, PrintType::Error) => true,
(NoiseLevel::Quiet, _) => false,
// Output everything under loud.
(NoiseLevel::Loud | NoiseLevel::Standard | NoiseLevel::Minimal, _) => true,
};
if !should_print {
return;
}
println!("{something}")
}

View File

@ -0,0 +1,11 @@
This file is automatically generated using the cli-reference subcommand.
Use `cargo run -- cli-reference` to generate it.
{% for command in commands %}
{%- set name = command.0 -%}
{%- set usage = command.1 -%}
// ANCHOR: {{ name }}
$ hooked {{ name }} --help
{{ usage }}
// ANCHOR_END: {{ name }}
{% endfor %}

View File

@ -1,14 +0,0 @@
//! Miscellaneous utilities.
/// Simple function to create a pluralized string.
pub fn plural(count: usize, singular: &str, plural: Option<&str>) -> String {
if count == 1 {
return singular.to_string();
}
if let Some(plural) = plural {
return plural.to_string();
}
format!("{singular}s")
}

View File

@ -0,0 +1,27 @@
//! Miscellaneous utilities.
use color_eyre::Result;
use globset::{Glob, GlobSet, GlobSetBuilder};
/// Simple function to create a pluralized string.
pub fn plural(count: usize, singular: &str, plural: Option<&str>) -> String {
if count == 1 {
return singular.to_string();
}
if let Some(plural) = plural {
return plural.to_string();
}
format!("{singular}s")
}
/// Create a [`GlobSet`] from a list of strings.
pub fn globset_from_strings(input: &[String]) -> Result<GlobSet> {
let mut builder = GlobSetBuilder::new();
for glob in input {
builder.add(Glob::new(glob)?);
}
builder.build().map_err(Into::into)
}

View File

@ -1,7 +1,10 @@
[package]
name = "hooked-config"
description = "Configuration for Hooked."
documentation = "https://docs.rs/hooked-config"
homepage = "https://hooked.holllo.org"
repository = "https://git.bauke.xyz/Holllo/hooked"
readme = "../README.md"
license = "AGPL-3.0-or-later"
version = "0.1.0"
authors = ["Holllo <helllo@holllo.org>"]
@ -10,14 +13,17 @@ edition = "2021"
[lib]
path = "source/lib.rs"
[lints]
workspace = true
[dependencies]
color-eyre = "0.6.2"
toml = "0.5.9"
toml = "0.8.8"
[dependencies.serde]
features = ["derive"]
version = "1.0.147"
version = "1.0.195"
[dev-dependencies]
insta = "1.21.0"
test-case = "2.2.2"
insta = "1.34.0"
test-case = "3.3.1"

1
hooked-config/LICENSE Symbolic link
View File

@ -0,0 +1 @@
LICENSE

View File

@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ExitAction {
/// Regardless of the hook's exit code, allow Hooked to continue.
Continue,
/// Stop on a non-zero hook exit code.
Stop,
}

View File

@ -4,6 +4,8 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::NoiseLevel;
/// General Hooked configuration.
#[derive(Debug, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
@ -13,6 +15,12 @@ pub struct General {
/// The directory to use for hooks.
pub directory: PathBuf,
/// The noise level tasks should output logs with by default.
pub noise_level: NoiseLevel,
/// Path to a script template for use with the install subcommand.
pub template: Option<PathBuf>,
}
impl Default for General {
@ -20,6 +28,8 @@ impl Default for General {
Self {
config: PathBuf::from("Hooked.toml"),
directory: PathBuf::from("hooks"),
noise_level: NoiseLevel::default(),
template: None,
}
}
}

View File

@ -9,11 +9,13 @@ use {
mod exit_action;
mod general;
mod noise_level;
mod pre_commit;
mod task;
pub use exit_action::*;
pub use general::*;
pub use noise_level::*;
pub use pre_commit::*;
pub use task::*;

View File

@ -0,0 +1,37 @@
//! The noise level Hooked should output logs with.
use serde::{Deserialize, Serialize};
/// The noise level Hooked should output logs with.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum NoiseLevel {
/// Output only errors.
Quiet,
/// Output everything.
Loud,
/// Print a list of tasks and output warnings and errors, this is the default.
Standard,
/// The same as [`NoiseLevel::Standard`] except don't output task names or
/// warnings.
Minimal,
}
impl Default for NoiseLevel {
fn default() -> Self {
Self::Standard
}
}
// Implement `From<String>` so we can use Clap's automatic parsing in the CLI.
impl From<String> for NoiseLevel {
fn from(value: String) -> Self {
match value.to_lowercase().as_str() {
"quiet" => Self::Quiet,
"loud" => Self::Loud,
"standard" => Self::Standard,
"minimal" => Self::Minimal,
_ => NoiseLevel::default(),
}
}
}

View File

@ -2,7 +2,7 @@
use serde::{Deserialize, Serialize};
use crate::{ExitAction, Task};
use crate::{ExitAction, NoiseLevel, Task};
/// A pre-commit hook.
#[derive(Debug, Deserialize, Serialize)]
@ -11,10 +11,17 @@ pub struct PreCommit {
/// Display name for this hook.
pub name: Option<String>,
/// The noise level this task should output with.
pub noise_level: Option<NoiseLevel>,
/// What to do when the hook exits with a non-zero status code.
#[serde(default)]
pub on_failure: ExitAction,
/// List of globs to check against staged files.
#[serde(default)]
pub staged: Vec<String>,
/// Task to perform when this hook is called.
#[serde(flatten)]
pub task: Task,

View File

@ -1,12 +1,16 @@
[general]
directory = "hooked"
noise_level = "minimal"
template = "test.sh"
[[pre_commit]]
name = "Pre Commit 1"
command = "exit 0"
staged = ["*.txt"]
on_failure = "continue"
[[pre_commit]]
name = "Pre Commit 2"
noise_level = "loud"
script = "test.sh"
on_failure = "stop"

View File

@ -1,5 +1,5 @@
use {
hooked_config::{Config, ExitAction, PreCommit, Task},
hooked_config::{Config, ExitAction, NoiseLevel, PreCommit, Task},
toml::to_string_pretty,
};
@ -9,7 +9,9 @@ use insta::assert_snapshot;
fn test_serialize() {
let pre_commit_command = PreCommit {
name: Some("Command Test".to_string()),
noise_level: None,
on_failure: ExitAction::Continue,
staged: vec!["*.txt".to_string()],
task: Task {
command: Some("exit 0".to_string()),
script: None,
@ -18,7 +20,9 @@ fn test_serialize() {
let pre_commit_script = PreCommit {
name: Some("Script Test".to_string()),
noise_level: Some(NoiseLevel::Loud),
on_failure: ExitAction::Stop,
staged: vec![],
task: Task {
command: None,
script: Some("test.sh".into()),

View File

@ -6,11 +6,15 @@ Config {
general: General {
config: "Hooked.toml",
directory: "hooks",
noise_level: Standard,
template: None,
},
pre_commit: [
PreCommit {
name: None,
noise_level: None,
on_failure: Stop,
staged: [],
task: Task {
command: None,
script: None,

View File

@ -6,13 +6,21 @@ Config {
general: General {
config: "Hooked.toml",
directory: "hooked",
noise_level: Minimal,
template: Some(
"test.sh",
),
},
pre_commit: [
PreCommit {
name: Some(
"Pre Commit 1",
),
noise_level: None,
on_failure: Continue,
staged: [
"*.txt",
],
task: Task {
command: Some(
"exit 0",
@ -24,7 +32,11 @@ Config {
name: Some(
"Pre Commit 2",
),
noise_level: Some(
Loud,
),
on_failure: Stop,
staged: [],
task: Task {
command: None,
script: Some(

View File

@ -3,16 +3,20 @@ source: hooked-config/tests/serialize.rs
expression: to_string_pretty(&config).unwrap()
---
[general]
config = 'Hooked.toml'
directory = 'hooks'
config = "Hooked.toml"
directory = "hooks"
noise_level = "standard"
[[pre_commit]]
name = 'Command Test'
on_failure = 'continue'
command = 'exit 0'
name = "Command Test"
on_failure = "continue"
staged = ["*.txt"]
command = "exit 0"
[[pre_commit]]
name = 'Script Test'
on_failure = 'stop'
script = 'test.sh'
name = "Script Test"
noise_level = "loud"
on_failure = "stop"
staged = []
script = "test.sh"

3
rustup-toolchain.toml Normal file
View File

@ -0,0 +1,3 @@
[toolchain]
channel = "stable"
components = ["cargo", "clippy", "rustfmt", "rust-src"]

33
shell.nix Normal file
View File

@ -0,0 +1,33 @@
{ pkgs ? import <nixpkgs> { } }:
with pkgs;
let
rustup-toolchain = rust-bin.fromRustupToolchainFile ./rustup-toolchain.toml;
in
mkShell rec {
packages = [
cargo-audit
cargo-edit
cargo-insta
cargo-make
cargo-outdated
cargo-tarpaulin
mdbook
mdbook-linkcheck
rustup-toolchain
typos
];
shellHook = ''
# Add the outputs directory to PATH where local tools will be installed.
PATH="$PATH:$out/bin"
# If Hooked isn't installed, use cargo to install the local version of it.
if ! [[ -x "$(command -v hooked)" ]]; then
cargo install --path hooked-cli --root $out
fi
hooked install --silent
'';
}