Przeglądaj źródła

fix(workspace-changes): count an appended line as one addition

The file tools persist both sides of a hunk without a trailing newline,
so re-diffing an append read the previous last line as rewritten and
reported +2 -1 for +1 -0. Both sides are line-terminated before the
diff, the rule the per-call diff card already applies.
creatixchu 3 dni temu
rodzic
commit
da6e3f9edf

+ 11 - 2
packages/deliverables/workspace-changes/src/numstat.ts

@@ -39,10 +39,19 @@ export function parseNumstat(output: string): NumstatEntry[] {
   return entries
 }
 
+/**
+ * A side's text with every line terminated, so the last line compares by
+ * content alone: the file tools persist hunk sides without a trailing newline,
+ * and the same rule reads empty text as no lines rather than one empty line.
+ */
+function terminated(text: string): string {
+  return text === '' || text.endsWith('\n') ? text : `${text}\n`
+}
+
 /**
  * Sum the added and deleted lines over recorded hunks. Context lines appear on
  * both sides of a hunk and cancel out; a hunk without prior text counts every
- * line as added.
+ * line as added; a trailing newline never counts as a changed line.
  * @param diffs - the applied hunks a file tool persisted with its result.
  * @returns line totals for one file.
  */
@@ -50,7 +59,7 @@ export function hunkLineCounts(diffs: readonly FileDiff[]): { added: number; del
   let added = 0
   let deleted = 0
   for (const diff of diffs) {
-    const patch = structuredPatch('', '', diff.oldText ?? '', diff.newText)
+    const patch = structuredPatch('', '', terminated(diff.oldText ?? ''), terminated(diff.newText))
     for (const hunk of patch.hunks) {
       for (const line of hunk.lines) {
         if (line.startsWith('+')) added += 1

+ 7 - 0
packages/deliverables/workspace-changes/tests/numstat.spec.ts

@@ -30,6 +30,13 @@ describe('hunkLineCounts', () => {
       { path: 'a', oldText: null, newText: 'x\ny\n' },
     ])).toEqual({ added: 4, deleted: 1 })
   })
+
+  it('appends a line after an unterminated last line without counting that line as rewritten', () => {
+    // The file tools persist hunk sides without a trailing newline; an append reads +1 -0, not +2 -1.
+    expect(hunkLineCounts([{ path: 'a', oldText: 'last', newText: 'last\nadded' }])).toEqual({ added: 1, deleted: 0 })
+    expect(hunkLineCounts([{ path: 'a', oldText: 'last\n', newText: 'last\nadded\n' }])).toEqual({ added: 1, deleted: 0 })
+    expect(hunkLineCounts([{ path: 'a', oldText: 'gone', newText: '' }])).toEqual({ added: 0, deleted: 1 })
+  })
 })
 
 describe('fileDiffsOf', () => {

+ 1 - 1
packages/deliverables/workspace-changes/tests/plugin.spec.ts

@@ -259,7 +259,7 @@ describe('workspace-changes without a repository', () => {
     expect(changes(ctx, session)).toEqual([{
       turn: 1, cwd, total: 2,
       files: [
-        { path: 'existing.txt', display: 'existing.txt', added: 4, deleted: 2 },
+        { path: 'existing.txt', display: 'existing.txt', added: 3, deleted: 1 },
         { path: join(await realpath(outside), 'note.txt'), display: `~/${outside.slice(homedir().length + 1)}/note.txt`, added: 2, deleted: 0 },
       ],
     }])