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
91 changes: 91 additions & 0 deletions crates/prek/src/cli/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::{Context, Result};

use crate::cli::ExitStatus;
use crate::cli::reporter::{HookInitReporter, HookInstallReporter};
use crate::cli::run::{InstallCache, Selectors, install_hooks};
use crate::fs::CWD;
use crate::printer::Printer;
use crate::store::Store;
use crate::workspace::{HookInitFilters, Workspace};

pub(crate) async fn exec(
store: &Store,
config: Option<PathBuf>,
selector: String,
command: Vec<OsString>,
refresh: bool,
printer: Printer,
) -> Result<ExitStatus> {
let workspace_root = Workspace::find_root(config.as_deref(), &CWD)?;
let selectors = Selectors::from_include(&selector, &workspace_root)?;
let workspace = Workspace::discover(store, workspace_root, config, Some(&selectors), refresh)?;

let init_reporter = HookInitReporter::new(printer);
let lock = store.lock_async().await?;
store.track_configs(workspace.config_files())?;

let hooks = workspace
.init_hooks(
store,
HookInitFilters::new(Some(&selectors), None),
Some(&init_reporter),
)
.await
.context("Failed to init hooks")?
.into_iter()
.filter(|hook| selectors.matches_hook(hook))
.map(Arc::new)
.collect::<Vec<_>>();

let hook = match hooks.as_slice() {
[] => anyhow::bail!("Hook selector `{selector}` did not match any hooks"),
[hook] => Arc::clone(hook),
_ => {
let matches = hooks
.iter()
.map(|hook| format!(" - {}", hook.full_id()))
.collect::<Vec<_>>()
.join("\n");
anyhow::bail!(
"Hook selector `{selector}` matched multiple hooks:\n{matches}\nUse a `project-path:hook-id` selector to select one hook"
);
}
};
hook.language.ensure_exec_supported(&hook)?;

let install_reporter = HookInstallReporter::new(printer);
let mut install_cache = InstallCache::new();
let installed_hooks =
install_hooks(vec![hook], store, &install_reporter, &mut install_cache).await?;
install_reporter.on_complete();
let installed_hook = installed_hooks
.into_iter()
.next()
.context("Failed to prepare the selected hook environment")?;
drop(lock);

let status = installed_hook
.language
.exec(store, &installed_hook, &CWD, &command)
.await?;
Ok(external_exit_status(status))
}

fn external_exit_status(status: std::process::ExitStatus) -> ExitStatus {
let code = status.code().and_then(|code| u8::try_from(code).ok());

#[cfg(unix)]
let code = code.or_else(|| {
use std::os::unix::process::ExitStatusExt;

status
.signal()
.and_then(|signal| u8::try_from(128 + signal).ok())
});

code.unwrap_or(1).into()
}
2 changes: 1 addition & 1 deletion crates/prek/src/cli/hook_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ async fn run_legacy(
return Ok(0);
}

let entry = resolve_command(vec![legacy_hook.into_os_string()], None);
let entry = resolve_command(vec![legacy_hook.into_os_string()], None, &CWD);
let mut cmd = Cmd::new(&entry[0]);
cmd.check(false).args(&entry[1..]).args(args);
cmd.env(EnvVars::PREK_RUNNING_LEGACY, "1");
Expand Down
28 changes: 28 additions & 0 deletions crates/prek/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod cache_clean;
mod cache_gc;
mod cache_size;
mod completion;
mod exec;
mod hook_impl;
mod identify;
mod install;
Expand All @@ -36,6 +37,7 @@ pub(crate) use cache_clean::cache_clean;
pub(crate) use cache_gc::cache_gc;
pub(crate) use cache_size::cache_size;
use completion::selector_completer;
pub(crate) use exec::exec;
pub(crate) use hook_impl::hook_impl;
pub(crate) use identify::identify;
pub(crate) use install::{init_template_dir, install, prepare_hooks, uninstall};
Expand Down Expand Up @@ -259,6 +261,8 @@ pub(crate) enum Command {
PrepareHooks(PrepareHooksArgs),
/// Run configured hooks.
Run(Box<RunArgs>),
/// Run a command in the environment prepared for a configured hook.
Exec(ExecArgs),
/// List configured hooks.
List(ListArgs),
/// Uninstall prek Git hook shims.
Expand Down Expand Up @@ -295,6 +299,30 @@ pub(crate) enum Command {
Self_(SelfNamespace),
}

#[derive(Debug, Args)]
pub(crate) struct ExecArgs {
/// Hook whose execution environment should be used.
///
/// Supports `hook-id` and `project-path:hook-id` selectors and must resolve
/// to exactly one configured hook.
#[arg(
value_name = "HOOK",
value_hint = ValueHint::Other,
add = ArgValueCompleter::new(selector_completer)
)]
pub(crate) selector: String,

/// Command and arguments to execute.
#[arg(
value_name = "COMMAND",
required = true,
num_args = 1..,
last = true,
value_hint = ValueHint::CommandWithArguments
)]
pub(crate) command: Vec<OsString>,
}

#[derive(Debug, Args)]
pub(crate) struct InstallArgs {
/// Include the specified hooks or projects.
Expand Down
7 changes: 1 addition & 6 deletions crates/prek/src/cli/run/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,7 @@ pub(crate) async fn run(
let reporter = HookInitReporter::new(printer);
let hooks = {
let _lock = store.lock_async().await?;
store.track_configs(
workspace
.projects()
.iter()
.map(|project| project.config_file()),
)?;
store.track_configs(workspace.config_files())?;

workspace
.init_hooks(
Expand Down
17 changes: 17 additions & 0 deletions crates/prek/src/cli/run/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,23 @@ pub(crate) struct Selectors {
}

impl Selectors {
/// Create selectors for one explicit include without applying skip environment variables.
pub(crate) fn from_include(include: &str, workspace_root: &Path) -> Result<Self, Error> {
let include = parse_single_selector(
include,
workspace_root,
SelectorSource::CliArg,
RealFileSystem,
)?;
trace!("Include selector: `{include}`");

Ok(Self {
includes: vec![include],
skips: Vec::new(),
usage: Arc::default(),
})
}

/// Load include and skip selectors from CLI args and environment variables.
pub(crate) fn load(
includes: &[String],
Expand Down
32 changes: 22 additions & 10 deletions crates/prek/src/hook_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub(crate) struct PreparedHookEntry {
}

impl PreparedHookEntry {
fn direct(argv: Vec<OsString>) -> Self {
pub(crate) fn direct(argv: Vec<OsString>) -> Self {
Self {
argv,
_temp_dir: None,
Expand Down Expand Up @@ -65,11 +65,12 @@ impl HookEntry {
pub(crate) fn resolve(
&self,
env_path: Option<&OsStr>,
cwd: &Path,
store: &Store,
) -> Result<PreparedHookEntry, Error> {
match self {
Self::Direct(entry) => entry.resolve(env_path),
Self::Shell(entry) => entry.resolve(env_path, store),
Self::Direct(entry) => entry.resolve(env_path, cwd),
Self::Shell(entry) => entry.resolve(env_path, cwd, store),
}
}

Expand All @@ -81,11 +82,12 @@ impl HookEntry {
&self,
repo_path: &Path,
env_path: Option<&OsStr>,
cwd: &Path,
store: &Store,
) -> Result<PreparedHookEntry, Error> {
match self {
Self::Direct(entry) => entry.resolve_script(repo_path, env_path),
Self::Shell(entry) => entry.resolve(env_path, store),
Self::Direct(entry) => entry.resolve_script(repo_path, env_path, cwd),
Self::Shell(entry) => entry.resolve(env_path, cwd, store),
}
}

Expand Down Expand Up @@ -120,23 +122,28 @@ pub(crate) struct DirectHookEntry {

impl DirectHookEntry {
/// Split the entry and resolve the command by parsing its shebang.
fn resolve(&self, env_path: Option<&OsStr>) -> Result<PreparedHookEntry, Error> {
fn resolve(&self, env_path: Option<&OsStr>, cwd: &Path) -> Result<PreparedHookEntry, Error> {
let split = self.split()?;

Ok(PreparedHookEntry::direct(resolve_command(split, env_path)))
Ok(PreparedHookEntry::direct(resolve_command(
split, env_path, cwd,
)))
}

/// Resolve a direct `language: script` entry.
fn resolve_script(
&self,
repo_path: &Path,
env_path: Option<&OsStr>,
cwd: &Path,
) -> Result<PreparedHookEntry, Error> {
let mut split = self.split()?;
let cmd = repo_path.join(&split[0]);
split[0] = cmd.into_os_string();

Ok(PreparedHookEntry::direct(resolve_command(split, env_path)))
Ok(PreparedHookEntry::direct(resolve_command(
split, env_path, cwd,
)))
}

/// Split the entry into a list of commands.
Expand Down Expand Up @@ -174,7 +181,12 @@ pub(crate) struct ShellHookEntry {
}

impl ShellHookEntry {
fn resolve(&self, env_path: Option<&OsStr>, store: &Store) -> Result<PreparedHookEntry, Error> {
fn resolve(
&self,
env_path: Option<&OsStr>,
cwd: &Path,
store: &Store,
) -> Result<PreparedHookEntry, Error> {
let temp_dir = tempfile::tempdir_in(store.scratch_path())?;
let script_path = temp_dir
.path()
Expand All @@ -185,7 +197,7 @@ impl ShellHookEntry {
error: anyhow::anyhow!(err).context("Failed to write shell entry script"),
})?;

let argv = resolve_command(self.shell.argv_for_script(&script_path), env_path);
let argv = resolve_command(self.shell.argv_for_script(&script_path), env_path, cwd);
Ok(PreparedHookEntry::shell(argv, temp_dir))
}
}
Expand Down
46 changes: 9 additions & 37 deletions crates/prek/src/languages/bun/bun.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
use std::ffi::OsStr;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;

use anyhow::{Context, Result};
Expand All @@ -9,14 +7,12 @@ use prek_consts::prepend_paths;
use tracing::debug;

use crate::cli::reporter::HookInstallReporter;
use crate::cli::run::HookRunReporter;
use crate::hook::InstalledHook;
use crate::hook::{Hook, InstallInfo};
use crate::languages::LanguageBackend;
use crate::languages::bun::BunRequest;
use crate::languages::bun::installer::{BunInstaller, BunResult, bin_dir, lib_dir};
use crate::languages::{ExecutionEnvironment, LanguageBackend};
use crate::process::Cmd;
use crate::run::run_by_batch;
use crate::store::{Store, ToolBucket};

#[derive(Debug, Copy, Clone)]
Expand Down Expand Up @@ -118,44 +114,20 @@ impl LanguageBackend for Bun {
Ok(())
}

async fn run(
fn execution_environment(
&self,
store: &Store,
_store: &Store,
hook: &InstalledHook,
filenames: &[&Path],
reporter: &HookRunReporter,
) -> Result<(i32, Vec<u8>)> {
let progress = reporter.on_run_start(hook, filenames.len());

) -> Result<ExecutionEnvironment> {
let env_dir = hook.env_path().expect("Bun must have env path");
let bun_bin = hook.toolchain_dir().expect("Bun binary must have parent");
let new_path =
prepend_paths(&[&bin_dir(env_dir), bun_bin]).context("Failed to join PATH")?;

let entry = hook.entry.resolve(Some(&new_path), store)?;
let run = async |batch: &[&Path]| {
let output = Cmd::new(&entry[0])
.current_dir(hook.work_dir())
.args(&entry[1..])
.env(EnvVars::PATH, &new_path)
.env(EnvVars::BUN_INSTALL, env_dir)
.envs(&hook.env)
.args(&hook.args)
.file_args(batch)
.check(false)
.stdin(Stdio::null())
.pty_output_with_sink(reporter.output_sink(progress))
.await?;

reporter.on_run_progress(progress, batch.len() as u64);

anyhow::Ok(output)
};

let output = run_by_batch(hook, filenames, entry.argv(), run).await?;

reporter.on_run_complete(progress);

Ok(output)
let mut environment = ExecutionEnvironment::new();
environment
.set_path(&new_path)
.env(EnvVars::BUN_INSTALL, env_dir);
Ok(environment)
}
}
Loading
Loading