mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-13 15:32:19 +00:00
fix(attribution): preserve unstaged AI edits across cleanup branches
uxU5 + uxVQ + uxUO (Copilot): every cleanup branch in attachCommitAttribution that called clearAttributions(true) was wholesale-erasing pending AI edits for files the user never staged in this commit. Reviewer scenarios: - multi-commit chain (`commit a && commit b`) bails out without writing a note, but unstaged edits to file Z (touched by neither commit) get cleared along with the chain's committed files. - attribution toggle off: same — toggling the flag wipes pending unstaged work. - analysis failure (shallow clone, --amend without reflog, partial diff failure): the finally-block fallback wholesale-cleared every pending file, consuming unrelated AI edits. - 0%-AI commit: when no file in the commit was AI-touched, generateNotePayload was emitting an "0% AI" note attached to a commit that legitimately had no AI involvement — actively misleading metadata. Add `noteCommitWithoutClearing()` to the service: snapshots the prompt counter as the new "at last commit" but leaves the per-file map alone. Use it in the multi-commit, no-tracked-edits, toggle-off, and analysis-failure paths. The committed-files partial-clear (clearAttributedFiles) still runs in the success path. The 0%-AI no-match case now skips the note write entirely.
This commit is contained in:
parent
325a12c3c1
commit
1ece87438f
2 changed files with 47 additions and 11 deletions
|
|
@ -329,6 +329,20 @@ export class CommitAttributionService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Snapshot the prompt counter as the new "last commit" without
|
||||
* clearing per-file attribution. Used when a commit landed but we
|
||||
* can't reliably determine which files were in it (multi-commit
|
||||
* chain we won't write a note for, attribution toggle off, diff
|
||||
* analysis failed). Wholesale-clearing in those branches would
|
||||
* silently wipe pending AI edits for *unrelated* files the user
|
||||
* didn't stage — a worse failure mode than the small risk of
|
||||
* stale per-file state for files that did just land.
|
||||
*/
|
||||
noteCommitWithoutClearing(): void {
|
||||
this.promptCountAtLastCommit = this.promptCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a set of repo-relative file paths to the canonical absolute
|
||||
* keys actually stored in the attribution map. Used by cleanup to
|
||||
|
|
|
|||
|
|
@ -1914,7 +1914,13 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||
: `multi-commit shell command (${commitCount} commits since ` +
|
||||
`${preHead ? preHead.slice(0, 12) : 'repo root'})`;
|
||||
debugLogger.warn(`Refusing AI attribution: ${reason}.`);
|
||||
attributionService.clearAttributions(true);
|
||||
// Snapshot the prompt counter but do NOT clear per-file
|
||||
// attributions: in a `commit a && commit b` chain, the user
|
||||
// may have unstaged AI edits to files that appeared in NEITHER
|
||||
// commit. Wholesale-clearing here would erase those even
|
||||
// though the rest of the flow is built to preserve unstaged
|
||||
// entries across partial commits.
|
||||
attributionService.noteCommitWithoutClearing();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1924,17 +1930,19 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||
// "at last commit" so a later `gh pr create` doesn't report an
|
||||
// inflated N-shotted count spanning multiple commits.
|
||||
if (!attributionService.hasAttributions()) {
|
||||
attributionService.clearAttributions(true);
|
||||
attributionService.noteCommitWithoutClearing();
|
||||
return null;
|
||||
}
|
||||
|
||||
const gitCoAuthorSettings = this.config.getGitCoAuthor();
|
||||
if (!gitCoAuthorSettings.commit) {
|
||||
// Commit succeeded but attribution is disabled. Still snapshot
|
||||
// the prompt counters as "at last commit" so the next commit
|
||||
// starts a fresh window — otherwise the user would carry stale
|
||||
// counts forward forever.
|
||||
attributionService.clearAttributions(true);
|
||||
// Commit succeeded but attribution is disabled. Snapshot the
|
||||
// prompt counters as "at last commit" but leave per-file
|
||||
// attributions alone — a wholesale clear here would lose the
|
||||
// user's pending unstaged AI work just because they toggled
|
||||
// attribution off, which is a much harsher contract than the
|
||||
// toggle name suggests.
|
||||
attributionService.noteCommitWithoutClearing();
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -1994,6 +2002,16 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||
canonicalBase,
|
||||
);
|
||||
|
||||
// No file in this commit was AI-touched in the current session.
|
||||
// Writing a note anyway would emit an all-zero "0% AI" payload
|
||||
// attached to a commit that legitimately had no AI involvement
|
||||
// — actively misleading. Skip the note; the partial clear in
|
||||
// the finally block is a no-op (empty set) so unrelated pending
|
||||
// attributions stay tracked for a later commit.
|
||||
if (committedAbsolutePaths.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const note = attributionService.generateNotePayload(
|
||||
stagedInfo,
|
||||
baseDir,
|
||||
|
|
@ -2063,13 +2081,17 @@ export class ShellToolInvocation extends BaseToolInvocation<
|
|||
// Partial clear: only drop tracking for the files that actually
|
||||
// landed in this commit. Files the AI edited but the user
|
||||
// omitted from `git add` stay pending for a later commit.
|
||||
// If we never determined the committed set (early failure in
|
||||
// getCommittedFileInfo), fall back to a full clear so we don't
|
||||
// leak stale per-file state — counters still get snapshotted.
|
||||
// If we never determined the committed set (analysis failure:
|
||||
// shallow clone, --amend without reflog, partial diff failure,
|
||||
// exception), DO NOT wholesale-clear: that would erase pending
|
||||
// AI edits for files the user never staged in this commit. The
|
||||
// small risk is stale per-file state for the just-committed
|
||||
// file (re-attributed if it appears in a future commit) — much
|
||||
// less harmful than losing unrelated unstaged work.
|
||||
if (committedAbsolutePaths) {
|
||||
attributionService.clearAttributedFiles(committedAbsolutePaths);
|
||||
} else {
|
||||
attributionService.clearAttributions(true);
|
||||
attributionService.noteCommitWithoutClearing();
|
||||
}
|
||||
}
|
||||
return warning;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue