diff --git a/CLAUDE.md b/CLAUDE.md index 1604f17..d4797f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,8 +45,8 @@ mkdir -p /tmp/gtr-test && cd /tmp/gtr-test && git init && git commit --allow-emp ### Binary Structure -- `bin/git-gtr` — Thin wrapper enabling `git gtr` subcommand invocation -- `bin/gtr` — Entry point: sources libraries and commands, contains `main()` dispatcher +- `bin/git-gtr` — Main entry point: sources libraries and commands, contains `main()` dispatcher +- `bin/gtr` — Convenience wrapper for development (`exec bin/git-gtr`) ### Module Structure @@ -134,7 +134,7 @@ cmd_editor() → resolve_target() → load_editor_adapter() → editor_open() ### Updating the Version -Update `GTR_VERSION` in `bin/gtr` (line 8). +Update `GTR_VERSION` in `bin/git-gtr`. ### Shell Completion Updates @@ -146,7 +146,7 @@ When adding commands or flags, update all three files: ## Critical Gotcha: `set -e` -`bin/gtr` runs with `set -e`. Any unguarded non-zero return silently exits the entire script. When calling functions that may `return 1`, guard with `|| true`: +`bin/git-gtr` runs with `set -e`. Any unguarded non-zero return silently exits the entire script. When calling functions that may `return 1`, guard with `|| true`: ```bash result=$(my_func) || true # Prevents silent exit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 33ca58e..5f22a38 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,7 +32,8 @@ We welcome feature suggestions! Please: ``` git-worktree-runner/ -├── bin/gtr # Main executable (~105 lines: sources libs, dispatches commands) +├── bin/git-gtr # Main executable (sources libs, dispatches commands) +├── bin/gtr # Convenience wrapper (exec bin/git-gtr) ├── lib/ # Core functionality │ ├── ui.sh # User interface (logging, prompts) │ ├── config.sh # Configuration (git-config wrapper) diff --git a/README.md b/README.md index ca6277d..c376751 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,13 @@ gtr cd my-feature gtr cd 1 ``` +> **Note:** If `gtr` conflicts with another command (e.g., GNU `tr` from coreutils), use `--as` to pick a different name: +> +> ```bash +> eval "$(git gtr init zsh --as gwtr)" +> gwtr cd my-feature +> ``` + ### `git gtr run ` Execute command in worktree directory. diff --git a/bin/git-gtr b/bin/git-gtr index 4d7bf28..4637496 100755 --- a/bin/git-gtr +++ b/bin/git-gtr @@ -1,15 +1,118 @@ #!/usr/bin/env bash -# git-gtr - Git worktree runner (git subcommand wrapper) -# Allows running as: git gtr - -# Find this script's real location (resolve symlinks) -SRC="${BASH_SOURCE[0]}" -while [ -h "$SRC" ]; do - DIR="$(cd -P "$(dirname "$SRC")" && pwd)" - SRC="$(readlink "$SRC")" - [[ $SRC != /* ]] && SRC="$DIR/$SRC" +# git-gtr - Git worktree runner +# Portable, cross-platform git worktree management +# Invoked as: git gtr (git subcommand via PATH discovery) + +set -e + +# Debug: show file:line:function on set -e failures +if [ -n "${GTR_DEBUG:-}" ]; then + trap 'printf "ERROR at %s:%s in %s()\n" "${BASH_SOURCE[0]}" "$LINENO" "${FUNCNAME[0]:-main}" >&2' ERR +fi + +# Version +GTR_VERSION="2.3.0" + +# Find the script directory (resolve symlinks; allow env override) +resolve_script_dir() { + local src="${BASH_SOURCE[0]}" + while [ -h "$src" ]; do + local dir + dir="$(cd -P "$(dirname "$src")" && pwd)" + src="$(readlink "$src")" + [[ "$src" != /* ]] && src="$dir/$src" + done + cd -P "$(dirname "$src")/.." && pwd +} +: "${GTR_DIR:=$(resolve_script_dir)}" + +# Source library files +. "$GTR_DIR/lib/ui.sh" +. "$GTR_DIR/lib/args.sh" +. "$GTR_DIR/lib/config.sh" +_ui_apply_color_config +. "$GTR_DIR/lib/platform.sh" +. "$GTR_DIR/lib/core.sh" +. "$GTR_DIR/lib/copy.sh" +. "$GTR_DIR/lib/hooks.sh" +. "$GTR_DIR/lib/provider.sh" +. "$GTR_DIR/lib/adapters.sh" +. "$GTR_DIR/lib/launch.sh" + +# Source command handlers +for _cmd_file in "$GTR_DIR"/lib/commands/*.sh; do + # shellcheck disable=SC1090 + . "$_cmd_file" done -SCRIPT_DIR="$(cd -P "$(dirname "$SRC")" && pwd)" +unset _cmd_file + +# Main dispatcher +main() { + local cmd="${1:-help}" + shift 2>/dev/null || true + + # Set for per-command help (used by show_command_help in ui.sh) + _GTR_CURRENT_COMMAND="$cmd" + + case "$cmd" in + new) + cmd_create "$@" + ;; + rm) + cmd_remove "$@" + ;; + mv|rename) + cmd_rename "$@" + ;; + go) + cmd_go "$@" + ;; + run) + cmd_run "$@" + ;; + editor) + cmd_editor "$@" + ;; + ai) + cmd_ai "$@" + ;; + copy) + cmd_copy "$@" + ;; + ls|list) + cmd_list "$@" + ;; + clean) + cmd_clean "$@" + ;; + doctor) + cmd_doctor "$@" + ;; + adapter|adapters) + cmd_adapter "$@" + ;; + config) + cmd_config "$@" + ;; + completion) + cmd_completion "$@" + ;; + init) + cmd_init "$@" + ;; + version|--version|-v) + echo "git gtr version $GTR_VERSION" + ;; + help|--help|-h) + cmd_help "$@" + ;; + *) + log_error "Unknown command: $cmd" + echo "Use 'git gtr help' for available commands" + exit 1 + ;; + esac +} -# Execute the main gtr script -exec "$SCRIPT_DIR/gtr" "$@" +# Run main +main "$@" diff --git a/bin/gtr b/bin/gtr index e0b1584..d5b269f 100755 --- a/bin/gtr +++ b/bin/gtr @@ -1,117 +1,15 @@ #!/usr/bin/env bash -# gtr - Git worktree runner -# Portable, cross-platform git worktree management - -set -e - -# Debug: show file:line:function on set -e failures -if [ -n "${GTR_DEBUG:-}" ]; then - trap 'printf "ERROR at %s:%s in %s()\n" "${BASH_SOURCE[0]}" "$LINENO" "${FUNCNAME[0]:-main}" >&2' ERR -fi - -# Version -GTR_VERSION="2.3.0" - -# Find the script directory (resolve symlinks; allow env override) -resolve_script_dir() { - local src="${BASH_SOURCE[0]}" - while [ -h "$src" ]; do - local dir - dir="$(cd -P "$(dirname "$src")" && pwd)" - src="$(readlink "$src")" - [[ $src != /* ]] && src="$dir/$src" - done - cd -P "$(dirname "$src")/.." && pwd -} -: "${GTR_DIR:=$(resolve_script_dir)}" - -# Source library files -. "$GTR_DIR/lib/ui.sh" -. "$GTR_DIR/lib/args.sh" -. "$GTR_DIR/lib/config.sh" -_ui_apply_color_config -. "$GTR_DIR/lib/platform.sh" -. "$GTR_DIR/lib/core.sh" -. "$GTR_DIR/lib/copy.sh" -. "$GTR_DIR/lib/hooks.sh" -. "$GTR_DIR/lib/provider.sh" -. "$GTR_DIR/lib/adapters.sh" -. "$GTR_DIR/lib/launch.sh" - -# Source command handlers -for _cmd_file in "$GTR_DIR"/lib/commands/*.sh; do - # shellcheck disable=SC1090 - . "$_cmd_file" +# gtr - Convenience wrapper for git-gtr +# The main binary is bin/git-gtr; this wrapper allows ./bin/gtr for development. + +# Find this script's real location (resolve symlinks) +SRC="${BASH_SOURCE[0]}" +while [ -h "$SRC" ]; do + DIR="$(cd -P "$(dirname "$SRC")" && pwd)" + SRC="$(readlink "$SRC")" + [[ $SRC != /* ]] && SRC="$DIR/$SRC" done -unset _cmd_file - -# Main dispatcher -main() { - local cmd="${1:-help}" - shift 2>/dev/null || true - - # Set for per-command help (used by show_command_help in ui.sh) - _GTR_CURRENT_COMMAND="$cmd" - - case "$cmd" in - new) - cmd_create "$@" - ;; - rm) - cmd_remove "$@" - ;; - mv|rename) - cmd_rename "$@" - ;; - go) - cmd_go "$@" - ;; - run) - cmd_run "$@" - ;; - editor) - cmd_editor "$@" - ;; - ai) - cmd_ai "$@" - ;; - copy) - cmd_copy "$@" - ;; - ls|list) - cmd_list "$@" - ;; - clean) - cmd_clean "$@" - ;; - doctor) - cmd_doctor "$@" - ;; - adapter|adapters) - cmd_adapter "$@" - ;; - config) - cmd_config "$@" - ;; - completion) - cmd_completion "$@" - ;; - init) - cmd_init "$@" - ;; - version|--version|-v) - echo "git gtr version $GTR_VERSION" - ;; - help|--help|-h) - cmd_help "$@" - ;; - *) - log_error "Unknown command: $cmd" - echo "Use 'git gtr help' for available commands" - exit 1 - ;; - esac -} +SCRIPT_DIR="$(cd -P "$(dirname "$SRC")" && pwd)" -# Run main -main "$@" \ No newline at end of file +# Execute the main git-gtr script +exec "$SCRIPT_DIR/git-gtr" "$@" diff --git a/completions/_git-gtr b/completions/_git-gtr index f0eda69..83153ac 100644 --- a/completions/_git-gtr +++ b/completions/_git-gtr @@ -112,7 +112,11 @@ _git-gtr() { # Complete action or scope flags _values 'config action' list get set add unset --local --global --system ;; - completion|init) + completion) + # Complete shell names + _values 'shell' bash zsh fish + ;; + init) # Complete shell names _values 'shell' bash zsh fish ;; @@ -149,6 +153,9 @@ _git-gtr() { worktrees) _describe 'branch names' all_options ;; esac ;; + init) + _arguments '--as[Custom function name]:name:' + ;; config) # Find action by scanning all config args (handles flexible flag positioning) # Use offset 3 to start from words[4] (first arg after 'config') diff --git a/completions/git-gtr.fish b/completions/git-gtr.fish index 1654a31..564c5aa 100644 --- a/completions/git-gtr.fish +++ b/completions/git-gtr.fish @@ -53,6 +53,7 @@ complete -f -c git -n '__fish_git_gtr_needs_command' -a completion -d 'Generate complete -f -c git -n '__fish_git_gtr_using_command completion' -a 'bash zsh fish' -d 'Shell type' complete -f -c git -n '__fish_git_gtr_needs_command' -a init -d 'Generate shell integration for cd support' complete -f -c git -n '__fish_git_gtr_using_command init' -a 'bash zsh fish' -d 'Shell type' +complete -c git -n '__fish_git_gtr_using_command init' -l as -d 'Custom function name' -r complete -f -c git -n '__fish_git_gtr_needs_command' -a version -d 'Show version' complete -f -c git -n '__fish_git_gtr_needs_command' -a help -d 'Show help' diff --git a/completions/gtr.bash b/completions/gtr.bash index d2b715c..3902c5e 100644 --- a/completions/gtr.bash +++ b/completions/gtr.bash @@ -104,12 +104,19 @@ _git_gtr() { COMPREPLY=($(compgen -W "auto remote local none" -- "$cur")) fi ;; - completion|init) + completion) # Complete with shell names if [ "$cword" -eq 3 ]; then COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) fi ;; + init) + if [ "$cword" -eq 3 ]; then + COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) + elif [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--as" -- "$cur")) + fi + ;; config) # Find action by scanning all config args (handles flexible flag positioning) local config_action="" diff --git a/lib/commands/help.sh b/lib/commands/help.sh index 7e9c3d1..5f469cc 100644 --- a/lib/commands/help.sh +++ b/lib/commands/help.sh @@ -340,13 +340,17 @@ _help_init() { cat <<'EOF' git gtr init - Generate shell integration -Usage: git gtr init +Usage: git gtr init [--as ] Generates shell functions for enhanced features like 'gtr cd ' which changes directory to a worktree. Add to your shell configuration. Supported shells: bash, zsh, fish +Options: + --as Set custom function name (default: gtr) + Useful if 'gtr' conflicts with another command (e.g., GNU tr) + Setup: # Bash (add to ~/.bashrc) eval "$(git gtr init bash)" @@ -357,9 +361,13 @@ Setup: # Fish (add to ~/.config/fish/config.fish) git gtr init fish | source + # Custom function name (avoids conflict with coreutils gtr) + eval "$(git gtr init zsh --as gwtr)" + After setup: gtr cd my-feature # cd to worktree gtr cd 1 # cd to main repo + gtr # same as git gtr EOF } @@ -537,8 +545,9 @@ SETUP & MAINTENANCE: Generate shell completions (bash, zsh, fish) Usage: eval "$(git gtr completion zsh)" - init + init [--as ] Generate shell integration for cd support (bash, zsh, fish) + --as : custom function name (default: gtr) Usage: eval "$(git gtr init bash)" version diff --git a/lib/commands/init.sh b/lib/commands/init.sh index f582aa7..09b385e 100644 --- a/lib/commands/init.sh +++ b/lib/commands/init.sh @@ -2,20 +2,83 @@ # Init command (generate shell integration for cd support) cmd_init() { - if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then - show_command_help - fi + local shell="" func_name="gtr" - local shell="${1:-}" + # Parse arguments + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_command_help + ;; + --as) + if [ -z "${2:-}" ]; then + log_error "--as requires a function name" + return 1 + fi + func_name="$2" + shift 2 + ;; + -*) + log_error "Unknown flag: $1" + log_info "Run 'git gtr init --help' for usage" + return 1 + ;; + *) + if [ -n "$shell" ]; then + log_error "Unexpected argument: $1" + return 1 + fi + shell="$1" + shift + ;; + esac + done + + # Validate function name is a legal shell identifier + case "$func_name" in + [a-zA-Z_]*) ;; + *) + log_error "Invalid function name: $func_name (must start with a letter or underscore)" + return 1 + ;; + esac + # Check remaining characters (Bash 3.2 compatible — no regex operator) + local _stripped + _stripped="$(printf '%s' "$func_name" | tr -d 'a-zA-Z0-9_')" + if [ -n "$_stripped" ]; then + log_error "Invalid function name: $func_name (only letters, digits, and underscores allowed)" + return 1 + fi case "$shell" in bash) - cat <<'BASH' + _init_bash | sed "s/__FUNC__/$func_name/g" + ;; + zsh) + _init_zsh | sed "s/__FUNC__/$func_name/g" + ;; + fish) + _init_fish | sed "s/__FUNC__/$func_name/g" + ;; + "") + show_command_help + ;; + *) + log_error "Unknown shell: $shell" + log_error "Supported shells: bash, zsh, fish" + log_info "Run 'git gtr init --help' for usage" + return 1 + ;; + esac +} + +_init_bash() { + cat <<'BASH' # git-gtr shell integration # Add to ~/.bashrc: # eval "$(git gtr init bash)" -gtr() { +__FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift local dir @@ -48,7 +111,7 @@ gtr() { [ -z "$_gtr_hook" ] && continue case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac _gtr_seen="$_gtr_seen|$_gtr_hook|" - eval "$_gtr_hook" || echo "gtr: postCd hook failed: $_gtr_hook" >&2 + eval "$_gtr_hook" || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2 done <<< "$_gtr_hooks" unset WORKTREE_PATH REPO_ROOT BRANCH fi @@ -58,19 +121,37 @@ gtr() { fi } -# Forward completions to gtr wrapper -if type _git_gtr &>/dev/null; then - complete -F _git_gtr gtr -fi +# Completion for __FUNC__ wrapper +___FUNC___completion() { + local cur + cur="${COMP_WORDS[COMP_CWORD]}" + + if [ "$COMP_CWORD" -eq 1 ]; then + # First argument: cd + all git-gtr subcommands + COMPREPLY=($(compgen -W "cd new go run copy editor ai rm mv rename ls list clean doctor adapter config completion init help version" -- "$cur")) + elif [ "${COMP_WORDS[1]}" = "cd" ] && [ "$COMP_CWORD" -eq 2 ]; then + # Worktree names for cd + local worktrees + worktrees="1 $(git gtr list --porcelain 2>/dev/null | cut -f2 | tr '\n' ' ')" + COMPREPLY=($(compgen -W "$worktrees" -- "$cur")) + elif type _git_gtr &>/dev/null; then + # Delegate to git-gtr completions (adjust words to match expected format) + COMP_WORDS=(git gtr "${COMP_WORDS[@]:1}") + (( COMP_CWORD += 1 )) + _git_gtr + fi +} +complete -F ___FUNC___completion __FUNC__ BASH - ;; - zsh) - cat <<'ZSH' +} + +_init_zsh() { + cat <<'ZSH' # git-gtr shell integration # Add to ~/.zshrc: # eval "$(git gtr init zsh)" -gtr() { +__FUNC__() { if [ "$#" -gt 0 ] && [ "$1" = "cd" ]; then shift local dir @@ -103,7 +184,7 @@ gtr() { [ -z "$_gtr_hook" ] && continue case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac _gtr_seen="$_gtr_seen|$_gtr_hook|" - eval "$_gtr_hook" || echo "gtr: postCd hook failed: $_gtr_hook" >&2 + eval "$_gtr_hook" || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2 done <<< "$_gtr_hooks" unset WORKTREE_PATH REPO_ROOT BRANCH fi @@ -113,19 +194,43 @@ gtr() { fi } -# Forward completions to gtr wrapper -if (( $+functions[_git-gtr] )); then - compdef gtr=git-gtr -fi +# Completion for __FUNC__ wrapper +___FUNC___completion() { + local at_subcmd=0 + (( CURRENT == 2 )) && at_subcmd=1 + + if [[ "${words[2]}" == "cd" ]] && (( CURRENT >= 3 )); then + # Completing worktree name after "cd" + if (( CURRENT == 3 )); then + local -a worktrees + worktrees=("1" ${(f)"$(git gtr list --porcelain 2>/dev/null | cut -f2)"}) + _describe 'worktree' worktrees + fi + return + fi + + # Delegate to _git-gtr for standard command completions + if (( $+functions[_git-gtr] )); then + _git-gtr + fi + + # When completing the subcommand position, also offer "cd" + if (( at_subcmd )); then + local -a extra=('cd:Change directory to worktree') + _describe 'extra commands' extra + fi +} +compdef ___FUNC___completion __FUNC__ ZSH - ;; - fish) - cat <<'FISH' +} + +_init_fish() { + cat <<'FISH' # git-gtr shell integration # Add to ~/.config/fish/config.fish: # git gtr init fish | source -function gtr +function __FUNC__ if test (count $argv) -gt 0; and test "$argv[1]" = "cd" set -l dir (command git gtr go $argv[2..]) and cd $dir @@ -150,7 +255,7 @@ function gtr if test -n "$_gtr_hook" if not contains -- "$_gtr_hook" $_gtr_seen set -a _gtr_seen "$_gtr_hook" - eval "$_gtr_hook"; or echo "gtr: postCd hook failed: $_gtr_hook" >&2 + eval "$_gtr_hook"; or echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2 end end end @@ -161,40 +266,47 @@ function gtr end end -# Forward completions to gtr wrapper -complete -c gtr -w git-gtr +# Completion helpers for __FUNC__ wrapper +function ___FUNC___needs_subcommand + set -l cmd (commandline -opc) + test (count $cmd) -eq 1 +end + +function ___FUNC___using_subcommand + set -l cmd (commandline -opc) + if test (count $cmd) -ge 2 + for i in $argv + if test "$cmd[2]" = "$i" + return 0 + end + end + end + return 1 +end + +# Subcommands (cd + all git gtr commands) +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a cd -d 'Change directory to worktree' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a new -d 'Create a new worktree' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a go -d 'Navigate to worktree' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a run -d 'Execute command in worktree' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a copy -d 'Copy files between worktrees' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a rm -d 'Remove worktree(s)' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a mv -d 'Rename worktree and branch' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a rename -d 'Rename worktree and branch' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a editor -d 'Open worktree in editor' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a ai -d 'Start AI coding tool' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a ls -d 'List all worktrees' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a list -d 'List all worktrees' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a clean -d 'Remove stale worktrees' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a doctor -d 'Health check' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a adapter -d 'List available adapters' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a config -d 'Manage configuration' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a completion -d 'Generate shell completions' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a init -d 'Generate shell integration' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a version -d 'Show version' +complete -f -c __FUNC__ -n '___FUNC___needs_subcommand' -a help -d 'Show help' + +# Worktree name completions for cd +complete -f -c __FUNC__ -n '___FUNC___using_subcommand cd' -a '(echo 1; git gtr list --porcelain 2>/dev/null | cut -f2)' FISH - ;; - ""|--help|-h) - echo "Generate shell integration for git gtr" - echo "" - echo "Usage: git gtr init " - echo "" - echo "This outputs a gtr() shell function that enables:" - echo " gtr cd Change directory to worktree" - echo " gtr Passes through to git gtr" - echo "" - echo "Shells:" - echo " bash Generate Bash integration" - echo " zsh Generate Zsh integration" - echo " fish Generate Fish integration" - echo "" - echo "Examples:" - echo " # Bash: add to ~/.bashrc" - echo " eval \"\$(git gtr init bash)\"" - echo "" - echo " # Zsh: add to ~/.zshrc" - echo " eval \"\$(git gtr init zsh)\"" - echo "" - echo " # Fish: add to ~/.config/fish/config.fish" - echo " git gtr init fish | source" - return 0 - ;; - *) - log_error "Unknown shell: $shell" - log_error "Supported shells: bash, zsh, fish" - log_info "Run 'git gtr init --help' for usage" - return 1 - ;; - esac -} \ No newline at end of file +} diff --git a/scripts/generate-completions.sh b/scripts/generate-completions.sh index 4701112..9f483c6 100755 --- a/scripts/generate-completions.sh +++ b/scripts/generate-completions.sh @@ -198,12 +198,19 @@ MIDDLE1 COMPREPLY=($(compgen -W "auto remote local none" -- "$cur")) fi ;; - completion|init) + completion) # Complete with shell names if [ "$cword" -eq 3 ]; then COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) fi ;; + init) + if [ "$cword" -eq 3 ]; then + COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur")) + elif [[ "$cur" == -* ]]; then + COMPREPLY=($(compgen -W "--as" -- "$cur")) + fi + ;; config) # Find action by scanning all config args (handles flexible flag positioning) local config_action="" @@ -364,7 +371,11 @@ _git-gtr() { # Complete action or scope flags _values 'config action' list get set add unset --local --global --system ;; - completion|init) + completion) + # Complete shell names + _values 'shell' bash zsh fish + ;; + init) # Complete shell names _values 'shell' bash zsh fish ;; @@ -405,6 +416,9 @@ MIDDLE1 worktrees) _describe 'branch names' all_options ;; esac ;; + init) + _arguments '--as[Custom function name]:name:' + ;; config) # Find action by scanning all config args (handles flexible flag positioning) # Use offset 3 to start from words[4] (first arg after 'config') @@ -512,6 +526,7 @@ complete -f -c git -n '__fish_git_gtr_needs_command' -a completion -d 'Generate complete -f -c git -n '__fish_git_gtr_using_command completion' -a 'bash zsh fish' -d 'Shell type' complete -f -c git -n '__fish_git_gtr_needs_command' -a init -d 'Generate shell integration for cd support' complete -f -c git -n '__fish_git_gtr_using_command init' -a 'bash zsh fish' -d 'Shell type' +complete -c git -n '__fish_git_gtr_using_command init' -l as -d 'Custom function name' -r complete -f -c git -n '__fish_git_gtr_needs_command' -a version -d 'Show version' complete -f -c git -n '__fish_git_gtr_needs_command' -a help -d 'Show help' @@ -609,6 +624,7 @@ MIDDLE2 gtr.hook.preRemove) desc="Pre-remove hook (abort on failure)" ;; gtr.hook.postRemove) desc="Post-remove hook" ;; gtr.hook.postCd) desc="Post-cd hook (shell integration only)" ;; + gtr.ui.color) desc="Color output mode (auto, always, never)" ;; *) desc="$key" ;; esac printf " %s\t'%s'\n" "$key" "$desc" diff --git a/tests/init.bats b/tests/init.bats new file mode 100644 index 0000000..25f8b5f --- /dev/null +++ b/tests/init.bats @@ -0,0 +1,174 @@ +#!/usr/bin/env bats +# Tests for the init command (lib/commands/init.sh) + +load test_helper + +setup() { + source "$PROJECT_ROOT/lib/commands/init.sh" +} + +# ── Default function name ──────────────────────────────────────────────────── + +@test "bash output defines gtr() function by default" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"gtr()"* ]] +} + +@test "zsh output defines gtr() function by default" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"gtr()"* ]] +} + +@test "fish output defines 'function gtr' by default" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"function gtr"* ]] +} + +# ── --as flag ──────────────────────────────────────────────────────────────── + +@test "bash --as gwtr defines gwtr() function" { + run cmd_init bash --as gwtr + [ "$status" -eq 0 ] + [[ "$output" == *"gwtr()"* ]] + [[ "$output" != *"gtr()"* ]] +} + +@test "zsh --as gwtr defines gwtr() function" { + run cmd_init zsh --as gwtr + [ "$status" -eq 0 ] + [[ "$output" == *"gwtr()"* ]] + [[ "$output" != *"gtr()"* ]] +} + +@test "fish --as gwtr defines 'function gwtr'" { + run cmd_init fish --as gwtr + [ "$status" -eq 0 ] + [[ "$output" == *"function gwtr"* ]] + [[ "$output" != *"function gtr"* ]] +} + +@test "--as replaces function name in completion registration (bash)" { + run cmd_init bash --as myfn + [ "$status" -eq 0 ] + [[ "$output" == *"complete -F _myfn_completion myfn"* ]] +} + +@test "--as replaces function name in compdef (zsh)" { + run cmd_init zsh --as myfn + [ "$status" -eq 0 ] + [[ "$output" == *"compdef _myfn_completion myfn"* ]] +} + +@test "--as replaces function name in fish completions" { + run cmd_init fish --as myfn + [ "$status" -eq 0 ] + [[ "$output" == *"complete -f -c myfn"* ]] +} + +@test "--as replaces error message prefix" { + run cmd_init bash --as gwtr + [ "$status" -eq 0 ] + [[ "$output" == *"gwtr: postCd hook failed"* ]] +} + +@test "--as can appear before shell argument" { + run cmd_init --as gwtr bash + [ "$status" -eq 0 ] + [[ "$output" == *"gwtr()"* ]] +} + +# ── --as validation ────────────────────────────────────────────────────────── + +@test "--as rejects name starting with digit" { + run cmd_init bash --as 123bad + [ "$status" -eq 1 ] +} + +@test "--as rejects name with hyphens" { + run cmd_init bash --as foo-bar + [ "$status" -eq 1 ] +} + +@test "--as rejects name with spaces" { + run cmd_init bash --as "foo bar" + [ "$status" -eq 1 ] +} + +@test "--as accepts underscore-prefixed name" { + run cmd_init bash --as _my_func + [ "$status" -eq 0 ] + [[ "$output" == *"_my_func()"* ]] +} + +@test "--as without value fails" { + run cmd_init bash --as + [ "$status" -eq 1 ] +} + +# ── cd completions ─────────────────────────────────────────────────────────── + +@test "bash output includes cd in subcommand completions" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *'"cd new go run'* ]] +} + +@test "bash output uses git gtr list --porcelain for cd completion" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *"git gtr list --porcelain"* ]] +} + +@test "zsh output includes cd completion" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"cd:Change directory to worktree"* ]] +} + +@test "zsh output uses git gtr list --porcelain for cd completion" { + run cmd_init zsh + [ "$status" -eq 0 ] + [[ "$output" == *"git gtr list --porcelain"* ]] +} + +@test "fish output includes cd subcommand completion" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"-a cd -d"* ]] +} + +@test "fish output uses git gtr list --porcelain for cd completion" { + run cmd_init fish + [ "$status" -eq 0 ] + [[ "$output" == *"git gtr list --porcelain"* ]] +} + +# ── Error cases ────────────────────────────────────────────────────────────── + +@test "unknown shell fails" { + run cmd_init powershell + [ "$status" -eq 1 ] +} + +@test "unknown flag fails" { + run cmd_init bash --unknown + [ "$status" -eq 1 ] +} + +# ── git gtr passthrough preserved ──────────────────────────────────────────── + +@test "bash output passes non-cd commands to git gtr" { + run cmd_init bash + [ "$status" -eq 0 ] + [[ "$output" == *'command git gtr "$@"'* ]] +} + +@test "--as does not replace 'git gtr' invocations" { + run cmd_init bash --as myfn + [ "$status" -eq 0 ] + [[ "$output" == *"command git gtr"* ]] + [[ "$output" == *"git gtr list --porcelain"* ]] +}