Skip to content

Commit f55de74

Browse files
author
Copilot CI
committed
Replay a winning fix through a three-way merge when the tree moved
Build 15098930 proved two independent fixes for #27332 on Windows, selected one, and published neither. Between candidates the panel restores the tree; by the time the winner was replayed, ItemsViewStyles.xaml no longer matched its diff at line 82, strict `git apply` refused, and the phase reported "No fix arms were run" and threw away ninety minutes of validated work. This was not rare. Across the cached logs, 26 of the 94 runs that produced a *passing* fix candidate died at exactly this line -- and every run that failed to apply had a passing candidate to lose. It was the largest single source of discarded work in the fix phase, and 20 distinct issues are sitting on a reproduction and a fix that were both proven. The old comment refused a merge because "guessing at a merge would measure something else". That is true of fuzzy context matching; it is not true of --3way, which reconstructs the preimage from the blob ids the diff already carries and merges against that, and which reports failure instead of inventing a resolution. The stronger argument is that the result is not trusted either way: both arms still run, so a merge that landed the wrong change cannot become a published fix -- it can only fail to reproduce. Strict apply is still tried first and is still the normal path, so a clean replay is byte-identical to before. Two consequences of --3way are handled rather than inherited: a conflicted merge is not all-or-nothing, so the tree is restored before returning (the reproduction is published from it), and a successful merge lands staged because --3way implies --index, so the scope is unstaged back to a plain working-tree edit. Four mutants, all killed: removing the fallback, removing the restore, leaving the merge staged, and reaching for the merge when apply succeeded. Suite by explicit file list: 4037 passed / 35 failed, the same 35 pre-existing failures.
1 parent 0663054 commit f55de74

2 files changed

Lines changed: 98 additions & 7 deletions

File tree

.github/scripts/Replicate-Issue.Tests.ps1

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10171,6 +10171,12 @@ Describe 'Proving the fix is what turned the reproduction green' {
1017110171
$script:appliedPathsBefore = @()
1017210172
$script:gitApplied = $false
1017310173
$script:gitApplySucceeds = $true
10174+
# Reality: the three-way fallback is only reached when strict apply
10175+
# already refused, so it defaults to the outcome that makes the winner
10176+
# replayable.
10177+
$script:gitThreeWaySucceeds = $true
10178+
$script:gitThreeWayAttempted = $false
10179+
$script:gitResetPaths = @()
1017410180

1017510181
function Invoke-LoggedChildProcess {
1017610182
param($ScriptPath, $Arguments, $LogPath, $Description, $TimeoutSeconds)
@@ -10210,8 +10216,17 @@ Describe 'Proving the fix is what turned the reproduction green' {
1021010216
}
1021110217
function git {
1021210218
if ($args[0] -eq 'apply') {
10213-
$script:gitApplied = $script:gitApplySucceeds
10214-
$global:LASTEXITCODE = if ($script:gitApplySucceeds) { 0 } else { 1 }
10219+
$threeWay = @($args) -contains '--3way'
10220+
$succeeded = if ($threeWay) {
10221+
$script:gitThreeWayAttempted = $true
10222+
$script:gitThreeWaySucceeds
10223+
} else { $script:gitApplySucceeds }
10224+
if ($succeeded) { $script:gitApplied = $true }
10225+
$global:LASTEXITCODE = if ($succeeded) { 0 } else { 1 }
10226+
}
10227+
elseif ($args[0] -eq 'reset') {
10228+
$script:gitResetPaths = @(@($args) | Select-Object -Skip 1)
10229+
$global:LASTEXITCODE = 0
1021510230
}
1021610231
}
1021710232

@@ -10264,11 +10279,57 @@ Describe 'Proving the fix is what turned the reproduction green' {
1026410279
}
1026510280

1026610281
It 'runs nothing when the winning diff no longer applies' {
10282+
# Both routes have to refuse before the winner is abandoned. Strict
10283+
# apply alone refusing is routine -- see the three-way test below.
1026710284
$script:gitApplySucceeds = $false
10285+
$script:gitThreeWaySucceeds = $false
1026810286
Invoke-ReplicationFixArms @script:armArgs | Should -BeNullOrEmpty
1026910287
$script:childRuns | Should -HaveCount 0
1027010288
}
1027110289

10290+
It 'replays a winner whose context moved by falling back to a three-way merge' {
10291+
# Build 15098930 proved two fixes for #27332 and published neither: the
10292+
# panel restored ItemsViewStyles.xaml to HEAD between candidates, and
10293+
# the winner's diff would no longer apply at line 82. Across the cached
10294+
# logs this destroyed a passing candidate 26 times in 94 fix runs.
10295+
$script:gitApplySucceeds = $false
10296+
$script:gitThreeWaySucceeds = $true
10297+
10298+
$evidence = Invoke-ReplicationFixArms @script:armArgs
10299+
10300+
$script:gitThreeWayAttempted | Should -BeTrue
10301+
$evidence | Should -Not -BeNullOrEmpty
10302+
$script:childRuns | Should -HaveCount 2
10303+
}
10304+
10305+
It 'does not reach for a three-way merge when the diff applied cleanly' {
10306+
Invoke-ReplicationFixArms @script:armArgs | Out-Null
10307+
$script:gitThreeWayAttempted | Should -BeFalse
10308+
}
10309+
10310+
It 'puts the tree back when a three-way merge conflicts' {
10311+
# A conflicted three-way is not all-or-nothing like strict apply: it
10312+
# leaves markers and unmerged index entries. The reproduction is
10313+
# published from this tree, so it cannot be left carrying them.
10314+
$script:gitApplySucceeds = $false
10315+
$script:gitThreeWaySucceeds = $false
10316+
10317+
Invoke-ReplicationFixArms @script:armArgs | Should -BeNullOrEmpty
10318+
10319+
$script:restoreCalls | Should -Be 1
10320+
}
10321+
10322+
It 'unstages the scope a three-way merge staged behind it' {
10323+
# --3way implies --index, so the merge lands staged, which no other
10324+
# path in the phase produces.
10325+
$script:gitApplySucceeds = $false
10326+
$script:gitThreeWaySucceeds = $true
10327+
10328+
Invoke-ReplicationFixArms @script:armArgs | Out-Null
10329+
10330+
$script:gitResetPaths | Should -Contain 'src/Core/src/Handlers/EntryHandler.cs'
10331+
}
10332+
1027210333
It 'refuses a diff that turns out to touch a file outside the scope' {
1027310334
$script:appliedPaths = @('src/Core/src/Handlers/EntryHandler.cs', 'eng/Versions.props')
1027410335
Invoke-ReplicationFixArms @script:armArgs | Should -BeNullOrEmpty

.github/scripts/Replicate-Issue.ps1

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5044,11 +5044,41 @@ function Invoke-ReplicationFixArms {
50445044
Where-Object { $ScopeFiles -cnotcontains $_ })
50455045
& git apply --whitespace=nowarn -- $PatchPath
50465046
if ($LASTEXITCODE -ne 0) {
5047-
# The panel restores the tree between candidates, so the winner's work
5048-
# has to be replayed here. If it will not replay there is nothing to
5049-
# measure, and guessing at a merge would measure something else.
5050-
Write-Host 'No fix arms were run: the winning diff no longer applies to the tree.'
5051-
return $null
5047+
# Strict apply is exact but brittle. The panel restores the tree
5048+
# between candidates, so by the time the winner is replayed the context
5049+
# around its change can have moved -- on Windows a restore routinely
5050+
# rewrites line endings, which is enough on its own. Measured over the
5051+
# cached logs, 26 of the 94 runs that produced a *passing* fix candidate
5052+
# died right here, and every run that failed to apply had a passing
5053+
# candidate to lose. It was the largest single source of discarded work
5054+
# in the fix phase.
5055+
#
5056+
# A three-way merge is not the guess the strict path was guarding
5057+
# against. It reconstructs the preimage from the blob ids the diff
5058+
# already carries and merges against that; when it cannot, it says so
5059+
# instead of inventing a resolution. And whatever it produces is still
5060+
# put through both arms below, so a merge that landed the wrong change
5061+
# cannot become a published fix -- it can only fail to reproduce.
5062+
& git apply --3way --whitespace=nowarn -- $PatchPath
5063+
if ($LASTEXITCODE -ne 0) {
5064+
# Unlike strict apply, which is all-or-nothing, a conflicted
5065+
# three-way leaves markers in the file and unmerged entries in the
5066+
# index. The reproduction is published from this tree, so it has to
5067+
# be put back before returning.
5068+
Restore-ReplicationFixTree `
5069+
-TrustedScriptRoot $TrustedScriptRoot `
5070+
-ScopeFiles $ScopeFiles | Out-Null
5071+
Write-Host 'No fix arms were run: the winning diff no longer applies to the tree.'
5072+
return $null
5073+
}
5074+
# --3way implies --index, so the merge lands staged. Nothing downstream
5075+
# is fooled by that -- status is porcelain and the winner diff is taken
5076+
# against HEAD -- but leaving the tree in a shape no other path produces
5077+
# invites a later reader to assume wrongly, so put it back to a plain
5078+
# working-tree edit. Best effort: if it fails the change is still there.
5079+
& git reset --quiet -- @ScopeFiles 2>&1 | Out-Null
5080+
Write-Host ('The winning diff needed a three-way merge to replay, because the ' +
5081+
'tree moved under it. Both arms still decide whether the fix is real.')
50525082
}
50535083

50545084
$applied = @(Get-ReplicationFixCandidateChanges -ExcludePaths ($ReproductionPaths + $inheritedDirt))

0 commit comments

Comments
 (0)