From daeec2c0178ad103f64a9b062ddbbd7b81f63097 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 21:00:46 -0700 Subject: [PATCH] =?UTF-8?q?fix(slug):=20terminate=20the=20marker=20walk-up?= =?UTF-8?q?=20on=20dirname's=20fixed=20point=20=E2=80=94=20hung=20every=20?= =?UTF-8?q?bin=20on=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> . forever: dirname's fixed point there is never "/", so the walk-up loop spun and every bin that evals gstack-slug (learnings-log first among them) hung until spawn timeout. Caught by windows-free-tests CI on the wave PR. Break on the fixed point itself with a depth cap for exotic forms; regression tests drive the extracted function with hostile path shapes under a hard timeout. Co-Authored-By: Claude Fable 5 --- bin/gstack-slug | 12 ++++++++++-- test/gstack-slug-cwd-walk-up.test.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/bin/gstack-slug b/bin/gstack-slug index 12c5b4643..e9b2aaf02 100755 --- a/bin/gstack-slug +++ b/bin/gstack-slug @@ -68,7 +68,12 @@ _outermost_project_root() { local dir="$1" local outermost_strong="" local outermost_weak="" - while [[ -n "$dir" && "$dir" != "/" ]]; do + local parent="" depth=0 + # Terminate on dirname's FIXED POINT, not on a literal "/": under git-bash + # on Windows a mixed-form path walks C:/Users -> C: -> . -> . forever, which + # hung every bin that evals gstack-slug (caught by windows-free-tests CI). + # The depth cap is belt-and-braces for exotic path forms (UNC, //server). + while [[ -n "$dir" && "$dir" != "/" && $depth -lt 64 ]]; do if [[ -e "$dir/.git" \ || -f "$dir/.project.yaml" \ || -f "$dir/package.json" \ @@ -84,7 +89,10 @@ _outermost_project_root() { || -f "$dir/LICENSE.md" ]]; then outermost_weak="$dir" fi - dir=$(dirname "$dir") + parent=$(dirname "$dir") + [[ "$parent" == "$dir" ]] && break # dirname fixed point (C:/, ., //srv) + dir="$parent" + depth=$((depth + 1)) done # Strong markers win over weak; either wins over nothing. if [[ -n "$outermost_strong" ]]; then diff --git a/test/gstack-slug-cwd-walk-up.test.ts b/test/gstack-slug-cwd-walk-up.test.ts index dac5cd919..96db5e303 100644 --- a/test/gstack-slug-cwd-walk-up.test.ts +++ b/test/gstack-slug-cwd-walk-up.test.ts @@ -289,3 +289,23 @@ describe('gstack-slug — outermost project-root resolution', () => { expect(slug).toBe('custom-override'); }); }); + +describe('_outermost_project_root termination (windows-free-tests regression)', () => { + // Under git-bash on Windows a mixed-form path walks C:/Users -> C: -> . -> . + // forever: dirname's fixed point there is never "/". The loop must break on + // the fixed point itself. Extract the function and drive it with hostile + // path forms under a hard timeout — a hang fails the spawn, not the suite. + const script = fs.readFileSync(path.join(ROOT, 'bin', 'gstack-slug'), 'utf-8'); + const fnMatch = script.match(/_outermost_project_root\(\) \{[\s\S]*?\n\}/); + + test.each(['C:/Users/nobody/project', '.', '//server/share/dir'])( + 'terminates on hostile path form: %s', + (hostile) => { + expect(fnMatch).not.toBeNull(); + const r = Bun.spawnSync(['bash', '-c', `${fnMatch![0]}\n_outermost_project_root "$1"; echo TERMINATED`, '_', hostile], { + timeout: 5000, + }); + expect(r.stdout.toString()).toContain('TERMINATED'); + }, + ); +});