mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-10-28 08:36:04 +08:00
fix(release): reconcile preview deltas against full history (#50132)
## Summary of the Pull Request Fixes preview release branch-transition deltas that incorrectly report stable cherry-picks as removed when the matching PR exists before the merge base in `main`. The resolver now compares PR identities and equivalent patches against each branch's full reachable history. It also excludes content-free aggregate promotion merges, such as `main`-to-`stable` release merges, from semantic release notes. ## PR Checklist - [x] **Tests:** Added/updated and all pass - [x] **Dev docs:** Added/updated - [x] **New binaries:** N/A ## Detailed Description of the Pull Request / Additional comments `get-preview-release-delta.ps1` previously built identity sets only from commits after the merge base. A stable cherry-pick after that merge base was therefore reported as removed even when the same PR had already merged into `main` earlier with a different SHA. The updated resolver: - Collects PR identities from the full reachable history of both release commits. - Uses `git cherry` to recognize equivalent patches across the full histories. - Ignores merge commits with no independent patch when they only promote a parent already contained by the opposite branch. - Documents the full-history comparison in `.github/skills/release-note-generation/references/preview-delta-resolution.md`. - Adds regression coverage in `.github/skills/release-note-generation/tests/preview-release.Tests.ps1`. This was exercised against preview build `155850552`, where the corrected delta changed from 11 added / 71 removed PRs to 11 added / 0 removed PRs. Of the false removals, 68 had the same PR identity in `main`, and three were aggregate promotion merges. ## Validation Steps Performed - `Invoke-Pester .\.github\skills\release-note-generation\tests\preview-release.Tests.ps1 -EnableExit` - 26/26 passed. - Recomputed build `155850552` against published stable `v0.101.2362.0`: 11 added, 0 removed, 0 unattributed commits. - Ran the canonical preview dry-run verifier: PASS with 7/7 assets verified. --------- Copilot-Session: 5e9606ff-44ec-4d35-990e-a1fb2c0a10b1 Copilot-Session: 4953c1b4-3f0f-45b3-84a5-1d26d8800de1
This commit is contained in:
1 parent
b701ce788f
commit
08e9edb3d3
3 files changed
+258
No files matched your search
@@ -20,6 +20,18 @@ Added = Target identities - Previous identities
|
||||
Removed = Previous identities - Target identities
|
||||
```
|
||||
|
||||
Compare each side against the other commit's full reachable history, not only
|
||||
the commits after the merge base. Stable cherry-picks can appear after the
|
||||
merge base even when the same PR was merged into `main` earlier. Equal PR
|
||||
identities or equivalent patches anywhere in the opposite history cancel.
|
||||
Compare delta records against stable patch-ID sets built from each tip's full
|
||||
reachable history, including root commits. Do not use `git cherry` for this:
|
||||
it excludes shared history before the merge base from patch comparison.
|
||||
Aggregate merge commits with no independent patch also cancel when all
|
||||
merged-in parents (excluding the first parent) are ancestors of the opposite
|
||||
tip; they are not independent release-note changes. A shared first parent
|
||||
alone does not make a feature merge a promotion.
|
||||
|
||||
Resolve commit identity in this order:
|
||||
|
||||
1. Squash subject ending in `(#<number>)`.
|
||||
|
||||
@@ -195,6 +195,66 @@ function New-IdentitySet {
|
||||
return ,$set
|
||||
}
|
||||
|
||||
function Get-HistoryPrIdentitySet {
|
||||
param([Parameter(Mandatory)][string]$Commit)
|
||||
|
||||
$set = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($subject in @(Invoke-Git log --format=%s $Commit)) {
|
||||
$prNumber = Get-SubjectPrNumber -Subject ([string]$subject)
|
||||
if ($prNumber) {
|
||||
[void]$set.Add("pr:$prNumber")
|
||||
}
|
||||
}
|
||||
return ,$set
|
||||
}
|
||||
|
||||
function Get-HistoryPatchIdSet {
|
||||
param([Parameter(Mandatory)][string]$Commit)
|
||||
|
||||
# Stream each full history once instead of spawning git for every commit.
|
||||
$patchOutput = & {
|
||||
& git -C $RepoPath log --format=%H --patch --cc --no-ext-diff --binary $Commit
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git log failed while collecting patches reachable from $Commit."
|
||||
}
|
||||
} | & git -C $RepoPath patch-id --stable
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git patch-id failed for history reachable from $Commit."
|
||||
}
|
||||
|
||||
$set = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($line in $patchOutput) {
|
||||
if ([string]$line -notmatch "^([0-9a-fA-F]{40})\s+[0-9a-fA-F]{40}$") {
|
||||
throw "Unexpected git patch-id output: $line"
|
||||
}
|
||||
[void]$set.Add($matches[1])
|
||||
}
|
||||
return ,$set
|
||||
}
|
||||
|
||||
function Test-PromotionMerge {
|
||||
param(
|
||||
[Parameter(Mandatory)]$Record,
|
||||
[Parameter(Mandatory)][string]$OtherTip
|
||||
)
|
||||
|
||||
if ($Record.patchId) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$parents = @(([string](Invoke-Git show -s --format=%P $Record.sha)).Trim() -split "\s+" | Where-Object { $_ })
|
||||
if ($parents.Count -lt 2) {
|
||||
return $false
|
||||
}
|
||||
|
||||
foreach ($parent in $parents[1..($parents.Count - 1)]) {
|
||||
if (-not (Test-Ancestor -Ancestor $parent -Descendant $OtherTip)) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function Resolve-CrossSidePatchIdentities {
|
||||
param(
|
||||
[Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Left,
|
||||
@@ -275,6 +335,32 @@ Resolve-CrossSidePatchIdentities -Left $targetRecords -Right $previousRecords
|
||||
$previousIdentities = New-IdentitySet -Records $previousRecords
|
||||
$targetIdentities = New-IdentitySet -Records $targetRecords
|
||||
|
||||
if ($deltaMode -eq "branch-transition") {
|
||||
$previousHistoryPrIdentities = Get-HistoryPrIdentitySet -Commit $previousSha
|
||||
$targetHistoryPrIdentities = Get-HistoryPrIdentitySet -Commit $targetSha
|
||||
foreach ($identity in $previousHistoryPrIdentities) {
|
||||
[void]$previousIdentities.Add($identity)
|
||||
}
|
||||
foreach ($identity in $targetHistoryPrIdentities) {
|
||||
[void]$targetIdentities.Add($identity)
|
||||
}
|
||||
|
||||
$previousHistoryPatchIds = Get-HistoryPatchIdSet -Commit $previousSha
|
||||
$targetHistoryPatchIds = Get-HistoryPatchIdSet -Commit $targetSha
|
||||
foreach ($record in $previousRecords) {
|
||||
if (($record.patchId -and $targetHistoryPatchIds.Contains([string]$record.patchId)) -or
|
||||
(Test-PromotionMerge -Record $record -OtherTip $targetSha)) {
|
||||
[void]$targetIdentities.Add([string]$record.identity)
|
||||
}
|
||||
}
|
||||
foreach ($record in $targetRecords) {
|
||||
if (($record.patchId -and $previousHistoryPatchIds.Contains([string]$record.patchId)) -or
|
||||
(Test-PromotionMerge -Record $record -OtherTip $previousSha)) {
|
||||
[void]$previousIdentities.Add([string]$record.identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$addedPrs = Get-PrOutput -Records $targetRecords -OtherSide $previousIdentities
|
||||
$removedPrs = if ($deltaMode -eq "branch-transition") {
|
||||
Get-PrOutput -Records $previousRecords -OtherSide $targetIdentities
|
||||
|
||||
@@ -37,6 +37,7 @@ function Add-TestCommit {
|
||||
[Parameter(Mandatory)][string]$Message
|
||||
)
|
||||
|
||||
Invoke-TestGit -Repository $Repository config core.safecrlf false | Out-Null
|
||||
Set-Content -LiteralPath (Join-Path $Repository $FileName) -Value $Content
|
||||
Invoke-TestGit -Repository $Repository add $FileName | Out-Null
|
||||
Invoke-TestGit -Repository $Repository commit -q -m $Message | Out-Null
|
||||
@@ -466,6 +467,165 @@ Describe "preview release delta" {
|
||||
($result.addedPrNumbers -join ",") | Should Be "102,104"
|
||||
($result.removedPrNumbers -join ",") | Should Be "103"
|
||||
}
|
||||
|
||||
It "does not remove a PR already present before the merge base on the target branch" {
|
||||
$repo = Join-Path $TestDrive "full-history-identity"
|
||||
New-Item -ItemType Directory -Path $repo | Out-Null
|
||||
Invoke-TestGit -Repository $repo init -q | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.email "test@example.com" | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.name "Test User" | Out-Null
|
||||
$root = Add-TestCommit -Repository $repo -FileName "root.txt" -Content "root" -Message "Root"
|
||||
Invoke-TestGit -Repository $repo checkout -q -b main | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "feature.txt" -Content "main" -Message "Shared feature (#201)" | Out-Null
|
||||
$mainTip = Add-TestCommit -Repository $repo -FileName "anchor.txt" -Content "anchor" -Message "Main anchor (#202)"
|
||||
|
||||
Invoke-TestGit -Repository $repo checkout -q -b stable $mainTip | Out-Null
|
||||
$previous = Add-TestCommit -Repository $repo -FileName "stable.txt" -Content "stable adaptation" -Message "Shared feature (#201)"
|
||||
Invoke-TestGit -Repository $repo checkout -q main | Out-Null
|
||||
$target = Add-TestCommit -Repository $repo -FileName "added.txt" -Content "added" -Message "Main addition (#203)"
|
||||
$output = Join-Path $TestDrive "full-history-output"
|
||||
|
||||
$result = & (Join-Path $scripts "get-preview-release-delta.ps1") `
|
||||
-PreviousCommit $previous `
|
||||
-TargetCommit $target `
|
||||
-RepoPath $repo `
|
||||
-OutputDirectory $output `
|
||||
-NoGitHubLookup
|
||||
|
||||
($result.addedPrNumbers -join ",") | Should Be "203"
|
||||
$result.removedPrNumbers.Count | Should Be 0
|
||||
}
|
||||
|
||||
It "matches unattributed patches before the merge base in either direction" -TestCases @(
|
||||
@{ Reverse = $false; AddedCount = 1; RemovedCount = 0 }
|
||||
@{ Reverse = $true; AddedCount = 0; RemovedCount = 1 }
|
||||
) {
|
||||
param($Reverse, $AddedCount, $RemovedCount)
|
||||
|
||||
$repo = Join-Path $TestDrive "full-history-patch-$Reverse"
|
||||
New-Item -ItemType Directory -Path $repo | Out-Null
|
||||
Invoke-TestGit -Repository $repo init -q | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.email "test@example.com" | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.name "Test User" | Out-Null
|
||||
$original = Add-TestCommit -Repository $repo -FileName "shared.txt" -Content "shared" -Message "Original shared change"
|
||||
Invoke-TestGit -Repository $repo revert --no-edit $original | Out-Null
|
||||
$anchor = Add-TestCommit -Repository $repo -FileName "anchor.txt" -Content "anchor" -Message "Shared anchor"
|
||||
Invoke-TestGit -Repository $repo checkout -q -b stable | Out-Null
|
||||
Invoke-TestGit -Repository $repo cherry-pick --no-commit $original | Out-Null
|
||||
Invoke-TestGit -Repository $repo commit -q -m "Reapplied shared change" | Out-Null
|
||||
$previous = ([string](Invoke-TestGit -Repository $repo rev-parse HEAD)).Trim()
|
||||
Invoke-TestGit -Repository $repo checkout -q -b main $anchor | Out-Null
|
||||
$unique = Add-TestCommit -Repository $repo -FileName "unique.txt" -Content "unique" -Message "Unique main change"
|
||||
$target = $unique
|
||||
if ($Reverse) {
|
||||
$target = $previous
|
||||
$previous = $unique
|
||||
}
|
||||
$output = Join-Path $TestDrive "full-history-patch-output-$Reverse"
|
||||
|
||||
$result = & (Join-Path $scripts "get-preview-release-delta.ps1") `
|
||||
-PreviousCommit $previous `
|
||||
-TargetCommit $target `
|
||||
-RepoPath $repo `
|
||||
-OutputDirectory $output `
|
||||
-NoGitHubLookup
|
||||
|
||||
$result.deltaMode | Should Be "branch-transition"
|
||||
$result.addedPrNumbers.Count | Should Be 0
|
||||
$result.removedPrNumbers.Count | Should Be 0
|
||||
$result.unattributedCommitCount | Should Be $AddedCount
|
||||
$commits = Get-Content -LiteralPath (Join-Path $output "delta-commits.json") -Raw | ConvertFrom-Json
|
||||
$commits.removedUnattributedCommits.Count | Should Be $RemovedCount
|
||||
$added = Get-Content -LiteralPath (Join-Path $output "unattributed-commits.json") -Raw | ConvertFrom-Json
|
||||
$unmatched = @($added) + @($commits.removedUnattributedCommits) | Where-Object { $_ }
|
||||
@($unmatched).Count | Should Be 1
|
||||
$unmatched.sha | Should Be $unique
|
||||
}
|
||||
|
||||
It "retains a PR merge when only its first parent is in the opposite history" {
|
||||
$repo = Join-Path $TestDrive "feature-merge"
|
||||
New-Item -ItemType Directory -Path $repo | Out-Null
|
||||
Invoke-TestGit -Repository $repo init -q | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.email "test@example.com" | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.name "Test User" | Out-Null
|
||||
$root = Add-TestCommit -Repository $repo -FileName "root.txt" -Content "root" -Message "Root"
|
||||
Invoke-TestGit -Repository $repo checkout -q -b feature | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "feature.txt" -Content "feature" -Message "Feature implementation" | Out-Null
|
||||
Invoke-TestGit -Repository $repo checkout -q -b stable $root | Out-Null
|
||||
Invoke-TestGit -Repository $repo merge -q --no-ff feature -m "Merge pull request #401 from feature" | Out-Null
|
||||
$previous = ([string](Invoke-TestGit -Repository $repo rev-parse HEAD)).Trim()
|
||||
Invoke-TestGit -Repository $repo checkout -q -b main $root | Out-Null
|
||||
$target = Add-TestCommit -Repository $repo -FileName "main.txt" -Content "main" -Message "Main addition (#402)"
|
||||
$output = Join-Path $TestDrive "feature-merge-output"
|
||||
|
||||
$result = & (Join-Path $scripts "get-preview-release-delta.ps1") `
|
||||
-PreviousCommit $previous `
|
||||
-TargetCommit $target `
|
||||
-RepoPath $repo `
|
||||
-OutputDirectory $output `
|
||||
-NoGitHubLookup
|
||||
|
||||
($result.addedPrNumbers -join ",") | Should Be "402"
|
||||
($result.removedPrNumbers -join ",") | Should Be "401"
|
||||
}
|
||||
|
||||
It "retains an octopus merge when a merged-in parent is absent from the opposite history" {
|
||||
$repo = Join-Path $TestDrive "octopus-merge"
|
||||
New-Item -ItemType Directory -Path $repo | Out-Null
|
||||
Invoke-TestGit -Repository $repo init -q | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.email "test@example.com" | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.name "Test User" | Out-Null
|
||||
$root = Add-TestCommit -Repository $repo -FileName "root.txt" -Content "root" -Message "Root"
|
||||
Invoke-TestGit -Repository $repo checkout -q -b main | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "shared.txt" -Content "shared" -Message "Shared feature (#501)" | Out-Null
|
||||
Invoke-TestGit -Repository $repo checkout -q -b feature $root | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "feature.txt" -Content "feature" -Message "Feature implementation" | Out-Null
|
||||
Invoke-TestGit -Repository $repo checkout -q -b stable $root | Out-Null
|
||||
Invoke-TestGit -Repository $repo merge -q --no-ff main feature -m "Merge features (#502)" | Out-Null
|
||||
$previous = ([string](Invoke-TestGit -Repository $repo rev-parse HEAD)).Trim()
|
||||
Invoke-TestGit -Repository $repo checkout -q main | Out-Null
|
||||
$target = Add-TestCommit -Repository $repo -FileName "later.txt" -Content "later" -Message "Later main feature (#503)"
|
||||
$output = Join-Path $TestDrive "octopus-merge-output"
|
||||
|
||||
$result = & (Join-Path $scripts "get-preview-release-delta.ps1") `
|
||||
-PreviousCommit $previous `
|
||||
-TargetCommit $target `
|
||||
-RepoPath $repo `
|
||||
-OutputDirectory $output `
|
||||
-NoGitHubLookup
|
||||
|
||||
($result.addedPrNumbers -join ",") | Should Be "503"
|
||||
($result.removedPrNumbers -join ",") | Should Be "502"
|
||||
}
|
||||
|
||||
It "does not report a merge that only promotes the other branch" {
|
||||
$repo = Join-Path $TestDrive "promotion-merge"
|
||||
New-Item -ItemType Directory -Path $repo | Out-Null
|
||||
Invoke-TestGit -Repository $repo init -q | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.email "test@example.com" | Out-Null
|
||||
Invoke-TestGit -Repository $repo config user.name "Test User" | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "root.txt" -Content "root" -Message "Root" | Out-Null
|
||||
Invoke-TestGit -Repository $repo checkout -q -b main | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "feature.txt" -Content "feature" -Message "Main feature (#301)" | Out-Null
|
||||
|
||||
Invoke-TestGit -Repository $repo checkout -q -b stable HEAD~1 | Out-Null
|
||||
Add-TestCommit -Repository $repo -FileName "stable.txt" -Content "stable" -Message "Stable fix (#302)" | Out-Null
|
||||
Invoke-TestGit -Repository $repo merge -q --no-ff main -m "Merge main into stable (#303)" | Out-Null
|
||||
$previous = ([string](Invoke-TestGit -Repository $repo rev-parse HEAD)).Trim()
|
||||
Invoke-TestGit -Repository $repo checkout -q main | Out-Null
|
||||
$target = Add-TestCommit -Repository $repo -FileName "later.txt" -Content "later" -Message "Later main feature (#304)"
|
||||
$output = Join-Path $TestDrive "promotion-output"
|
||||
|
||||
$result = & (Join-Path $scripts "get-preview-release-delta.ps1") `
|
||||
-PreviousCommit $previous `
|
||||
-TargetCommit $target `
|
||||
-RepoPath $repo `
|
||||
-OutputDirectory $output `
|
||||
-NoGitHubLookup
|
||||
|
||||
($result.addedPrNumbers -join ",") | Should Be "304"
|
||||
($result.removedPrNumbers -join ",") | Should Be "302"
|
||||
}
|
||||
}
|
||||
|
||||
Describe "preview release asset links" {
|
||||
|
||||
Reference in new issue
Block a user