Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 51 additions & 13 deletions packages/rstack/src/setup/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const quoteShellPath = (value: string): string => {
return `'${shellPath.replaceAll("'", `'"'"'`)}'`;
};

const createDispatcher = (nodeExecutable: string): string => `#!/usr/bin/env sh
const createRunner = (nodeExecutable: string): string => `#!/usr/bin/env sh
# Generated by Rstack. Do not edit.

rs_name=\${0##*/}
Expand All @@ -54,31 +54,69 @@ if ! command -v node >/dev/null 2>&1 && [ -x "$rs_node_fallback" ]; then
PATH="\${PATH:+$PATH:}\${rs_node_fallback%/*}"
fi

cd "$rs_root/$rs_project_path" || exit 1
export PATH="node_modules/.bin\${PATH:+:$PATH}"
rs_run() {
cd "$rs_root/$rs_project_path" || return 1
export PATH="node_modules/.bin\${PATH:+:$PATH}"

rs_code=0
sh -e "$rs_hook" "$@" || rs_code=$?
rs_code=0
sh -e "$rs_hook" "$@" || rs_code=$?

[ "$rs_code" = "0" ] || echo "Rstack - $rs_name hook failed (code $rs_code)"
[ "$rs_code" = "127" ] && echo "Rstack - command not found in PATH=$PATH"
exit "$rs_code"
[ "$rs_code" = "0" ] || echo "Rstack - $rs_name hook failed (code $rs_code)"
[ "$rs_code" = "127" ] && echo "Rstack - command not found in PATH=$PATH"
return "$rs_code"
}
`;

// Every generated Git hook sources the same dispatcher to keep runtime behavior
// consistent and make future initialization changes local to one file.
const shim = `#!/usr/bin/env sh
const createShim = (prepareArguments = ''): string => `#!/usr/bin/env sh
rs_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) || exit 1
. "$rs_dir/runner"

${prepareArguments}
rs_run "$@"
`;

export const createHookFiles = (
nodeExecutable: string = process.execPath,
): Record<string, string> => {
const files: Record<string, string> = { runner: createDispatcher(nodeExecutable) };
const messageShim = createShim(`# Keep the message file valid after changing directories.
[ -n "\${1-}" ] || exit 1
case "$1" in
/*|[A-Za-z]:/*) ;;
*)
rs_file=$1
shift
set -- "$rs_root/$rs_file" "$@"
;;
esac
`);

const prePushShim = createShim(`# Keep a local remote path valid after changing directories.
rs_remote_name=\${1-}
rs_remote_location=\${2-}
[ -n "$rs_remote_name" ] && [ -n "$rs_remote_location" ] || exit 1
case "$rs_remote_location" in
/*|[A-Za-z]:/*) ;;
Comment thread
chenjiahan marked this conversation as resolved.
*:*)
# Git treats a colon before any slash as a URL or SCP-style remote.
case "\${rs_remote_location%%:*}" in
*/*) rs_remote_location="$rs_root/$rs_remote_location" ;;
esac
;;
*) rs_remote_location="$rs_root/$rs_remote_location" ;;
esac
shift 2
set -- "$rs_remote_name" "$rs_remote_location" "$@"
`);

const defaultShim = createShim();
const files: Record<string, string> = { runner: createRunner(nodeExecutable) };

for (const name of hookNames) {
files[name] = shim;
files[name] = name.endsWith('-msg')
? messageShim
: name === 'pre-push'
? prePushShim
: defaultShim;
}

return files;
Expand Down
3 changes: 3 additions & 0 deletions packages/rstack/tests/setup/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const writeInit = (cwd: string, content: string): void => {
export const runHook = (cwd: string, value?: string): SpawnSyncReturns<string> =>
git(cwd, ['hook', 'run', 'pre-commit'], hookEnv(cwd, value));

export const runGitHook = (cwd: string, name: string, args: string[]): SpawnSyncReturns<string> =>
git(cwd, ['hook', 'run', name, '--', ...args], hookEnv(cwd));

export const withRepository = (callback: (cwd: string) => void): void =>
withDirectory((cwd) => {
const globalConfig = process.env.GIT_CONFIG_GLOBAL;
Expand Down
3 changes: 1 addition & 2 deletions packages/rstack/tests/setup/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect, test } from 'rstack/test';
import { createHookFiles } from '../../src/setup/hooks.ts';
import { withDirectory } from './helpers.ts';

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

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

test.runIf(process.platform === 'win32')('converts Windows Node paths', () => {
Expand Down
35 changes: 34 additions & 1 deletion packages/rstack/tests/setup/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'n
import path from 'node:path';
import { expect, test } from 'rstack/test';
import { installHooks } from '../../src/setup/install.ts';
import { runHook, withRepository, writeHook, writeInit } from './helpers.ts';
import { runGitHook, runHook, withRepository, writeHook, writeInit } from './helpers.ts';

test('loads user init and project binaries', () => {
withRepository((cwd) => {
Expand Down Expand Up @@ -35,6 +35,36 @@ rstack-hook-command
});
});

test('preserves cwd-sensitive hook arguments for a nested project', () => {
withRepository((cwd) => {
const projectDirectory = path.join(cwd, 'frontend');
const messagePath = '.git/COMMIT_EDITMSG';
mkdirSync(projectDirectory);
writeFileSync(path.join(cwd, messagePath), 'commit message\n');

expect(installHooks({ cwd: projectDirectory }).status).toBe('installed');

for (const name of ['applypatch-msg', 'commit-msg', 'prepare-commit-msg']) {
writeFileSync(path.join(cwd, '.rstack', 'hooks', name), 'cat "$1"\n');

expect(runGitHook(cwd, name, [messagePath])).toMatchObject({
status: 0,
stderr: 'commit message\n',
});
}

mkdirSync(path.join(cwd, 'remote.git'));
writeFileSync(
path.join(cwd, '.rstack', 'hooks', 'pre-push'),
'[ -d "$2" ] || [ "$2" = "git@example.com:repo.git" ]\n',
);

for (const remote of ['remote.git', 'git@example.com:repo.git']) {
expect(runGitHook(cwd, 'pre-push', ['origin', remote]).status).toBe(0);
}
});
});

test('skips user hooks when disabled by the environment or init', () => {
withRepository((cwd) => {
writeHook(cwd, 'echo ran >> hook-ran\n');
Expand All @@ -47,5 +77,8 @@ test('skips user hooks when disabled by the environment or init', () => {

expect(runHook(cwd).status).toBe(0);
expect(existsSync(path.join(cwd, 'hook-ran'))).toBe(false);

writeFileSync(path.join(cwd, '.rstack', 'hooks', 'commit-msg'), 'exit 1\n');
expect(runGitHook(cwd, 'commit-msg', []).status).toBe(0);
});
});
2 changes: 2 additions & 0 deletions scripts/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ applypatch
cdpath
clippy
dirents
editmsg
errexit
esac
extglob
fnames
huskyrc
Expand Down