Skip to content
Draft
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 crates/prek/src/cli/auto_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ fn render_updated_toml_config(
.and_then(|value| value.as_str())
.unwrap_or_default();

if matches!(repo_value, "local" | "meta" | "builtin") {
if matches!(repo_value, "local" | "meta" | "builtin" | "self") {
continue;
}

Expand Down
38 changes: 34 additions & 4 deletions crates/prek/src/cli/cache_gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ use std::path::Path;

use anyhow::Result;
use owo_colors::OwoColorize;
use prek_consts::PRE_COMMIT_HOOKS_YAML;
use rustc_hash::FxHashMap;
use rustc_hash::FxHashSet;
use strum::IntoEnumIterator;
use tracing::{debug, trace, warn};

use crate::cli::ExitStatus;
use crate::cli::cache_size::{dir_size_bytes, human_readable_bytes};
use crate::config::{self, Error as ConfigError, Repo as ConfigRepo, load_config};
use crate::config::{self, Error as ConfigError, Repo as ConfigRepo, load_config, read_manifest};
use crate::hook::{HOOK_MARKER, HookEnvKey, HookSpec, InstallInfo, Repo as HookRepo};
use crate::printer::Printer;
use crate::store::{CacheBucket, REPO_MARKER, Store, ToolBucket};
Expand Down Expand Up @@ -180,7 +181,7 @@ pub(crate) async fn cache_gc(
};
kept_configs.insert(config_path);

used_env_keys.extend(hook_env_keys_from_config(store, &config));
used_env_keys.extend(hook_env_keys_from_config(store, config_path, &config));

// Mark repos referenced by this config (if present in store).
// We do this via config parsing (no clone), so GC won't keep repos for missing configs.
Expand Down Expand Up @@ -338,7 +339,11 @@ fn print_removed_details(printer: Printer, verb: &str, removal: &Removal) -> Res
Ok(())
}

fn hook_env_keys_from_config(store: &Store, config: &config::Config) -> Vec<HookEnvKey> {
fn hook_env_keys_from_config(
store: &Store,
config_path: &Path,
config: &config::Config,
) -> Vec<HookEnvKey> {
let mut keys = Vec::new();

for repo_config in &config.repos {
Expand Down Expand Up @@ -392,7 +397,32 @@ fn hook_env_keys_from_config(store: &Store, config: &config::Config) -> Vec<Hook
}
}
}
_ => {} // Meta repos and builtin repos do not have hook envs.
ConfigRepo::SelfRepo(repo_config) => {
let Some(project_root) = config_path.parent() else {
continue;
};
let manifest_path = project_root.join(PRE_COMMIT_HOOKS_YAML);
let Ok(manifest) = read_manifest(&manifest_path) else {
continue;
};
for hook_config in &repo_config.hooks {
let Some(manifest_hook) =
manifest.hooks.iter().find(|h| h.id == hook_config.id)
else {
continue;
};
let mut hook_spec: HookSpec = manifest_hook.clone().into();
hook_spec.apply_remote_hook_overrides(hook_config);
match HookEnvKey::from_hook_spec(config, hook_spec, None) {
Ok(Some(key)) => keys.push(key),
Ok(None) => {}
Err(err) => {
warn!(hook = %hook_config.id, %err, "Failed to compute hook env key, skipping");
}
}
}
}
_ => {} // Meta and builtin repos do not have cached hook envs.
}
}

Expand Down
5 changes: 5 additions & 0 deletions crates/prek/src/cli/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ fn all_hooks(proj: &Project) -> Vec<(String, Option<String>)> {
out.push((h.id.clone(), Some(h.name.clone())));
}
}
config::Repo::SelfRepo(cfg) => {
for h in &cfg.hooks {
out.push((h.id.clone(), h.name.as_ref().map(ToString::to_string)));
}
}
}
}
out
Expand Down
115 changes: 114 additions & 1 deletion crates/prek/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use crate::fs::Simplified;
use crate::identify;
use crate::install_source::InstallSource;
#[cfg(feature = "schemars")]
use crate::schema::{schema_repo_builtin, schema_repo_local, schema_repo_meta, schema_repo_remote};
use crate::schema::{
schema_repo_builtin, schema_repo_local, schema_repo_meta, schema_repo_remote, schema_repo_self,
};
use crate::version;
use crate::warn_user;
use crate::warn_user_once;
Expand Down Expand Up @@ -672,12 +674,31 @@ pub(crate) struct BuiltinRepo {
_unused_keys: BTreeMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub(crate) struct SelfRepo {
#[cfg_attr(feature = "schemars", schemars(schema_with = "schema_repo_self"))]
pub repo: String,
#[serde(skip_serializing)]
pub hooks: Vec<RemoteHook>,

#[serde(skip_serializing, flatten)]
_unused_keys: BTreeMap<String, serde_json::Value>,
}

impl Display for SelfRepo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("self")
}
}

#[derive(Debug, Clone)]
pub(crate) enum Repo {
Remote(RemoteRepo),
Local(LocalRepo),
Meta(MetaRepo),
Builtin(BuiltinRepo),
SelfRepo(SelfRepo),
}

impl<'de> Deserialize<'de> for Repo {
Expand Down Expand Up @@ -797,6 +818,22 @@ impl<'de> Deserialize<'de> for Repo {
_unused_keys: unused,
}))
}
"self" => {
if rev.is_some() {
return Err(M::Error::custom("`rev` is not allowed for self repos"));
}
let hooks = match hooks.ok_or_else(|| M::Error::missing_field("hooks"))? {
HooksValue::Remote(hooks) => hooks,
HooksValue::Local(_) | HooksValue::Meta(_) | HooksValue::Builtin(_) => {
return Err(M::Error::custom("invalid hooks for self repo"));
}
};
Ok(Repo::SelfRepo(SelfRepo {
repo: "self".to_string(),
hooks,
_unused_keys: unused,
}))
}
_ => {
let rev = rev.ok_or_else(|| M::Error::missing_field("rev"))?;
let hooks = match hooks.ok_or_else(|| M::Error::missing_field("hooks"))? {
Expand Down Expand Up @@ -949,6 +986,10 @@ fn collect_unused_paths(config: &Config) -> Vec<String> {
&builtin._unused_keys,
Box::new(builtin.hooks.iter().map(|h| &h.options)),
),
Repo::SelfRepo(self_repo) => (
&self_repo._unused_keys,
Box::new(self_repo.hooks.iter().map(|h| &h.options)),
),
};

push_unused_paths(
Expand Down Expand Up @@ -1967,4 +2008,76 @@ mod tests {
let config = serde_saphyr::from_str::<Config>(yaml).unwrap();
insta::assert_debug_snapshot!(config);
}

#[test]
fn parse_self_repo() {
let yaml = indoc::indoc! {r"
repos:
- repo: self
hooks:
- id: my-hook
"};
let result = serde_saphyr::from_str::<Config>(yaml).unwrap();
insta::assert_debug_snapshot!(result);
}

#[test]
fn parse_self_repo_with_rev() {
let yaml = indoc::indoc! {r"
repos:
- repo: self
rev: v1.0.0
hooks:
- id: my-hook
"};
let err = serde_saphyr::from_str::<Config>(yaml).unwrap_err();
insta::assert_snapshot!(err, @r"
error: line 2 column 5: `rev` is not allowed for self repos at line 2, column 5
--> <input>:2:5
|
1 | repos:
2 | - repo: self
| ^ `rev` is not allowed for self repos at line 2, column 5
3 | rev: v1.0.0
4 | hooks:
|
");
}

#[test]
fn parse_self_repo_with_overrides() {
let yaml = indoc::indoc! {r"
repos:
- repo: self
hooks:
- id: my-hook
args: [--flag]
files: ^src/
"};
let result = serde_saphyr::from_str::<Config>(yaml).unwrap();
insta::assert_debug_snapshot!(result);
}

#[test]
fn parse_self_repo_rev_before_repo() {
let yaml = indoc::indoc! {r"
repos:
- rev: v1.0.0
repo: self
hooks:
- id: my-hook
"};
let err = serde_saphyr::from_str::<Config>(yaml).unwrap_err();
insta::assert_snapshot!(err, @r"
error: line 2 column 5: `rev` is not allowed for self repos at line 2, column 5
--> <input>:2:5
|
1 | repos:
2 | - rev: v1.0.0
| ^ `rev` is not allowed for self repos at line 2, column 5
3 | repo: self
4 | hooks:
|
");
}
}
35 changes: 29 additions & 6 deletions crates/prek/src/hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ pub(crate) enum Repo {
Builtin {
hooks: Vec<HookSpec>,
},
#[expect(clippy::enum_variant_names)]
SelfRepo {
/// Path to the project root containing the manifest.
path: PathBuf,
hooks: Vec<HookSpec>,
},
}

impl Repo {
Expand Down Expand Up @@ -202,21 +208,37 @@ impl Repo {
}
}

/// Get the path to the cloned repo if it is a remote repo.
/// Construct a self repo from the project root, reading its manifest.
pub(crate) fn self_repo(project_root: PathBuf) -> Result<Self, Error> {
let manifest_path = project_root.join(PRE_COMMIT_HOOKS_YAML);
let manifest = read_manifest(&manifest_path).map_err(|e| Error::Manifest {
repo: "self".to_string(),
error: e,
})?;
let hooks = manifest.hooks.into_iter().map(Into::into).collect();

Ok(Self::SelfRepo {
path: project_root,
hooks,
})
}

/// Get the path to the repo if it is a remote or self repo.
pub(crate) fn path(&self) -> Option<&Path> {
match self {
Repo::Remote { path, .. } => Some(path),
Repo::Remote { path, .. } | Repo::SelfRepo { path, .. } => Some(path),
_ => None,
}
}

/// Get a hook by id.
pub(crate) fn get_hook(&self, id: &str) -> Option<&HookSpec> {
let hooks = match self {
Repo::Remote { hooks, .. } => hooks,
Repo::Local { hooks } => hooks,
Repo::Meta { hooks } => hooks,
Repo::Builtin { hooks } => hooks,
Repo::Remote { hooks, .. }
| Repo::SelfRepo { hooks, .. }
| Repo::Local { hooks }
| Repo::Meta { hooks }
| Repo::Builtin { hooks } => hooks,
};
hooks.iter().find(|hook| hook.id == id)
}
Expand All @@ -229,6 +251,7 @@ impl Display for Repo {
Repo::Local { .. } => write!(f, "local"),
Repo::Meta { .. } => write!(f, "meta"),
Repo::Builtin { .. } => write!(f, "builtin"),
Repo::SelfRepo { .. } => write!(f, "self"),
}
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/prek/src/hooks/meta_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ pub(crate) async fn check_useless_excludes(
config::Repo::Local(r) => Box::new(r.hooks.iter().map(|h| (&h.id, &h.options))),
config::Repo::Meta(r) => Box::new(r.hooks.iter().map(|h| (&h.id, &h.options))),
config::Repo::Builtin(r) => Box::new(r.hooks.iter().map(|h| (&h.id, &h.options))),
config::Repo::SelfRepo(r) => Box::new(r.hooks.iter().map(|h| (&h.id, &h.options))),
};

for (hook_id, opts) in hooks_iter {
Expand Down
2 changes: 1 addition & 1 deletion crates/prek/src/languages/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ impl Language {
return hooks::run_fast_path(store, hook, filenames, reporter).await;
}
}
Repo::Local { .. } => {}
Repo::Local { .. } | Repo::SelfRepo { .. } => {}
}

match self {
Expand Down
18 changes: 14 additions & 4 deletions crates/prek/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::borrow::Cow;

use crate::config::{
BuiltinHook, BuiltinRepo, FilePattern, LocalRepo, MetaHook, MetaRepo, RemoteHook, RemoteRepo,
Repo,
Repo, SelfRepo,
};

impl schemars::JsonSchema for FilePattern {
Expand Down Expand Up @@ -141,15 +141,23 @@ pub(crate) fn schema_repo_builtin(
})
}

pub(crate) fn schema_repo_self(_gen: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"const": "self",
"description": "Must be `self`. References the project's own `.pre-commit-hooks.yaml`.",
})
}

pub(crate) fn schema_repo_remote(
_gen: &mut schemars::generate::SchemaGenerator,
) -> schemars::Schema {
schemars::json_schema!({
"type": "string",
"not": {
"enum": ["local", "meta", "builtin"],
"enum": ["local", "meta", "builtin", "self"],
},
"description": "Remote repository location. Must not be `local`, `meta`, or `builtin`.",
"description": "Remote repository location. Must not be `local`, `meta`, `builtin`, or `self`.",
})
}

Expand All @@ -163,15 +171,17 @@ impl schemars::JsonSchema for Repo {
let local_schema = r#gen.subschema_for::<LocalRepo>();
let meta_schema = r#gen.subschema_for::<MetaRepo>();
let builtin_schema = r#gen.subschema_for::<BuiltinRepo>();
let self_schema = r#gen.subschema_for::<SelfRepo>();

schemars::json_schema!({
"type": "object",
"description": "A repository of hooks, which can be remote, local, meta, or builtin.",
"description": "A repository of hooks, which can be remote, local, meta, builtin, or self.",
"oneOf": [
remote_schema,
local_schema,
meta_schema,
builtin_schema,
self_schema,
],
})
}
Expand Down
Loading