Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/scripts/Fix-MilestoneDrift.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,60 @@ Describe 'Resolve-MergedAfterCutoff' {
}
}

Describe 'Get-PrNumbersFromGitLog — terminal squash PR suffix' {
It 'parses an ordinary terminal PR suffix' {
$result = @(Get-PrNumbersFromGitLog @('abc1234 Fix a layout regression (#35031)'))
$result.Count | Should -Be 1
$result[0] | Should -Be 35031
}

It 'returns only the terminal PR when an issue reference appears earlier' {
$result = @(Get-PrNumbersFromGitLog @(
'abc1234 Fix Shell.Items.Clear() memory leak (#34898) (#35031)'
))
$result.Count | Should -Be 1
$result[0] | Should -Be 35031
}

It 'returns only the terminal PR after multiple earlier parenthesized references' {
$result = @(Get-PrNumbersFromGitLog @(
'abc1234 Reconcile reports (#100) and (#200) before the fix (#34719)'
))
$result.Count | Should -Be 1
$result[0] | Should -Be 34719
}

It 'ignores a subject with no terminal PR suffix' {
@(Get-PrNumbersFromGitLog @(
'abc1234 Follow up on (#35031) without a squash suffix'
)).Count | Should -Be 0
}

It 'ignores malformed or non-numeric terminal references "<Reference>"' -ForEach @(
@{ Reference = '(#)' }
@{ Reference = '(#abc)' }
@{ Reference = '(#12x)' }
@{ Reference = '(#999999999999999999999999999999)' }
) {
@(Get-PrNumbersFromGitLog @("abc1234 Malformed reference $Reference")).Count | Should -Be 0
}

It 'tolerates trailing whitespace after the terminal PR suffix' {
$result = @(Get-PrNumbersFromGitLog @('abc1234 Fix a layout regression (#35031) '))
$result.Count | Should -Be 1
$result[0] | Should -Be 35031
}

It 'deduplicates repeated PR subjects and sorts the result' {
$result = @(Get-PrNumbersFromGitLog @(
'abc1234 First appearance (#35031)',
'def5678 Another PR (#34719)',
'fed9876 Cherry-pick the first PR (#35031)'
))
($result -join ',') | Should -Be '34719,35031'
}
}

Describe 'Get-PrInfo — merged-after cutoff enforcement' {
BeforeAll {
# Build a GitHub-pulls-API-shaped object (ConvertFrom-Json style) for the mock.
Expand Down Expand Up @@ -1616,6 +1670,46 @@ Describe 'Get-OnBranchShaFromLog — grep fallback subject precision' {
}
}

# ---------------------------------------------------------------------------
# Git-log PR parsing integration block (no mocks, no GitHub).
# ---------------------------------------------------------------------------
Describe 'Git-log PR parsing paths (unmocked)' -Skip:(-not (Get-Command git -ErrorAction SilentlyContinue)) {
BeforeAll {
$script:prLogTmp = Join-Path ([IO.Path]::GetTempPath()) "prlogtest-$(New-Guid)"
New-Item -ItemType Directory -Path $script:prLogTmp -Force | Out-Null

git -C $script:prLogTmp init -q
git -C $script:prLogTmp config user.email 'milestone-test@example.invalid'
git -C $script:prLogTmp config user.name 'Milestone Test'
git -C $script:prLogTmp config commit.gpgsign false

git -C $script:prLogTmp commit -q --allow-empty -m 'Base change (#100)'
git -C $script:prLogTmp tag 'from'
git -C $script:prLogTmp commit -q --allow-empty -m 'Fix Shell.Items.Clear() memory leak (#34898) (#35031)'
git -C $script:prLogTmp commit -q --allow-empty -m 'Fix VisualStateGroups (#50) (#34716) (#34719)'
git -C $script:prLogTmp commit -q --allow-empty -m 'Subject mentions (#99999) but has no terminal PR suffix'
git -C $script:prLogTmp commit -q --allow-empty -m 'Malformed suffix (#not-a-pr)'
git -C $script:prLogTmp commit -q --allow-empty -m 'Duplicate cherry-pick (#35031)'
git -C $script:prLogTmp tag 'to'
}

AfterAll {
if ($script:prLogTmp -and (Test-Path $script:prLogTmp)) {
Remove-Item -Recurse -Force $script:prLogTmp -ErrorAction SilentlyContinue
}
}

It 'Get-PrNumbersBetweenTags returns only terminal PR suffixes and deduplicates them' {
$result = @(Get-PrNumbersBetweenTags 'from' 'to' $script:prLogTmp)
($result -join ',') | Should -Be '34719,35031'
}

It 'Get-PrNumbersReachableFromTag uses the same terminal-suffix behavior' {
$result = @(Get-PrNumbersReachableFromTag 'to' $script:prLogTmp)
($result -join ',') | Should -Be '100,34719,35031'
}
}

# ---------------------------------------------------------------------------
# Git-backed integration block (no mocks, no GitHub).
#
Expand Down
30 changes: 17 additions & 13 deletions .github/scripts/Fix-MilestoneDrift.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -239,29 +239,33 @@ function Get-OnBranchShaFromLog([string[]]$LogLines, [int]$PrNum) {
return $null
}

function Get-PrNumbersBetweenTags([string]$TagFrom, [string]$TagTo, [string]$Repo) {
$output = git -C $Repo --no-pager log --oneline "$TagFrom..$TagTo" 2>&1
if ($LASTEXITCODE -ne 0) { throw "git log failed: $output" }
function Get-PrNumbersFromGitLog([string[]]$LogLines) {
<# GitHub squash and merge commit subjects end with the merged PR token.
Earlier parenthesized references may be linked issues and must not be
treated as PRs. #>
$prs = [System.Collections.Generic.HashSet[int]]::new()
foreach ($line in ($output -split "`n")) {
foreach ($m in [regex]::Matches($line, '\(#(\d+)\)')) {
[void]$prs.Add([int]$m.Groups[1].Value)
foreach ($line in $LogLines) {
if ($line -match '\(#(\d+)\)\s*$') {
$prNumber = 0
if ([int]::TryParse($Matches[1], [ref]$prNumber)) {
[void]$prs.Add($prNumber)
}
}
}
return ($prs | Sort-Object)
}

function Get-PrNumbersBetweenTags([string]$TagFrom, [string]$TagTo, [string]$Repo) {
$output = git -C $Repo --no-pager log --oneline "$TagFrom..$TagTo" 2>&1
if ($LASTEXITCODE -ne 0) { throw "git log failed: $output" }
return Get-PrNumbersFromGitLog ($output -split "`n")
}

function Get-PrNumbersReachableFromTag([string]$TagName, [string]$Repo) {
<# Returns PR numbers reachable from a tag (all commits up to and including the tag). #>
$output = git -C $Repo --no-pager log --oneline $TagName 2>&1
if ($LASTEXITCODE -ne 0) { throw "git log failed: $output" }
$prs = [System.Collections.Generic.HashSet[int]]::new()
foreach ($line in ($output -split "`n")) {
foreach ($m in [regex]::Matches($line, '\(#(\d+)\)')) {
[void]$prs.Add([int]$m.Groups[1].Value)
}
}
return ($prs | Sort-Object)
return Get-PrNumbersFromGitLog ($output -split "`n")
}

function Find-TagContainingPr([int]$PrNum, [string]$Repo, [int]$Major) {
Expand Down
Loading