Skip to main content

git_claw/workflow/
shell.rs

1//! Shell integration workflow (`git claw shell-hook`).
2//!
3//! Emits shell wrapper functions for Bash, Zsh, and Fish enabling automatic directory
4//! navigation (`cd`) upon worktree creation, switching, and finishing.
5
6use crate::workflow::error::WorkflowError;
7
8/// Options for shell hook generation.
9///
10/// :param shell: Target shell name (e.g., "bash", "zsh", "fish").
11///
12/// # Example
13///
14/// ```
15/// use git_claw::workflow::ShellHookOptions;
16///
17/// let options = ShellHookOptions { shell: "fish" };
18/// assert_eq!(options.shell, "fish");
19/// ```
20pub struct ShellHookOptions<'a> {
21    pub shell: &'a str,
22}
23
24/// Generates POSIX-compatible shell integration functions for Bash and Zsh.
25///
26/// :return: Shell script containing `claw` and `git` wrapper functions.
27///
28/// # Example
29///
30/// ```
31/// use git_claw::workflow::shell::generate_posix_hook;
32///
33/// let script = generate_posix_hook();
34/// assert!(script.contains("claw()"));
35/// assert!(script.contains("main_dest"));
36/// assert!(script.contains("should_cd"));
37/// ```
38pub fn generate_posix_hook() -> String {
39    r#"# git-claw shell integration for bash and zsh
40# Usage: eval "$(git claw shell-hook)" or eval "$(git-claw shell-hook)"
41
42claw() {
43    if [ "$1" = "cd" ]; then
44        local dest
45        if [ -n "$2" ]; then
46            dest="$(command git-claw cd "$2")" || return $?
47        else
48            dest="$(command git-claw cd)" || return $?
49        fi
50        if [ -n "$dest" ]; then
51            cd "$dest" || return $?
52        fi
53    elif [ "$2" = "start" ] && { [ "$1" = "feature" ] || [ "$1" = "bugfix" ] || [ "$1" = "hotfix" ] || [ "$1" = "spike" ]; }; then
54        local target_name=""
55        local should_cd=0
56        for arg in "$@"; do
57            case "$arg" in
58                --cd) should_cd=1 ;;
59                feature|bugfix|hotfix|spike|start|--*) ;;
60                *) [ -z "$target_name" ] && target_name="$arg" ;;
61            esac
62        done
63        command git-claw "$@" || return $?
64        if [ $should_cd -eq 1 ] && [ -n "$target_name" ]; then
65            local dest
66            dest="$(command git-claw cd "$target_name")" || return $?
67            if [ -n "$dest" ]; then
68                cd "$dest" || return $?
69            fi
70        fi
71    elif [ "$1" = "finish" ] || [ "$2" = "finish" ] || { [ "$1" = "spike" ] && [ "$2" = "drop" ]; }; then
72        local main_dest
73        main_dest="$(command git-claw cd main 2>/dev/null || command git-claw cd 2>/dev/null)"
74        command git-claw "$@"
75        local ret=$?
76        if [ $ret -eq 0 ] && [ ! -d "$PWD" ]; then
77            if [ -z "$main_dest" ] && [ -f /tmp/.git_claw_target_cd ]; then
78                main_dest="$(cat /tmp/.git_claw_target_cd 2>/dev/null)"
79            fi
80            if [ -n "$main_dest" ]; then
81                cd "$main_dest" || return $?
82            fi
83        fi
84        rm -f /tmp/.git_claw_target_cd 2>/dev/null
85        return $ret
86    else
87        command git-claw "$@"
88    fi
89}
90
91git() {
92    if [ "$1" = "claw" ]; then
93        shift
94        claw "$@"
95    else
96        command git "$@"
97    fi
98}
99"#
100    .to_string()
101}
102
103/// Generates native shell integration functions for Fish shell.
104///
105/// :return: Fish script containing `claw` and `git` function definitions.
106///
107/// # Example
108///
109/// ```
110/// use git_claw::workflow::shell::generate_fish_hook;
111///
112/// let script = generate_fish_hook();
113/// assert!(script.contains("function claw"));
114/// assert!(script.contains("function git"));
115/// assert!(script.contains("main_dest"));
116/// assert!(script.contains("should_cd"));
117/// ```
118pub fn generate_fish_hook() -> String {
119    r#"# git-claw shell integration for fish
120# Usage: git-claw shell-hook fish | source
121
122function claw
123    if test (count $argv) -eq 0
124        command git-claw
125        return $status
126    end
127
128    if test "$argv[1]" = "cd"
129        set -l dest
130        if test (count $argv) -gt 1
131            set dest (command git-claw cd $argv[2])
132        else
133            set dest (command git-claw cd)
134        end
135        if test $status -eq 0; and test -n "$dest"
136            builtin cd "$dest"
137        end
138    else if test (count $argv) -ge 2; and test "$argv[2]" = "start"; and contains -- "$argv[1]" feature bugfix hotfix spike
139        set -l target_name
140        set -l should_cd 0
141        for arg in $argv
142            switch $arg
143                case "--cd"
144                    set should_cd 1
145                case feature bugfix hotfix spike start "--*"
146                case "*"
147                    if test -z "$target_name"
148                        set target_name $arg
149                    end
150            end
151        end
152        command git-claw $argv
153        set -l ret $status
154        if test $ret -eq 0; and test $should_cd -eq 1; and test -n "$target_name"
155            set -l dest (command git-claw cd $target_name)
156            if test $status -eq 0; and test -n "$dest"
157                builtin cd "$dest"
158            end
159        end
160        return $ret
161    else if test "$argv[1]" = "finish"; or begin test (count $argv) -ge 2; and test "$argv[2]" = "finish"; end; or begin test (count $argv) -ge 2; and test "$argv[1]" = "spike"; and test "$argv[2]" = "drop"; end
162        set -l main_dest (command git-claw cd main 2>/dev/null; or command git-claw cd 2>/dev/null)
163        command git-claw $argv
164        set -l ret $status
165        if test $ret -eq 0; and not test -d "$PWD"
166            if test -z "$main_dest"; and test -f /tmp/.git_claw_target_cd
167                set main_dest (cat /tmp/.git_claw_target_cd 2>/dev/null)
168            end
169            if test -n "$main_dest"
170                builtin cd "$main_dest"
171            end
172        end
173        command rm -f /tmp/.git_claw_target_cd 2>/dev/null
174        return $ret
175    else
176        command git-claw $argv
177    end
178end
179
180function git
181    if test (count $argv) -gt 0; and test "$argv[1]" = "claw"
182
183        claw $argv[2..-1]
184    else
185        command git $argv
186    end
187end
188"#
189    .to_string()
190}
191
192/// Generates the shell hook script for the specified shell.
193///
194/// :param shell: Target shell name ("fish", "bash", "zsh", etc.).
195/// :return: Wrapper script as a String.
196///
197/// # Example
198///
199/// ```
200/// use git_claw::workflow::shell::generate_shell_hook;
201///
202/// let script = generate_shell_hook("bash");
203/// assert!(script.contains("claw()"));
204/// assert!(script.contains("command git-claw"));
205///
206/// let fish_script = generate_shell_hook("fish");
207/// assert!(fish_script.contains("function claw"));
208/// ```
209pub fn generate_shell_hook(shell: &str) -> String {
210    match shell.to_lowercase().as_str() {
211        "fish" => generate_fish_hook(),
212        _ => generate_posix_hook(),
213    }
214}
215
216/// Runs the shell hook workflow, outputting the wrapper script to stdout.
217///
218/// :param options: Options containing the target shell.
219/// :return: Result indicating success or [`WorkflowError`].
220///
221/// # Example
222///
223/// ```
224/// use git_claw::workflow::shell::{run_shell_hook, ShellHookOptions};
225///
226/// let res = run_shell_hook(ShellHookOptions { shell: "bash" });
227/// assert!(res.is_ok());
228/// ```
229pub fn run_shell_hook(options: ShellHookOptions) -> Result<(), WorkflowError> {
230    let script = generate_shell_hook(options.shell);
231    print!("{}", script);
232    Ok(())
233}