@@ -69,6 +69,9 @@ export interface StageLinesResult {
6969}
7070
7171export class GitStagingManager {
72+ // Keep in sync with UI working-tree diff context (DiffManager.WORKING_DIFF_CONTEXT_LINES).
73+ private readonly UI_WORKING_DIFF_CONTEXT_LINES = 3 ;
74+
7275 constructor (
7376 private gitExecutor : GitExecutor ,
7477 private statusManager : GitStatusManager
@@ -93,7 +96,7 @@ export class GitStagingManager {
9396 console . debug ( '[GitStagingManager] Got diff, length:' , fullDiff . length ) ;
9497
9598 // Check for binary files
96- if ( fullDiff . includes ( 'Binary files differ' ) ) {
99+ if ( this . isBinaryDiffOutput ( fullDiff ) ) {
97100 return {
98101 success : false ,
99102 error : 'Cannot stage individual lines of binary files' ,
@@ -151,23 +154,18 @@ export class GitStagingManager {
151154 async stageHunk ( options : StageHunkOptions ) : Promise < StageLinesResult > {
152155 try {
153156 const scope = options . isStaging ? 'unstaged' : 'staged' ;
154- // Keep hunk operations aligned with the UI diff (unified=0), so the hunk header matches.
155- const fullDiff = await this . getFileDiff ( options . worktreePath , options . filePath , scope , options . sessionId , 0 ) ;
156-
157- if ( fullDiff . includes ( 'Binary files differ' ) ) {
158- return { success : false , error : 'Cannot stage hunks of binary files' } ;
159- }
160-
161- const hunks = this . parseDiffIntoHunks ( fullDiff ) ;
162- if ( hunks . length === 0 ) {
163- return { success : false , error : 'No changes found in diff' } ;
164- }
165-
166- const normalizedHeader = options . hunkHeader . trim ( ) ;
167- const targetHunk = hunks . find ( ( h ) => h . header . trim ( ) === normalizedHeader ) ;
168- if ( ! targetHunk ) {
169- return { success : false , error : 'Target hunk not found in diff' } ;
157+ const resolved = await this . resolveTargetHunkByHeader ( {
158+ worktreePath : options . worktreePath ,
159+ filePath : options . filePath ,
160+ scope,
161+ sessionId : options . sessionId ,
162+ hunkHeader : options . hunkHeader ,
163+ binaryError : 'Cannot stage hunks of binary files' ,
164+ } ) ;
165+ if ( 'error' in resolved ) {
166+ return { success : false , error : resolved . error } ;
170167 }
168+ const targetHunk = resolved . hunk ;
171169
172170 const patch = this . generateHunkPatch ( targetHunk , options . filePath ) ;
173171 return await this . applyPatch ( options . worktreePath , patch , options . isStaging , options . sessionId , {
@@ -190,23 +188,18 @@ export class GitStagingManager {
190188 async restoreHunk ( options : RestoreHunkOptions ) : Promise < StageLinesResult > {
191189 try {
192190 const fullDiffScope = options . scope === 'staged' ? 'staged' : 'unstaged' ;
193- // Keep hunk operations aligned with the UI diff (unified=0), so the hunk header matches.
194- const fullDiff = await this . getFileDiff ( options . worktreePath , options . filePath , fullDiffScope , options . sessionId , 0 ) ;
195-
196- if ( fullDiff . includes ( 'Binary files differ' ) ) {
197- return { success : false , error : 'Cannot restore hunks of binary files' } ;
198- }
199-
200- const hunks = this . parseDiffIntoHunks ( fullDiff ) ;
201- if ( hunks . length === 0 ) {
202- return { success : false , error : 'No changes found in diff' } ;
203- }
204-
205- const normalizedHeader = options . hunkHeader . trim ( ) ;
206- const targetHunk = hunks . find ( ( h ) => h . header . trim ( ) === normalizedHeader ) ;
207- if ( ! targetHunk ) {
208- return { success : false , error : 'Target hunk not found in diff' } ;
191+ const resolved = await this . resolveTargetHunkByHeader ( {
192+ worktreePath : options . worktreePath ,
193+ filePath : options . filePath ,
194+ scope : fullDiffScope ,
195+ sessionId : options . sessionId ,
196+ hunkHeader : options . hunkHeader ,
197+ binaryError : 'Cannot restore hunks of binary files' ,
198+ } ) ;
199+ if ( 'error' in resolved ) {
200+ return { success : false , error : resolved . error } ;
209201 }
202+ const targetHunk = resolved . hunk ;
210203
211204 const patch = this . generateHunkPatch ( targetHunk , options . filePath ) ;
212205
@@ -331,6 +324,51 @@ export class GitStagingManager {
331324 }
332325 }
333326
327+ private async resolveTargetHunkByHeader ( options : {
328+ worktreePath : string ;
329+ filePath : string ;
330+ scope : 'staged' | 'unstaged' ;
331+ sessionId : string ;
332+ hunkHeader : string ;
333+ binaryError : string ;
334+ } ) : Promise < { hunk : Hunk } | { error : string } > {
335+ const normalizedHeader = options . hunkHeader . trim ( ) ;
336+ if ( ! normalizedHeader ) {
337+ return { error : 'Invalid hunk header' } ;
338+ }
339+
340+ let sawAnyHunks = false ;
341+ const unifiedCandidates = [ 0 , this . UI_WORKING_DIFF_CONTEXT_LINES ] ;
342+ const attemptedUnified = new Set < number > ( ) ;
343+
344+ for ( const unified of unifiedCandidates ) {
345+ if ( attemptedUnified . has ( unified ) ) continue ;
346+ attemptedUnified . add ( unified ) ;
347+
348+ const fullDiff = await this . getFileDiff (
349+ options . worktreePath ,
350+ options . filePath ,
351+ options . scope ,
352+ options . sessionId ,
353+ unified
354+ ) ;
355+
356+ if ( this . isBinaryDiffOutput ( fullDiff ) ) {
357+ return { error : options . binaryError } ;
358+ }
359+
360+ const hunks = this . parseDiffIntoHunks ( fullDiff ) ;
361+ if ( hunks . length === 0 ) continue ;
362+ sawAnyHunks = true ;
363+
364+ const target = hunks . find ( ( h ) => h . header . trim ( ) === normalizedHeader ) ;
365+ if ( target ) return { hunk : target } ;
366+ }
367+
368+ if ( ! sawAnyHunks ) return { error : 'No changes found in diff' } ;
369+ return { error : 'Target hunk not found in diff' } ;
370+ }
371+
334372 private async getFileDiff (
335373 worktreePath : string ,
336374 filePath : string ,
@@ -360,6 +398,15 @@ export class GitStagingManager {
360398 return result . stdout ;
361399 }
362400
401+ private isBinaryDiffOutput ( diffText : string ) : boolean {
402+ for ( const rawLine of diffText . split ( '\n' ) ) {
403+ const line = rawLine . trimEnd ( ) ;
404+ if ( line === 'GIT binary patch' ) return true ;
405+ if ( line . startsWith ( 'Binary files ' ) && line . endsWith ( ' differ' ) ) return true ;
406+ }
407+ return false ;
408+ }
409+
363410 /**
364411 * Parse diff text into hunks with line number tracking
365412 */
0 commit comments