diff --git a/packages/core/src/tools/shell.test.ts b/packages/core/src/tools/shell.test.ts index 773eb344d..5e97236a1 100644 --- a/packages/core/src/tools/shell.test.ts +++ b/packages/core/src/tools/shell.test.ts @@ -1765,6 +1765,16 @@ describe('ShellTool', () => { // contract as `cd /elsewhere && git commit`. ['env -C', 'env -C /tmp/other git commit -m "msg"'], ['env --chdir', 'env --chdir /tmp/other git commit -m "msg"'], + // Attached-value forms: `shell-quote` tokenises `--chdir=/tmp` + // and `-C/tmp` as single argv entries, so the bare-flag set + // membership check would miss them. Without explicit + // attached-form handling, `sudo --chdir=/tmp git commit` and + // `env -C/tmp git commit` would silently land our trailer on + // a commit in the wrong repo. + ['env --chdir=', 'env --chdir=/tmp/other git commit -m "msg"'], + ['env -C attached', 'env -C/tmp/other git commit -m "msg"'], + ['sudo --chdir=', 'sudo --chdir=/tmp/other git commit -m "msg"'], + ['sudo -D attached', 'sudo -D/tmp/other git commit -m "msg"'], ])( 'should NOT add co-author for repo-redirecting %s assignment', async (_label, command) => { diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index b2e887be4..933de97ca 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -230,10 +230,21 @@ function tokeniseSegment(segment: string): string[] | null { // segment as repo-shifting (same contract as a leading // `GIT_DIR=...` assignment) so we don't stamp our trailer onto // a commit that landed in a different repository. - if ( - (wrapper === 'env' && ENV_FLAGS_SHIFT_CWD.has(flag)) || - (wrapper === 'sudo' && SUDO_FLAGS_SHIFT_CWD.has(flag)) - ) { + // + // Also catch the attached-value forms `--chdir=DIR` and the + // short-form `-CDIR` / `-DDIR` that shell-quote tokenises as a + // single argv entry. Without this, `sudo --chdir=/tmp git + // commit` and `env -C/tmp git commit` would both pass through + // the bare-flag check (which is set-membership, not prefix- + // match) and silently land our trailer on a commit in the + // wrong repo. + const shiftSet = + wrapper === 'env' + ? ENV_FLAGS_SHIFT_CWD + : wrapper === 'sudo' + ? SUDO_FLAGS_SHIFT_CWD + : null; + if (shiftSet && isShiftCwdFlag(flag, shiftSet)) { return null; } // Value-taking flag tables, per wrapper: `sudo -u user`, @@ -303,6 +314,30 @@ const ENV_FLAGS_SHIFT_CWD = new Set(['-C', '--chdir']); // targets DIR's repo, not ours. Refuse the segment. const SUDO_FLAGS_SHIFT_CWD = new Set(['-D', '--chdir']); +/** + * Match a flag token against a SHIFT_CWD set, including attached-value + * forms. Bare `--chdir`/`-D`/`-C` are caught by direct set membership; + * the long attached form `--name=value` matches when `--name` is in the + * set, and the short attached form `-Xvalue` matches when `-X` is in + * the set AND the token is longer than the flag (so `-D` alone doesn't + * spuriously match `-D` against itself twice). + */ +function isShiftCwdFlag(flag: string, set: ReadonlySet): boolean { + if (set.has(flag)) return true; + for (const f of set) { + if (f.startsWith('--') && flag.startsWith(f + '=')) return true; + if ( + f.length === 2 && + f.startsWith('-') && + flag.startsWith(f) && + flag.length > 2 + ) { + return true; + } + } + return false; +} + /** * Environment variables that redirect git's repository selection. A * leading `GIT_DIR=...`, `GIT_WORK_TREE=...`, etc. on a command makes