From 07726289be3e290201fed265b4f00dcb54ff36b3 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Fri, 11 Sep 2026 20:10:56 +0800 Subject: [PATCH 1/2] feat(shell): jump to main worktree with bare worktree flags Make bare `cd -jw` and `cd --jump-worktree` navigate directly to the main worktree. Keep Tab completion and the standalone worktree picker unchanged. Refs #72 --- README.md | 15 ++++-- src/cli.rs | 70 ++++++++++++++++++++++++++- src/main.rs | 1 + src/resolver.rs | 2 +- src/shell.rs | 46 +++++++++++++++--- tests/external_integration.rs | 24 +++++---- tests/windows_external_integration.rs | 24 ++++----- tests/worktree_space_cross_shell.rs | 7 +-- 8 files changed, 146 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 5a06116..7f2a33a 100644 --- a/README.md +++ b/README.md @@ -159,9 +159,10 @@ cj -w --format json cj -w --relative ``` -Choose a worktree with Tab completion: +Jump to the main worktree with Enter, or choose a worktree with Tab completion: ```console +cd -jw # jump directly to the main worktree cd -jw # complete a worktree path, then press Enter cd -jw # a space before Tab also works cd --jump-worktree # long form of the same completion trigger @@ -176,8 +177,16 @@ such as fzf-tab may render those candidates with fzf, but cj does not launch a nested picker during completion and works without fzf. Choosing a candidate only inserts its path; press Enter to change directory. After whitespace, Bash, Zsh, and Nushell keep the flag and complete its literal destination argument; PowerShell -replaces the flag and whitespace together. Pressing Enter with a jump flag but -no destination shows a reminder to use Tab and leaves the directory unchanged. +replaces the flag and whitespace together. + +Pressing Enter on bare `cd -jw` or `cd --jump-worktree` jumps directly to the +primary worktree, whichever branch it currently has checked out. This shortcut +is independent of configured keyword names and local directories named `origin` +or `og`; it does not switch branches or launch fzf. A successful move records one +history entry, so `cd v` returns to the previous directory. Already at the primary +root is a no-op; outside a Git repository, the command reports an error and leaves +the directory and history unchanged. Trailing spaces before Enter behave the same. + Zsh completion menus, including fzf-tab, display paths under `$HOME` as `~/…`; the inserted destination and preview path remain absolute. diff --git a/src/cli.rs b/src/cli.rs index 6a64044..35803ab 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -51,6 +51,7 @@ pub enum Command { jump: bool, }, WorktreePaths0, + MainWorktree, KeyBindingZoxide { zoxide: PathBuf, fzf: PathBuf, @@ -110,6 +111,7 @@ impl Cli { let mut relative = false; let mut jump = false; let mut worktree_paths0 = false; + let mut main_worktree = false; let mut key_binding_zoxide = None; let mut setup_key_binding = false; let mut no_setup_key_binding = false; @@ -159,6 +161,11 @@ impl Cli { jump = true; } else if options && text == Some("--worktree-paths0") { worktree_paths0 = true; + } else if options && text == Some("--internal-main-worktree") { + if main_worktree { + return Err("--internal-main-worktree cannot be repeated".into()); + } + main_worktree = true; } else if options && text == Some("--internal-key-binding-zoxide") { if key_binding_zoxide.is_some() { return Err("--internal-key-binding-zoxide cannot be repeated".into()); @@ -200,7 +207,25 @@ impl Cli { ); } - let command = if let Some((zoxide, fzf)) = key_binding_zoxide { + let command = if main_worktree { + if force_resolve + || !targets.is_empty() + || key_binding_zoxide.is_some() + || worktree_paths0 + || worktree + || format.is_some() + || relative + || resolver != ResolverOverride::Configured + || setup_key_binding + || preamp + || config_path.is_some() + || output.is_some() + || verbose + { + return Err("--internal-main-worktree cannot be combined with another mode".into()); + } + Command::MainWorktree + } else if let Some((zoxide, fzf)) = key_binding_zoxide { if force_resolve || !targets.is_empty() || worktree_paths0 @@ -573,6 +598,49 @@ mod tests { assert!(Cli::parse(["--worktree-paths0", "--"].map(Into::into)).is_err()); } + #[test] + fn main_worktree_protocol_is_hidden_and_rejects_other_modes() { + const FLAG: &str = "--internal-main-worktree"; + assert_eq!( + Cli::parse([FLAG].map(Into::into)).unwrap().command, + Command::MainWorktree + ); + assert!(!HELP.contains(FLAG)); + assert_eq!( + Cli::parse(["--", FLAG].map(Into::into)).unwrap().command, + Command::Resolve { + targets: vec![FLAG.into()], + resolver: ResolverOverride::Configured, + } + ); + for extra in [ + vec![FLAG], + vec!["target"], + vec!["--"], + vec!["-jw"], + vec!["--worktree-paths0"], + vec!["--internal-key-binding-zoxide", "zoxide", "fzf"], + vec!["-z"], + vec!["-R"], + vec!["-f", "json"], + vec!["-C", "config.toml"], + vec!["-o", "out"], + vec!["--no-setup-key-binding"], + vec!["--preamp"], + vec!["-v"], + ] { + assert!( + Cli::parse( + std::iter::once(FLAG) + .chain(extra.iter().copied()) + .map(Into::into) + ) + .is_err(), + "accepted incompatible arguments: {extra:?}" + ); + } + } + #[test] fn parses_hidden_key_binding_zoxide_protocol() { assert_eq!( diff --git a/src/main.rs b/src/main.rs index 06a9c39..cf4efd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,6 +142,7 @@ fn execute(cli: Cli) -> Result { .unwrap_or_else(|| Ok(Execution::stdout(String::new()))) } Command::WorktreePaths0 => worktree::paths0(&worktree::list()?).map(Execution::raw), + Command::MainWorktree => resolver::main_worktree().and_then(Execution::path), Command::MountsScan { format } => { let context = mounts::DiscoveryContext::from_process()?; let report = mounts::scan(&context); diff --git a/src/resolver.rs b/src/resolver.rs index d9ef581..f86fc11 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -237,7 +237,7 @@ fn component_eq(left: Component<'_>, right: Component<'_>) -> bool { } } -fn main_worktree() -> Result { +pub fn main_worktree() -> Result { let output = Command::new("git") .args(["worktree", "list", "--porcelain", "-z"]) .output() diff --git a/src/shell.rs b/src/shell.rs index ea5d0b6..42e9603 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -123,11 +123,18 @@ function cd() {{ _cj_nav_arg= ;; -jw|--jump-worktree) + if [[ $# -eq 1 ]]; then + target="$(\command cj --internal-main-worktree && printf .)" || return + target="${{target%$'\n.'}}" + [[ -n "$target" ]] || return 1 + _cj_builtin_cd -- "$target" + return + fi if [[ $# -eq 2 && -n "$2" ]]; then _cj_builtin_cd -- "$2" return fi - printf '%s\n' 'cj: type cd -jw and press Tab to select a worktree' >&2 + printf '%s\n' 'cj: --jump-worktree accepts at most one non-empty destination' >&2 return 2 ;; -Z|--no-zoxide) @@ -373,10 +380,21 @@ export def --env --wrapped __cj_cd [...args: string] {{ }} if ($args.0 in ['-jw' '--jump-worktree']) {{ - if (($args | length) != 2) or ($args.1 | is-empty) {{ - error make {{ msg: 'cj: type cd -jw and press Tab to select a worktree' }} + let target = if ($args | length) == 1 {{ + let result = (^cj --internal-main-worktree | complete) + if $result.exit_code != 0 {{ + if not ($result.stderr | is-empty) {{ print --stderr --no-newline $result.stderr }} + error make {{ msg: $'cj exited with status ($result.exit_code)' }} + }} + let path = ($result.stdout | str replace --regex '\n$' '') + if ($path | is-empty) {{ error make {{ msg: 'cj returned an empty destination' }} }} + $path + }} else if (($args | length) == 2) and (not ($args.1 | is-empty)) {{ + $args.1 + }} else {{ + error make {{ msg: 'cj: --jump-worktree accepts at most one non-empty destination' }} }} - cd $args.1 + cd $target _cj-record-move $before return }} @@ -548,8 +566,19 @@ function global:cd {{ $first = [string]$cjArgs[0] if (($first -ceq '-jw') -or ($first -ceq '--jump-worktree')) {{ - if ($cjArgs.Count -ne 2 -or [string]::IsNullOrEmpty([string]$cjArgs[1])) {{ throw 'cj: type cd -jw and press Tab to select a worktree' }} - Microsoft.PowerShell.Management\Set-Location -LiteralPath $cjArgs[1] -ErrorAction Stop + if ($cjArgs.Count -eq 1) {{ + $executable = $script:__cj_executable.Path + $target = @(& $executable --internal-main-worktree) + $status = $LASTEXITCODE + if ($status -ne 0) {{ throw "cj exited with status $status" }} + if ($target.Count -ne 1 -or [string]::IsNullOrEmpty($target[0])) {{ throw 'cj returned an invalid destination' }} + $destination = $target[0] + }} elseif ($cjArgs.Count -eq 2 -and -not [string]::IsNullOrEmpty([string]$cjArgs[1])) {{ + $destination = $cjArgs[1] + }} else {{ + throw 'cj: --jump-worktree accepts at most one non-empty destination' + }} + Microsoft.PowerShell.Management\Set-Location -LiteralPath $destination -ErrorAction Stop Add-CjHistory $before return }} @@ -739,10 +768,11 @@ mod tests { } #[test] - fn wrappers_explain_that_jump_tokens_require_tab() { + fn wrappers_resolve_bare_jump_tokens_and_validate_selected_targets() { for shell in [Shell::Bash, Shell::Zsh, Shell::Nu, Shell::Pwsh] { let source = render(shell, None, None, &Config::default()).unwrap(); - assert!(source.contains("type cd -jw and press Tab to select a worktree")); + assert!(source.contains("--internal-main-worktree")); + assert!(source.contains("--jump-worktree accepts at most one non-empty destination")); } } diff --git a/tests/external_integration.rs b/tests/external_integration.rs index 9c710c0..14f4b31 100644 --- a/tests/external_integration.rs +++ b/tests/external_integration.rs @@ -225,8 +225,8 @@ fn fzf_cancellation_and_failure_are_controlled() { } #[test] -fn generated_wrappers_require_tab_for_worktree_selection() { - let fixture = PickerFixture::new("worktree-tab-only"); +fn generated_wrappers_enter_main_worktree_without_the_picker() { + let fixture = PickerFixture::new("worktree-enter-main"); let binary_dir = fixture.git.temp.path().join("binary dir"); fs::create_dir_all(&binary_dir).expect("create binary directory"); symlink(env!("CARGO_BIN_EXE_cj"), binary_dir.join("cj")).expect("link cj binary"); @@ -257,28 +257,26 @@ fn generated_wrappers_require_tab_for_worktree_selection() { .env("CJ_FAKE_ARGS", &fixture.args) .env("CJ_FAKE_STDIN", &fixture.stdin) .env("CJ_FAKE_MODE", "success") - .env("CJ_FAKE_SELECTION", "1"); + .env("CJ_FAKE_SELECTION", "1") + .env_remove("GIT_DIR") + .env_remove("GIT_WORK_TREE") + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE"); let output = match command.output() { Ok(output) => output, Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, Err(error) => panic!("cannot run {shell}: {error}"), }; exercised += 1; - assert!( - !output.status.success(), - "{shell} accepted {flag} without Tab" - ); - assert_eq!(output.stdout, path_output(&fixture.git.main_nested)); - assert!( - String::from_utf8_lossy(&output.stderr) - .contains("type cd -jw and press Tab to select a worktree") - ); + assert_success(&output); + assert_eq!(output.stdout, path_output(&fixture.git.main)); + assert!(output.stderr.is_empty()); } } assert!(exercised > 0, "neither bash nor zsh is available"); assert!( !fixture.args.exists() && !fixture.stdin.exists(), - "executing the Tab token must not invoke fzf" + "bare worktree Enter must not invoke fzf" ); } diff --git a/tests/windows_external_integration.rs b/tests/windows_external_integration.rs index 69f27ce..3640115 100644 --- a/tests/windows_external_integration.rs +++ b/tests/windows_external_integration.rs @@ -101,7 +101,7 @@ fn powershell_wrapper_changes_its_calling_session_with_zoxide() { } #[test] -fn powershell_wrapper_requires_tab_for_worktree_selection() { +fn powershell_wrapper_enters_main_worktree_without_the_picker() { let git = GitFixture::new("windows-powershell-fzf"); let tool = copy_tool(git.temp.path(), "fzf.exe"); let config = git.temp.path().join("fzf config.toml"); @@ -136,24 +136,20 @@ fn powershell_wrapper_requires_tab_for_worktree_selection() { .env("CJ_FAKE_ARGS", &args) .env("CJ_FAKE_STDIN", &stdin) .env("PATH", path_with_cj()) + .env_remove("GIT_DIR") + .env_remove("GIT_WORK_TREE") + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE") .output() .expect("run PowerShell integration"); - assert!( - !output.status.success(), - "PowerShell accepted {flag} without Tab" - ); - assert_eq!( - output.stdout, - git.main_nested.as_os_str().as_encoded_bytes() - ); - assert!( - String::from_utf8_lossy(&output.stderr) - .contains("type cd -jw and press Tab to select a worktree") - ); + assert_success(&output); + let actual = dunce::canonicalize(String::from_utf8(output.stdout).unwrap()).unwrap(); + assert_eq!(actual, dunce::canonicalize(&git.main).unwrap()); + assert!(output.stderr.is_empty()); } assert!( !args.exists() && !stdin.exists(), - "executing the Tab token must not invoke fzf" + "bare worktree Enter must not invoke fzf" ); } diff --git a/tests/worktree_space_cross_shell.rs b/tests/worktree_space_cross_shell.rs index b47d7d0..55fd424 100644 --- a/tests/worktree_space_cross_shell.rs +++ b/tests/worktree_space_cross_shell.rs @@ -56,7 +56,8 @@ fn command(fixture: &GitFixture, shell: &str) -> Command { .env("CJ_EXPECTED", &fixture.linked) .env_remove("GIT_DIR") .env_remove("GIT_WORK_TREE") - .env_remove("GIT_COMMON_DIR"); + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE"); command } @@ -104,7 +105,7 @@ fn nushell_worktree_completion_after_spaces_executes_and_records_history() { assert_success(&output); } let script = format!( - "{prelude}let before = $env.PWD; let matches = ($env.CJ_LINE | commandline complete --detailed); if ($matches | length) != 1 {{ error make {{msg: 'partial worktree path did not filter candidates'}} }}; for args in [[$env.CJ_FLAG] [$env.CJ_FLAG ''] [$env.CJ_FLAG 'missing-worktree'] [$env.CJ_FLAG $env.CJ_EXPECTED 'extra']] {{ let failed = (try {{ __cj_cd ...$args; false }} catch {{ true }}); if not $failed {{ error make {{msg: 'invalid worktree invocation accepted'}} }} }}; if $env.PWD != $before or ($env.__cj_history | length) != 0 {{ error make {{msg: 'failed navigation changed state'}} }}" + "{prelude}let before = $env.PWD; let matches = ($env.CJ_LINE | commandline complete --detailed); if ($matches | length) != 1 {{ error make {{msg: 'partial worktree path did not filter candidates'}} }}; for args in [[$env.CJ_FLAG ''] [$env.CJ_FLAG 'missing-worktree'] [$env.CJ_FLAG $env.CJ_EXPECTED 'extra']] {{ let failed = (try {{ __cj_cd ...$args; false }} catch {{ true }}); if not $failed {{ error make {{msg: 'invalid worktree invocation accepted'}} }} }}; if $env.PWD != $before or ($env.__cj_history | length) != 0 {{ error make {{msg: 'failed navigation changed state'}} }}" ); let output = command(&fixture, "nu") .args(["--no-config-file", "-c", &script]) @@ -148,7 +149,7 @@ foreach ($flag in @('-jw', '--jump-worktree')) { cd $flag $env:CJ_EXPECTED if ($global:__cj_history.Count -ne 1) { throw 'literal selected argument did not record history' } cd v - foreach ($arguments in @(@($flag), @($flag, ''), @($flag, 'missing-worktree'), @($flag, $env:CJ_EXPECTED, 'extra'))) { + foreach ($arguments in @(@($flag, ''), @($flag, 'missing-worktree'), @($flag, $env:CJ_EXPECTED, 'extra'))) { $failed = $false try { cd @arguments } catch { $failed = $true } if (-not $failed) { throw 'invalid worktree invocation accepted' } From 524e3a5329a2bc82bb28834580dbafcbfe3c7016 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Fri, 11 Sep 2026 20:11:33 +0800 Subject: [PATCH 2/2] test(shell): cover main-worktree Enter shortcut Test the shortcut across Bash, Zsh, Nushell, and PowerShell, including command history, keyword shadowing, and non-repository failures. Closes #72 --- tests/main_worktree_enter.rs | 191 +++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 tests/main_worktree_enter.rs diff --git a/tests/main_worktree_enter.rs b/tests/main_worktree_enter.rs new file mode 100644 index 0000000..f52156b --- /dev/null +++ b/tests/main_worktree_enter.rs @@ -0,0 +1,191 @@ +mod support; + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; +use support::{GitFixture, assert_success, cj}; + +fn available(shell: &str) -> bool { + if cfg!(windows) && matches!(shell, "bash" | "zsh") { + return false; + } + let found = Command::new(shell) + .arg("--version") + .output() + .is_ok_and(|out| out.status.success()); + assert!( + found + || (env::var_os("CJ_REQUIRE_SHELLS").is_none() + && !(shell == "pwsh" && env::var_os("CJ_REQUIRE_POWERSHELL").is_some())), + "required shell unavailable: {shell}" + ); + found +} + +fn run(fixture: &GitFixture, shell: &str, config: &Path, cwd: &Path, body: &str) { + let source = cj(cwd, fixture.temp.path()) + .arg("-C") + .arg(config) + .args(["init", shell, "--no-setup-key-binding"]) + .output() + .unwrap(); + assert_success(&source); + let file = fixture.temp.path().join(match shell { + "nu" => "init.nu", + "pwsh" => "init.ps1", + _ => "init.sh", + }); + fs::write(&file, source.stdout).unwrap(); + let mut paths = vec![ + Path::new(env!("CARGO_BIN_EXE_cj")) + .parent() + .unwrap() + .to_path_buf(), + ]; + paths.extend(env::split_paths(&env::var_os("PATH").unwrap_or_default())); + let mut command = Command::new(shell); + match shell { + "bash" | "zsh" => { + command.args(if shell == "bash" { + &["--noprofile", "--norc", "-c"][..] + } else { + &["-f", "-c"][..] + }); + command.arg(format!("set -e; . \"$CJ_SOURCE\"; {body}")); + } + "nu" => { + command + .args(["--no-config-file", "-c"]) + .arg(format!("use r###'{}'### *; {body}", file.display())); + } + _ => { + command.args(["-NoLogo", "-NoProfile", "-NonInteractive", "-Command"]).arg(format!(r#"$ErrorActionPreference = 'Stop'; . $env:CJ_SOURCE +function Same-Path($a, $b) {{ + $comparison = if ($env:OS -eq 'Windows_NT') {{ [StringComparison]::OrdinalIgnoreCase }} else {{ [StringComparison]::Ordinal }} + [IO.Path]::GetFullPath($a).Equals([IO.Path]::GetFullPath($b), $comparison) +}} +{body}"#)); + } + } + let output = command + .current_dir(cwd) + .env("PATH", env::join_paths(paths).unwrap()) + .env("CJ_SOURCE", file) + .env("CJ_MAIN", &fixture.main) + .env("CJ_START", cwd) + .env("CJ_COUNT", if cwd == fixture.main { "0" } else { "1" }) + .env("CJ_OUTSIDE", fixture.temp.path().join("outside")) + .env("GIT_CEILING_DIRECTORIES", fixture.temp.path()) + .env_remove("GIT_DIR") + .env_remove("GIT_WORK_TREE") + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE") + .output() + .unwrap(); + assert_success(&output); +} + +#[test] +fn bare_worktree_enter_uses_primary_checkout_and_records_only_real_moves() { + let fixture = GitFixture::new("main-enter-‘’“”"); + let output = Command::new("git") + .args(["branch", "-m", "production"]) + .current_dir(&fixture.main) + .env_remove("GIT_DIR") + .env_remove("GIT_WORK_TREE") + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE") + .output() + .unwrap(); + assert_success(&output); + for cwd in [&fixture.linked_nested, &fixture.main_nested, &fixture.main] { + for shadow in ["origin", "og", "homebase"] { + fs::create_dir_all(cwd.join(shadow)).unwrap(); + } + } + fs::create_dir(fixture.temp.path().join("outside")).unwrap(); + let config = fixture.temp.path().join("enter.toml"); + let mut exercised = 0; + for keywords in ["[]", "[\"homebase\"]"] { + fs::write(&config, format!("[keywords]\nmain-worktree = {keywords}\n[programs]\nfzf = \"missing-main-enter-fzf\"\nzoxide = \"missing-main-enter-zoxide\"\n")).unwrap(); + for shell in ["bash", "zsh", "nu", "pwsh"] { + if !available(shell) { + continue; + } + exercised += 1; + let success = match shell { + "bash" | "zsh" => { + r#" +for flag in -jw --jump-worktree; do + cd "$flag" + [[ $PWD == "$CJ_MAIN" && ${#_cj_history[@]} == "$CJ_COUNT" ]] + if (( CJ_COUNT )); then cd v; fi + [[ $PWD == "$CJ_START" && ${#_cj_history[@]} == 0 ]] + cd "$flag" "$CJ_OUTSIDE" + [[ $PWD == "$CJ_OUTSIDE" && ${#_cj_history[@]} == 1 ]] + if cd "$flag" '' 2>/dev/null; then exit 1; else [[ $? == 2 ]]; fi + if cd "$flag" "$CJ_MAIN" extra 2>/dev/null; then exit 1; else [[ $? == 2 ]]; fi + [[ $PWD == "$CJ_OUTSIDE" && ${#_cj_history[@]} == 1 ]] + cd v; [[ $PWD == "$CJ_START" && ${#_cj_history[@]} == 0 ]] +done"# + } + "nu" => { + r#" +for flag in [-jw --jump-worktree] { + __cj_cd $flag + if ($env.PWD | path expand) != ($env.CJ_MAIN | path expand) or ($env.__cj_history | length) != ($env.CJ_COUNT | into int) { error make {msg: 'wrong primary destination or history'} } + if ($env.CJ_COUNT | into int) > 0 { __cj_cd v } + if ($env.PWD | path expand) != ($env.CJ_START | path expand) or ($env.__cj_history | length) != 0 { error make {msg: 'back did not restore start'} } +}"# + } + _ => { + r#" +foreach ($flag in @('-jw', '--jump-worktree')) { + cd $flag + if (-not (Same-Path (Get-Location).ProviderPath $env:CJ_MAIN) -or $global:__cj_history.Count -ne [int]$env:CJ_COUNT) { throw 'wrong primary destination or history' } + if ([int]$env:CJ_COUNT -gt 0) { cd v } + if (-not (Same-Path (Get-Location).ProviderPath $env:CJ_START) -or $global:__cj_history.Count -ne 0) { throw 'back did not restore start' } +}"# + } + }; + for cwd in [&fixture.linked_nested, &fixture.main_nested, &fixture.main] { + run(&fixture, shell, &config, cwd, success); + } + let failure = match shell { + "bash" | "zsh" => { + r#" +cd "$CJ_OUTSIDE" +for flag in -jw --jump-worktree; do + if cd "$flag" 2>/dev/null; then exit 1; else [[ $? == 2 ]]; fi + [[ $PWD == "$CJ_OUTSIDE" && ${#_cj_history[@]} == 1 ]] +done +cd v; [[ $PWD == "$CJ_START" && ${#_cj_history[@]} == 0 ]]"# + } + "nu" => { + r#" +__cj_cd $env.CJ_OUTSIDE +for flag in [-jw --jump-worktree] { + let failed = (try { __cj_cd $flag; false } catch { true }) + if not $failed or ($env.PWD | path expand) != ($env.CJ_OUTSIDE | path expand) or ($env.__cj_history | length) != 1 { error make {msg: 'nonrepo failure changed state'} } +} +__cj_cd v +if ($env.PWD | path expand) != ($env.CJ_START | path expand) or ($env.__cj_history | length) != 0 { error make {msg: 'failed command changed history target'} }"# + } + _ => { + r#" +cd $env:CJ_OUTSIDE +foreach ($flag in @('-jw', '--jump-worktree')) { + $failed = $false + try { cd $flag } catch { $failed = $true } + if (-not $failed -or -not (Same-Path (Get-Location).ProviderPath $env:CJ_OUTSIDE) -or $global:__cj_history.Count -ne 1) { throw 'nonrepo failure changed state' } +} +cd v +if (-not (Same-Path (Get-Location).ProviderPath $env:CJ_START) -or $global:__cj_history.Count -ne 0) { throw 'failed command changed history target' }"# + } + }; + run(&fixture, shell, &config, fixture.temp.path(), failure); + } + } + assert!(exercised > 0, "no supported shell available"); +}