Skip to content

Commit 8bbf922

Browse files
committed
fix: address PR 162 regressions
1 parent f1795fd commit 8bbf922

File tree

12 files changed

+341
-55
lines changed

12 files changed

+341
-55
lines changed

lib/adapters.sh

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,7 @@ editor_open() {
166166
target="$workspace"
167167
fi
168168

169-
# Split multi-word commands (e.g., "code --wait") into an array for safe execution
170-
local _cmd_arr
171-
read -ra _cmd_arr <<< "$GTR_EDITOR_CMD"
172-
"${_cmd_arr[@]}" "$target"
169+
_run_configured_command "$GTR_EDITOR_CMD" "$target"
173170
}
174171

175172
# Globals set by load_ai_adapter: GTR_AI_CMD, GTR_AI_CMD_NAME
@@ -180,10 +177,20 @@ ai_can_start() {
180177
ai_start() {
181178
local path="$1"
182179
shift
183-
# Split multi-word commands (e.g., "bunx @github/copilot@latest") into an array for safe execution
184-
local _cmd_arr
185-
read -ra _cmd_arr <<< "$GTR_AI_CMD"
186-
(cd "$path" && "${_cmd_arr[@]}" "$@")
180+
(cd "$path" && _run_configured_command "$GTR_AI_CMD" "$@")
181+
}
182+
183+
# Parse and run a config-supplied command string while preserving quoted args.
184+
_run_configured_command() {
185+
local command_string="$1"
186+
shift
187+
local extra_args=("$@")
188+
189+
(
190+
eval "set -- $command_string" || exit 1
191+
[ "$#" -gt 0 ] || exit 1
192+
"$@" "${extra_args[@]}"
193+
)
187194
}
188195

189196
# Standard AI adapter builder — used by adapter files that follow the common pattern
@@ -297,23 +304,21 @@ resolve_workspace_file() {
297304
# Usage: _load_adapter <type> <name> <label> <builtin_list> <path_hint>
298305
_load_adapter() {
299306
local type="$1" name="$2" label="$3" builtin_list="$4" path_hint="$5"
307+
local adapter_selector="${name%% *}"
300308

301-
# Reject adapter names containing path traversal characters
302-
case "$name" in
303-
*/* | *..* | *\\*)
304-
log_error "$label name '$name' contains invalid characters"
305-
return 1
306-
;;
307-
esac
308-
309-
local adapter_file="$GTR_DIR/adapters/${type}/${name}.sh"
309+
local adapter_file="$GTR_DIR/adapters/${type}/${adapter_selector}.sh"
310310

311311
# 1. Try loading explicit adapter file (custom overrides like claude, nano)
312-
if [ -f "$adapter_file" ]; then
313-
# shellcheck disable=SC1090
314-
. "$adapter_file"
315-
return 0
316-
fi
312+
case "$adapter_selector" in
313+
*/* | *..* | *\\*) ;;
314+
*)
315+
if [ -f "$adapter_file" ]; then
316+
# shellcheck disable=SC1090
317+
. "$adapter_file"
318+
return 0
319+
fi
320+
;;
321+
esac
317322

318323
# 2. Try registry lookup (declarative adapters)
319324
local registry entry

lib/commands/init.sh

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ cmd_init() {
6565
;;
6666
esac
6767

68-
# Generate output (cached to ~/.cache/gtr/, auto-invalidates on version change)
68+
# Generate output (cached to ~/.cache/gtr/, auto-invalidates on shell integration changes)
6969
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/gtr"
7070
local cache_file="$cache_dir/init-${func_name}.${shell}"
71-
local cache_stamp="# gtr-cache: version=${GTR_VERSION:-unknown} func=$func_name shell=$shell"
71+
local cache_schema="${GTR_INIT_CACHE_VERSION:-2}"
72+
local cache_stamp="# gtr-cache: version=${GTR_VERSION:-unknown} init=${cache_schema} func=$func_name shell=$shell"
7273

7374
# Return cached output if version matches
7475
if [ -f "$cache_file" ]; then
@@ -94,6 +95,26 @@ _init_bash() {
9495
# git-gtr shell integration (cached to ~/.cache/gtr/)
9596
# Setup: see git gtr help init
9697
98+
__FUNC___gtrconfig_path() {
99+
local _gtr_git_common_dir _gtr_repo_root
100+
_gtr_git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
101+
102+
if [ "$_gtr_git_common_dir" = ".git" ]; then
103+
_gtr_repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
104+
else
105+
_gtr_repo_root="${_gtr_git_common_dir%/.git}"
106+
fi
107+
108+
printf '%s/.gtrconfig\n' "$_gtr_repo_root"
109+
}
110+
111+
__FUNC___hooks_hash() {
112+
local _gtr_config_file="$1"
113+
local _gtr_hook_defs
114+
_gtr_hook_defs="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null)" || return 1
115+
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
116+
}
117+
97118
__FUNC___run_post_cd_hooks() {
98119
local dir="$1"
99120
local _gtr_trust_dir="${XDG_CONFIG_HOME:-$HOME/.config}/gtr/trusted"
@@ -105,14 +126,14 @@ __FUNC___run_post_cd_hooks() {
105126
# Read from git config (local > global > system)
106127
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
107128
# Read from .gtrconfig if it exists — only if trusted
108-
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
129+
_gtr_config_file="$(__FUNC___gtrconfig_path 2>/dev/null)" || true
109130
if [ -f "$_gtr_config_file" ]; then
110131
local _gtr_file_hooks
111132
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
112133
if [ -n "$_gtr_file_hooks" ]; then
113134
# Verify trust before including .gtrconfig hooks
114135
local _gtr_hook_hash
115-
_gtr_hook_hash="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)" || true
136+
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || true
116137
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
117138
if [ -n "$_gtr_hooks" ]; then
118139
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
@@ -253,7 +274,7 @@ ___FUNC___completion() {
253274
254275
if [ "$COMP_CWORD" -eq 1 ]; then
255276
# First argument: cd + all git-gtr subcommands
256-
COMPREPLY=($(compgen -W "cd new go run copy editor ai rm mv rename ls list clean doctor adapter config completion init help version" -- "$cur"))
277+
COMPREPLY=($(compgen -W "cd new go run copy editor ai rm mv rename ls list clean doctor adapter config completion init trust help version" -- "$cur"))
257278
elif [ "${COMP_WORDS[1]}" = "cd" ] && [ "$COMP_CWORD" -eq 2 ]; then
258279
# Worktree names for cd
259280
local worktrees
@@ -278,6 +299,28 @@ _init_zsh() {
278299
# git-gtr shell integration (cached to ~/.cache/gtr/)
279300
# Setup: see git gtr help init
280301
302+
__FUNC___gtrconfig_path() {
303+
emulate -L zsh
304+
local _gtr_git_common_dir _gtr_repo_root
305+
_gtr_git_common_dir="$(git rev-parse --git-common-dir 2>/dev/null)" || return 1
306+
307+
if [ "$_gtr_git_common_dir" = ".git" ]; then
308+
_gtr_repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || return 1
309+
else
310+
_gtr_repo_root="${_gtr_git_common_dir%/.git}"
311+
fi
312+
313+
printf '%s/.gtrconfig\n' "$_gtr_repo_root"
314+
}
315+
316+
__FUNC___hooks_hash() {
317+
emulate -L zsh
318+
local _gtr_config_file="$1"
319+
local _gtr_hook_defs
320+
_gtr_hook_defs="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null)" || return 1
321+
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
322+
}
323+
281324
__FUNC___run_post_cd_hooks() {
282325
emulate -L zsh
283326
local dir="$1"
@@ -290,14 +333,14 @@ __FUNC___run_post_cd_hooks() {
290333
# Read from git config (local > global > system)
291334
_gtr_hooks="$(git config --get-all gtr.hook.postCd 2>/dev/null)" || true
292335
# Read from .gtrconfig if it exists — only if trusted
293-
_gtr_config_file="$(git rev-parse --show-toplevel 2>/dev/null)/.gtrconfig"
336+
_gtr_config_file="$(__FUNC___gtrconfig_path 2>/dev/null)" || true
294337
if [ -f "$_gtr_config_file" ]; then
295338
local _gtr_file_hooks
296339
_gtr_file_hooks="$(git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)" || true
297340
if [ -n "$_gtr_file_hooks" ]; then
298341
# Verify trust before including .gtrconfig hooks
299342
local _gtr_hook_hash
300-
_gtr_hook_hash="$(git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)" || true
343+
_gtr_hook_hash="$(__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)" || true
301344
if [ -n "$_gtr_hook_hash" ] && [ -f "$_gtr_trust_dir/$_gtr_hook_hash" ]; then
302345
if [ -n "$_gtr_hooks" ]; then
303346
_gtr_hooks="$_gtr_hooks"$'\n'"$_gtr_file_hooks"
@@ -465,6 +508,26 @@ _init_fish() {
465508
# Add to ~/.config/fish/config.fish:
466509
# git gtr init fish | source
467510
511+
function __FUNC___gtrconfig_path
512+
set -l _gtr_git_common_dir (git rev-parse --git-common-dir 2>/dev/null)
513+
test -n "$_gtr_git_common_dir"; or return 1
514+
515+
if test "$_gtr_git_common_dir" = ".git"
516+
set -l _gtr_repo_root (git rev-parse --show-toplevel 2>/dev/null)
517+
test -n "$_gtr_repo_root"; or return 1
518+
printf '%s/.gtrconfig\n' "$_gtr_repo_root"
519+
else
520+
printf '%s/.gtrconfig\n' (string replace -r '/\\.git$' '' -- "$_gtr_git_common_dir")
521+
end
522+
end
523+
524+
function __FUNC___hooks_hash
525+
set -l _gtr_config_file "$argv[1]"
526+
set -l _gtr_hook_defs (git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null)
527+
test $status -eq 0; or return 1
528+
printf '%s\n' "$_gtr_hook_defs" | shasum -a 256 | cut -d' ' -f1
529+
end
530+
468531
function __FUNC___run_post_cd_hooks
469532
set -l dir "$argv[1]"
470533
set -l _gtr_trust_dir "$HOME/.config/gtr/trusted"
@@ -478,13 +541,13 @@ function __FUNC___run_post_cd_hooks
478541
# Read from git config (local > global > system)
479542
set -l _gtr_git_hooks (git config --get-all gtr.hook.postCd 2>/dev/null)
480543
# Read from .gtrconfig if it exists — only if trusted
481-
set -l _gtr_config_file (git rev-parse --show-toplevel 2>/dev/null)"/.gtrconfig"
544+
set -l _gtr_config_file (__FUNC___gtrconfig_path 2>/dev/null)
482545
set -l _gtr_file_hooks
483546
if test -f "$_gtr_config_file"
484547
set -l _gtr_candidate_hooks (git config -f "$_gtr_config_file" --get-all hooks.postCd 2>/dev/null)
485548
if test (count $_gtr_candidate_hooks) -gt 0
486549
# Verify trust before including .gtrconfig hooks
487-
set -l _gtr_hook_hash (git config -f "$_gtr_config_file" --get-regexp '^hooks\.' 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
550+
set -l _gtr_hook_hash (__FUNC___hooks_hash "$_gtr_config_file" 2>/dev/null)
488551
if test -n "$_gtr_hook_hash"; and test -f "$_gtr_trust_dir/$_gtr_hook_hash"
489552
set _gtr_file_hooks $_gtr_candidate_hooks
490553
else

lib/commands/trust.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,12 @@ cmd_trust() {
3333
log_warn "These commands will execute on your machine during gtr operations."
3434

3535
if prompt_yes_no "Trust these hooks?"; then
36-
_hooks_mark_trusted "$config_file"
37-
log_info "Hooks marked as trusted"
36+
if _hooks_mark_trusted "$config_file"; then
37+
log_info "Hooks marked as trusted"
38+
else
39+
log_error "Failed to mark hooks as trusted"
40+
return 1
41+
fi
3842
else
3943
log_info "Hooks remain untrusted and will not execute"
4044
fi

lib/copy.sh

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,14 +282,21 @@ _apply_directory_excludes() {
282282
local pattern_prefix="${exclude_pattern%%/*}"
283283
local pattern_suffix="${exclude_pattern#*/}"
284284

285-
# Reject bare glob-only suffixes that would match everything
285+
# Reject empty suffixes and protect Git metadata from removal
286286
case "$pattern_suffix" in
287-
""|"*"|"**"|".*")
287+
"")
288288
log_warn "Skipping overly broad exclude suffix: $exclude_pattern"
289289
continue
290290
;;
291291
esac
292292

293+
case "$exclude_pattern" in
294+
.git|*/.git|.git/*|*/.git/*)
295+
log_warn "Skipping exclude pattern targeting .git metadata: $exclude_pattern"
296+
continue
297+
;;
298+
esac
299+
293300
# Intentional glob pattern matching for directory prefix
294301
# shellcheck disable=SC2254
295302
case "$dir_path" in

lib/hooks.sh

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ _hooks_file_hash() {
2020
if [ -z "$hook_content" ]; then
2121
return 1
2222
fi
23-
printf '%s' "$hook_content" | shasum -a 256 | cut -d' ' -f1
23+
printf '%s\n' "$hook_content" | shasum -a 256 | cut -d' ' -f1
24+
}
25+
26+
# Resolve the trust marker path for a .gtrconfig file
27+
# Usage: _hooks_trust_path <config_file>
28+
_hooks_trust_path() {
29+
local config_file="$1"
30+
local hash
31+
hash=$(_hooks_file_hash "$config_file") || return 1
32+
printf '%s/%s\n' "$_GTR_TRUST_DIR" "$hash"
2433
}
2534

2635
# Check if .gtrconfig hooks are trusted for the current repository
@@ -30,21 +39,21 @@ _hooks_are_trusted() {
3039
local config_file="$1"
3140
[ ! -f "$config_file" ] && return 0
3241

33-
local hash
34-
hash=$(_hooks_file_hash "$config_file") || return 0 # no hooks = trusted
42+
local trust_path
43+
trust_path=$(_hooks_trust_path "$config_file") || return 0 # no hooks = trusted
3544

36-
[ -f "$_GTR_TRUST_DIR/$hash" ]
45+
[ -f "$trust_path" ]
3746
}
3847

3948
# Mark .gtrconfig hooks as trusted
4049
# Usage: _hooks_mark_trusted <config_file>
4150
_hooks_mark_trusted() {
4251
local config_file="$1"
43-
local hash
44-
hash=$(_hooks_file_hash "$config_file") || return 0
52+
local trust_path
53+
trust_path=$(_hooks_trust_path "$config_file") || return 0
4554

4655
mkdir -p "$_GTR_TRUST_DIR"
47-
printf '%s\n' "$config_file" > "$_GTR_TRUST_DIR/$hash"
56+
printf '%s\n' "$config_file" > "$trust_path"
4857
}
4958

5059
# Get hooks, filtering out untrusted .gtrconfig hooks with a warning

lib/platform.sh

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,32 +126,52 @@ spawn_terminal_in() {
126126
fi
127127
;;
128128
linux)
129-
# Escape cmd and path for safe embedding in sh -c strings
130-
local safe_sh_cmd safe_sh_path
131-
safe_sh_cmd=$(printf '%q' "$cmd")
132-
safe_sh_path=$(printf '%q' "$path")
129+
local shell_after="${SHELL:-/bin/sh}"
130+
# shellcheck disable=SC2016 # The script is intentionally evaluated by sh in the new terminal.
131+
local terminal_script='cd "$1" || exit 1
132+
shift
133+
if [ "$#" -gt 0 ]; then
134+
"$@"
135+
fi
136+
exec "$SHELL_AFTER"'
133137
# Try common terminal emulators
134138
if command -v gnome-terminal >/dev/null 2>&1; then
135-
gnome-terminal --working-directory="$path" --title="$title" -- sh -c "$safe_sh_cmd; exec \$SHELL" 2>/dev/null || true
139+
if [ -n "$cmd" ]; then
140+
gnome-terminal --working-directory="$path" --title="$title" -- env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" bash -lc "$cmd" 2>/dev/null || true
141+
else
142+
gnome-terminal --working-directory="$path" --title="$title" -- env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" 2>/dev/null || true
143+
fi
136144
elif command -v konsole >/dev/null 2>&1; then
137-
konsole --workdir "$path" -p "tabtitle=$title" -e sh -c "$safe_sh_cmd; exec \$SHELL" 2>/dev/null || true
145+
if [ -n "$cmd" ]; then
146+
konsole --workdir "$path" -p "tabtitle=$title" -e env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" bash -lc "$cmd" 2>/dev/null || true
147+
else
148+
konsole --workdir "$path" -p "tabtitle=$title" -e env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" 2>/dev/null || true
149+
fi
138150
elif command -v xterm >/dev/null 2>&1; then
139-
xterm -T "$title" -e "cd $safe_sh_path && $safe_sh_cmd && exec \$SHELL" 2>/dev/null || true
151+
if [ -n "$cmd" ]; then
152+
xterm -T "$title" -e env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" bash -lc "$cmd" 2>/dev/null || true
153+
else
154+
xterm -T "$title" -e env SHELL_AFTER="$shell_after" sh -c "$terminal_script" sh "$path" 2>/dev/null || true
155+
fi
140156
else
141157
log_warn "No supported terminal emulator found"
142158
return 1
143159
fi
144160
;;
145161
windows)
146-
# Escape for safe embedding in cmd.exe strings
147-
local safe_win_cmd safe_win_path
148-
safe_win_cmd=$(printf '%q' "$cmd")
149-
safe_win_path=$(printf '%q' "$path")
150162
# Try Windows Terminal, then fallback to cmd
151163
if command -v wt >/dev/null 2>&1; then
152-
wt -d "$path" "$cmd" 2>/dev/null || true
164+
if [ -n "$cmd" ]; then
165+
wt -d "$path" cmd.exe /k "$cmd" 2>/dev/null || true
166+
else
167+
wt -d "$path" 2>/dev/null || true
168+
fi
153169
else
154-
cmd.exe /c start "$title" cmd.exe /k "cd /d $safe_win_path && $safe_win_cmd" 2>/dev/null || true
170+
if [ -n "$cmd" ]; then
171+
cmd.exe /c start "$title" cmd.exe /k "cd /d \"$path\" && $cmd" 2>/dev/null || true
172+
else
173+
cmd.exe /c start "$title" cmd.exe /k "cd /d \"$path\"" 2>/dev/null || true
174+
fi
155175
fi
156176
;;
157177
*)

0 commit comments

Comments
 (0)