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 eval "$(gw --generate-completion bash 2>/dev/null || true)"
121fi
122
123# Tab completion for zsh
124if [ -n "$ZSH_VERSION" ]; then
125 # Register clap completion for gw/cw CLI inline
126 eval "$(gw --generate-completion zsh 2>/dev/null)"
127 compdef _gw gw
128 compdef _gw cw
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# Tab completion for gw/cw CLI (clap-generated)
254gw --generate-completion fish 2>/dev/null | source
255"#;
256
257const POWERSHELL_FUNCTION: &str = r#"# git-worktree-manager shell functions for PowerShell
258# Source this file to enable shell functions:
259# gw _shell-function powershell | Out-String | Invoke-Expression
260
261# Navigate to a worktree by branch name
262# If no argument is provided, show interactive worktree selector
263# Use -g to search across all registered repositories
264# Supports repo:branch notation (auto-enables global mode)
265function gw-cd {
266 param(
267 [Parameter(Mandatory=$false, Position=0)]
268 [string]$Branch,
269 [Alias('global')]
270 [switch]$g
271 )
272
273 # Auto-detect repo:branch notation → enable global mode
274 if (-not $g -and $Branch -match ':') {
275 $g = [switch]::Present
276 }
277
278 $worktreePath = $null
279
280 if (-not $Branch) {
281 # No argument — interactive selector
282 if ($g) {
283 $worktreePath = gw _path -g --interactive
284 } else {
285 $worktreePath = gw _path --interactive
286 }
287 if ($LASTEXITCODE -ne 0) {
288 return
289 }
290 } elseif ($g) {
291 # Global mode: delegate to gw _path -g
292 $worktreePath = gw _path -g $Branch
293 if ($LASTEXITCODE -ne 0) {
294 return
295 }
296 } else {
297 # Local mode: get worktree path from git directly
298 $worktreePath = git worktree list --porcelain 2>&1 |
299 Where-Object { $_ -is [string] } |
300 ForEach-Object {
301 if ($_ -match '^worktree (.+)$') { $path = $Matches[1] }
302 if ($_ -match "^branch refs/heads/$Branch$") { $path }
303 } | Select-Object -First 1
304 }
305
306 if (-not $worktreePath) {
307 if (-not $Branch) {
308 Write-Error "Error: No worktree found (not in a git repository?)"
309 } else {
310 Write-Error "Error: No worktree found for branch '$Branch'"
311 }
312 return
313 }
314
315 if (Test-Path -Path $worktreePath -PathType Container) {
316 Set-Location -Path $worktreePath
317 Write-Host "Switched to worktree: $worktreePath"
318 } else {
319 Write-Error "Error: Worktree directory not found: $worktreePath"
320 return
321 }
322}
323
324# Backward compatibility: cw-cd alias
325Set-Alias -Name cw-cd -Value gw-cd
326
327# Tab completion for gw-cd
328Register-ArgumentCompleter -CommandName gw-cd -ParameterName Branch -ScriptBlock {
329 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
330
331 $branches = $null
332 if ($fakeBoundParameters.ContainsKey('g')) {
333 # Global mode: get repo:branch from all registered repos
334 $branches = gw _path --list-branches -g 2>&1 |
335 Where-Object { $_ -is [string] -and $_.Trim() } |
336 Sort-Object -Unique
337 } else {
338 # Local mode: get branches from git
339 $branches = git worktree list --porcelain 2>&1 |
340 Where-Object { $_ -is [string] } |
341 Select-String -Pattern '^branch ' |
342 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
343 Sort-Object -Unique
344 }
345
346 # Filter branches that match the current word
347 $branches | Where-Object { $_ -like "$wordToComplete*" } |
348 ForEach-Object {
349 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
350 }
351}
352
353# Tab completion for cw-cd (backward compat)
354Register-ArgumentCompleter -CommandName cw-cd -ParameterName Branch -ScriptBlock {
355 param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
356
357 $branches = $null
358 if ($fakeBoundParameters.ContainsKey('g')) {
359 $branches = gw _path --list-branches -g 2>&1 |
360 Where-Object { $_ -is [string] -and $_.Trim() } |
361 Sort-Object -Unique
362 } else {
363 $branches = git worktree list --porcelain 2>&1 |
364 Where-Object { $_ -is [string] } |
365 Select-String -Pattern '^branch ' |
366 ForEach-Object { $_ -replace '^branch refs/heads/', '' } |
367 Sort-Object -Unique
368 }
369
370 $branches | Where-Object { $_ -like "$wordToComplete*" } |
371 ForEach-Object {
372 [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
373 }
374}
375"#;
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn test_generate_bash() {
383 let result = generate("bash");
384 assert!(result.is_some());
385 let script = result.unwrap();
386 assert!(script.contains("gw-cd()"));
387 assert!(script.contains("_gw_cd_completion"));
388 assert!(script.contains("cw-cd"));
389 assert!(script.contains("BASH_VERSION"));
390 assert!(script.contains("ZSH_VERSION"));
391 assert!(script.contains("_gw_cd_zsh"));
392 }
393
394 #[test]
395 fn test_generate_zsh() {
396 let result = generate("zsh");
397 assert!(result.is_some());
398 let script = result.unwrap();
399 assert!(script.contains("compdef _gw_cd_zsh gw-cd"));
400 assert!(script.contains("compdef _gw_cd_zsh cw-cd"));
401 }
402
403 #[test]
404 fn test_generate_fish() {
405 let result = generate("fish");
406 assert!(result.is_some());
407 let script = result.unwrap();
408 assert!(script.contains("function gw-cd"));
409 assert!(script.contains("complete -c gw-cd"));
410 assert!(script.contains("function cw-cd"));
411 assert!(script.contains("complete -c cw-cd -w gw-cd"));
412 }
413
414 #[test]
415 fn test_generate_powershell() {
416 let result = generate("powershell");
417 assert!(result.is_some());
418 let script = result.unwrap();
419 assert!(script.contains("function gw-cd"));
420 assert!(script.contains("Register-ArgumentCompleter"));
421 assert!(script.contains("Set-Alias -Name cw-cd -Value gw-cd"));
422 }
423
424 #[test]
425 fn test_generate_pwsh_alias() {
426 let result = generate("pwsh");
427 assert!(result.is_some());
428 assert_eq!(result, generate("powershell"));
430 }
431
432 #[test]
433 fn test_generate_unknown() {
434 assert!(generate("unknown").is_none());
435 assert!(generate("").is_none());
436 }
437
438 #[test]
440 #[cfg(not(windows))]
441 fn test_bash_script_syntax() {
442 let script = generate("bash").unwrap();
443
444 let output = std::process::Command::new("bash")
446 .arg("-n")
447 .stdin(std::process::Stdio::piped())
448 .stdout(std::process::Stdio::piped())
449 .stderr(std::process::Stdio::piped())
450 .spawn()
451 .and_then(|mut child| {
452 use std::io::Write;
453 child.stdin.take().unwrap().write_all(script.as_bytes())?;
454 child.wait_with_output()
455 });
456
457 match output {
458 Ok(out) => {
459 let stderr = String::from_utf8_lossy(&out.stderr);
460 assert!(
461 out.status.success(),
462 "bash -n failed for generated bash/zsh script:\n{}",
463 stderr
464 );
465 }
466 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
467 eprintln!("bash not found, skipping syntax check");
468 }
469 Err(e) => panic!("failed to run bash -n: {}", e),
470 }
471 }
472
473 #[test]
475 fn test_fish_script_syntax() {
476 let script = generate("fish").unwrap();
477
478 let output = std::process::Command::new("fish")
479 .arg("--no-execute")
480 .stdin(std::process::Stdio::piped())
481 .stdout(std::process::Stdio::piped())
482 .stderr(std::process::Stdio::piped())
483 .spawn()
484 .and_then(|mut child| {
485 use std::io::Write;
486 child.stdin.take().unwrap().write_all(script.as_bytes())?;
487 child.wait_with_output()
488 });
489
490 match output {
491 Ok(out) => {
492 let stderr = String::from_utf8_lossy(&out.stderr);
493 assert!(
494 out.status.success(),
495 "fish --no-execute failed for generated fish script:\n{}",
496 stderr
497 );
498 }
499 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
500 eprintln!("fish not found, skipping syntax check");
501 }
502 Err(e) => panic!("failed to run fish --no-execute: {}", e),
503 }
504 }
505}