Thanks to visit codestin.com
Credit goes to github.com

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
6 changes: 4 additions & 2 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[profile.ci-core]
# Exclude language-specific integration tests from the main CI runs (except unimplemented/unsupported/script/fail/pygrep).
default-filter = "not binary_id(prek::languages) or (binary_id(prek::languages) and (test(unimplemented::) or test(unsupported::) or test(script::) or test(fail::) or test(pygrep::)))"
# Exclude heavy language-specific integration tests from the main CI runs.
# Keep this as a deny-list so new language test modules run in ci-core until
# they are deliberately moved to the language-test matrix.
default-filter = "not binary_id(prek::languages) or (binary_id(prek::languages) and not (test(bun::) or test(deno::) or test(docker::) or test(docker_image::) or test(dotnet::) or test(golang::) or test(haskell::) or test(julia::) or test(lua::) or test(node::) or test(python::) or test(ruby::) or test(rust::) or test(swift::)))"
status-level = "skip"
final-status-level = "slow"
failure-output = "immediate"
Expand Down
16 changes: 16 additions & 0 deletions crates/prek/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,19 @@ impl<'de> Deserialize<'de> for PassFilenames {
}
}

/// A predefined shell adapter used to run hook entries as shell source.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(rename_all = "lowercase"))]
pub(crate) enum Shell {
Sh,
Bash,
Pwsh,
Powershell,
Cmd,
}

/// Common hook options.
#[derive(Debug, Clone, Default, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand Down Expand Up @@ -575,6 +588,8 @@ pub(crate) struct HookOptions {
pub language_version: Option<String>,
/// Write the output of the hook to a file when the hook fails or verbose is enabled.
pub log_file: Option<String>,
/// Run the hook entry through a predefined shell adapter.
pub shell: Option<Shell>,
Comment thread
j178 marked this conversation as resolved.
/// This hook will execute using a single process instead of in parallel.
/// Default is false.
pub require_serial: Option<bool>,
Expand Down Expand Up @@ -620,6 +635,7 @@ impl HookOptions {
description,
language_version,
log_file,
shell,
require_serial,
stages,
verbose,
Expand Down
99 changes: 50 additions & 49 deletions crates/prek/src/hook.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::path::{Path, PathBuf};
Expand All @@ -18,8 +17,9 @@ use crate::config::{
self, BuiltinHook, Config, FilePattern, HookOptions, Language, LocalHook, ManifestHook,
MetaHook, PassFilenames, RemoteHook, Stages, read_manifest,
};
use crate::hook_entry::HookEntry;
use crate::languages::version::LanguageRequest;
use crate::languages::{extract_metadata, resolve_command};
use crate::languages::{ShellSupport, extract_metadata};
use crate::store::Store;
use crate::workspace::Project;

Expand Down Expand Up @@ -266,6 +266,7 @@ impl HookBuilder {
let HookOptions {
language_version,
additional_dependencies,
shell,
..
} = &self.hook_spec.options;

Expand Down Expand Up @@ -310,6 +311,37 @@ impl HookBuilder {
}
}

if shell.is_some() {
match self.repo.as_ref() {
Repo::Meta { .. } => {
return Err(Error::Hook {
hook: self.hook_spec.id.clone(),
error: anyhow::anyhow!(
"Hook specified `shell` but meta hooks do not support shell execution",
),
});
}
Repo::Builtin { .. } => {
return Err(Error::Hook {
hook: self.hook_spec.id.clone(),
error: anyhow::anyhow!(
"Hook specified `shell` but builtin hooks do not support shell execution",
),
});
}
Repo::Remote { .. } | Repo::Local { .. } => {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Block shell hooks from pre-commit fast path

Allowing shell for all Repo::Remote hooks here lets pre-commit-hooks IDs pass validation, but those hooks can still be executed via hooks::check_fast_path and the Rust fast-path implementations are not shell-aware. Fresh evidence: PreCommitHooks::check_supported does not gate on hook.entry.shell(), so some hooks panic on expect_direct() while others ignore the configured shell entry entirely and run built-in logic instead. This makes shell behavior incorrect for a common remote repo and should be rejected or forced off fast-path.

Useful? React with 👍 / 👎.

}

if let ShellSupport::Unsupported(reason) = language.shell_support() {
return Err(Error::Hook {
hook: self.hook_spec.id.clone(),
error: anyhow::anyhow!(
"Hook specified `shell` but the language `{language}` does not support shell execution: {reason}",
),
});
}
}

Ok(())
}

Expand All @@ -333,6 +365,7 @@ impl HookBuilder {
let require_serial = options.require_serial.unwrap_or(false);
let verbose = options.verbose.unwrap_or(false);
let stages = options.stages.unwrap_or(Stages::ALL);
let shell = options.shell;
let additional_dependencies = options
.additional_dependencies
.unwrap_or_default()
Expand All @@ -345,7 +378,7 @@ impl HookBuilder {
error: anyhow::anyhow!(e),
})?;

let entry = Entry::new(self.hook_spec.id.clone(), self.hook_spec.entry);
let entry = HookEntry::new(self.hook_spec.id.clone(), self.hook_spec.entry, shell);

let priority = self
.hook_spec
Expand Down Expand Up @@ -397,45 +430,6 @@ impl HookBuilder {
}
}

#[derive(Debug, Clone)]
pub(crate) struct Entry {
hook: String,
entry: String,
}

impl Entry {
pub(crate) fn new(hook: String, entry: String) -> Self {
Self { hook, entry }
}

/// Split the entry and resolve the command by parsing its shebang.
pub(crate) fn resolve(&self, env_path: Option<&OsStr>) -> Result<Vec<String>, Error> {
let split = self.split()?;

Ok(resolve_command(split, env_path))
}

/// Split the entry into a list of commands.
pub(crate) fn split(&self) -> Result<Vec<String>, Error> {
let splits = shlex::split(&self.entry).ok_or_else(|| Error::Hook {
hook: self.hook.clone(),
error: anyhow::anyhow!("Failed to parse entry `{}` as commands", &self.entry),
})?;
if splits.is_empty() {
return Err(Error::Hook {
hook: self.hook.clone(),
error: anyhow::anyhow!("Failed to parse entry: entry is empty"),
});
}
Ok(splits)
}

/// Get the original entry string.
pub(crate) fn raw(&self) -> &str {
&self.entry
}
}

#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct Hook {
Expand All @@ -448,7 +442,7 @@ pub(crate) struct Hook {
pub idx: usize,
pub id: String,
pub name: String,
pub entry: Entry,
pub entry: HookEntry,
pub language: Language,
pub alias: String,
pub files: Option<FilePattern>,
Expand Down Expand Up @@ -843,7 +837,9 @@ mod tests {
use prek_identify::tags;
use rustc_hash::FxHashMap;

use crate::config::{Config, HookOptions, Language, PassFilenames, RemoteHook, Stage, Stages};
use crate::config::{
Config, HookOptions, Language, PassFilenames, RemoteHook, Shell, Stage, Stages,
};
use crate::hook::HookSpec;
use crate::languages::version::LanguageRequest;
use crate::workspace::Project;
Expand Down Expand Up @@ -872,7 +868,7 @@ mod tests {
)?);
let repo = Arc::new(Repo::Local { hooks: vec![] });

// Base hook spec (e.g. from a manifest): minimal options, one env var.
// Base hook spec (e.g. from a manifest): options that config can merge or override.
let mut base_env = FxHashMap::default();
base_env.insert("BASE".to_string(), "1".to_string());

Expand All @@ -884,6 +880,7 @@ mod tests {
priority: None,
options: HookOptions {
env: Some(base_env),
shell: Some(Shell::Sh),
..Default::default()
},
};
Expand All @@ -907,6 +904,7 @@ mod tests {
pass_filenames: Some(PassFilenames::None),
verbose: Some(true),
description: Some("desc".to_string()),
shell: Some(Shell::Bash),
..Default::default()
},
};
Expand Down Expand Up @@ -952,10 +950,13 @@ mod tests {
idx: 7,
id: "test-hook",
name: "override-name",
entry: Entry {
hook: "test-hook",
entry: "python3 -c 'print(2)'",
},
entry: Shell(
ShellHookEntry {
hook: "test-hook",
entry: "python3 -c 'print(2)'",
shell: Bash,
},
),
language: Python,
alias: "alias-1",
files: None,
Expand Down
Loading
Loading