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
120 complete -F _gw_cd_completion cw-cd
121 eval "$(gw --generate-completion bash 2>/dev/null || true)"
122
123 # Wrap _gw to add config key completion for bash
124 _gw_with_config() {
125 # If completing "config get <key>" or "config set <key>"
126 if [[ ${COMP_WORDS[1]} == "config" && ( ${COMP_WORDS[2]} == "get" || ${COMP_WORDS[2]} == "set" ) && $COMP_CWORD -eq 3 ]]; then
127 local keys
128 keys=$(gw _config-keys 2>/dev/null)
129 COMPREPLY=($(compgen -W "$keys" -- "${COMP_WORDS[COMP_CWORD]}"))
130 return
131 fi
132 _gw "$@"
133 }
134 complete -F _gw_with_config -o bashdefault -o default gw
135 complete -F _gw_with_config -o bashdefault -o default cw
136fi
137
138# Tab completion for zsh
139if [ -n "$ZSH_VERSION" ]; then
140 # Register clap completion for gw/cw CLI inline
141 eval "$(gw --generate-completion zsh 2>/dev/null)"
142
143 # Wrap _gw to add config key completion
144 _gw_with_config() {
145 # If completing "config get <key>" or "config set <key>", offer config keys
146 if [[ ${words[2]} == "config" && ( ${words[3]} == "get" || ${words[3]} == "set" ) && $CURRENT -eq 4 ]]; then
147 local -a keys
148 keys=(${(f)"$(gw _config-keys 2>/dev/null)"})
149 _describe 'config key' keys
150 return
151 fi
152 _gw "$@"
153 }
154 compdef _gw_with_config gw
155 compdef _gw_with_config cw
156
157 _gw_cd_zsh() {
158 local has_global=0
159 local i
160 for i in "${words[@]}"; do
161 case "$i" in -g|--global) has_global=1 ;; esac
162 done
163
164 # Complete flags
165 if [[ "$PREFIX" == -* ]]; then
166 local -a flags
167 flags=('-g:Search all registered repositories' '--global:Search all registered repositories')
168 _describe 'flags' flags
169 return
170 fi
171
172 local -a branches
173 if [ $has_global -eq 1 ]; then
174 branches=(${(f)"$(gw _path --list-branches -g 2>/dev/null)"})
175 else
176 branches=(${(f)"$(git worktree list --porcelain 2>/dev/null | grep '^branch ' | sed 's/^branch refs\/heads\///' | sort -u)"})
177 fi
178 compadd -a branches
179 }
180 compdef _gw_cd_zsh gw-cd
181fi
182
183# Backward compatibility: cw-cd alias
184cw-cd() { gw-cd "$@"; }
185if [ -n "$BASH_VERSION" ]; then
186 complete -F _gw_cd_completion cw-cd
187fi
188if [ -n "$ZSH_VERSION" ]; then
189 compdef _gw_cd_zsh cw-cd
190fi
191"#;
192
193const FISH_FUNCTION: &str = r#"# git-worktree-manager shell functions for fish
194# Source this file to enable shell functions:
195# gw _shell-function fish | source
196
197# Navigate to a worktree by branch name
198# If no argument is provided, show interactive worktree selector
199# Use -g/--global to search across all registered repositories
200# Supports repo:branch notation (auto-enables global mode)
201function gw-cd
202 set -l global_mode 0
203 set -l branch ""
204
205 # Parse arguments
206 for arg in $argv
207 switch $arg
208 case -g --global
209 set global_mode 1
210 case '-*'
211 echo "Error: Unknown option '$arg'" >&2
212 echo "Usage: gw-cd [-g|--global] [branch|repo:branch]" >&2
213 return 1
214 case '*'
215 set branch $arg
216 end
217 end
218
219 # Auto-detect repo:branch notation → enable global mode
220 if test $global_mode -eq 0; and string match -q '*:*' -- "$branch"
221 set global_mode 1
222 end
223
224 set -l worktree_path
225
226 if test -z "$branch"
227 # No argument — interactive selector
228 if test $global_mode -eq 1
229 set worktree_path (gw _path -g --interactive)
230 else
231 set worktree_path (gw _path --interactive)
232 end
233 if test $status -ne 0
234 return 1
235 end
236 else if test $global_mode -eq 1
237 # Global mode: delegate to gw _path -g
238 set worktree_path (gw _path -g "$branch")
239 if test $status -ne 0
240 return 1
241 end
242 else
243 # Local mode: get worktree path from git directly
244 set worktree_path (git worktree list --porcelain 2>/dev/null | awk -v branch="$branch" '
245 /^worktree / { path=$2 }
246 /^branch / && $2 == "refs/heads/"branch { print path; exit }
247 ')
248 end
249
250 if test -z "$worktree_path"
251 if test -z "$branch"
252 echo "Error: No worktree found (not in a git repository?)" >&2
253 else
254 echo "Error: No worktree found for branch '$branch'" >&2
255 end
256 return 1
257 end
258
259 if test -d "$worktree_path"
260 cd "$worktree_path"; or return 1
261 echo "Switched to worktree: $worktree_path"
262 else
263 echo "Error: Worktree directory not found: $worktree_path" >&2
264 return 1
265 end
266end
267
268# Tab completion for gw-cd
269# Complete -g/--global flag
270complete -c gw-cd -s g -l global -d 'Search all registered repositories'
271
272# Complete branch names: global mode if -g is present, otherwise local git
273complete -c gw-cd -f -n '__fish_contains_opt -s g global' -a '(gw _path --list-branches -g 2>/dev/null)'
274complete -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)'
275
276# Backward compatibility: cw-cd alias
277function cw-cd; gw-cd $argv; end
278complete -c cw-cd -w gw-cd
279
280# Tab completion for gw/cw CLI (clap-generated)
281gw --generate-completion fish 2>/dev/null | source
282
283# Config key completion for gw config get/set
284complete -c gw -f -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set' -a '(gw _config-keys 2>/dev/null)'
285complete -c cw -f -n '__fish_seen_subcommand_from config; and __fish_seen_subcommand_from get set' -a '(gw _config-keys 2>/dev/null)'
286"#;
287
288const POWERSHELL_FUNCTION: &str = r#"# git-worktree-manager shell functions for PowerShell
289# Source this file to enable shell functions:
290# gw _shell-function powershell | Out-String | Invoke-Expression
291
292# Navigate to a worktree by branch name
293# If no argument is provided, show interactive worktree selector
294# Use -g to search across all registered repositories
295# Supports repo:branch notation (auto-enables global mode)
296function gw-cd {
297 param(
298 [Parameter(Mandatory=$false, Position=0)]
299 [string]$Branch,
300 [Alias('global')]
301 [switch]$g
302 )
303
304 # Auto-detect repo:branch notation → enable global mode
305 if (-not $g -and $Branch -match ':') {
306 $g = [switch]::Present
307 }
308
309 $worktreePath = $null
310
311 if (-not $Branch) {
312 # No argument — interactive selector
313 if ($g) {
314 $worktreePath = gw _path -g --interactive
315 } else {
316 $worktreePath = gw _path --interactive
317 }
318 if ($LASTEXITCODE -ne 0) {
319 return
320 }
321 } elseif ($g) {
322 # Global mode: delegate to gw _path -g
323 $worktreePath = gw _path -g $Branch
324 if ($LASTEXITCODE -ne 0) {
325 return
326 }
327 } else {
328 # Local mode: get worktree path from git directly
329 $worktreePath = git worktree list --porcelain 2>&1 |
330 Where-Object { $_ -is [string] } |
331 ForEach-Object {
332 if ($_ -match '^worktree (.+)$') { $path = $Matches[1] }
333 if ($_ -match "^branch refs/heads/$Branch$") { $path }
334 } | Select-Object -First 1
335 }
336
337 if (-not $worktreePath) {
338 if (-not $Branch) {
339 Write-Error "Error: No worktree found (not in a git repository?)"
340 } else {
341 Write-Error "Error: No worktree found for branch '$Branch'"
342 }
343 return
344 }
345
346 if (Test-Path -Path $worktreePath -PathType Container) {
347 Set-Location -Path $worktreePath
348 Write-Host "Switched to worktree: $worktreePath"
349 } else {
350 Write-Error "Error: Worktree directory not found: $worktreePath"
351 return
352 }
353}
354
355# Backward compatibility: cw-cd alias
356Set-Alias -Name cw-cd -Value gw-cd
357
358# Tab completion for gw-cd
359Register-ArgumentCompleter -CommandName gw-cd -ParameterName Branch -ScriptBlock {
360 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
361
362 $branches = $null
363 if ($fakeBoundParameters.ContainsKey('g')) {
364 # Global mode: get repo:branch from all registered repos
365 $branches = gw _path --list-branches -g 2>&1 |
366 Where-Object { $_ -is [string] -and $_.Trim() } |
367 Sort-Object -Unique
368 } else {
369 # Local mode: get branches from git
370 $branches = git worktree list --porcelain 2>&1 |
371 Where-Object { $_ -is [string] } |
372 Select-String -Pattern '^branch ' |
373 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
374 Sort-Object -Unique
375 }
376
377 # Filter branches that match the current word
378 $branches | Where-Object { $_ -like "$wordToComplete*" } |
379 ForEach-Object {
380 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
381 }
382}
383
384# Tab completion for cw-cd (backward compat)
385Register-ArgumentCompleter -CommandName cw-cd -ParameterName Branch -ScriptBlock {
386 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
387
388 $branches = $null
389 if ($fakeBoundParameters.ContainsKey('g')) {
390 $branches = gw _path --list-branches -g 2>&1 |
391 Where-Object { $_ -is [string] -and $_.Trim() } |
392 Sort-Object -Unique
393 } else {
394 $branches = git worktree list --porcelain 2>&1 |
395 Where-Object { $_ -is [string] } |
396 Select-String -Pattern '^branch ' |
397 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
398 Sort-Object -Unique
399 }
400
401 $branches | Where-Object { $_ -like "$wordToComplete*" } |
402 ForEach-Object {
403 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
404 }
405}
406"#;
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn test_generate_bash() {
414 let result = generate("bash");
415 assert!(result.is_some());
416 let script = result.unwrap();
417 assert!(script.contains("gw-cd()"));
418 assert!(script.contains("_gw_cd_completion"));
419 assert!(script.contains("cw-cd"));
420 assert!(script.contains("BASH_VERSION"));
421 assert!(script.contains("ZSH_VERSION"));
422 assert!(script.contains("_gw_cd_zsh"));
423 }
424
425 #[test]
426 fn test_generate_zsh() {
427 let result = generate("zsh");
428 assert!(result.is_some());
429 let script = result.unwrap();
430 assert!(script.contains("compdef _gw_cd_zsh gw-cd"));
431 assert!(script.contains("compdef _gw_cd_zsh cw-cd"));
432 }
433
434 #[test]
435 fn test_generate_fish() {
436 let result = generate("fish");
437 assert!(result.is_some());
438 let script = result.unwrap();
439 assert!(script.contains("function gw-cd"));
440 assert!(script.contains("complete -c gw-cd"));
441 assert!(script.contains("function cw-cd"));
442 assert!(script.contains("complete -c cw-cd -w gw-cd"));
443 }
444
445 #[test]
446 fn test_generate_powershell() {
447 let result = generate("powershell");
448 assert!(result.is_some());
449 let script = result.unwrap();
450 assert!(script.contains("function gw-cd"));
451 assert!(script.contains("Register-ArgumentCompleter"));
452 assert!(script.contains("Set-Alias -Name cw-cd -Value gw-cd"));
453 }
454
455 #[test]
456 fn test_generate_pwsh_alias() {
457 let result = generate("pwsh");
458 assert!(result.is_some());
459 assert_eq!(result, generate("powershell"));
461 }
462
463 #[test]
464 fn test_generate_unknown() {
465 assert!(generate("unknown").is_none());
466 assert!(generate("").is_none());
467 }
468
469 #[test]
471 #[cfg(not(windows))]
472 fn test_bash_script_syntax() {
473 let script = generate("bash").unwrap();
474
475 let output = std::process::Command::new("bash")
477 .arg("-n")
478 .stdin(std::process::Stdio::piped())
479 .stdout(std::process::Stdio::piped())
480 .stderr(std::process::Stdio::piped())
481 .spawn()
482 .and_then(|mut child| {
483 use std::io::Write;
484 child.stdin.take().unwrap().write_all(script.as_bytes())?;
485 child.wait_with_output()
486 });
487
488 match output {
489 Ok(out) => {
490 let stderr = String::from_utf8_lossy(&out.stderr);
491 assert!(
492 out.status.success(),
493 "bash -n failed for generated bash/zsh script:\n{}",
494 stderr
495 );
496 }
497 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
498 eprintln!("bash not found, skipping syntax check");
499 }
500 Err(e) => panic!("failed to run bash -n: {}", e),
501 }
502 }
503
504 #[test]
506 fn test_fish_script_syntax() {
507 let script = generate("fish").unwrap();
508
509 let output = std::process::Command::new("fish")
510 .arg("--no-execute")
511 .stdin(std::process::Stdio::piped())
512 .stdout(std::process::Stdio::piped())
513 .stderr(std::process::Stdio::piped())
514 .spawn()
515 .and_then(|mut child| {
516 use std::io::Write;
517 child.stdin.take().unwrap().write_all(script.as_bytes())?;
518 child.wait_with_output()
519 });
520
521 match output {
522 Ok(out) => {
523 let stderr = String::from_utf8_lossy(&out.stderr);
524 assert!(
525 out.status.success(),
526 "fish --no-execute failed for generated fish script:\n{}",
527 stderr
528 );
529 }
530 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
531 eprintln!("fish not found, skipping syntax check");
532 }
533 Err(e) => panic!("failed to run fish --no-execute: {}", e),
534 }
535 }
536}