feat(git): safely apply individual diff hunks (#3443)

Resolve the Changes-view conflict without reverting branch or commit comparisons. Pair canonical action patches with displayed blob identities, refresh all path views after mutation, exclude historical snapshots, and reject stale or multi-file patches at the server boundary.

Preserve CRLF bytes and make long hunk menus keyboard-reachable. Workspace type-check, lint and build passed; focused parser, menu, view and real Git regressions passed.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 19:39:28 +03:00
10 changed files with 662 additions and 72 deletions
+17 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { extractHunkPatch, splitPatchIntoHunks } from "./patchFileDiff";
import { extractHunkPatch, splitPatchIntoHunks, haveMatchingPatchVersions } from "./patchFileDiff";
const SAMPLE_PATCH = `diff --git a/foo.txt b/foo.txt
index 1111111..2222222 100644
@@ -67,6 +67,22 @@ describe("splitPatchIntoHunks", () => {
});
describe("extractHunkPatch", () => {
test("pairs display and action patches only with identical full blob identities and file headers", () => {
const patch = (hash: string, file = 'f') => `diff --git a/${file} b/${file}\nindex ${'a'.repeat(40)}..${hash} 100644\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-a\n+b\n`;
const first = patch('b'.repeat(40));
expect(haveMatchingPatchVersions(first, first)).toBe(true);
expect(haveMatchingPatchVersions(first, patch('c'.repeat(40)))).toBe(false);
expect(haveMatchingPatchVersions(first, patch('b'.repeat(40), 'other'))).toBe(false);
expect(haveMatchingPatchVersions(SAMPLE_PATCH, SAMPLE_PATCH)).toBe(false);
});
test("preserves CRLF content and mixed endings byte for byte", () => {
const header = 'diff --git a/f b/f\n--- a/f\n+++ b/f\n';
const first = '@@ -1,2 +1,2 @@\n-before\r\n+after\r\n context\n';
const second = '@@ -20 +20 @@\n-old\n+new\r\n';
expect(splitPatchIntoHunks(header + first + second)).toEqual([header + first, header + second]);
expect(extractHunkPatch(header + first + second, 1)).toBe(header + second);
});
test("returns the standalone patch for the requested index", () => {
const second = extractHunkPatch(SAMPLE_PATCH, 1);
expect(second).not.toBeNull();