microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.1.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/dev-tools/Generate-PrReference.ps1

503lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3#Requires -Version 7.0
4
5<#
6.SYNOPSIS
7Generates the Copilot PR reference XML using git history and diff data.
8
9.DESCRIPTION
10Creates .copilot-tracking/pr/pr-reference.xml relative to the repository root,
11mirroring the behaviour of scripts/pr-ref-gen.sh. Supports excluding markdown
12files from the diff and specifying an alternate base branch for comparisons.
13
14.PARAMETER BaseBranch
15Git branch used as the comparison base. Defaults to "main".
16
17.PARAMETER ExcludeMarkdownDiff
18When supplied, excludes markdown (*.md) files from the diff output.
19#>
20
21[CmdletBinding()]
22param(
23 [Parameter()]
24 [string]$BaseBranch = "main",
25
26 [Parameter()]
27 [switch]$ExcludeMarkdownDiff
28)
29
30$ErrorActionPreference = 'Stop'
31
32Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force
33
34function Test-GitAvailability {
35<#
36.SYNOPSIS
37Verifies the git executable is available.
38.DESCRIPTION
39Throws a terminating error when git can't be resolved from PATH.
40#>
41 [OutputType([void])]
42 param()
43
44 if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
45 throw "Git is required but was not found on PATH."
46 }
47}
48
49function Get-RepositoryRoot {
50<#
51.SYNOPSIS
52Gets the repository root path.
53.DESCRIPTION
54Runs git rev-parse --show-toplevel and throws when the command fails.
55.OUTPUTS
56System.String
57#>
58 [OutputType([string])]
59 param()
60
61 $repoRoot = (& git rev-parse --show-toplevel).Trim()
62 if (-not $repoRoot) {
63 throw "Unable to determine repository root."
64 }
65
66 return $repoRoot
67}
68
69function New-PrDirectory {
70<#
71.SYNOPSIS
72Creates the PR tracking directory when missing.
73.DESCRIPTION
74Ensures .copilot-tracking/pr exists beneath the supplied repository root.
75.PARAMETER RepoRoot
76Absolute path to the git repository root.
77.OUTPUTS
78System.String
79#>
80 [CmdletBinding(SupportsShouldProcess = $true)]
81 [OutputType([string])]
82 param(
83 [Parameter(Mandatory = $true)]
84 [string]$RepoRoot
85 )
86
87 $prDirectory = Join-Path $RepoRoot '.copilot-tracking/pr'
88 if (-not (Test-Path $prDirectory)) {
89 if ($PSCmdlet.ShouldProcess($prDirectory, 'Create PR tracking directory')) {
90 $null = New-Item -ItemType Directory -Path $prDirectory -Force
91 }
92 }
93
94 return $prDirectory
95}
96
97function Resolve-ComparisonReference {
98<#
99.SYNOPSIS
100Resolves the git reference used for comparisons.
101.DESCRIPTION
102Prefers origin/<BaseBranch> when available and falls back to the provided branch.
103.PARAMETER BaseBranch
104Branch name supplied by the caller.
105.OUTPUTS
106PSCustomObject
107#>
108 [OutputType([PSCustomObject])]
109 param(
110 [Parameter(Mandatory = $true)]
111 [string]$BaseBranch
112 )
113
114 $candidates = @()
115 if ($BaseBranch -notlike 'origin/*' -and $BaseBranch -notlike 'refs/*') {
116 $candidates += "origin/$BaseBranch"
117 }
118 $candidates += $BaseBranch
119
120 foreach ($candidate in $candidates) {
121 & git rev-parse --verify $candidate *> $null
122 if ($LASTEXITCODE -eq 0) {
123 $label = if ($candidate -eq $BaseBranch) {
124 $BaseBranch
125 } else {
126 "$BaseBranch (via $candidate)"
127 }
128
129 return [PSCustomObject]@{
130 Ref = $candidate
131 Label = $label
132 }
133 }
134 }
135
136 throw "Branch '$BaseBranch' does not exist or is not accessible."
137}
138
139function Get-ShortCommitHash {
140<#
141.SYNOPSIS
142Retrieves the short commit hash for a ref.
143.DESCRIPTION
144Uses git rev-parse --short to resolve the supplied ref.
145.PARAMETER Ref
146Git reference to resolve.
147.OUTPUTS
148System.String
149#>
150 [OutputType([string])]
151 param(
152 [Parameter(Mandatory = $true)]
153 [string]$Ref
154 )
155
156 $commit = (& git rev-parse --short $Ref).Trim()
157 if ($LASTEXITCODE -ne 0) {
158 throw "Failed to resolve ref '$Ref'."
159 }
160
161 return $commit
162}
163
164function Get-CurrentBranchOrRef {
165<#
166.SYNOPSIS
167Retrieves the current branch name or a fallback reference.
168.DESCRIPTION
169Returns the current branch name when on a branch. In detached HEAD state
170(common in CI environments), falls back to a short commit SHA prefixed with
171'detached@'.
172.OUTPUTS
173System.String
174#>
175 [OutputType([string])]
176 param()
177
178 $branchOutput = & git --no-pager branch --show-current 2>$null
179 if ($branchOutput) {
180 return $branchOutput.Trim()
181 }
182
183 # Detached HEAD - fall back to short SHA
184 $sha = (& git rev-parse --short HEAD 2>$null)
185 if ($LASTEXITCODE -eq 0 -and $sha) {
186 return "detached@$($sha.Trim())"
187 }
188
189 return 'unknown'
190}
191
192function Get-CommitEntry {
193<#
194.SYNOPSIS
195Collects formatted commit metadata.
196.DESCRIPTION
197Runs git log to gather commit entries relative to the supplied comparison ref.
198.PARAMETER ComparisonRef
199Git reference that acts as the diff base.
200.OUTPUTS
201System.String[]
202#>
203 [OutputType([string[]])]
204 param(
205 [Parameter(Mandatory = $true)]
206 [string]$ComparisonRef
207 )
208
209 $logArgs = @(
210 '--no-pager',
211 'log',
212 '--pretty=format:<commit hash="%h" date="%cd"><message><subject><![CDATA[%s]]></subject><body><![CDATA[%b]]></body></message></commit>',
213 '--date=short',
214 "${ComparisonRef}..HEAD"
215 )
216
217 $entries = & git @logArgs
218 if ($LASTEXITCODE -ne 0) {
219 throw "Failed to retrieve commit history."
220 }
221
222 return $entries
223}
224
225function Get-CommitCount {
226<#
227.SYNOPSIS
228Counts commits between HEAD and the comparison ref.
229.DESCRIPTION
230Executes git rev-list --count to measure branch divergence.
231.PARAMETER ComparisonRef
232Git reference that acts as the diff base.
233.OUTPUTS
234System.Int32
235#>
236 [OutputType([int])]
237 param(
238 [Parameter(Mandatory = $true)]
239 [string]$ComparisonRef
240 )
241
242 $countText = (& git --no-pager rev-list --count "${ComparisonRef}..HEAD").Trim()
243 if ($LASTEXITCODE -ne 0) {
244 throw "Failed to count commits."
245 }
246
247 if (-not $countText) {
248 return 0
249 }
250
251 return [int]$countText
252}
253
254function Get-DiffOutput {
255<#
256.SYNOPSIS
257Builds the git diff output for the comparison ref.
258.DESCRIPTION
259Runs git diff against the comparison ref with optional markdown exclusion.
260.PARAMETER ComparisonRef
261Git reference that acts as the diff base.
262.PARAMETER ExcludeMarkdownDiff
263Switch to omit markdown files from the diff.
264.OUTPUTS
265System.String[]
266#>
267 [OutputType([string[]])]
268 param(
269 [Parameter(Mandatory = $true)]
270 [string]$ComparisonRef,
271
272 [Parameter()]
273 [switch]$ExcludeMarkdownDiff
274 )
275
276 $diffArgs = @('--no-pager', 'diff', $ComparisonRef)
277 if ($ExcludeMarkdownDiff) {
278 $diffArgs += @('--', ':!*.md')
279 }
280
281 $diffOutput = & git @diffArgs
282 if ($LASTEXITCODE -ne 0) {
283 throw "Failed to retrieve diff output."
284 }
285
286 return $diffOutput
287}
288
289function Get-DiffSummary {
290<#
291.SYNOPSIS
292Summarizes the diff for quick reporting.
293.DESCRIPTION
294Uses git diff --shortstat against the comparison ref.
295.PARAMETER ComparisonRef
296Git reference that acts as the diff base.
297.PARAMETER ExcludeMarkdownDiff
298Switch to omit markdown files from the summary.
299.OUTPUTS
300System.String
301#>
302 [OutputType([string])]
303 param(
304 [Parameter(Mandatory = $true)]
305 [string]$ComparisonRef,
306
307 [Parameter()]
308 [switch]$ExcludeMarkdownDiff
309 )
310
311 $diffStatArgs = @('--no-pager', 'diff', '--shortstat', $ComparisonRef)
312 if ($ExcludeMarkdownDiff) {
313 $diffStatArgs += @('--', ':!*.md')
314 }
315
316 $summary = & git @diffStatArgs
317 if ($LASTEXITCODE -ne 0) {
318 throw "Failed to summarize diff output."
319 }
320
321 if (-not $summary) {
322 return '0 files changed'
323 }
324
325 return $summary
326}
327
328function Get-PrXmlContent {
329<#
330.SYNOPSIS
331Constructs the PR reference XML document.
332.DESCRIPTION
333Creates XML containing the current branch, base branch, commits, and diff.
334.PARAMETER CurrentBranch
335Name of the active git branch.
336.PARAMETER BaseBranch
337Branch used as the base reference.
338.PARAMETER CommitEntries
339Formatted commit entries produced by Get-CommitEntry.
340.PARAMETER DiffOutput
341Diff lines produced by Get-DiffOutput.
342.OUTPUTS
343System.String
344#>
345 [OutputType([string])]
346 param(
347 [Parameter(Mandatory = $true)]
348 [string]$CurrentBranch,
349
350 [Parameter(Mandatory = $true)]
351 [string]$BaseBranch,
352
353 [Parameter()]
354 [string[]]$CommitEntries,
355
356 [Parameter()]
357 [string[]]$DiffOutput
358 )
359
360 $commitBlock = if ($CommitEntries) {
361 ($CommitEntries | ForEach-Object { " $_" }) -join [Environment]::NewLine
362 } else {
363 ""
364 }
365
366 $diffBlock = if ($DiffOutput) {
367 ($DiffOutput | ForEach-Object { " $_" }) -join [Environment]::NewLine
368 } else {
369 ""
370 }
371
372 return @"
373<commit_history>
374 <current_branch>
375 $CurrentBranch
376 </current_branch>
377
378 <base_branch>
379 $BaseBranch
380 </base_branch>
381
382 <commits>
383$commitBlock
384 </commits>
385
386 <full_diff>
387$diffBlock
388 </full_diff>
389</commit_history>
390"@
391}
392
393function Get-LineImpact {
394<#
395.SYNOPSIS
396Calculates total line impact from a diff summary.
397.DESCRIPTION
398Parses insertion and deletion counts from git diff --shortstat output.
399.PARAMETER DiffSummary
400Short diff summary text.
401.OUTPUTS
402System.Int32
403#>
404 [OutputType([int])]
405 param(
406 [Parameter(Mandatory = $true)]
407 [string]$DiffSummary
408 )
409
410 $lineImpact = 0
411 if ($DiffSummary -match '(\d+) insertions') {
412 $lineImpact += [int]$matches[1]
413 }
414 if ($DiffSummary -match '(\d+) deletions') {
415 $lineImpact += [int]$matches[1]
416 }
417
418 return $lineImpact
419}
420
421function Invoke-PrReferenceGeneration {
422<#
423.SYNOPSIS
424Generates the pr-reference.xml file.
425.DESCRIPTION
426Coordinates git queries, XML creation, and console reporting for Copilot usage.
427.PARAMETER BaseBranch
428Branch used as the comparison base.
429.PARAMETER ExcludeMarkdownDiff
430Switch to omit markdown files from the diff and summary.
431.OUTPUTS
432System.IO.FileInfo
433#>
434 [OutputType([System.IO.FileInfo])]
435 param(
436 [Parameter(Mandatory = $true)]
437 [string]$BaseBranch,
438
439 [Parameter()]
440 [switch]$ExcludeMarkdownDiff
441 )
442
443 Test-GitAvailability
444
445 $repoRoot = Get-RepositoryRoot
446 $prDirectory = New-PrDirectory -RepoRoot $repoRoot
447 $prReferencePath = Join-Path $prDirectory 'pr-reference.xml'
448
449 $diffSummary = '0 files changed'
450 $commitCount = 0
451 $comparisonInfo = $null
452 $baseCommit = ''
453
454 Push-Location $repoRoot
455 try {
456 $currentBranch = Get-CurrentBranchOrRef
457 $comparisonInfo = Resolve-ComparisonReference -BaseBranch $BaseBranch
458 $baseCommit = Get-ShortCommitHash -Ref $comparisonInfo.Ref
459 $commitEntries = Get-CommitEntry -ComparisonRef $comparisonInfo.Ref
460 $commitCount = Get-CommitCount -ComparisonRef $comparisonInfo.Ref
461 $diffOutput = Get-DiffOutput -ComparisonRef $comparisonInfo.Ref -ExcludeMarkdownDiff:$ExcludeMarkdownDiff
462 $diffSummary = Get-DiffSummary -ComparisonRef $comparisonInfo.Ref -ExcludeMarkdownDiff:$ExcludeMarkdownDiff
463
464 $xmlContent = Get-PrXmlContent -CurrentBranch $currentBranch -BaseBranch $BaseBranch -CommitEntries $commitEntries -DiffOutput $diffOutput
465 $xmlContent | Set-Content -LiteralPath $prReferencePath
466 }
467 finally {
468 Pop-Location
469 }
470
471 $lineCount = (Get-Content -LiteralPath $prReferencePath).Count
472 $lineImpact = Get-LineImpact -DiffSummary $diffSummary
473
474 Write-Host "Created $prReferencePath"
475 if ($ExcludeMarkdownDiff) {
476 Write-Host 'Note: Markdown files were excluded from diff output'
477 }
478 Write-Host "Lines: $lineCount"
479 Write-Host "Base branch: $($comparisonInfo.Label) (@ $baseCommit)"
480 Write-Host "Commits compared: $commitCount"
481 Write-Host "Diff summary: $diffSummary"
482
483 if ($lineImpact -gt 1000) {
484 Write-Host 'Large diff detected. Rebase onto the intended base branch or narrow your changes if this scope is unexpected.'
485 }
486
487 return Get-Item -LiteralPath $prReferencePath
488}
489
490#region Main Execution
491try {
492 # Execute only when run directly, not when dot-sourced for testing
493 if ($MyInvocation.InvocationName -ne '.') {
494 Invoke-PrReferenceGeneration -BaseBranch $BaseBranch -ExcludeMarkdownDiff:$ExcludeMarkdownDiff | Out-Null
495 exit 0
496 }
497}
498catch {
499 Write-Error "Generate PR Reference failed: $($_.Exception.Message)"
500 Write-CIAnnotation -Message $_.Exception.Message -Level Error
501 exit 1
502}
503#endregion