@@ -219,54 +219,14 @@ protected async Task<bool> TryResolvingConflictWithCrossingFlow(
219219 {
220220 _logger . LogDebug ( "Trying to auto-resolve a conflict in {filePath} based on a crossing flow..." , conflictedFile ) ;
221221
222- UnixPath vmrRepoPath = VmrInfo . GetRelativeRepoSourcesPath ( codeflowOptions . Mapping . Name ) ;
223- if ( codeflowOptions . CurrentFlow . IsForwardFlow && ! conflictedFile . Path . StartsWith ( vmrRepoPath + '/' ) )
222+ var crossingFlowPatch = await TryCreateCrossingFlowPatchAsync ( codeflowOptions , vmr , repo , conflictedFile , crossingFlow , cancellationToken ) ;
223+ if ( crossingFlowPatch is null )
224224 {
225- _logger . LogWarning ( "Conflict in {file} is not in the source repo, skipping auto-resolution" , conflictedFile ) ;
226225 return false ;
227226 }
228227
229- // Create patch for the file represent only the most current flow
230- var ( fromSha , toSha ) = codeflowOptions . CurrentFlow . IsForwardFlow
231- ? ( crossingFlow . RepoSha , codeflowOptions . CurrentFlow . RepoSha )
232- : ( crossingFlow . VmrSha , codeflowOptions . CurrentFlow . VmrSha ) ;
233-
234- var patchName = _vmrInfo . TmpPath / $ "{ codeflowOptions . Mapping . Name } -{ Guid . NewGuid ( ) } .patch";
235- List < VmrIngestionPatch > patches = await _patchHandler . CreatePatches (
236- patchName ,
237- fromSha ,
238- toSha ,
239- path : codeflowOptions . CurrentFlow . IsForwardFlow
240- ? new UnixPath ( conflictedFile . Path . Substring ( vmrRepoPath . Length + 1 ) )
241- : conflictedFile ,
242- filters : null ,
243- relativePaths : true ,
244- workingDir : codeflowOptions . CurrentFlow . IsForwardFlow
245- ? repo . Path
246- : vmr . Path / vmrRepoPath ,
247- applicationPath : codeflowOptions . CurrentFlow . IsForwardFlow
248- ? vmrRepoPath
249- : null ,
250- ignoreLineEndings : true ,
251- cancellationToken ) ;
252-
253- if ( patches . Count > 1 )
254- {
255- foreach ( var patch in patches )
256- {
257- try
258- {
259- _fileSystem . DeleteFile ( patch . Path ) ;
260- }
261- catch
262- {
263- }
264- }
265-
266- throw new InvalidOperationException ( "Cannot auto-resolve conflicts for files over 1GB in size" ) ;
267- }
228+ var ( targetRepo , patch ) = crossingFlowPatch . Value ;
268229
269- var targetRepo = codeflowOptions . CurrentFlow . IsForwardFlow ? vmr : repo ;
270230 try
271231 {
272232 await targetRepo . ResolveConflict ( conflictedFile , ours : true ) ;
@@ -278,7 +238,7 @@ protected async Task<bool> TryResolvingConflictWithCrossingFlow(
278238 return false ;
279239 }
280240
281- if ( _fileSystem . GetFileInfo ( patches . First ( ) . Path ) . Length == 0 )
241+ if ( _fileSystem . GetFileInfo ( patch . Path ) . Length == 0 )
282242 {
283243 // This file did not change in the last flow
284244 return true ;
@@ -287,7 +247,7 @@ protected async Task<bool> TryResolvingConflictWithCrossingFlow(
287247 try
288248 {
289249 await _patchHandler . ApplyPatch (
290- patches [ 0 ] ,
250+ patch ,
291251 targetRepo . Path ,
292252 removePatchAfter : true ,
293253 keepConflicts : false ,
@@ -311,6 +271,116 @@ await _patchHandler.ApplyPatch(
311271 }
312272 }
313273
274+ /// <summary>
275+ /// Reapplies the changes from the crossing flow on top of a non-conflicted file.
276+ /// </summary>
277+ /// <returns>True when the patch was applied successfully (or was a no-op)</returns>
278+ private async Task < bool > TryReapplyingCrossingFlowChangesAsync (
279+ CodeflowOptions codeflowOptions ,
280+ ILocalGitRepo vmr ,
281+ ILocalGitRepo repo ,
282+ UnixPath file ,
283+ Codeflow crossingFlow ,
284+ CancellationToken cancellationToken )
285+ {
286+ _logger . LogDebug ( "Trying to reapply crossing-flow changes to {filePath}..." , file ) ;
287+
288+ var crossingFlowPatch = await TryCreateCrossingFlowPatchAsync ( codeflowOptions , vmr , repo , file , crossingFlow , cancellationToken ) ;
289+ if ( crossingFlowPatch is null )
290+ {
291+ return false ;
292+ }
293+
294+ var ( targetRepo , patch ) = crossingFlowPatch . Value ;
295+
296+ if ( _fileSystem . GetFileInfo ( patch . Path ) . Length == 0 )
297+ {
298+ // This file did not change in the last flow
299+ return true ;
300+ }
301+
302+ try
303+ {
304+ await _patchHandler . ApplyPatch (
305+ patch ,
306+ targetRepo . Path ,
307+ removePatchAfter : true ,
308+ keepConflicts : false ,
309+ cancellationToken : cancellationToken ) ;
310+ _logger . LogDebug ( "Successfully reapplied crossing-flow changes to {filePath}" , file ) ;
311+ return true ;
312+ }
313+ catch ( PatchApplicationFailedException )
314+ {
315+ _logger . LogInformation ( "Failed to reapply crossing-flow changes to {filePath}" , file ) ;
316+ return false ;
317+ }
318+ }
319+
320+ /// <summary>
321+ /// Builds a patch capturing the source-side changes between the crossing flow and the current flow
322+ /// for a single file. Returns null if the file is outside the source repo path (forward flow only),
323+ /// throws if the resulting patch would be too large to handle.
324+ /// </summary>
325+ private async Task < ( ILocalGitRepo TargetRepo , VmrIngestionPatch Patch ) ? > TryCreateCrossingFlowPatchAsync (
326+ CodeflowOptions codeflowOptions ,
327+ ILocalGitRepo vmr ,
328+ ILocalGitRepo repo ,
329+ UnixPath file ,
330+ Codeflow crossingFlow ,
331+ CancellationToken cancellationToken )
332+ {
333+ UnixPath vmrRepoPath = VmrInfo . GetRelativeRepoSourcesPath ( codeflowOptions . Mapping . Name ) ;
334+ if ( codeflowOptions . CurrentFlow . IsForwardFlow && ! file . Path . StartsWith ( vmrRepoPath + '/' ) )
335+ {
336+ _logger . LogWarning ( "File {file} is not in the source repo, skipping crossing-flow auto-resolution" , file ) ;
337+ return null ;
338+ }
339+
340+ // Create patch for the file representing only the most current flow
341+ var ( fromSha , toSha ) = codeflowOptions . CurrentFlow . IsForwardFlow
342+ ? ( crossingFlow . RepoSha , codeflowOptions . CurrentFlow . RepoSha )
343+ : ( crossingFlow . VmrSha , codeflowOptions . CurrentFlow . VmrSha ) ;
344+
345+ var patchName = _vmrInfo . TmpPath / $ "{ codeflowOptions . Mapping . Name } -{ Guid . NewGuid ( ) } .patch";
346+ List < VmrIngestionPatch > patches = await _patchHandler . CreatePatches (
347+ patchName ,
348+ fromSha ,
349+ toSha ,
350+ path : codeflowOptions . CurrentFlow . IsForwardFlow
351+ ? new UnixPath ( file . Path . Substring ( vmrRepoPath . Length + 1 ) )
352+ : file ,
353+ filters : null ,
354+ relativePaths : true ,
355+ workingDir : codeflowOptions . CurrentFlow . IsForwardFlow
356+ ? repo . Path
357+ : vmr . Path / vmrRepoPath ,
358+ applicationPath : codeflowOptions . CurrentFlow . IsForwardFlow
359+ ? vmrRepoPath
360+ : null ,
361+ ignoreLineEndings : true ,
362+ cancellationToken ) ;
363+
364+ if ( patches . Count > 1 )
365+ {
366+ foreach ( var patch in patches )
367+ {
368+ try
369+ {
370+ _fileSystem . DeleteFile ( patch . Path ) ;
371+ }
372+ catch
373+ {
374+ }
375+ }
376+
377+ throw new InvalidOperationException ( "Cannot auto-resolve conflicts for files over 1GB in size" ) ;
378+ }
379+
380+ var targetRepo = codeflowOptions . CurrentFlow . IsForwardFlow ? vmr : repo ;
381+ return ( targetRepo , patches [ 0 ] ) ;
382+ }
383+
314384 /// <summary>
315385 /// If a file was added and then removed again in the original repo, it won't exist in the PR branch,
316386 /// so in case of a conflict + recreation, we cannot remove it (if it does not exist).
@@ -552,7 +622,7 @@ private async Task FixRevertedFiles(
552622 return ;
553623 }
554624
555- if ( ! await TryResolvingConflictWithCrossingFlow (
625+ if ( ! await TryReapplyingCrossingFlowChangesAsync (
556626 codeflowOptions ,
557627 vmr ,
558628 productRepo ,
0 commit comments