scripts/Resolve-CommonMergeConflicts.ps1
|
#Requires -Version 7.4 <# .SYNOPSIS Auto-resolves common Git merge-conflict patterns in additive files (changelogs, docs, manifest entries) used by the `pr-auto-rebase.yml` workflow. .DESCRIPTION Reads a file with conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and applies a "union" merge strategy appropriate for additive content: - Changelog Concatenates both sides' entries inside each conflict block and dedupes by trimmed content. Both [Unreleased] additions survive. - Manifest Same union merge, then validates the resulting file parses as JSON. Aborts (throws) when both sides modified the same keys and the merged document is invalid JSON. - DocAddition Same union merge for README / docs/ where both sides added new sections. The script never touches lines outside conflict blocks. Throws when the conflict block is malformed, no markers are found, or post-merge validation fails. Pure PowerShell; no external dependencies. .PARAMETER Path File with conflict markers. Modified in place on success. .PARAMETER Strategy One of: Changelog, Manifest, DocAddition. .OUTPUTS Hashtable with Resolved (bool), BlockCount (int), Path (string). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [ValidateSet('Changelog', 'Manifest', 'DocAddition')] [string] $Strategy ) $ErrorActionPreference = 'Stop' function Get-ConflictBlock { param([string[]] $Lines) $blocks = @() $i = 0 while ($i -lt $Lines.Count) { if ($Lines[$i] -match '^<<<<<<<') { $start = $i $sep = -1 $end = -1 for ($j = $i + 1; $j -lt $Lines.Count; $j++) { if ($sep -lt 0 -and $Lines[$j] -match '^=======\s*$') { $sep = $j } elseif ($Lines[$j] -match '^>>>>>>>') { $end = $j break } } if ($sep -lt 0 -or $end -lt 0) { throw "Malformed conflict block starting at line $($start + 1) in input" } $oursRange = if ($sep - 1 -ge $start + 1) { $Lines[($start + 1)..($sep - 1)] } else { @() } $theirsRange = if ($end - 1 -ge $sep + 1) { $Lines[($sep + 1)..($end - 1)] } else { @() } $blocks += [pscustomobject]@{ Start = $start Sep = $sep End = $end Ours = @($oursRange) Theirs = @($theirsRange) } $i = $end + 1 } else { $i++ } } return , $blocks } function Merge-ConflictBlocksUnion { param( [string[]] $Lines, [object[]] $Blocks ) $out = New-Object 'System.Collections.Generic.List[string]' $cursor = 0 foreach ($b in $Blocks) { for ($k = $cursor; $k -lt $b.Start; $k++) { $out.Add($Lines[$k]) | Out-Null } $seen = New-Object 'System.Collections.Generic.HashSet[string]' foreach ($l in $b.Ours) { if ($seen.Add($l.Trim())) { $out.Add($l) | Out-Null } } foreach ($l in $b.Theirs) { if ($seen.Add($l.Trim())) { $out.Add($l) | Out-Null } } $cursor = $b.End + 1 } for ($k = $cursor; $k -lt $Lines.Count; $k++) { $out.Add($Lines[$k]) | Out-Null } return , $out.ToArray() } function Resolve-FileWithUnion { param( [string] $FilePath, [scriptblock] $Validator ) if (-not (Test-Path -LiteralPath $FilePath)) { throw "File not found: $FilePath" } $raw = Get-Content -LiteralPath $FilePath -Raw if ([string]::IsNullOrEmpty($raw)) { throw "File is empty: $FilePath" } $eol = if ($raw -match "`r`n") { "`r`n" } else { "`n" } $hadTrailingNewline = $raw.EndsWith("`n") $lines = $raw -split "`r?`n" if ($hadTrailingNewline -and $lines[-1] -eq '') { $lines = $lines[0..($lines.Count - 2)] } $blocks = Get-ConflictBlock -Lines $lines if ($blocks.Count -eq 0) { throw "No conflict markers found in $FilePath" } $merged = Merge-ConflictBlocksUnion -Lines $lines -Blocks $blocks $content = ($merged -join $eol) if ($hadTrailingNewline) { $content += $eol } if ($Validator) { $err = & $Validator $content if ($err) { throw "Auto-resolved content failed validation for ${FilePath}: $err" } } Set-Content -LiteralPath $FilePath -Value $content -NoNewline return @{ Resolved = $true; BlockCount = $blocks.Count; Path = $FilePath } } function Resolve-ChangelogConflict { param([string] $Path) return Resolve-FileWithUnion -FilePath $Path } function Resolve-DocAdditionConflict { param([string] $Path) return Resolve-FileWithUnion -FilePath $Path } function Resolve-ManifestConflict { param([string] $Path) return Resolve-FileWithUnion -FilePath $Path -Validator { param($content) try { $null = $content | ConvertFrom-Json -ErrorAction Stop return $null } catch { return $_.Exception.Message } } } if ($MyInvocation.InvocationName -ne '.') { switch ($Strategy) { 'Changelog' { Resolve-ChangelogConflict -Path $Path } 'Manifest' { Resolve-ManifestConflict -Path $Path } 'DocAddition' { Resolve-DocAdditionConflict -Path $Path } } } |