Skip to content

Commit

Permalink
feat: show unload messages/run leave hook (#3532)
Browse files Browse the repository at this point in the history
Fixes #3523
Fixes #83
  • Loading branch information
jdx authored Dec 14, 2024
1 parent f0c4d7f commit ca453f4
Show file tree
Hide file tree
Showing 19 changed files with 199 additions and 165 deletions.
3 changes: 1 addition & 2 deletions docs/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This hook is run when the project is entered. Changing directories while in the
enter = "echo 'I entered the project'"
```

## Leave hook (not yet implemented)
## Leave hook

This hook is run when the project is left. Changing directories while in the project will not trigger this hook.

Expand Down Expand Up @@ -80,7 +80,6 @@ implemented something similar.

I think in most situations this is probably fine, though worth keeping in mind.

The leave hook (when it's implemented) will give you a way to manually reset the state.
:::

## Multiple hooks syntax
Expand Down
26 changes: 15 additions & 11 deletions e2e/config/test_hooks
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ cat <<EOF >mise.toml
[tools]
dummy = 'latest'
[hooks]
enter = 'echo ENTER'
#leave = 'echo LEAVE'
cd = 'echo CD'
enter = 'echo HOOK-ENTER'
leave = 'echo HOOK-LEAVE'
cd = 'echo HOOK-CD'
preinstall = 'echo PREINSTALL'
postinstall = 'echo POSTINSTALL'
EOF
Expand All @@ -15,17 +15,21 @@ assert_contains "mise i 2>&1" "PREINSTALL"
assert_contains "mise i dummy@1 2>&1" "POSTINSTALL"

eval "$(mise hook-env)"
assert_not_contains "mise hook-env 2>&1" "CD"
assert_not_contains "mise hook-env 2>&1" "ENTER"
assert_not_contains "mise hook-env 2>&1" "HOOK-CD"
assert_not_contains "mise hook-env 2>&1" "HOOK-ENTER"
assert_not_contains "mise hook-env 2>&1" "HOOK-LEAVE"
pushd .. || exit 1
assert_not_contains "mise hook-env 2>&1" "CD"
assert_not_contains "mise hook-env 2>&1" "ENTER"
assert_not_contains "mise hook-env 2>&1" "HOOK-CD"
assert_not_contains "mise hook-env 2>&1" "HOOK-ENTER"
assert_contains "mise hook-env 2>&1" "HOOK-LEAVE"
eval "$(mise hook-env)"
popd || exit 1
assert_contains "mise hook-env 2>&1" "CD"
assert_contains "mise hook-env 2>&1" "ENTER"
assert_contains "mise hook-env 2>&1" "HOOK-CD"
assert_contains "mise hook-env 2>&1" "HOOK-ENTER"
assert_not_contains "mise hook-env 2>&1" "HOOK-LEAVE"
eval "$(mise hook-env)"
mkdir foo
cd foo || exit 1
assert_contains "mise hook-env 2>&1" "CD"
assert_not_contains "mise hook-env 2>&1" "ENTER"
assert_contains "mise hook-env 2>&1" "HOOK-CD"
assert_not_contains "mise hook-env 2>&1" "HOOK-ENTER"
assert_not_contains "mise hook-env 2>&1" "HOOK-LEAVE"
121 changes: 51 additions & 70 deletions src/cli/hook_env.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use crate::config::{Config, Settings};
use crate::direnv::DirenvDiff;
use crate::env::{join_paths, split_paths, PATH_ENV_SEP};
use crate::env::{PATH_KEY, TERM_WIDTH, __MISE_DIFF};
use crate::env_diff::{EnvDiff, EnvDiffOperation};
use crate::file::display_rel_path;
use crate::hook_env::{WatchFilePattern, CUR_SESSION};
use crate::shell::{get_shell, ShellType};
use crate::toolset::Toolset;
use crate::{env, hook_env, hooks, watch_files};
use console::truncate_str;
use eyre::Result;
use indexmap::IndexSet;
use itertools::Itertools;
use std::ops::Deref;
use std::path::PathBuf;

use crate::config::{Config, Settings};
use crate::direnv::DirenvDiff;
use crate::env::{PATH_KEY, TERM_WIDTH, __MISE_DIFF};
use crate::env_diff::{EnvDiff, EnvDiffOperation};
use crate::hook_env::WatchFilePattern;
use crate::hooks::Hooks;
use crate::shell::{get_shell, Shell, ShellType};
use crate::toolset::{Toolset, ToolsetBuilder};
use crate::{dirs, env, hook_env, hooks, watch_files};

/// [internal] called by activate hook to update env vars directory change
#[derive(Debug, clap::Args)]
#[clap(hide = true)]
Expand Down Expand Up @@ -42,7 +42,7 @@ impl HookEnv {
return Ok(());
}
time!("should_exit_early false");
let ts = ToolsetBuilder::new().build(&config)?;
let ts = config.get_toolset()?;
let shell = get_shell(self.shell).expect("no shell provided, use `--shell=zsh`");
miseprint!("{}", hook_env::clear_old_env(&*shell))?;
let mut env = ts.env(&config)?;
Expand All @@ -60,61 +60,36 @@ impl HookEnv {
let settings = Settings::try_get()?;
patches.extend(self.build_path_operations(&settings, &paths, &__MISE_DIFF.path)?);
patches.push(self.build_diff_operation(&diff)?);
patches.push(self.build_watch_operation(watch_files.clone())?);
patches.push(self.build_dir_operation()?);
patches.push(self.build_session_operation(watch_files.clone())?);

let output = hook_env::build_env_commands(&*shell, &patches);
miseprint!("{output}")?;
self.display_status(&config, &ts)?;
self.display_status(&config, ts)?;

self.run_shell_hooks(&config, &*shell)?;
hooks::run_all_hooks(&ts);
watch_files::execute_runs(&ts);
hooks::run_all_hooks(ts, &*shell);
watch_files::execute_runs(ts);

Ok(())
}

fn run_shell_hooks(&self, config: &Config, shell: &dyn Shell) -> Result<()> {
let hooks = config.hooks()?;
for h in hooks::SCHEDULED_HOOKS.lock().unwrap().iter() {
let hooks = hooks
.iter()
.map(|(_p, hook)| hook)
.filter(|hook| hook.hook == *h && hook.shell == Some(shell.to_string()))
.collect_vec();
match *h {
Hooks::Enter => {
for hook in hooks {
miseprintln!("{}", hook.script);
}
}
Hooks::Cd => {
for hook in hooks {
miseprintln!("{}", hook.script);
}
}
Hooks::Leave => {
for _hook in hooks {
warn!("leave hook not yet implemented");
}
}
_ => {}
}
}
Ok(())
}

fn display_status(&self, config: &Config, ts: &Toolset) -> Result<()> {
let settings = Settings::get();
if self.status || settings.status.show_tools {
let installed_versions = ts
let prev = &CUR_SESSION.loaded_tools;
let cur = ts
.list_current_installed_versions()
.into_iter()
.rev()
.map(|(_, tv)| format!("{}@{}", tv.short(), tv.version))
.collect_vec();
if !installed_versions.is_empty() {
let status = installed_versions.into_iter().rev().join(" ");
.collect::<IndexSet<_>>();
let removed = prev.difference(&cur).collect::<IndexSet<_>>();
let new = cur.difference(prev).collect::<IndexSet<_>>();
if !new.is_empty() {
let status = new.into_iter().map(|t| format!("+{t}")).rev().join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
if !removed.is_empty() {
let status = removed.into_iter().map(|t| format!("-{t}")).rev().join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
}
Expand All @@ -124,6 +99,27 @@ impl HookEnv {
let env_diff = env_diff.into_iter().map(patch_to_status).join(" ");
info!("{}", truncate_str(&env_diff, TERM_WIDTH.max(60) - 5, "…"));
}
let new_paths: IndexSet<PathBuf> = config
.path_dirs()
.map(|p| p.iter().cloned().collect())
.unwrap_or_default();
let old_paths = &CUR_SESSION.config_paths;
let removed_paths = old_paths.difference(&new_paths).collect::<IndexSet<_>>();
let added_paths = new_paths.difference(old_paths).collect::<IndexSet<_>>();
if !added_paths.is_empty() {
let status = added_paths
.iter()
.map(|p| format!("+{}", display_rel_path(p)))
.join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
if !removed_paths.is_empty() {
let status = removed_paths
.iter()
.map(|p| format!("-{}", display_rel_path(p)))
.join(" ");
info!("{}", truncate_str(&status, TERM_WIDTH.max(60) - 5, "…"));
}
}
ts.notify_if_versions_missing();
Ok(())
Expand Down Expand Up @@ -199,29 +195,14 @@ impl HookEnv {
))
}

/// set the directory where hook-env was last run from
/// prefixed with ":" so it does not conflict with zsh's auto_name_dirs feature
fn build_dir_operation(&self) -> Result<EnvDiffOperation> {
Ok(EnvDiffOperation::Add(
"__MISE_DIR".into(),
format!(
":{}",
dirs::CWD
.as_ref()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
),
))
}

fn build_watch_operation(
fn build_session_operation(
&self,
watch_files: impl IntoIterator<Item = WatchFilePattern>,
) -> Result<EnvDiffOperation> {
let watches = hook_env::build_watches(watch_files)?;
let session = hook_env::build_session(watch_files)?;
Ok(EnvDiffOperation::Add(
"__MISE_WATCH".into(),
hook_env::serialize_watches(&watches)?,
"__MISE_SESSION".into(),
hook_env::serialize(&session)?,
))
}
}
Expand Down
29 changes: 16 additions & 13 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub struct Config {
env_with_sources: OnceCell<EnvWithSources>,
redactions: OnceCell<IndexSet<String>>,
shorthands: OnceLock<Shorthands>,
hooks: OnceCell<Vec<(PathBuf, Hook)>>,
tasks: OnceCell<BTreeMap<String, Task>>,
tool_request_set: OnceCell<ToolRequestSet>,
toolset: OnceCell<Toolset>,
Expand Down Expand Up @@ -572,19 +573,21 @@ impl Config {
Ok(env_results)
}

pub fn hooks(&self) -> Result<Vec<(PathBuf, Hook)>> {
self.config_files
.values()
.map(|cf| Ok((cf.project_root(), cf.hooks()?)))
.filter_map_ok(|(root, hooks)| root.map(|r| (r.to_path_buf(), hooks)))
.map_ok(|(root, hooks)| {
hooks
.into_iter()
.map(|h| (root.clone(), h))
.collect::<Vec<_>>()
})
.flatten_ok()
.collect()
pub fn hooks(&self) -> Result<&Vec<(PathBuf, Hook)>> {
self.hooks.get_or_try_init(|| {
self.config_files
.values()
.map(|cf| Ok((cf.project_root(), cf.hooks()?)))
.filter_map_ok(|(root, hooks)| root.map(|r| (r.to_path_buf(), hooks)))
.map_ok(|(root, hooks)| {
hooks
.into_iter()
.map(|h| (root.clone(), h))
.collect::<Vec<_>>()
})
.flatten_ok()
.collect()
})
}

pub fn watch_file_hooks(&self) -> Result<IndexSet<(PathBuf, WatchFile)>> {
Expand Down
15 changes: 0 additions & 15 deletions src/env.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::cli::args::{ENV_ARG, PROFILE_ARG};
use crate::env_diff::{EnvDiff, EnvDiffOperation, EnvDiffPatches};
use crate::file::replace_path;
use crate::hook_env::{deserialize_watches, HookEnvWatches};
use indexmap::IndexSet;
use itertools::Itertools;
use log::LevelFilter;
Expand Down Expand Up @@ -154,21 +153,7 @@ pub static MISE_TIMINGS: Lazy<u8> = Lazy::new(|| var_u8("MISE_TIMINGS"));
pub static MISE_PID: Lazy<String> = Lazy::new(|| process::id().to_string());
pub static __MISE_SCRIPT: Lazy<bool> = Lazy::new(|| var_is_true("__MISE_SCRIPT"));
pub static __MISE_DIFF: Lazy<EnvDiff> = Lazy::new(get_env_diff);
/// the directory where hook-env was last run from
/// prefixed with ":" so it does not conflict with zsh's auto_name_dirs feature
pub static __MISE_DIR: Lazy<Option<PathBuf>> = Lazy::new(|| {
var("__MISE_DIR")
.map(|d| PathBuf::from(d.strip_prefix(":").unwrap_or(&d)))
.ok()
});
pub static __MISE_ORIG_PATH: Lazy<Option<String>> = Lazy::new(|| var("__MISE_ORIG_PATH").ok());
pub static __MISE_WATCH: Lazy<Option<HookEnvWatches>> = Lazy::new(|| match var("__MISE_WATCH") {
Ok(raw) => deserialize_watches(raw)
// TODO: enable this later when the bigint change goes out
.map_err(|e| debug!("Failed to deserialize __MISE_WATCH {e}"))
.ok(),
_ => None,
});
pub static LINUX_DISTRO: Lazy<Option<String>> = Lazy::new(linux_distro);
pub static PREFER_STALE: Lazy<bool> = Lazy::new(|| prefer_stale(&ARGS.read().unwrap()));
/// essentially, this is whether we show spinners or build output on runtime install
Expand Down
8 changes: 8 additions & 0 deletions src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,14 @@ pub fn display_path<P: AsRef<Path>>(path: P) -> String {
}
}

pub fn display_rel_path<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
match path.strip_prefix(dirs::CWD.as_ref().unwrap()) {
Ok(rel) => format!("./{}", rel.display()),
Err(_) => display_path(path),
}
}

/// replaces $HOME in a string with "~" and $PATH with "$PATH", generally used to clean up output
/// after it is rendered
pub fn replace_paths_in_string<S: Display>(input: S) -> String {
Expand Down
Loading

0 comments on commit ca453f4

Please sign in to comment.