Skip to content

Commit 3522914

Browse files
committed
fix(setup): preserve cwd-sensitive hook arguments
1 parent d97ae53 commit 3522914

3 files changed

Lines changed: 66 additions & 31 deletions

File tree

‎packages/rstack/src/setup/hooks.ts‎

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const quoteShellPath = (value: string): string => {
3030
return `'${shellPath.replaceAll("'", `'"'"'`)}'`;
3131
};
3232

33-
const createDispatcher = (nodeExecutable: string): string => `#!/usr/bin/env sh
33+
const createRunner = (nodeExecutable: string): string => `#!/usr/bin/env sh
3434
# Generated by Rstack. Do not edit.
3535
3636
rs_name=\${0##*/}
@@ -54,46 +54,69 @@ if ! command -v node >/dev/null 2>&1 && [ -x "$rs_node_fallback" ]; then
5454
PATH="\${PATH:+$PATH:}\${rs_node_fallback%/*}"
5555
fi
5656
57-
# Keep message file paths valid after changing to the owning project.
58-
case "$rs_name" in
59-
*-msg)
60-
[ -n "\${1-}" ] || exit 1
61-
case "$1" in
62-
/*|[A-Za-z]:/*) ;;
63-
*)
64-
rs_file=$1
65-
shift
66-
set -- "$rs_root/$rs_file" "$@"
67-
;;
68-
esac
69-
;;
70-
esac
57+
rs_run() {
58+
cd "$rs_root/$rs_project_path" || return 1
59+
export PATH="node_modules/.bin\${PATH:+:$PATH}"
7160
72-
cd "$rs_root/$rs_project_path" || exit 1
73-
export PATH="node_modules/.bin\${PATH:+:$PATH}"
61+
rs_code=0
62+
sh -e "$rs_hook" "$@" || rs_code=$?
7463
75-
rs_code=0
76-
sh -e "$rs_hook" "$@" || rs_code=$?
77-
78-
[ "$rs_code" = "0" ] || echo "Rstack - $rs_name hook failed (code $rs_code)"
79-
[ "$rs_code" = "127" ] && echo "Rstack - command not found in PATH=$PATH"
80-
exit "$rs_code"
64+
[ "$rs_code" = "0" ] || echo "Rstack - $rs_name hook failed (code $rs_code)"
65+
[ "$rs_code" = "127" ] && echo "Rstack - command not found in PATH=$PATH"
66+
return "$rs_code"
67+
}
8168
`;
8269

83-
// Every generated Git hook sources the same dispatcher to keep runtime behavior
84-
// consistent and make future initialization changes local to one file.
85-
const shim = `#!/usr/bin/env sh
70+
const createShim = (prepareArguments = ''): string => `#!/usr/bin/env sh
8671
rs_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
8772
. "$rs_dir/runner"
73+
74+
${prepareArguments}
75+
rs_run "$@"
8876
`;
8977

9078
export const createHookFiles = (
9179
nodeExecutable: string = process.execPath,
9280
): Record<string, string> => {
93-
const files: Record<string, string> = { runner: createDispatcher(nodeExecutable) };
81+
const messageShim = createShim(`# Keep the message file valid after changing directories.
82+
[ -n "\${1-}" ] || exit 1
83+
case "$1" in
84+
/*|[A-Za-z]:/*) ;;
85+
*)
86+
rs_file=$1
87+
shift
88+
set -- "$rs_root/$rs_file" "$@"
89+
;;
90+
esac
91+
`);
92+
93+
const prePushShim = createShim(`# Keep a local remote path valid after changing directories.
94+
rs_remote_name=\${1-}
95+
rs_remote_location=\${2-}
96+
[ -n "$rs_remote_name" ] && [ -n "$rs_remote_location" ] || exit 1
97+
case "$rs_remote_location" in
98+
/*|[A-Za-z]:/*) ;;
99+
*:*)
100+
# Git treats a colon before any slash as a URL or SCP-style remote.
101+
case "\${rs_remote_location%%:*}" in
102+
*/*) rs_remote_location="$rs_root/$rs_remote_location" ;;
103+
esac
104+
;;
105+
*) rs_remote_location="$rs_root/$rs_remote_location" ;;
106+
esac
107+
shift 2
108+
set -- "$rs_remote_name" "$rs_remote_location" "$@"
109+
`);
110+
111+
const defaultShim = createShim();
112+
const files: Record<string, string> = { runner: createRunner(nodeExecutable) };
94113

95114
for (const name of hookNames) {
96-
files[name] = shim;
115+
files[name] = name.endsWith('-msg')
116+
? messageShim
117+
: name === 'pre-push'
118+
? prePushShim
119+
: defaultShim;
97120
}
98121

99122
return files;

‎packages/rstack/tests/setup/hooks.test.ts‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { expect, test } from 'rstack/test';
55
import { createHookFiles } from '../../src/setup/hooks.ts';
66
import { withDirectory } from './helpers.ts';
77

8-
test('generates the dispatcher and all client-side Git hook shims', () => {
8+
test('generates the runner and all client-side Git hook shims', () => {
99
const { runner, ...shims } = createHookFiles();
1010

1111
expect(Object.keys(shims)).toEqual([
@@ -25,7 +25,6 @@ test('generates the dispatcher and all client-side Git hook shims', () => {
2525
'pre-auto-gc',
2626
]);
2727
expect(runner).toBeTruthy();
28-
expect(new Set(Object.values(shims)).size).toBe(1);
2928
});
3029

3130
test.runIf(process.platform === 'win32')('converts Windows Node paths', () => {

‎packages/rstack/tests/setup/runtime.test.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ rstack-hook-command
3535
});
3636
});
3737

38-
test('preserves file arguments for hooks owned by a nested project', () => {
38+
test('preserves cwd-sensitive hook arguments for a nested project', () => {
3939
withRepository((cwd) => {
4040
const projectDirectory = path.join(cwd, 'frontend');
4141
const messagePath = '.git/COMMIT_EDITMSG';
@@ -52,6 +52,16 @@ test('preserves file arguments for hooks owned by a nested project', () => {
5252
stderr: 'commit message\n',
5353
});
5454
}
55+
56+
mkdirSync(path.join(cwd, 'remote.git'));
57+
writeFileSync(
58+
path.join(cwd, '.rstack', 'hooks', 'pre-push'),
59+
'[ -d "$2" ] || [ "$2" = "git@example.com:repo.git" ]\n',
60+
);
61+
62+
for (const remote of ['remote.git', 'git@example.com:repo.git']) {
63+
expect(runGitHook(cwd, 'pre-push', ['origin', remote]).status).toBe(0);
64+
}
5565
});
5666
});
5767

@@ -67,5 +77,8 @@ test('skips user hooks when disabled by the environment or init', () => {
6777

6878
expect(runHook(cwd).status).toBe(0);
6979
expect(existsSync(path.join(cwd, 'hook-ran'))).toBe(false);
80+
81+
writeFileSync(path.join(cwd, '.rstack', 'hooks', 'commit-msg'), 'exit 1\n');
82+
expect(runGitHook(cwd, 'commit-msg', []).status).toBe(0);
7083
});
7184
});

0 commit comments

Comments
 (0)