Skip to content

Commit 025de7f

Browse files
acevifhelizaga
andauthored
fix(hooks): prevent stdin drain from skipping subsequent hooks (#165)
* fix(hooks): prevent stdin drain from skipping subsequent hooks Add </dev/null to hook subshell to prevent commands that read stdin (e.g. cat, read) from consuming the heredoc that feeds hook lines, which caused all subsequent hooks to be silently skipped. Fixes #164 * test(hooks): add regression test for stdin-draining hook Verify that a hook running `cat` (which reads stdin) does not prevent subsequent hooks from executing. * test(hooks): extend stdin-drain regression tests to preRemove and postRemove * fix(hooks): prevent stdin drain in run_hooks_export (postCd) Add </dev/null to eval in run_hooks_export so hooks that read stdin (e.g. cat, read) do not consume the heredoc feeding hook lines, which caused subsequent postCd hooks to be silently skipped. Also adds a regression test for the postCd case. * fix(init): isolate postCd hook stdin in generated shells * test(init): skip runtime shell checks when shell missing --------- Co-authored-by: Tom Elizaga <tom.elizaga@gmail.com>
1 parent f079e1c commit 025de7f

File tree

4 files changed

+102
-4
lines changed

4 files changed

+102
-4
lines changed

lib/commands/init.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ __FUNC___run_post_cd_hooks() {
126126
[ -z "$_gtr_hook" ] && continue
127127
case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac
128128
_gtr_seen="$_gtr_seen|$_gtr_hook|"
129-
eval "$_gtr_hook" || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2
129+
eval "$_gtr_hook" </dev/null || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2
130130
done <<< "$_gtr_hooks"
131131
unset WORKTREE_PATH REPO_ROOT BRANCH
132132
fi
@@ -303,7 +303,7 @@ __FUNC___run_post_cd_hooks() {
303303
[ -z "$_gtr_hook" ] && continue
304304
case "$_gtr_seen" in *"|$_gtr_hook|"*) continue ;; esac
305305
_gtr_seen="$_gtr_seen|$_gtr_hook|"
306-
eval "$_gtr_hook" || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2
306+
eval "$_gtr_hook" </dev/null || echo "__FUNC__: postCd hook failed: $_gtr_hook" >&2
307307
done <<< "$_gtr_hooks"
308308
unset WORKTREE_PATH REPO_ROOT BRANCH
309309
fi

lib/hooks.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ run_hooks() {
4141
done
4242
# Execute the hook
4343
eval "$hook"
44-
); then
44+
) </dev/null; then
4545
log_info "Hook $hook_count completed successfully"
4646
else
4747
local rc=$?
@@ -118,7 +118,7 @@ run_hooks_export() {
118118
log_info "Hook $hook_count: $hook"
119119

120120
# eval directly (no subshell) so exports persist
121-
eval "$hook" || log_warn "Hook $hook_count failed (continuing)"
121+
eval "$hook" </dev/null || log_warn "Hook $hook_count failed (continuing)"
122122
done <<EOF
123123
$hooks
124124
EOF

tests/hooks.bats

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,33 @@ teardown() {
8383
[ "$status" -eq 1 ]
8484
}
8585

86+
@test "run_hooks continues after hook that reads stdin (postCreate)" {
87+
git config --add gtr.hook.postCreate 'echo first >> "$REPO_ROOT/order"'
88+
git config --add gtr.hook.postCreate 'cat'
89+
git config --add gtr.hook.postCreate 'echo third >> "$REPO_ROOT/order"'
90+
run_hooks postCreate REPO_ROOT="$TEST_REPO"
91+
[ "$(head -1 "$TEST_REPO/order")" = "first" ]
92+
[ "$(tail -1 "$TEST_REPO/order")" = "third" ]
93+
}
94+
95+
@test "run_hooks continues after hook that reads stdin (preRemove)" {
96+
git config --add gtr.hook.preRemove 'echo first >> "$REPO_ROOT/order"'
97+
git config --add gtr.hook.preRemove 'cat'
98+
git config --add gtr.hook.preRemove 'echo third >> "$REPO_ROOT/order"'
99+
run_hooks preRemove REPO_ROOT="$TEST_REPO"
100+
[ "$(head -1 "$TEST_REPO/order")" = "first" ]
101+
[ "$(tail -1 "$TEST_REPO/order")" = "third" ]
102+
}
103+
104+
@test "run_hooks continues after hook that reads stdin (postRemove)" {
105+
git config --add gtr.hook.postRemove 'echo first >> "$REPO_ROOT/order"'
106+
git config --add gtr.hook.postRemove 'cat'
107+
git config --add gtr.hook.postRemove 'echo third >> "$REPO_ROOT/order"'
108+
run_hooks postRemove REPO_ROOT="$TEST_REPO"
109+
[ "$(head -1 "$TEST_REPO/order")" = "first" ]
110+
[ "$(tail -1 "$TEST_REPO/order")" = "third" ]
111+
}
112+
86113
@test "run_hooks REPO_ROOT and BRANCH env vars available" {
87114
git config --add gtr.hook.postCreate 'echo "$REPO_ROOT|$BRANCH" > "$REPO_ROOT/vars"'
88115
run_hooks postCreate REPO_ROOT="$TEST_REPO" BRANCH="test-branch"
@@ -134,3 +161,12 @@ teardown() {
134161
(cd "$TEST_REPO" && run_hooks_export postCd REPO_ROOT="$TEST_REPO")
135162
[ -z "${LEAK_TEST:-}" ]
136163
}
164+
165+
@test "run_hooks_export continues after hook that reads stdin (postCd)" {
166+
git config --add gtr.hook.postCd 'echo first >> "$REPO_ROOT/order"'
167+
git config --add gtr.hook.postCd 'cat'
168+
git config --add gtr.hook.postCd 'echo third >> "$REPO_ROOT/order"'
169+
(cd "$TEST_REPO" && run_hooks_export postCd REPO_ROOT="$TEST_REPO")
170+
[ "$(head -1 "$TEST_REPO/order")" = "first" ]
171+
[ "$(tail -1 "$TEST_REPO/order")" = "third" ]
172+
}

tests/init.bats

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,52 @@ printf 'REPLY=%s\n' "${COMPREPLY[*]}"
7070
BASH
7171
}
7272

73+
run_generated_post_cd_hooks() {
74+
local shell_name="$1"
75+
76+
"$shell_name" -s -- "$PROJECT_ROOT" "$shell_name" <<'SCRIPT'
77+
PROJECT_ROOT="$1"
78+
shell_name="$2"
79+
80+
log_info() { :; }
81+
log_warn() { :; }
82+
log_error() { :; }
83+
show_command_help() { :; }
84+
compdef() { :; }
85+
86+
# shellcheck disable=SC1090
87+
. "$PROJECT_ROOT/lib/commands/init.sh"
88+
89+
repo=$(mktemp -d)
90+
cleanup() {
91+
rm -rf "$repo"
92+
}
93+
trap cleanup EXIT
94+
95+
git -C "$repo" init --quiet
96+
git -C "$repo" config user.name "Test User"
97+
git -C "$repo" config user.email "test@example.com"
98+
git -C "$repo" commit --allow-empty -m "init" --quiet
99+
100+
git -C "$repo" config --add gtr.hook.postCd 'echo first >> "$REPO_ROOT/order"'
101+
git -C "$repo" config --add gtr.hook.postCd 'cat'
102+
git -C "$repo" config --add gtr.hook.postCd 'echo third >> "$REPO_ROOT/order"'
103+
104+
eval "$(cmd_init "$shell_name")"
105+
gtr_run_post_cd_hooks "$repo"
106+
107+
printf 'ORDER='
108+
paste -sd, "$repo/order"
109+
printf '\n'
110+
SCRIPT
111+
}
112+
113+
require_runtime_shell() {
114+
local shell_name="$1"
115+
116+
command -v "$shell_name" >/dev/null 2>&1 || skip "$shell_name is not installed"
117+
}
118+
73119
# ── Default function name ────────────────────────────────────────────────────
74120

75121
@test "bash output defines gtr() function by default" {
@@ -253,6 +299,22 @@ BASH
253299
[[ "$output" == *'could not determine new directory for --cd'* ]]
254300
}
255301
302+
@test "bash generated postCd hooks continue after stdin read" {
303+
require_runtime_shell bash
304+
run run_generated_post_cd_hooks bash
305+
306+
[ "$status" -eq 0 ]
307+
[ "$output" = "ORDER=first,third" ]
308+
}
309+
310+
@test "zsh generated postCd hooks continue after stdin read" {
311+
require_runtime_shell zsh
312+
run run_generated_post_cd_hooks zsh
313+
314+
[ "$status" -eq 0 ]
315+
[ "$output" = "ORDER=first,third" ]
316+
}
317+
256318
@test "fish output uses worktree diff to locate the new directory for --cd" {
257319
run cmd_init fish
258320
[ "$status" -eq 0 ]

0 commit comments

Comments
 (0)