mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 22:14:25 +01:00
Git Bash hands native Bun C:/ paths while gstack-hook-check looked them up as /c/, so every TypeScript hook was refused and team-mode auto-update held every release since 1.91.31. The checker maps Bun's paths back to the shell spelling, the auto-updater lets the incoming release's own checker decide (installed checker only as a fallback, hook-check-unavailable when neither verifies), and docs/troubleshooting.md gives stuck installs one recovery command. /gstack-upgrade and /spec guards use the --show-prefix root check, gstack-wtree treats drive-letter --git-path answers as absolute, and setup refuses to replace a runtime root that is or contains its own checkout, Kiro's in-place root included (#3084), with --status suggesting the move. New sourced helper bin/gstack-path-spelling.sh and TS twin lib/path-spelling.ts. Co-authored-by: yuriysan <63948747+yuriysan@users.noreply.github.com> Co-authored-by: y$un_ <3579304+szsunyuan@users.noreply.github.com> Co-authored-by: Frederik Reventlow <267160539+dmfrede@users.noreply.github.com> Co-authored-by: skleinbe-ny <226563731+skleinbe-ny@users.noreply.github.com> Co-authored-by: bkermen <184220642+bkermen@users.noreply.github.com> Co-authored-by: Drew Jankowski <3200063+andrewkjankowski@users.noreply.github.com>
89 lines
4.0 KiB
TypeScript
89 lines
4.0 KiB
TypeScript
/**
|
|
* #3101: gstack-wtree must fingerprint a linked worktree on Windows, where
|
|
* `git rev-parse --git-path index|objects` answers with a drive-letter absolute
|
|
* path (C:/…). Read as relative, that path was re-rooted under the worktree, so
|
|
* the private-object-directory fingerprint (gstack-review-log --start, E4) lost
|
|
* the repository's objects and /review and /ship could not record a start.
|
|
*
|
|
* Windows-safe by construction: gstack-wtree runs through explicit `bash <script>`,
|
|
* never shebang execution, and paths are handed to bash in forward-slash form.
|
|
*/
|
|
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
|
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { delimiter, join, resolve } from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const ROOT = resolve(import.meta.dir, '..');
|
|
const WTREE = join(ROOT, 'bin', 'gstack-wtree');
|
|
const posix = (value: string) => value.replace(/\\/g, '/');
|
|
|
|
let root = '', worktree = '', objects = '', env: NodeJS.ProcessEnv = {};
|
|
const git = (cwd: string, ...args: string[]) => {
|
|
const result = spawnSync('git', args, { cwd, env, encoding: 'utf8', timeout: 30_000 });
|
|
expect(result.status, result.stderr).toBe(0);
|
|
return result.stdout.trim();
|
|
};
|
|
|
|
beforeEach(() => {
|
|
root = realpathSync(mkdtempSync(join(tmpdir(), 'gstack-wtree-paths-')));
|
|
const main = join(root, 'main');
|
|
worktree = join(root, 'linked worktree');
|
|
objects = join(root, 'objects');
|
|
mkdirSync(main);
|
|
mkdirSync(objects);
|
|
const globalConfig = join(root, 'gitconfig');
|
|
writeFileSync(globalConfig, '');
|
|
env = {
|
|
...process.env, HOME: root, USERPROFILE: root,
|
|
GIT_CONFIG_GLOBAL: globalConfig, GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_COUNT: '0',
|
|
GIT_DIR: undefined, GIT_WORK_TREE: undefined, GIT_COMMON_DIR: undefined, GIT_INDEX_FILE: undefined,
|
|
GIT_OBJECT_DIRECTORY: undefined, GIT_ALTERNATE_OBJECT_DIRECTORIES: undefined,
|
|
};
|
|
git(main, 'init', '-q');
|
|
// A real file, so HEAD's tree lives only in the repository's object store.
|
|
writeFileSync(join(main, 'a.txt'), 'hello\n');
|
|
git(main, 'add', 'a.txt');
|
|
git(main, '-c', 'user.email=fixture@example.test', '-c', 'user.name=Fixture', '-c', 'commit.gpgsign=false', 'commit', '-qm', 'fixture');
|
|
git(main, 'worktree', 'add', '-q', '--detach', worktree);
|
|
});
|
|
|
|
afterEach(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
// The review-start call shape: new objects go to a private directory the caller owns.
|
|
function expectFingerprintOfHead() {
|
|
const result = spawnSync('bash', [WTREE], {
|
|
cwd: worktree, env: { ...env, GSTACK_WTREE_OBJECT_DIR: posix(objects) }, encoding: 'utf8', timeout: 30_000,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout.trim()).toBe(git(worktree, 'rev-parse', 'HEAD^{tree}'));
|
|
}
|
|
|
|
test('a linked worktree with a private object directory gets its fingerprint (#3101)', () => {
|
|
expectFingerprintOfHead();
|
|
});
|
|
|
|
// Linux and macOS reproduce Git for Windows: `--git-path` answers in drive-letter
|
|
// form, and GIT_ALTERNATE_OBJECT_DIRECTORIES is one ;-separated list in which C:/…
|
|
// is absolute. Linux git splits on ':', so the shim hands that list over as one
|
|
// quoted entry, the way Windows git reads it.
|
|
test.skipIf(process.platform === 'win32')('a drive-letter --git-path answer is not re-rooted under the worktree (#3101)', () => {
|
|
const real = JSON.stringify(Bun.which('git')!);
|
|
const shim = join(root, 'shim');
|
|
mkdirSync(shim);
|
|
writeFileSync(join(shim, 'git'), `#!/usr/bin/env bash
|
|
case " $* " in *" rev-parse --git-path "*)
|
|
out=$(${real} "$@") || exit $?
|
|
case "$out" in /*) printf 'C:%s\\n' "$out" ;; *) printf '%s\\n' "$out" ;; esac
|
|
exit 0 ;;
|
|
esac
|
|
if [ -n "\${GIT_ALTERNATE_OBJECT_DIRECTORIES:-}" ]; then
|
|
export GIT_ALTERNATE_OBJECT_DIRECTORIES="\\"\${GIT_ALTERNATE_OBJECT_DIRECTORIES#C:}\\""
|
|
fi
|
|
exec ${real} "$@"
|
|
`, { mode: 0o755 });
|
|
env.PATH = `${shim}${delimiter}${env.PATH}`;
|
|
expect(git(worktree, 'rev-parse', '--git-path', 'objects')).toStartWith('C:/');
|
|
expectFingerprintOfHead();
|
|
});
|