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
89 changes: 46 additions & 43 deletions src/commands/hook_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,32 +511,15 @@ fn render_user_hooks(
.filter_map(|ht| user_hooks.get(ht).map(|cfg| (ht, cfg)))
.collect();

let mut has_any = false;
for (hook_type, cfg) in &hooks {
// Apply filter if specified
if let Some(f) = filter
&& f != *hook_type
{
continue;
}

has_any = true;
render_hook_commands(
out,
*hook_type,
cfg,
HookSource::User,
approvals,
project_id,
ctx,
)?;
}

if !has_any {
writeln!(out, "{}", hint_message("(none configured)"))?;
}

Ok(())
render_hook_section(
out,
&hooks,
HookSource::User,
approvals,
project_id,
filter,
ctx,
)
}

/// Render project hooks section
Expand Down Expand Up @@ -572,25 +555,42 @@ fn render_project_hooks(
.filter_map(|ht| config.hooks.get(ht).map(|cfg| (ht, cfg)))
.collect();

render_hook_section(
out,
&hooks,
HookSource::Project,
approvals,
project_id,
filter,
ctx,
)
}

/// Render a section's body: every hook that survives `filter`, or
/// `(none configured)` when that leaves the section with nothing.
///
/// The fallback keys off what was printed, not off what the config declared —
/// a hook type carrying an empty command list (`post-switch = []`) has an entry
/// but no commands to show, and a section holding only those is empty.
fn render_hook_section(
out: &mut String,
hooks: &[(HookType, &CommandConfig)],
source: HookSource,
approvals: &Approvals,
project_id: Option<&str>,
filter: Option<HookType>,
ctx: Option<&CommandContext>,
) -> anyhow::Result<()> {
let mut has_any = false;
for (hook_type, cfg) in &hooks {
// Apply filter if specified
for (hook_type, config) in hooks {
if let Some(f) = filter
&& f != *hook_type
{
continue;
}

has_any = true;
render_hook_commands(
out,
*hook_type,
cfg,
HookSource::Project,
approvals,
project_id,
ctx,
)?;
has_any |=
render_hook_commands(out, *hook_type, config, source, approvals, project_id, ctx)?;
}

if !has_any {
Expand All @@ -600,7 +600,7 @@ fn render_project_hooks(
Ok(())
}

/// Render commands for a single hook type
/// Render commands for a single hook type, reporting whether it wrote any.
fn render_hook_commands(
out: &mut String,
hook_type: HookType,
Expand All @@ -609,8 +609,10 @@ fn render_hook_commands(
approvals: &Approvals,
project_id: Option<&str>,
ctx: Option<&CommandContext>,
) -> anyhow::Result<()> {
) -> anyhow::Result<bool> {
let mut wrote_any = false;
for row in hook_command_rows(config, ctx, hook_type, source)? {
wrote_any = true;
let label = command_label(hook_type, row.name.as_deref());

let needs_approval = needs_approval(source, approvals, project_id, &row.template);
Expand All @@ -627,7 +629,7 @@ fn render_hook_commands(
writeln!(out, "{}", format_bash_with_gutter(shown))?;
}

Ok(())
Ok(wrote_any)
}

/// Whether a listed command still needs the user's approval to run.
Expand Down Expand Up @@ -666,8 +668,9 @@ struct HookCommandRow {
/// builds what actually executes, so every context key the execution path
/// gains reaches this preview with no second edit here. Rendering then goes
/// through [`render_template_preview`], shared with `wt hook <type>
/// --dry-run`, which shows a `vars.*` template raw — its values resolve from
/// git config when the step runs, possibly written by an earlier step.
/// --dry-run`, which renders each `{{ vars.<key> }}` as itself while the rest
/// of the template expands — those values resolve from git config when the
/// step runs, possibly written by an earlier step.
///
/// A manual invocation has no source or destination worktree, so the
/// directional vars come from [`build_manual_hook_template_vars`], exactly as
Expand Down
26 changes: 26 additions & 0 deletions tests/integration_tests/hook_show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,32 @@ url = "http://localhost:8080"
});
}

/// A hook type declared with an empty command list contributes no commands, so
/// both sections read as `(none configured)` rather than a bare heading.
#[rstest]
fn test_hook_show_empty_command_lists(repo: TestRepo, temp_home: TempDir) {
repo.write_test_config(
r#"post-switch = []
"#,
);

repo.write_project_config(
r#"pre-merge = []
"#,
);
repo.commit("Add project config with an empty hook list");

let settings = setup_snapshot_settings_with_home(&repo, &temp_home);
settings.bind(|| {
let mut cmd = wt_command();
repo.configure_wt_cmd(&mut cmd);
cmd.arg("hook").arg("show").current_dir(repo.root_path());
set_temp_home_env(&mut cmd, temp_home.path());

assert_cmd_snapshot!(cmd);
});
}

#[rstest]
fn test_hook_show_outside_git_repo(temp_home: TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
source: tests/integration_tests/hook_show.rs
info:
program: wt
args:
- hook
- show
env:
APPDATA: "[TEST_CONFIG_HOME]"
CLAUDE_CONFIG_DIR: "[TEST_CLAUDE_CONFIG]"
CLICOLOR_FORCE: "1"
COLUMNS: "500"
GIT_ALLOW_PROTOCOL: file
GIT_AUTHOR_DATE: "2025-01-01T00:00:00Z"
GIT_COMMITTER_DATE: "2025-01-01T00:00:00Z"
GIT_CONFIG_GLOBAL: "[TEST_GIT_CONFIG]"
GIT_CONFIG_SYSTEM: /dev/null
GIT_TERMINAL_PROMPT: "0"
HOME: "[TEST_HOME]"
LANG: C
LC_ALL: C
LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]"
OPENCODE_CONFIG_DIR: "[TEST_OPENCODE_CONFIG]"
PATH: "[PATH]"
TERM: alacritty
USERPROFILE: "[TEST_HOME]"
WORKTRUNK_APPROVALS_PATH: "[TEST_APPROVALS]"
WORKTRUNK_CONFIG_PATH: "[TEST_CONFIG]"
WORKTRUNK_SYSTEM_CONFIG_PATH: "[TEST_SYSTEM_CONFIG]"
WORKTRUNK_TEST_BASH_INSTALLED: "0"
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
WORKTRUNK_TEST_CODEX_INSTALLED: "0"
WORKTRUNK_TEST_DELAYED_STREAM_MS: "-1"
WORKTRUNK_TEST_EPOCH: "1735776000"
WORKTRUNK_TEST_FISH_INSTALLED: "0"
WORKTRUNK_TEST_GEMINI_INSTALLED: "0"
WORKTRUNK_TEST_MOCK_CONFIG_DIR: "[TEST_MOCK_CONFIG]"
WORKTRUNK_TEST_NUSHELL_ENV: "0"
WORKTRUNK_TEST_OPENCODE_INSTALLED: "0"
WORKTRUNK_TEST_PARENT_SHELL: ""
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
WORKTRUNK_TEST_POWERSHELL_INSTALLED: "0"
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
WORKTRUNK_TEST_ZSH_INSTALLED: "0"
XDG_CONFIG_HOME: "[TEST_CONFIG_HOME]"
---
success: true
exit_code: 0
----- stdout -----
USER HOOKS @ [TEST_CONFIG]
↳ (none configured)

PROJECT HOOKS @ _REPO_/.config/wt.toml
↳ (none configured)

----- stderr -----
Loading