From dfeae12de047fc5ffd856fc0b2fabd8bdfa42c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=93=E8=89=AF?= <1204183885@qq.com> Date: Mon, 2 Nov 2026 00:43:20 +0800 Subject: [PATCH] fix(core): use the amend delta for loosely spelled amend commits (#16073) Co-authored-by: Qwen-Coder --- .../shell.session-commit-tracking.test.ts | 39 +++++++++++++++++++ packages/core/src/tools/shell.ts | 16 +++++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/packages/core/src/tools/shell.session-commit-tracking.test.ts b/packages/core/src/tools/shell.session-commit-tracking.test.ts index e94778a9575..b2d066c12b3 100644 --- a/packages/core/src/tools/shell.session-commit-tracking.test.ts +++ b/packages/core/src/tools/shell.session-commit-tracking.test.ts @@ -944,6 +944,45 @@ describe.skipIf(process.platform === 'win32')( expect(amendSpy.mock.calls[0]![2]).toBe(true); }); + it.each([ + [ + 'control-flow keywords', + () => 'if true; then git commit --amend --no-edit; fi', + ], + [ + 'absolute git path', + () => + `${execSync('command -v git', { encoding: 'utf-8' }).trim()} commit --amend --no-edit`, + ], + ['leading time keyword', () => 'time git commit --amend --no-edit'], + ])( + 'uses the amend delta in the attribution note for an amend spelled with %s (issue #12514)', + async (_label, buildCommand) => { + // #12514-C: `isAmendCommit` was the one recogniser that still required + // `tokens[0] === 'git'`. A loose-spelled amend therefore reached + // attribution classified as a fresh commit, so the note diffed + // `postHead~1..postHead` instead of `preHead..postHead`. The extra file + // below makes that observable: it landed in the pre-amend commit but + // was untouched by the amend, so only the wrong range includes it. + rawCommit('first.txt', 'first version\n', 'add first'); + rawCommit('second.txt', 'second version\n', 'add second'); + fs.writeFileSync(path.join(repoDir, 'first.txt'), 'first amended\n'); + rawGit('add first.txt'); + CommitAttributionService.getInstance().recordEdit( + path.join(repoDir, 'first.txt'), + 'first version\n', + 'first amended\n', + ); + + await runShellCommand(buildCommand()); + + const note = JSON.parse( + rawGit('notes --ref=refs/notes/ai-attribution show HEAD'), + ); + expect(Object.keys(note.files)).toEqual(['first.txt']); + }, + ); + it('registers the commit even when gitCoAuthor.commit attribution is disabled', async () => { // Regression ④: session tracking is deliberately independent of the // commit-attribution toggle. A user who turned attribution off must not diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index a015a2b796d..2315523bafb 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -687,8 +687,8 @@ const COMMIT_RECOGNITION_LEADING_NOISE: ReadonlySet = new Set([ ]); /** - * The program recogniser shared by `gitCommitContext` and - * `findAttributableCommitSegment` (#12514): skip leading + * The program recogniser shared by `gitCommitContext`, + * `findAttributableCommitSegment` and `isAmendCommit` (#12514): skip leading * control-flow/noise keywords and match the program on its * basename (see `programBasename`), so `then git commit`, * `time git commit`, `! git commit` and `/usr/bin/git commit` all @@ -936,13 +936,19 @@ function cdTargetMayChangeRepo(tokens: string[], cwd: string): boolean { * shell tool counts. `git -C ../other commit --amend && git commit -m x` * must not flip the diff range for the second (fresh) commit, since * `preHead` would be the inner repo's SHA there, not ours. + * + * Recognition matches the other commit walks: leading shell + * control-flow/noise keywords are skipped and the program is matched on + * its basename, so a loose spelling still gets the amend-range diff. */ function isAmendCommit(command: string, cwd: string): boolean { let cwdShifted = false; for (const sub of splitCommands(command)) { - const tokens = tokeniseSegment(sub); - if (!tokens || tokens.length === 0) continue; - const program = tokens[0]!; + const rawTokens = tokeniseSegment(sub); + if (!rawTokens || rawTokens.length === 0) continue; + const tokens = skipCommitRecognitionNoise(rawTokens); + if (!tokens) continue; + const program = programBasename(tokens[0]!); if (program === 'cd' || program === 'pushd') { if (!cwdShifted && cdTargetMayChangeRepo(tokens, cwd)) cwdShifted = true; continue;