@@ -26,7 +26,9 @@ import {
2626 MAX_OUTPUT_BYTES ,
2727} from "./constants.js" ;
2828import { resolveOrgPrefetched } from "./prefetch.js" ;
29+ import { replace } from "./replacers.js" ;
2930import type {
31+ ApplyPatchsetPatch ,
3032 ApplyPatchsetPayload ,
3133 CreateSentryProjectPayload ,
3234 DetectSentryPayload ,
@@ -59,25 +61,6 @@ const DEFAULT_JSON_INDENT: JsonIndent = {
5961 length : 2 ,
6062} ;
6163
62- /** Matches the first indented line in a string to detect whitespace style. */
63- const INDENT_PATTERN = / ^ ( \s + ) / m;
64-
65- /**
66- * Detect the indentation style of a JSON string by inspecting the first
67- * indented line. Returns a default of 2 spaces if no indentation is found.
68- */
69- function detectJsonIndent ( content : string ) : JsonIndent {
70- const match = content . match ( INDENT_PATTERN ) ;
71- if ( ! match ?. [ 1 ] ) {
72- return DEFAULT_JSON_INDENT ;
73- }
74- const indent = match [ 1 ] ;
75- if ( indent . includes ( "\t" ) ) {
76- return { replacer : Indenter . TAB , length : indent . length } ;
77- }
78- return { replacer : Indenter . SPACE , length : indent . length } ;
79- }
80-
8164/** Build the third argument for `JSON.stringify` from a `JsonIndent`. */
8265function jsonIndentArg ( indent : JsonIndent ) : string {
8366 return indent . replacer . repeat ( indent . length ) ;
@@ -600,39 +583,59 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
600583}
601584
602585/**
603- * Resolve the final file content for a patch, pretty-printing JSON files
604- * to preserve readable formatting. For `modify` actions, the existing file's
605- * indentation style is detected and preserved. For `create` actions, a default
606- * of 2-space indentation is used.
586+ * Resolve the final file content for a full-content patch (create only),
587+ * pretty-printing JSON files to preserve readable formatting.
607588 */
608- async function resolvePatchContent (
609- absPath : string ,
610- patch : ApplyPatchsetPayload [ "params" ] [ "patches" ] [ number ]
611- ) : Promise < string > {
589+ function resolvePatchContent ( patch : { path : string ; patch : string } ) : string {
612590 if ( ! patch . path . endsWith ( ".json" ) ) {
613591 return patch . patch ;
614592 }
615- if ( patch . action === "modify" ) {
616- const existing = await fs . promises . readFile ( absPath , "utf-8" ) ;
617- return prettyPrintJson ( patch . patch , detectJsonIndent ( existing ) ) ;
618- }
619593 return prettyPrintJson ( patch . patch , DEFAULT_JSON_INDENT ) ;
620594}
621595
622- type Patch = ApplyPatchsetPayload [ "params" ] [ "patches" ] [ number ] ;
623-
624596const VALID_PATCH_ACTIONS = new Set ( [ "create" , "modify" , "delete" ] ) ;
625597
626- async function applySinglePatch ( absPath : string , patch : Patch ) : Promise < void > {
598+ /**
599+ * Apply edits (oldString/newString pairs) to a file using fuzzy matching.
600+ * Edits are applied sequentially — each edit operates on the result of the
601+ * previous one. Returns the final file content.
602+ */
603+ async function applyEdits (
604+ absPath : string ,
605+ filePath : string ,
606+ edits : Array < { oldString : string ; newString : string } >
607+ ) : Promise < string > {
608+ let content = await fs . promises . readFile ( absPath , "utf-8" ) ;
609+
610+ for ( let i = 0 ; i < edits . length ; i ++ ) {
611+ const edit = edits [ i ] as ( typeof edits ) [ number ] ;
612+ try {
613+ content = replace ( content , edit . oldString , edit . newString ) ;
614+ } catch ( err ) {
615+ throw new Error (
616+ `Edit #${ i + 1 } failed on "${ filePath } ": ${ err instanceof Error ? err . message : String ( err ) } `
617+ ) ;
618+ }
619+ }
620+
621+ return content ;
622+ }
623+
624+ async function applySinglePatch (
625+ absPath : string ,
626+ patch : ApplyPatchsetPatch
627+ ) : Promise < void > {
627628 switch ( patch . action ) {
628629 case "create" : {
629630 await fs . promises . mkdir ( path . dirname ( absPath ) , { recursive : true } ) ;
630- const content = await resolvePatchContent ( absPath , patch ) ;
631+ const content = resolvePatchContent (
632+ patch as ApplyPatchsetPatch & { patch : string }
633+ ) ;
631634 await fs . promises . writeFile ( absPath , content , "utf-8" ) ;
632635 break ;
633636 }
634637 case "modify" : {
635- const content = await resolvePatchContent ( absPath , patch ) ;
638+ const content = await applyEdits ( absPath , patch . path , patch . edits ) ;
636639 await fs . promises . writeFile ( absPath , content , "utf-8" ) ;
637640 break ;
638641 }
0 commit comments