diff --git a/src/commands/command_executor.rs b/src/commands/command_executor.rs index c936f6499..55ac69f68 100644 --- a/src/commands/command_executor.rs +++ b/src/commands/command_executor.rs @@ -63,6 +63,14 @@ impl PreparedStep { Self::Concurrent(cmds) => cmds, } } + + /// Borrow the step's commands (Single becomes a one-element slice). + pub fn commands(&self) -> &[PreparedCommand] { + match self { + Self::Single(cmd) => std::slice::from_ref(cmd), + Self::Concurrent(cmds) => cmds, + } + } } /// Wraps a command failure (FailFast path) into the final error. @@ -727,14 +735,32 @@ pub fn map_config_steps( .collect() } -/// Prepare hook pipeline steps for execution, preserving serial/concurrent -/// structure. All hook preparation goes through this function (both -/// foreground and background paths). +/// Reject a prepared pipeline whose templates cannot parse, before its first +/// command runs. +/// +/// Separate from [`prepare_steps`] because it is an execution policy rather +/// than part of building a command: every path that runs hooks calls it, while +/// `wt hook show --expanded` prepares the same commands only to display them +/// and annotates a broken template in place instead of blanking the listing. +/// Semantic errors (undefined variable, filter failure) are not checked here — +/// rendering is deferred, so they surface at the failing step. +pub fn validate_pipeline_syntax(steps: &[PreparedStep]) -> Result<()> { + for cmd in steps.iter().flat_map(PreparedStep::commands) { + validate_template_syntax(&cmd.template, &cmd.template_name)?; + } + Ok(()) +} + +/// Prepare hook pipeline steps, preserving serial/concurrent structure. Sole +/// producer of hook command contexts: both the paths that run hooks (foreground +/// and background) and the `wt hook show --expanded` listing come through here, +/// so a context key added here reaches both with no second edit. /// /// Each command freezes its context as JSON and keeps its raw template; -/// rendering happens when the command runs. Syntax errors abort here — before -/// the first step runs — while semantic errors (undefined variable, filter -/// failure) surface at the failing step. +/// rendering happens when the command runs, so semantic errors (undefined +/// variable, filter failure) surface at the failing step. Execution paths +/// follow up with [`validate_pipeline_syntax`], which aborts an unparsable +/// pipeline before its first step runs. pub fn prepare_steps( command_config: &CommandConfig, ctx: &CommandContext<'_>, @@ -770,7 +796,6 @@ pub fn prepare_steps( Some(name) => format!("{source}:{name}"), None => format!("{source} {hook_type} hook"), }; - validate_template_syntax(&cmd.template, &template_name)?; Ok(PreparedCommand { name: cmd.name.clone(), diff --git a/src/commands/hook_commands.rs b/src/commands/hook_commands.rs index ae13fd662..514667ac9 100644 --- a/src/commands/hook_commands.rs +++ b/src/commands/hook_commands.rs @@ -11,8 +11,7 @@ use color_print::cformat; use strum::IntoEnumIterator; use worktrunk::HookType; use worktrunk::config::{ - ALIAS_ARGS_KEY, Approvals, CommandConfig, ProjectConfig, UserConfig, VarScope, - referenced_vars_for_config, + ALIAS_ARGS_KEY, Approvals, CommandConfig, ProjectConfig, UserConfig, referenced_vars_for_config, }; use worktrunk::git::Repository; use worktrunk::path::format_path_for_display; @@ -23,9 +22,10 @@ use worktrunk::styling::{ use super::command_approval::approve_hooks_filtered; use super::command_executor::{ - CommandContext, FailureStrategy, build_hook_context, render_template_preview, + CommandContext, FailureStrategy, PreparedStep, prepare_steps, render_template_preview, }; use super::context::CommandEnv; +use super::hook_filter::HookSource; use super::hooks::{HookAnnouncer, prepare_and_check, run_hooks_foreground}; use super::project_config::command_label; use super::template_vars::TemplateVars; @@ -82,8 +82,8 @@ fn run_post_hook( /// the current worktree path for directional path vars. /// /// This is the single source of truth for manual hook context — both `run_hook` -/// (execution + dry-run) and `expand_command_template` (hook show --expanded) -/// use this function. Returns a `TemplateVars` so callers can extend with +/// (execution + dry-run) and [`hook_command_rows`] (`hook show --expanded`) use +/// this function. Returns a `TemplateVars` so callers can extend with /// additional bindings (e.g. CLI shorthand) before materializing. fn build_manual_hook_template_vars( ctx: &CommandContext, @@ -381,7 +381,6 @@ pub fn handle_hook_show( project_id.as_deref(), filter, ctx.as_ref(), - expanded, ); } @@ -391,6 +390,7 @@ pub fn handle_hook_show( render_user_hooks( &mut output, config, + &approvals, project_id.as_deref(), filter, ctx.as_ref(), @@ -417,8 +417,8 @@ pub fn handle_hook_show( /// /// Each record carries the hook type, source (user or project), optional name, /// raw template, project approval status, and — when `--expanded` was passed — -/// the rendered command preview. -#[allow(clippy::too_many_arguments)] +/// the rendered command preview. `handle_hook_show` builds `ctx` only under +/// `--expanded`, so its presence is that flag. fn emit_hook_show_json( user_config: &UserConfig, project_config: Option<&ProjectConfig>, @@ -426,44 +426,28 @@ fn emit_hook_show_json( project_id: Option<&str>, filter: Option, ctx: Option<&CommandContext>, - expanded: bool, ) -> anyhow::Result<()> { let mut entries: Vec = Vec::new(); - let mut emit = |hook_type: HookType, - source: &'static str, - cfg: &CommandConfig, - needs_approval_for: Option<(&Approvals, Option<&str>)>| - -> anyhow::Result<()> { - for cmd in cfg.commands() { - let needs_approval = needs_approval_for - .map(|(approvals, project_id)| { - project_id.is_some_and(|pid| !approvals.is_command_approved(pid, &cmd.template)) - }) - .unwrap_or(false); - - let mut obj = serde_json::json!({ - "type": hook_type.to_string(), - "source": source, - "name": cmd.name, - "template": cmd.template, - "needs_approval": needs_approval, - }); - - if expanded && let Some(command_ctx) = ctx { - let rendered = expand_command_template( - &cmd.template, - command_ctx, - hook_type, - cmd.name.as_deref(), - )?; - obj["expanded"] = serde_json::Value::String(rendered); + let mut emit = + |hook_type: HookType, source: HookSource, cfg: &CommandConfig| -> anyhow::Result<()> { + for row in hook_command_rows(cfg, ctx, hook_type, source)? { + let mut obj = serde_json::json!({ + "type": hook_type.to_string(), + "source": source.to_string(), + "name": row.name, + "template": row.template, + "needs_approval": needs_approval(source, approvals, project_id, &row.template), + }); + + if ctx.is_some() { + obj["expanded"] = serde_json::Value::String(row.display); + } + + entries.push(obj); } - - entries.push(obj); - } - Ok(()) - }; + Ok(()) + }; // User hooks (merge global + per-project so the listing matches what runs) let user_hooks = user_config.hooks(project_id); @@ -474,7 +458,7 @@ fn emit_hook_show_json( continue; } if let Some(cfg) = user_hooks.get(hook_type) { - emit(hook_type, "user", cfg, None)?; + emit(hook_type, HookSource::User, cfg)?; } } @@ -487,7 +471,7 @@ fn emit_hook_show_json( continue; } if let Some(cfg) = project.hooks.get(hook_type) { - emit(hook_type, "project", cfg, Some((approvals, project_id)))?; + emit(hook_type, HookSource::Project, cfg)?; } } } @@ -500,6 +484,7 @@ fn emit_hook_show_json( fn render_user_hooks( out: &mut String, config: &UserConfig, + approvals: &Approvals, project_id: Option<&str>, filter: Option, ctx: Option<&CommandContext>, @@ -537,7 +522,15 @@ fn render_user_hooks( } has_any = true; - render_hook_commands(out, *hook_type, cfg, None, ctx)?; + render_hook_commands( + out, + *hook_type, + cfg, + HookSource::User, + approvals, + project_id, + ctx, + )?; } if !has_any { @@ -590,7 +583,15 @@ fn render_project_hooks( } has_any = true; - render_hook_commands(out, *hook_type, cfg, Some((approvals, project_id)), ctx)?; + render_hook_commands( + out, + *hook_type, + cfg, + HookSource::Project, + approvals, + project_id, + ctx, + )?; } if !has_any { @@ -605,24 +606,15 @@ fn render_hook_commands( out: &mut String, hook_type: HookType, config: &CommandConfig, - // For project hooks: (approvals, project_id) to check approval status - approval_context: Option<(&Approvals, Option<&str>)>, + source: HookSource, + approvals: &Approvals, + project_id: Option<&str>, ctx: Option<&CommandContext>, ) -> anyhow::Result<()> { - let commands: Vec<_> = config.commands().collect(); - if commands.is_empty() { - return Ok(()); - } + for row in hook_command_rows(config, ctx, hook_type, source)? { + let label = command_label(hook_type, row.name.as_deref()); - for cmd in commands { - let label = command_label(hook_type, cmd.name.as_deref()); - - // Check approval status for project hooks - let needs_approval = if let Some((approvals, Some(project_id))) = approval_context { - !approvals.is_command_approved(project_id, &cmd.template) - } else { - false - }; + let needs_approval = needs_approval(source, approvals, project_id, &row.template); // Use ❯ for needs approval, ○ for approved/user hooks let (emoji, suffix) = if needs_approval { @@ -632,49 +624,98 @@ fn render_hook_commands( }; writeln!(out, "{emoji} {label}{suffix}")?; - - // Show template or expanded command - let command_text = if let Some(command_ctx) = ctx { - // Expand template with current context - expand_command_template(&cmd.template, command_ctx, hook_type, cmd.name.as_deref())? - } else { - cmd.template.clone() - }; - - writeln!(out, "{}", format_bash_with_gutter(&command_text))?; + writeln!(out, "{}", format_bash_with_gutter(&row.display))?; } Ok(()) } -/// Expand a command template with context variables -fn expand_command_template( +/// Whether a listed command still needs the user's approval to run. +/// +/// Only project commands do — user config is the user's own. A repo with no +/// project identifier has nothing to key approvals by, so nothing is approved +/// and nothing is flagged. +fn needs_approval( + source: HookSource, + approvals: &Approvals, + project_id: Option<&str>, template: &str, - ctx: &CommandContext, +) -> bool { + match source { + HookSource::User => false, + HookSource::Project => { + project_id.is_some_and(|id| !approvals.is_command_approved(id, template)) + } + } +} + +/// One command in a `wt hook show` listing. +struct HookCommandRow { + name: Option, + template: String, + /// What to print: the command as it would run under `--expanded`, + /// otherwise the raw template. + display: String, +} + +/// The rows for one hook config, expanded when `ctx` is present — +/// `handle_hook_show` builds one only under `--expanded`. +/// +/// Expansion runs the config through [`prepare_steps`], the same function that +/// 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 +/// --dry-run`, which shows a `vars.*` template raw — its 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 +/// `run_hook` builds them. `args` is left unset, and `prepare_steps` defaults it +/// to the empty sequence — a listing has no CLI args to forward, which is what +/// that default encodes. +/// +/// A template that cannot expand renders as `# ` above its raw text +/// rather than propagating — `wt hook show` lists configuration, so one broken +/// template must not blank the rest of the listing. +fn hook_command_rows( + config: &CommandConfig, + ctx: Option<&CommandContext>, hook_type: HookType, - hook_name: Option<&str>, -) -> anyhow::Result { - let default_branch = ctx.repo.default_branch(); - let template_vars = build_manual_hook_template_vars(ctx, hook_type, default_branch.as_deref()); - let extra_vars = template_vars.as_extra_vars(); - let mut template_ctx = build_hook_context(ctx, &extra_vars, VarScope::All)?; - template_ctx.insert("hook_type", hook_type.to_string()); - if let Some(name) = hook_name { - template_ctx.insert("hook_name", name); + source: HookSource, +) -> anyhow::Result> { + // The emptiness check spares a config with no commands the git + // subprocesses `prepare_steps` spawns to build a context nothing reads. + if let Some(ctx) = ctx + && config.commands().next().is_some() + { + let default_branch = ctx.repo.default_branch(); + let template_vars = + build_manual_hook_template_vars(ctx, hook_type, default_branch.as_deref()); + let extra_vars = template_vars.as_extra_vars(); + + return Ok(prepare_steps(config, ctx, &extra_vars, hook_type, source)? + .into_iter() + .flat_map(PreparedStep::into_commands) + .map(|cmd| { + let template = cmd.template; + let display = + render_template_preview(&template, &cmd.context, ctx.repo, &cmd.template_name) + .unwrap_or_else(|err| format!("# {err}\n{template}")); + HookCommandRow { + name: cmd.name, + template, + display, + } + }) + .collect()); } - // Preview has no CLI args to forward. Inject an empty JSON sequence - // so templates that reference `{{ args }}` render cleanly rather than - // erroring with "undefined value" at the preview site. - template_ctx.insert(ALIAS_ARGS_KEY, "[]"); - - // Hooks always run through `Cmd::shell` (POSIX), so the preview is - // POSIX-escaped. On any error, show both the template and error message. - Ok(template_ctx - .expand( - template, - worktrunk::shell_exec::ShellEscapeMode::Posix, - ctx.repo, - "hook preview", - ) - .unwrap_or_else(|err| format!("# {}\n{}", err.message, template))) + + Ok(config + .commands() + .map(|cmd| HookCommandRow { + name: cmd.name.clone(), + template: cmd.template.clone(), + display: cmd.template.clone(), + }) + .collect()) } diff --git a/src/commands/hook_plan.rs b/src/commands/hook_plan.rs index 65c8dd6fb..95068c404 100644 --- a/src/commands/hook_plan.rs +++ b/src/commands/hook_plan.rs @@ -52,6 +52,7 @@ use worktrunk::git::add_hook_skip_hint; use super::command_approval::approve_command_batch; use super::command_executor::{ CommandContext, FailureStrategy, PipelineKind, execute_pipeline_foreground, prepare_steps, + validate_pipeline_syntax, }; use super::hook_announcement::SourcedStep; use super::hook_filter::HookSource; @@ -358,7 +359,9 @@ fn render_planned( ) -> anyhow::Result> { let mut out = Vec::new(); for (source, cfg) in entries { - for step in prepare_steps(cfg, ctx, extra_vars, hook_type, *source)? { + let steps = prepare_steps(cfg, ctx, extra_vars, hook_type, *source)?; + validate_pipeline_syntax(&steps)?; + for step in steps { out.push(SourcedStep { step, source: *source, diff --git a/src/commands/hooks.rs b/src/commands/hooks.rs index 39da3cfbd..7636a1e71 100644 --- a/src/commands/hooks.rs +++ b/src/commands/hooks.rs @@ -77,6 +77,7 @@ use worktrunk::styling::{ use super::command_executor::{ CommandContext, FailureStrategy, ForegroundStep, PipelineKind, PreparedCommand, PreparedStep, alias_error_wrapper, execute_pipeline_foreground, hook_error_wrapper, prepare_steps, + validate_pipeline_syntax, }; use super::hook_announcement::{SourcedStep, format_pipeline_summary}; use crate::commands::process::{HookLog, spawn_detached_exec}; @@ -127,6 +128,7 @@ pub(crate) fn prepare_and_check( } let steps = prepare_steps(config, ctx, extra_vars, hook_type, source)?; + validate_pipeline_syntax(&steps)?; for step in steps { if let Some(filtered) = filter_step_by_name(step, source, &parsed_filters) { result.push(SourcedStep { diff --git a/tests/integration_tests/hook_show.rs b/tests/integration_tests/hook_show.rs index 0f45806cb..093ba0eda 100644 --- a/tests/integration_tests/hook_show.rs +++ b/tests/integration_tests/hook_show.rs @@ -564,6 +564,69 @@ project-deps = "echo deps" } } +/// The `--expanded` listing and the pipeline agree on the template context. +/// +/// `wt hook show --expanded` prepares its commands through the same function +/// the execution path uses, so the pipeline-infrastructure keys — `hook_type`, +/// the per-command `hook_name`, and the `args` sequence a listing leaves empty +/// — render in the listing exactly as they do in `wt hook --dry-run`, +/// which previews what would actually run. +#[rstest] +fn test_hook_show_expanded_matches_dry_run(repo: TestRepo, temp_home: TempDir) { + let global_config_dir = temp_home.path().join(".config").join("worktrunk"); + fs::create_dir_all(&global_config_dir).unwrap(); + let config_path = global_config_dir.join("config.toml"); + fs::write( + &config_path, + r#"worktree-path = "../{{ repo }}.{{ branch }}" + +[pre-commit] +context = "echo type={{ hook_type }} name={{ hook_name }} args=[{{ args }}]" +"#, + ) + .unwrap(); + + let expected = "echo type=pre-commit name=context args=[]"; + + let mut show = wt_command(); + repo.configure_wt_cmd(&mut show); + show.env("WORKTRUNK_CONFIG_PATH", &config_path); + show.args(["hook", "show", "pre-commit", "--expanded", "--format=json"]) + .current_dir(repo.root_path()); + set_temp_home_env(&mut show, temp_home.path()); + let output = show.output().unwrap(); + assert!( + output.status.success(), + "hook show failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let parsed: serde_json::Value = + serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).expect("valid JSON"); + assert_eq!(parsed[0]["expanded"], expected); + + let mut dry_run = wt_command(); + repo.configure_wt_cmd(&mut dry_run); + dry_run.env("WORKTRUNK_CONFIG_PATH", &config_path); + // Plain output so the rendered command is one contiguous substring rather + // than a run of syntax-highlighting spans. + dry_run.env_remove("CLICOLOR_FORCE").env("NO_COLOR", "1"); + dry_run + .args(["hook", "pre-commit", "--dry-run"]) + .current_dir(repo.root_path()); + set_temp_home_env(&mut dry_run, temp_home.path()); + let output = dry_run.output().unwrap(); + assert!( + output.status.success(), + "hook --dry-run failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains(expected), + "dry-run should render the listing's command: {stdout}" + ); +} + /// Test that valid templates expand correctly with --expanded. #[rstest] fn test_hook_show_expanded_valid_template(repo: TestRepo, temp_home: TempDir) { diff --git a/tests/integration_tests/user_hooks.rs b/tests/integration_tests/user_hooks.rs index 0014ec1c7..3fd7e13c3 100644 --- a/tests/integration_tests/user_hooks.rs +++ b/tests/integration_tests/user_hooks.rs @@ -1204,6 +1204,50 @@ fn test_foreground_pipeline_undefined_var_runs_earlier_steps(repo: TestRepo) { ); } +/// The other half of that contract: a *syntax* error anywhere in the pipeline +/// aborts before step 1. Preparation parses every template +/// (`validate_pipeline_syntax`), so a pipeline that can't render in full never +/// starts — where a semantic error, rendered per step, lets earlier steps run. +/// +/// Driven through `wt merge`'s pre-commit hooks rather than `wt hook +/// pre-commit`, because the `wt hook` CLI parses every template up front to +/// route shorthand arguments (`referenced_vars_union`) and would catch the +/// error before preparation, leaving the preparation-time guard untested. +#[rstest] +fn test_foreground_pipeline_syntax_error_aborts_before_first_step(mut repo: TestRepo) { + let feature_wt = repo.add_worktree("feature"); + fs::write(feature_wt.join("uncommitted.txt"), "uncommitted content").unwrap(); + + repo.write_test_config( + r#"pre-commit = [ + { first = "echo FIRST_RAN > syntax_first_marker.txt" }, + { broken = "echo {{ bad..syntax }}" }, +] +"#, + ); + + let output = repo + .wt_command() + .args(["merge", "main", "--yes", "--no-remove"]) + .current_dir(&feature_wt) + .output() + .unwrap(); + + assert!( + !output.status.success(), + "an unparsable template should fail the merge" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("syntax error"), + "error should name the syntax failure, got: {stderr}" + ); + assert!( + !feature_wt.join("syntax_first_marker.txt").exists(), + "step 1 must not run when a later step's template can't parse: {stderr}" + ); +} + /// When removing the current worktree (cd back to main), both post-remove and /// post-switch hooks fire. They should appear on a single combined announcement line. #[rstest] diff --git a/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_syntax_error.snap b/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_syntax_error.snap index b9d1b14b8..d0e36490d 100644 --- a/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_syntax_error.snap +++ b/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_syntax_error.snap @@ -9,8 +9,10 @@ info: - "--expanded" 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]" @@ -37,6 +39,7 @@ info: 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" @@ -51,7 +54,7 @@ exit_code: 0 PROJECT HOOKS @ _REPO_/.config/wt.toml ❯ pre-commit broken: (requires approval) -  # Failed to expand hook preview: syntax error: unexpected end of input, expected end of variable block @ line 1 +  # Failed to expand project:broken: syntax error: unexpected end of input, expected end of variable block @ line 1   echo {{ branch ----- stderr ----- diff --git a/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_undefined_var.snap b/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_undefined_var.snap index 26f5abc6d..b67e7ddcf 100644 --- a/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_undefined_var.snap +++ b/tests/snapshots/integration__integration_tests__hook_show__hook_show_expanded_undefined_var.snap @@ -9,8 +9,10 @@ info: - "--expanded" 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]" @@ -37,6 +39,7 @@ info: 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" @@ -51,7 +54,7 @@ exit_code: 0 PROJECT HOOKS @ _REPO_/.config/wt.toml ❯ pre-commit optional-var: (requires approval) -  # Failed to expand hook preview: undefined value @ line 1 +  # Failed to expand project:optional-var: undefined value @ line 1   echo {{ base }} ----- stderr -----