git_worktree_manager/
shell_functions.rs1pub fn generate(shell: &str) -> Option<String> {
6 match shell {
7 "bash" | "zsh" => Some(BASH_ZSH_FUNCTION.to_string()),
8 "fish" => Some(FISH_FUNCTION.to_string()),
9 "powershell" | "pwsh" => Some(POWERSHELL_FUNCTION.to_string()),
10 _ => None,
11 }
12}
13
14const BASH_ZSH_FUNCTION: &str = r#"# git-worktree-manager shell functions for bash/zsh
15# Source this file to enable shell functions:
16# source <(gw _shell-function bash)
17
18# Navigate to a worktree by branch name
19# If no argument is provided, show interactive worktree selector
20# Use -g/--global to search across all registered repositories
21# Supports repo:branch notation (auto-enables global mode)
22gw-cd() {
23 local branch=""
24 local global_mode=0
25
26 # Parse arguments
27 while [ $# -gt 0 ]; do
28 case "$1" in
29 -g|--global)
30 global_mode=1
31 shift
32 ;;
33 -*)
34 echo "Error: Unknown option '$1'" >&2
35 echo "Usage: gw-cd [-g|--global] [branch|repo:branch]" >&2
36 return 1
37 ;;
38 *)
39 branch="$1"
40 shift
41 ;;
42 esac
43 done
44
45 # Auto-detect repo:branch notation → enable global mode
46 if [ $global_mode -eq 0 ] && [[ "$branch" == *:* ]]; then
47 global_mode=1
48 fi
49
50 local worktree_path
51
52 if [ -z "$branch" ]; then
53 # No argument — interactive selector
54 if [ $global_mode -eq 1 ]; then
55 worktree_path=$(gw _path -g --interactive)
56 else
57 worktree_path=$(gw _path --interactive)
58 fi
59 if [ $? -ne 0 ]; then return 1; fi
60 elif [ $global_mode -eq 1 ]; then
61 # Global mode: delegate to gw _path -g
62 worktree_path=$(gw _path -g "$branch")
63 if [ $? -ne 0 ]; then return 1; fi
64 else
65 # Local mode: get worktree path from git directly
66 worktree_path=$(git worktree list --porcelain 2>/dev/null | awk -v branch="$branch" '
67 /^worktree / { path=$2 }
68 /^branch / && $2 == "refs/heads/"branch { print path; exit }
69 ')
70 fi
71
72 if [ -z "$worktree_path" ]; then
73 echo "Error: No worktree found for branch '$branch'" >&2
74 return 1
75 fi
76
77 if [ -d "$worktree_path" ]; then
78 cd "$worktree_path" || return 1
79 echo "Switched to worktree: $worktree_path"
80 else
81 echo "Error: Worktree directory not found: $worktree_path" >&2
82 return 1
83 fi
84}
85
86# Tab completion for gw-cd (bash)
87_gw_cd_completion() {
88 local cur="${COMP_WORDS[COMP_CWORD]}"
89 local has_global=0
90
91 # Remove colon from word break chars for repo:branch completion
92 COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
93
94 # Check if -g or --global is already in the command
95 local i
96 for i in "${COMP_WORDS[@]}"; do
97 case "$i" in -g|--global) has_global=1 ;; esac
98 done
99
100 # If current word starts with -, complete flags
101 if [[ "$cur" == -* ]]; then
102 COMPREPLY=($(compgen -W "-g --global" -- "$cur"))
103 return
104 fi
105
106 local branches
107 if [ $has_global -eq 1 ]; then
108 # Global mode: get repo:branch from all registered repos
109 branches=$(gw _path --list-branches -g 2>/dev/null)
110 else
111 # Local mode: get branches directly from git
112 branches=$(git worktree list --porcelain 2>/dev/null | grep "^branch " | sed 's/^branch refs\/heads\///' | sort -u)
113 fi
114 COMPREPLY=($(compgen -W "$branches" -- "$cur"))
115}
116
117# Register completion for bash
118if [ -n "$BASH_VERSION" ]; then
119 complete -F _gw_cd_completion gw-cd
120fi
121
122# Tab completion for zsh
123if [ -n "$ZSH_VERSION" ]; then
124 # Register clap completion for gw CLI inline
125 # (eliminates need for ~/.zfunc/_gw file and FPATH setup)
126 _gw_completion() {
127 eval $(env _GW_COMPLETE=complete_zsh COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) gw --generate-completion zsh 2>/dev/null)
128 }
129
130 _gw_cd_zsh() {
131 local has_global=0
132 local i
133 for i in "${words[@]}"; do
134 case "$i" in -g|--global) has_global=1 ;; esac
135 done
136
137 # Complete flags
138 if [[ "$PREFIX" == -* ]]; then
139 local -a flags
140 flags=('-g:Search all registered repositories' '--global:Search all registered repositories')
141 _describe 'flags' flags
142 return
143 fi
144
145 local -a branches
146 if [ $has_global -eq 1 ]; then
147 branches=(${(f)"$(gw _path --list-branches -g 2>/dev/null)"})
148 else
149 branches=(${(f)"$(git worktree list --porcelain 2>/dev/null | grep '^branch ' | sed 's/^branch refs\/heads\///' | sort -u)"})
150 fi
151 compadd -a branches
152 }
153 compdef _gw_cd_zsh gw-cd
154fi
155
156# Backward compatibility: cw-cd alias
157cw-cd() { gw-cd "$@"; }
158if [ -n "$BASH_VERSION" ]; then
159 complete -F _gw_cd_completion cw-cd
160fi
161if [ -n "$ZSH_VERSION" ]; then
162 compdef _gw_cd_zsh cw-cd
163fi
164"#;
165
166const FISH_FUNCTION: &str = r#"# git-worktree-manager shell functions for fish
167# Source this file to enable shell functions:
168# gw _shell-function fish | source
169
170# Navigate to a worktree by branch name
171# If no argument is provided, show interactive worktree selector
172# Use -g/--global to search across all registered repositories
173# Supports repo:branch notation (auto-enables global mode)
174function gw-cd
175 set -l global_mode 0
176 set -l branch ""
177
178 # Parse arguments
179 for arg in $argv
180 switch $arg
181 case -g --global
182 set global_mode 1
183 case '-*'
184 echo "Error: Unknown option '$arg'" >&2
185 echo "Usage: gw-cd [-g|--global] [branch|repo:branch]" >&2
186 return 1
187 case '*'
188 set branch $arg
189 end
190 end
191
192 # Auto-detect repo:branch notation → enable global mode
193 if test $global_mode -eq 0; and string match -q '*:*' -- "$branch"
194 set global_mode 1
195 end
196
197 set -l worktree_path
198
199 if test -z "$branch"
200 # No argument — interactive selector
201 if test $global_mode -eq 1
202 set worktree_path (gw _path -g --interactive)
203 else
204 set worktree_path (gw _path --interactive)
205 end
206 if test $status -ne 0
207 return 1
208 end
209 else if test $global_mode -eq 1
210 # Global mode: delegate to gw _path -g
211 set worktree_path (gw _path -g "$branch")
212 if test $status -ne 0
213 return 1
214 end
215 else
216 # Local mode: get worktree path from git directly
217 set worktree_path (git worktree list --porcelain 2>/dev/null | awk -v branch="$branch" '
218 /^worktree / { path=$2 }
219 /^branch / && $2 == "refs/heads/"branch { print path; exit }
220 ')
221 end
222
223 if test -z "$worktree_path"
224 if test -z "$branch"
225 echo "Error: No worktree found (not in a git repository?)" >&2
226 else
227 echo "Error: No worktree found for branch '$branch'" >&2
228 end
229 return 1
230 end
231
232 if test -d "$worktree_path"
233 cd "$worktree_path"; or return 1
234 echo "Switched to worktree: $worktree_path"
235 else
236 echo "Error: Worktree directory not found: $worktree_path" >&2
237 return 1
238 end
239end
240
241# Tab completion for gw-cd
242# Complete -g/--global flag
243complete -c gw-cd -s g -l global -d 'Search all registered repositories'
244
245# Complete branch names: global mode if -g is present, otherwise local git
246complete -c gw-cd -f -n '__fish_contains_opt -s g global' -a '(gw _path --list-branches -g 2>/dev/null)'
247complete -c gw-cd -f -n 'not __fish_contains_opt -s g global' -a '(git worktree list --porcelain 2>/dev/null | grep "^branch " | sed "s|^branch refs/heads/||" | sort -u)'
248
249# Backward compatibility: cw-cd alias
250function cw-cd; gw-cd $argv; end
251complete -c cw-cd -w gw-cd
252"#;
253
254const POWERSHELL_FUNCTION: &str = r#"# git-worktree-manager shell functions for PowerShell
255# Source this file to enable shell functions:
256# gw _shell-function powershell | Out-String | Invoke-Expression
257
258# Navigate to a worktree by branch name
259# If no argument is provided, show interactive worktree selector
260# Use -g to search across all registered repositories
261# Supports repo:branch notation (auto-enables global mode)
262function gw-cd {
263 param(
264 [Parameter(Mandatory=$false, Position=0)]
265 [string]$Branch,
266 [Alias('global')]
267 [switch]$g
268 )
269
270 # Auto-detect repo:branch notation → enable global mode
271 if (-not $g -and $Branch -match ':') {
272 $g = [switch]::Present
273 }
274
275 $worktreePath = $null
276
277 if (-not $Branch) {
278 # No argument — interactive selector
279 if ($g) {
280 $worktreePath = gw _path -g --interactive
281 } else {
282 $worktreePath = gw _path --interactive
283 }
284 if ($LASTEXITCODE -ne 0) {
285 return
286 }
287 } elseif ($g) {
288 # Global mode: delegate to gw _path -g
289 $worktreePath = gw _path -g $Branch
290 if ($LASTEXITCODE -ne 0) {
291 return
292 }
293 } else {
294 # Local mode: get worktree path from git directly
295 $worktreePath = git worktree list --porcelain 2>&1 |
296 Where-Object { $_ -is [string] } |
297 ForEach-Object {
298 if ($_ -match '^worktree (.+)$') { $path = $Matches[1] }
299 if ($_ -match "^branch refs/heads/$Branch$") { $path }
300 } | Select-Object -First 1
301 }
302
303 if (-not $worktreePath) {
304 if (-not $Branch) {
305 Write-Error "Error: No worktree found (not in a git repository?)"
306 } else {
307 Write-Error "Error: No worktree found for branch '$Branch'"
308 }
309 return
310 }
311
312 if (Test-Path -Path $worktreePath -PathType Container) {
313 Set-Location -Path $worktreePath
314 Write-Host "Switched to worktree: $worktreePath"
315 } else {
316 Write-Error "Error: Worktree directory not found: $worktreePath"
317 return
318 }
319}
320
321# Backward compatibility: cw-cd alias
322Set-Alias -Name cw-cd -Value gw-cd
323
324# Tab completion for gw-cd
325Register-ArgumentCompleter -CommandName gw-cd -ParameterName Branch -ScriptBlock {
326 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
327
328 $branches = $null
329 if ($fakeBoundParameters.ContainsKey('g')) {
330 # Global mode: get repo:branch from all registered repos
331 $branches = gw _path --list-branches -g 2>&1 |
332 Where-Object { $_ -is [string] -and $_.Trim() } |
333 Sort-Object -Unique
334 } else {
335 # Local mode: get branches from git
336 $branches = git worktree list --porcelain 2>&1 |
337 Where-Object { $_ -is [string] } |
338 Select-String -Pattern '^branch ' |
339 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
340 Sort-Object -Unique
341 }
342
343 # Filter branches that match the current word
344 $branches | Where-Object { $_ -like "$wordToComplete*" } |
345 ForEach-Object {
346 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
347 }
348}
349
350# Tab completion for cw-cd (backward compat)
351Register-ArgumentCompleter -CommandName cw-cd -ParameterName Branch -ScriptBlock {
352 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
353
354 $branches = $null
355 if ($fakeBoundParameters.ContainsKey('g')) {
356 $branches = gw _path --list-branches -g 2>&1 |
357 Where-Object { $_ -is [string] -and $_.Trim() } |
358 Sort-Object -Unique
359 } else {
360 $branches = git worktree list --porcelain 2>&1 |
361 Where-Object { $_ -is [string] } |
362 Select-String -Pattern '^branch ' |
363 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
364 Sort-Object -Unique
365 }
366
367 $branches | Where-Object { $_ -like "$wordToComplete*" } |
368 ForEach-Object {
369 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
370 }
371}
372"#;
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_generate_bash() {
380 let result = generate("bash");
381 assert!(result.is_some());
382 let script = result.unwrap();
383 assert!(script.contains("gw-cd()"));
384 assert!(script.contains("_gw_cd_completion"));
385 assert!(script.contains("cw-cd"));
386 assert!(script.contains("BASH_VERSION"));
387 assert!(script.contains("ZSH_VERSION"));
388 assert!(script.contains("_gw_cd_zsh"));
389 }
390
391 #[test]
392 fn test_generate_zsh() {
393 let result = generate("zsh");
394 assert!(result.is_some());
395 let script = result.unwrap();
396 assert!(script.contains("compdef _gw_cd_zsh gw-cd"));
397 assert!(script.contains("compdef _gw_cd_zsh cw-cd"));
398 }
399
400 #[test]
401 fn test_generate_fish() {
402 let result = generate("fish");
403 assert!(result.is_some());
404 let script = result.unwrap();
405 assert!(script.contains("function gw-cd"));
406 assert!(script.contains("complete -c gw-cd"));
407 assert!(script.contains("function cw-cd"));
408 assert!(script.contains("complete -c cw-cd -w gw-cd"));
409 }
410
411 #[test]
412 fn test_generate_powershell() {
413 let result = generate("powershell");
414 assert!(result.is_some());
415 let script = result.unwrap();
416 assert!(script.contains("function gw-cd"));
417 assert!(script.contains("Register-ArgumentCompleter"));
418 assert!(script.contains("Set-Alias -Name cw-cd -Value gw-cd"));
419 }
420
421 #[test]
422 fn test_generate_pwsh_alias() {
423 let result = generate("pwsh");
424 assert!(result.is_some());
425 assert_eq!(result, generate("powershell"));
427 }
428
429 #[test]
430 fn test_generate_unknown() {
431 assert!(generate("unknown").is_none());
432 assert!(generate("").is_none());
433 }
434
435 #[test]
437 #[cfg(not(windows))]
438 fn test_bash_script_syntax() {
439 let script = generate("bash").unwrap();
440
441 let output = std::process::Command::new("bash")
443 .arg("-n")
444 .stdin(std::process::Stdio::piped())
445 .stdout(std::process::Stdio::piped())
446 .stderr(std::process::Stdio::piped())
447 .spawn()
448 .and_then(|mut child| {
449 use std::io::Write;
450 child.stdin.take().unwrap().write_all(script.as_bytes())?;
451 child.wait_with_output()
452 });
453
454 match output {
455 Ok(out) => {
456 let stderr = String::from_utf8_lossy(&out.stderr);
457 assert!(
458 out.status.success(),
459 "bash -n failed for generated bash/zsh script:\n{}",
460 stderr
461 );
462 }
463 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
464 eprintln!("bash not found, skipping syntax check");
465 }
466 Err(e) => panic!("failed to run bash -n: {}", e),
467 }
468 }
469
470 #[test]
472 fn test_fish_script_syntax() {
473 let script = generate("fish").unwrap();
474
475 let output = std::process::Command::new("fish")
476 .arg("--no-execute")
477 .stdin(std::process::Stdio::piped())
478 .stdout(std::process::Stdio::piped())
479 .stderr(std::process::Stdio::piped())
480 .spawn()
481 .and_then(|mut child| {
482 use std::io::Write;
483 child.stdin.take().unwrap().write_all(script.as_bytes())?;
484 child.wait_with_output()
485 });
486
487 match output {
488 Ok(out) => {
489 let stderr = String::from_utf8_lossy(&out.stderr);
490 assert!(
491 out.status.success(),
492 "fish --no-execute failed for generated fish script:\n{}",
493 stderr
494 );
495 }
496 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
497 eprintln!("fish not found, skipping syntax check");
498 }
499 Err(e) => panic!("failed to run fish --no-execute: {}", e),
500 }
501 }
502}