11import { Cause , Duration , Effect , Layer , Schedule , Semaphore , Context , Stream } from "effect"
22import { ChildProcess , ChildProcessSpawner } from "effect/unstable/process"
3+ import { formatPatch , structuredPatch } from "diff"
34import path from "path"
45import z from "zod"
56import { makeRuntime } from "@/effect/run-service" // kilocode_change
@@ -525,6 +526,144 @@ export namespace Snapshot {
525526 const diffFull = Effect . fnUntraced ( function * ( from : string , to : string ) {
526527 return yield * locked (
527528 Effect . gen ( function * ( ) {
529+ type Row = {
530+ file : string
531+ status : "added" | "deleted" | "modified"
532+ binary : boolean
533+ additions : number
534+ deletions : number
535+ }
536+
537+ type Ref = {
538+ file : string
539+ side : "before" | "after"
540+ ref : string
541+ }
542+
543+ const show = Effect . fnUntraced ( function * ( row : Row ) {
544+ if ( row . binary ) return [ "" , "" ]
545+ if ( row . status === "added" ) {
546+ return [
547+ "" ,
548+ yield * git ( [ ...cfg , ...args ( [ "show" , `${ to } :${ row . file } ` ] ) ] ) . pipe (
549+ Effect . map ( ( item ) => item . text ) ,
550+ ) ,
551+ ]
552+ }
553+ if ( row . status === "deleted" ) {
554+ return [
555+ yield * git ( [ ...cfg , ...args ( [ "show" , `${ from } :${ row . file } ` ] ) ] ) . pipe (
556+ Effect . map ( ( item ) => item . text ) ,
557+ ) ,
558+ "" ,
559+ ]
560+ }
561+ return yield * Effect . all (
562+ [
563+ git ( [ ...cfg , ...args ( [ "show" , `${ from } :${ row . file } ` ] ) ] ) . pipe ( Effect . map ( ( item ) => item . text ) ) ,
564+ git ( [ ...cfg , ...args ( [ "show" , `${ to } :${ row . file } ` ] ) ] ) . pipe ( Effect . map ( ( item ) => item . text ) ) ,
565+ ] ,
566+ { concurrency : 2 } ,
567+ )
568+ } )
569+
570+ const load = Effect . fnUntraced (
571+ function * ( rows : Row [ ] ) {
572+ const refs = rows . flatMap ( ( row ) => {
573+ if ( row . binary ) return [ ]
574+ if ( row . status === "added" )
575+ return [ { file : row . file , side : "after" , ref : `${ to } :${ row . file } ` } satisfies Ref ]
576+ if ( row . status === "deleted" ) {
577+ return [ { file : row . file , side : "before" , ref : `${ from } :${ row . file } ` } satisfies Ref ]
578+ }
579+ return [
580+ { file : row . file , side : "before" , ref : `${ from } :${ row . file } ` } satisfies Ref ,
581+ { file : row . file , side : "after" , ref : `${ to } :${ row . file } ` } satisfies Ref ,
582+ ]
583+ } )
584+ if ( ! refs . length ) return new Map < string , { before : string ; after : string } > ( )
585+
586+ const proc = ChildProcess . make ( "git" , [ ...cfg , ...args ( [ "cat-file" , "--batch" ] ) ] , {
587+ cwd : state . directory ,
588+ extendEnv : true ,
589+ stdin : Stream . make ( new TextEncoder ( ) . encode ( refs . map ( ( item ) => item . ref ) . join ( "\n" ) + "\n" ) ) ,
590+ } )
591+ const handle = yield * spawner . spawn ( proc )
592+ const [ out , err ] = yield * Effect . all (
593+ [ Stream . mkUint8Array ( handle . stdout ) , Stream . mkString ( Stream . decodeText ( handle . stderr ) ) ] ,
594+ { concurrency : 2 } ,
595+ )
596+ const code = yield * handle . exitCode
597+ if ( code !== 0 ) {
598+ log . info ( "git cat-file --batch failed during snapshot diff, falling back to per-file git show" , {
599+ stderr : err ,
600+ refs : refs . length ,
601+ } )
602+ return
603+ }
604+
605+ const fail = ( msg : string , extra ?: Record < string , string > ) => {
606+ log . info ( msg , { ...extra , refs : refs . length } )
607+ return undefined
608+ }
609+
610+ const map = new Map < string , { before : string ; after : string } > ( )
611+ const dec = new TextDecoder ( )
612+ let i = 0
613+ for ( const ref of refs ) {
614+ let end = i
615+ while ( end < out . length && out [ end ] !== 10 ) end += 1
616+ if ( end >= out . length ) {
617+ return fail (
618+ "git cat-file --batch returned a truncated header during snapshot diff, falling back to per-file git show" ,
619+ )
620+ }
621+
622+ const head = dec . decode ( out . slice ( i , end ) )
623+ i = end + 1
624+ const hit = map . get ( ref . file ) ?? { before : "" , after : "" }
625+ if ( head . endsWith ( " missing" ) ) {
626+ map . set ( ref . file , hit )
627+ continue
628+ }
629+
630+ const match = head . match ( / ^ [ 0 - 9 a - f ] + b l o b ( \d + ) $ / )
631+ if ( ! match ) {
632+ return fail (
633+ "git cat-file --batch returned an unexpected header during snapshot diff, falling back to per-file git show" ,
634+ { head } ,
635+ )
636+ }
637+
638+ const size = Number ( match [ 1 ] )
639+ if ( ! Number . isInteger ( size ) || size < 0 || i + size >= out . length || out [ i + size ] !== 10 ) {
640+ return fail (
641+ "git cat-file --batch returned truncated content during snapshot diff, falling back to per-file git show" ,
642+ { head } ,
643+ )
644+ }
645+
646+ const text = dec . decode ( out . slice ( i , i + size ) )
647+ if ( ref . side === "before" ) hit . before = text
648+ if ( ref . side === "after" ) hit . after = text
649+ map . set ( ref . file , hit )
650+ i += size + 1
651+ }
652+
653+ if ( i !== out . length ) {
654+ return fail (
655+ "git cat-file --batch returned trailing data during snapshot diff, falling back to per-file git show" ,
656+ )
657+ }
658+
659+ return map
660+ } ,
661+ Effect . scoped ,
662+ Effect . catch ( ( ) =>
663+ Effect . succeed < Map < string , { before : string ; after : string } > | undefined > ( undefined ) ,
664+ ) ,
665+ )
666+
528667 const result : Snapshot . FileDiff [ ] = [ ]
529668 const status = new Map < string , "added" | "deleted" | "modified" > ( )
530669
@@ -564,7 +703,7 @@ export namespace Snapshot {
564703 binary,
565704 additions : Number . isFinite ( additions ) ? additions : 0 ,
566705 deletions : Number . isFinite ( deletions ) ? deletions : 0 ,
567- } ,
706+ } satisfies Row ,
568707 ]
569708 } )
570709
@@ -577,9 +716,14 @@ export namespace Snapshot {
577716 }
578717
579718 const step = 100
719+ const patch = ( file : string , before : string , after : string ) =>
720+ formatPatch ( structuredPatch ( file , file , before , after , "" , "" , { context : Number . MAX_SAFE_INTEGER } ) )
721+
722+ // kilocode_change start — route patches through git (DiffFull.batch) instead of the
723+ // JS Myers implementation. Upstream Myers loop below is kept as dead code so our
724+ // diff from upstream stays minimal and future merges don't conflict.
580725 for ( let i = 0 ; i < rows . length ; i += step ) {
581726 const run = rows . slice ( i , i + step )
582- // kilocode_change start — bulk-load patches from `git diff`; no JS Myers fallback
583727 const patches = yield * DiffFull . batch (
584728 ( cmd ) => git ( [ ...quote , ...args ( cmd ) ] , { cwd : state . directory } ) ,
585729 from ,
@@ -595,7 +739,25 @@ export namespace Snapshot {
595739 status : row . status ,
596740 } )
597741 }
598- // kilocode_change end
742+ }
743+ return result
744+ // kilocode_change end
745+
746+ for ( let i = 0 ; i < rows . length ; i += step ) {
747+ const run = rows . slice ( i , i + step )
748+ const text = yield * load ( run )
749+
750+ for ( const row of run ) {
751+ const hit = text ?. get ( row . file ) ?? { before : "" , after : "" }
752+ const [ before , after ] = row . binary ? [ "" , "" ] : text ? [ hit . before , hit . after ] : yield * show ( row )
753+ result . push ( {
754+ file : row . file ,
755+ patch : row . binary ? "" : patch ( row . file , before , after ) ,
756+ additions : row . additions ,
757+ deletions : row . deletions ,
758+ status : row . status ,
759+ } )
760+ }
599761 }
600762
601763 return result
0 commit comments