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 @@ -85,7 +85,7 @@ jobs:
[[ "$file" == "Cargo.toml" || "$file" == "Cargo.lock" || "$file" =~ ^crates/.*/Cargo\.toml$ ]] && rust_deps_changed=1
[[ "$file" == "rust-toolchain.toml" || "$file" =~ ^\.cargo/ ]] && rust_config_changed=1
[[ "$file" == "pyproject.toml" ]] && pyproject_changed=1
[[ "$file" == "docs/reference/cli.md" || "$file" == "docs/reference/configuration.md" || "$file" == "prek.schema.json" ]] && generated_from_code_changed=1
[[ "$file" == "docs/reference/cli.md" || "$file" == "docs/reference/configuration.md" || "$file" == "prek.schema.json" || "$file" == "prek-hooks.schema.json" ]] && generated_from_code_changed=1
[[ "$file" =~ ^docs/ || "$file" == "mkdocs.yml" || "$file" == "pyproject.toml" || "$file" == "uv.lock" || "$file" == ".github/workflows/publish-docs.yml" ]] && docs_related_changed=1
[[ "$file" =~ ^\.github/workflows/.*\.ya?ml$ ]] && workflow_changed=1
[[ "$file" == ".github/workflows/ci.yml" ]] && ci_workflow_changed=1
Expand Down
9 changes: 9 additions & 0 deletions crates/prek/src/config/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,15 @@ impl TryFrom<RemoteHook> for ManifestHook {
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(transparent)]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(title = ".pre-commit-hooks.yaml"),
schemars(description = "The hook manifest for prek, a git hook manager written in Rust."),
schemars(extend(
"$id" = "https://raw.githubusercontent.com/j178/prek/master/prek-hooks.schema.json"
)),
)]
pub(crate) struct Manifest {
pub hooks: Vec<ManifestHook>,
}
Expand Down
88 changes: 49 additions & 39 deletions crates/prek/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,14 +421,15 @@ impl schemars::JsonSchema for Repo {
#[cfg(unix)]
#[cfg(all(test, feature = "schemars"))]
mod _gen {
use crate::config::Config;
use crate::config::{Config, Manifest};
use anyhow::bail;
use prek_consts::env_vars::{EnvVars, EnvVarsRead};
use pretty_assertions::StrComparison;
use std::path::PathBuf;

const ROOT_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../");

#[derive(Clone, Copy)]
enum Mode {
/// Update the content.
Write,
Expand All @@ -440,12 +441,12 @@ mod _gen {
DryRun,
}

fn generate() -> String {
fn generate<T: schemars::JsonSchema>() -> String {
let settings = schemars::generate::SchemaSettings::draft07()
.with_transform(schemars::transform::RestrictFormats::default())
.with_transform(super::RemoveNullTypes);
let generator = schemars::SchemaGenerator::new(settings);
let schema = generator.into_root_schema_for::<Config>();
let schema = generator.into_root_schema_for::<T>();
serde_json::to_string_pretty(&schema).unwrap() + "\n"
}

Expand All @@ -457,49 +458,58 @@ mod _gen {
Mode::Check
};

let schema_string = generate();
let filename = "prek.schema.json";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);
let schemas = [
("prek.schema.json", generate::<Config>()),
("prek-hooks.schema.json", generate::<Manifest>()),
];

match mode {
Mode::DryRun => {
anstream::println!("{schema_string}");
}
Mode::Check => match fs_err::read_to_string(schema_path) {
Ok(current) => {
if current == schema_string {
anstream::println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &schema_string);
for (filename, schema_string) in schemas {
let schema_path = PathBuf::from(ROOT_DIR).join(filename);

match mode {
Mode::DryRun => {
anstream::println!("{schema_string}");
}
Mode::Check => match fs_err::read_to_string(schema_path) {
Ok(current) => {
if current == schema_string {
anstream::println!("Up-to-date: {filename}");
} else {
let comparison = StrComparison::new(&current, &schema_string);
bail!(
"{filename} changed, please run `mise run generate` to update:\n{comparison}"
);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("{filename} not found, please run `mise run generate` to generate");
}
Err(err) => {
bail!(
"{filename} changed, please run `mise run generate` to update:\n{comparison}"
"{filename} changed, please run `mise run generate` to update:\n{err}"
);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
bail!("{filename} not found, please run `mise run generate` to generate");
}
Err(err) => {
bail!("{filename} changed, please run `mise run generate` to update:\n{err}");
}
},
Mode::Write => match fs_err::read_to_string(&schema_path) {
Ok(current) => {
if current == schema_string {
anstream::println!("Up-to-date: {filename}");
} else {
},
Mode::Write => match fs_err::read_to_string(&schema_path) {
Ok(current) => {
if current == schema_string {
anstream::println!("Up-to-date: {filename}");
} else {
anstream::println!("Updating: {filename}");
fs_err::write(schema_path, schema_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
anstream::println!("Updating: {filename}");
fs_err::write(schema_path, schema_string.as_bytes())?;
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
anstream::println!("Updating: {filename}");
fs_err::write(schema_path, schema_string.as_bytes())?;
}
Err(err) => {
bail!("{filename} changed, please run `mise run generate` to update:\n{err}");
}
},
Err(err) => {
bail!(
"{filename} changed, please run `mise run generate` to update:\n{err}"
);
}
},
}
}

Ok(())
Expand Down
24 changes: 22 additions & 2 deletions docs/authoring-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ each manifest hook:
| `shell` | No | Yes | string enum | Run `entry` through a predefined shell adapter (`sh`, `bash`, `pwsh`, `powershell`, or `cmd`). |
| `language` | Yes | No | string | Execution environment, for example `python`, `node`, or `system`. |
| `alias` | No | No | string | Alternate identifier accepted by `prek run`. |
| `files` | No | No | regex string | Include only matching files. |
| `exclude` | No | No | regex string | Exclude matching files. |
| `files` | No | No | regex string or glob map | Include only matching files. |
| `exclude` | No | No | regex string or glob map | Exclude matching files. |
| `types` | No | No | list of strings | Require all listed file type tags. |
| `types_or` | No | No | list of strings | Require at least one listed file type tag. |
| `exclude_types` | No | No | list of strings | Exclude files with any listed file type tag. |
Expand Down Expand Up @@ -76,6 +76,10 @@ manifest semantics. For the upstream reference, see:
`pass_filenames: n` with a positive integer is also a `prek` extension.
Upstream `pre-commit` only accepts a boolean value.

The `{ glob: ... }` mapping form for `files` and `exclude` is a `prek`
extension. Use the regex string form when a manifest must also work with
upstream `pre-commit`.

The `language_version` options map with `request` and `preference` fields is
a `prek` extension. Use the string form when a manifest must also work with
upstream `pre-commit`.
Expand All @@ -91,6 +95,22 @@ manifest semantics. For the upstream reference, see:
Project configuration-only fields, such as `priority` and `groups`, are not
manifest hook fields.

### Editor completion and validation

`prek` maintains a [`prek-hooks.schema.json`](https://raw.githubusercontent.com/j178/prek/master/prek-hooks.schema.json)
schema for `.pre-commit-hooks.yaml`. The schema stays in the `prek` repository
instead of being registered with SchemaStore, so editors must opt into it
explicitly.

With YAML Language Server, add this directive at the top of the manifest:

```yaml
# yaml-language-server: $schema=https://raw.githubusercontent.com/j178/prek/master/prek-hooks.schema.json
```

This enables completion and validation for both upstream fields and `prek`
extensions such as glob filters, `env`, and `shell`.

Example:

```yaml
Expand Down
3 changes: 3 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,7 @@ If you want IDE completion / validation, prek publishes a JSON Schema through th

That schema tracks what `prek` accepts today, but `prek` also intentionally tolerates unknown keys for forward compatibility.

Hook authors can opt into the separate schema for `.pre-commit-hooks.yaml` described in
[Editor completion and validation](authoring-hooks.md#editor-completion-and-validation).

For every accepted configuration key and hook option, see the [Configuration Reference](reference/configuration.md). For process environment controls, see the [Environment Variable Reference](reference/environment-variables.md).
4 changes: 2 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ run = "cargo test --bin prek cli::_gen::generate_cli_reference -- --exact"
env = { PREK_GENERATE = "1" }

[tasks.generate-json-schema]
description = "Generate JSON schema"
description = "Generate JSON schemas"
run = "cargo test --bin prek --features schemars schema::_gen::generate_json_schema -- --exact"
env = { PREK_GENERATE = "1" }

[tasks.generate]
description = "Generate CLI reference and JSON schema"
description = "Generate CLI reference and JSON schemas"
run = [{ task = "generate-cli-reference" }, { task = "generate-json-schema" }]

[tasks.preview-docs]
Expand Down
Loading
Loading