microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/886-python-lint-fix

Branches

Tags

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

Clone

HTTPS

Download ZIP

.github/skills/shared/pr-reference/scripts/generate.ps1

610lines · 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 a pr-reference.xml file (default: .copilot-tracking/pr/pr-reference.xml)
11relative to the repository root, mirroring the behaviour of generate.sh. Supports
12excluding markdown files from the diff and specifying an alternate base branch
13for comparisons.
14
15.PARAMETER BaseBranch
16Git branch used as the comparison base. Defaults to "main".
17Use "auto" to detect the remote default branch.
18
19.PARAMETER MergeBase
20When supplied, uses git merge-base for three-way comparison instead of
21direct diff against the base branch.
22
23.PARAMETER ExcludeMarkdownDiff
24When supplied, excludes markdown (*.md) files from the diff output.
25
26.PARAMETER ExcludeExt
27Comma-separated or array of file extensions to exclude from the diff
28(e.g., 'yml,yaml,json'). Leading dots are stripped automatically.
29
30.PARAMETER ExcludePath
31Comma-separated or array of path prefixes to exclude from the diff
32(e.g., 'docs/,.github/skills/').
33
34.PARAMETER OutputPath
35Custom output file path. When empty, defaults to
36.copilot-tracking/pr/pr-reference.xml relative to the repository root.
37#>
38
39[CmdletBinding()]
40param(
41 [Parameter()]
42 [string]$BaseBranch = "main",
43
44 [Parameter()]
45 [switch]$MergeBase,
46
47 [Parameter()]
48 [switch]$ExcludeMarkdownDiff,
49
50 [Parameter()]
51 [string[]]$ExcludeExt = @(),
52
53 [Parameter()]
54 [string[]]$ExcludePath = @(),
55
56 [Parameter()]
57 [string]$OutputPath = ""
58)
59
60$ErrorActionPreference = 'Stop'
61
62Import-Module (Join-Path $PSScriptRoot 'shared.psm1') -Force
63
64function Test-GitAvailability {
65<#
66.SYNOPSIS
67Verifies the git executable is available.
68.DESCRIPTION
69Throws a terminating error when git can't be resolved from PATH.
70#>
71 [OutputType([void])]
72 param()
73
74 if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
75 throw "Git is required but was not found on PATH."
76 }
77}
78
79function New-PrDirectory {
80<#
81.SYNOPSIS
82Creates the parent directory for the output file when missing.
83.DESCRIPTION
84Ensures the parent directory of the specified path exists.
85.PARAMETER OutputFilePath
86Absolute path to the output file whose parent directory should be created.
87.OUTPUTS
88System.String
89#>
90 [CmdletBinding(SupportsShouldProcess = $true)]
91 [OutputType([string])]
92 param(
93 [Parameter(Mandatory = $true)]
94 [string]$OutputFilePath
95 )
96
97 $parentDir = Split-Path -Parent $OutputFilePath
98 if (-not (Test-Path $parentDir)) {
99 if ($PSCmdlet.ShouldProcess($parentDir, 'Create output directory')) {
100 $null = New-Item -ItemType Directory -Path $parentDir -Force
101 }
102 }
103
104 return $parentDir
105}
106
107function Resolve-ComparisonReference {
108<#
109.SYNOPSIS
110Resolves the git reference used for comparisons.
111.DESCRIPTION
112Prefers origin/<BaseBranch> when available and falls back to the provided branch.
113When UseMergeBase is set, resolves the merge-base between HEAD and the branch.
114.PARAMETER BaseBranch
115Branch name supplied by the caller.
116.PARAMETER UseMergeBase
117When set, resolves the merge-base commit instead of using the branch directly.
118.OUTPUTS
119PSCustomObject
120#>
121 [OutputType([PSCustomObject])]
122 param(
123 [Parameter(Mandatory = $true)]
124 [string]$BaseBranch,
125
126 [Parameter()]
127 [switch]$UseMergeBase
128 )
129
130 $candidates = @()
131 if ($BaseBranch -notlike 'origin/*' -and $BaseBranch -notlike 'refs/*') {
132 $candidates += "origin/$BaseBranch"
133 }
134 $candidates += $BaseBranch
135
136 $resolvedRef = $null
137 $resolvedCandidate = $null
138 foreach ($candidate in $candidates) {
139 & git rev-parse --verify $candidate *> $null
140 if ($LASTEXITCODE -eq 0) {
141 $resolvedRef = $candidate
142 $resolvedCandidate = $candidate
143 break
144 }
145 }
146
147 if (-not $resolvedRef) {
148 throw "Branch '$BaseBranch' does not exist or is not accessible."
149 }
150
151 if ($UseMergeBase) {
152 $mb = (& git merge-base HEAD $resolvedRef 2>$null)
153 if ($LASTEXITCODE -eq 0 -and $mb) {
154 $resolvedRef = $mb.Trim()
155 } else {
156 Write-Warning "merge-base resolution failed, using direct comparison"
157 }
158 }
159
160 $label = if ($resolvedCandidate -eq $BaseBranch) {
161 $BaseBranch
162 } else {
163 "$BaseBranch (via $resolvedCandidate)"
164 }
165
166 return [PSCustomObject]@{
167 Ref = $resolvedRef
168 Label = $label
169 }
170}
171
172function Get-ShortCommitHash {
173<#
174.SYNOPSIS
175Retrieves the short commit hash for a ref.
176.DESCRIPTION
177Uses git rev-parse --short to resolve the supplied ref.
178.PARAMETER Ref
179Git reference to resolve.
180.OUTPUTS
181System.String
182#>
183 [OutputType([string])]
184 param(
185 [Parameter(Mandatory = $true)]
186 [string]$Ref
187 )
188
189 $commit = (& git rev-parse --short $Ref).Trim()
190 if ($LASTEXITCODE -ne 0) {
191 throw "Failed to resolve ref '$Ref'."
192 }
193
194 return $commit
195}
196
197function Get-CurrentBranchOrRef {
198<#
199.SYNOPSIS
200Retrieves the current branch name or a fallback reference.
201.DESCRIPTION
202Returns the current branch name when on a branch. In detached HEAD state
203(common in CI environments), falls back to a short commit SHA prefixed with
204'detached@'.
205.OUTPUTS
206System.String
207#>
208 [OutputType([string])]
209 param()
210
211 $branchOutput = & git --no-pager branch --show-current 2>$null
212 if ($branchOutput) {
213 return $branchOutput.Trim()
214 }
215
216 # Detached HEAD - fall back to short SHA
217 $sha = (& git rev-parse --short HEAD 2>$null)
218 if ($LASTEXITCODE -eq 0 -and $sha) {
219 return "detached@$($sha.Trim())"
220 }
221
222 return 'unknown'
223}
224
225function Get-CommitEntry {
226<#
227.SYNOPSIS
228Collects formatted commit metadata.
229.DESCRIPTION
230Runs git log to gather commit entries relative to the supplied comparison ref.
231.PARAMETER ComparisonRef
232Git reference that acts as the diff base.
233.OUTPUTS
234System.String[]
235#>
236 [OutputType([string[]])]
237 param(
238 [Parameter(Mandatory = $true)]
239 [string]$ComparisonRef
240 )
241
242 $logArgs = @(
243 '--no-pager',
244 'log',
245 '--pretty=format:<commit hash="%h" date="%cd"><message><subject><![CDATA[%s]]></subject><body><![CDATA[%b]]></body></message></commit>',
246 '--date=short',
247 "${ComparisonRef}..HEAD"
248 )
249
250 $entries = & git @logArgs
251 if ($LASTEXITCODE -ne 0) {
252 throw "Failed to retrieve commit history."
253 }
254
255 return $entries
256}
257
258function Get-CommitCount {
259<#
260.SYNOPSIS
261Counts commits between HEAD and the comparison ref.
262.DESCRIPTION
263Executes git rev-list --count to measure branch divergence.
264.PARAMETER ComparisonRef
265Git reference that acts as the diff base.
266.OUTPUTS
267System.Int32
268#>
269 [OutputType([int])]
270 param(
271 [Parameter(Mandatory = $true)]
272 [string]$ComparisonRef
273 )
274
275 $countText = (& git --no-pager rev-list --count "${ComparisonRef}..HEAD").Trim()
276 if ($LASTEXITCODE -ne 0) {
277 throw "Failed to count commits."
278 }
279
280 if (-not $countText) {
281 return 0
282 }
283
284 return [int]$countText
285}
286
287function Get-DiffOutput {
288<#
289.SYNOPSIS
290Builds the git diff output for the comparison ref.
291.DESCRIPTION
292Runs git diff against the comparison ref with optional file exclusions.
293.PARAMETER ComparisonRef
294Git reference that acts as the diff base.
295.PARAMETER ExcludeMarkdownDiff
296Switch to omit markdown files from the diff.
297.PARAMETER ExcludeExt
298File extensions to exclude from the diff.
299.PARAMETER ExcludePath
300Path prefixes to exclude from the diff.
301.OUTPUTS
302System.String[]
303#>
304 [OutputType([string[]])]
305 param(
306 [Parameter(Mandatory = $true)]
307 [string]$ComparisonRef,
308
309 [Parameter()]
310 [switch]$ExcludeMarkdownDiff,
311
312 [Parameter()]
313 [string[]]$ExcludeExt = @(),
314
315 [Parameter()]
316 [string[]]$ExcludePath = @()
317 )
318
319 $diffArgs = @('--no-pager', 'diff', $ComparisonRef)
320
321 $pathspecs = @()
322 if ($ExcludeMarkdownDiff) {
323 $pathspecs += ':!*.md'
324 }
325 $pathspecs += Build-PathspecExclusions -Extensions $ExcludeExt -Paths $ExcludePath
326 if ($pathspecs.Count -gt 0) {
327 $diffArgs += '--'
328 $diffArgs += $pathspecs
329 }
330
331 $diffOutput = & git @diffArgs
332 if ($LASTEXITCODE -ne 0) {
333 throw "Failed to retrieve diff output."
334 }
335
336 return $diffOutput
337}
338
339function Get-DiffSummary {
340<#
341.SYNOPSIS
342Summarizes the diff for quick reporting.
343.DESCRIPTION
344Uses git diff --shortstat against the comparison ref.
345.PARAMETER ComparisonRef
346Git reference that acts as the diff base.
347.PARAMETER ExcludeMarkdownDiff
348Switch to omit markdown files from the summary.
349.PARAMETER ExcludeExt
350File extensions to exclude from the summary.
351.PARAMETER ExcludePath
352Path prefixes to exclude from the summary.
353.OUTPUTS
354System.String
355#>
356 [OutputType([string])]
357 param(
358 [Parameter(Mandatory = $true)]
359 [string]$ComparisonRef,
360
361 [Parameter()]
362 [switch]$ExcludeMarkdownDiff,
363
364 [Parameter()]
365 [string[]]$ExcludeExt = @(),
366
367 [Parameter()]
368 [string[]]$ExcludePath = @()
369 )
370
371 $diffStatArgs = @('--no-pager', 'diff', '--shortstat', $ComparisonRef)
372
373 $pathspecs = @()
374 if ($ExcludeMarkdownDiff) {
375 $pathspecs += ':!*.md'
376 }
377 $pathspecs += Build-PathspecExclusions -Extensions $ExcludeExt -Paths $ExcludePath
378 if ($pathspecs.Count -gt 0) {
379 $diffStatArgs += '--'
380 $diffStatArgs += $pathspecs
381 }
382
383 $summary = & git @diffStatArgs
384 if ($LASTEXITCODE -ne 0) {
385 throw "Failed to summarize diff output."
386 }
387
388 if (-not $summary) {
389 return '0 files changed'
390 }
391
392 return $summary
393}
394
395function Get-PrXmlContent {
396<#
397.SYNOPSIS
398Constructs the PR reference XML document.
399.DESCRIPTION
400Creates XML containing the current branch, base branch, commits, and diff.
401.PARAMETER CurrentBranch
402Name of the active git branch.
403.PARAMETER BaseBranch
404Branch used as the base reference.
405.PARAMETER CommitEntries
406Formatted commit entries produced by Get-CommitEntry.
407.PARAMETER DiffOutput
408Diff lines produced by Get-DiffOutput.
409.OUTPUTS
410System.String
411#>
412 [OutputType([string])]
413 param(
414 [Parameter(Mandatory = $true)]
415 [string]$CurrentBranch,
416
417 [Parameter(Mandatory = $true)]
418 [string]$BaseBranch,
419
420 [Parameter()]
421 [string[]]$CommitEntries,
422
423 [Parameter()]
424 [string[]]$DiffOutput
425 )
426
427 $commitBlock = if ($CommitEntries) {
428 ($CommitEntries | ForEach-Object { " $_" }) -join [Environment]::NewLine
429 } else {
430 ""
431 }
432
433 $diffBlock = if ($DiffOutput) {
434 ($DiffOutput | ForEach-Object { " $_" }) -join [Environment]::NewLine
435 } else {
436 ""
437 }
438
439 return @"
440<commit_history>
441 <current_branch>
442 $CurrentBranch
443 </current_branch>
444
445 <base_branch>
446 $BaseBranch
447 </base_branch>
448
449 <commits>
450$commitBlock
451 </commits>
452
453 <full_diff>
454$diffBlock
455 </full_diff>
456</commit_history>
457"@
458}
459
460function Get-LineImpact {
461<#
462.SYNOPSIS
463Calculates total line impact from a diff summary.
464.DESCRIPTION
465Parses insertion and deletion counts from git diff --shortstat output.
466.PARAMETER DiffSummary
467Short diff summary text.
468.OUTPUTS
469System.Int32
470#>
471 [OutputType([int])]
472 param(
473 [Parameter(Mandatory = $true)]
474 [string]$DiffSummary
475 )
476
477 $lineImpact = 0
478 if ($DiffSummary -match '(\d+) insertions') {
479 $lineImpact += [int]$matches[1]
480 }
481 if ($DiffSummary -match '(\d+) deletions') {
482 $lineImpact += [int]$matches[1]
483 }
484
485 return $lineImpact
486}
487
488function Invoke-PrReferenceGeneration {
489<#
490.SYNOPSIS
491Generates the pr-reference.xml file.
492.DESCRIPTION
493Coordinates git queries, XML creation, and console reporting for Copilot usage.
494.PARAMETER BaseBranch
495Branch used as the comparison base. Use 'auto' to detect the remote default.
496.PARAMETER MergeBase
497When supplied, uses git merge-base for three-way comparison.
498.PARAMETER ExcludeMarkdownDiff
499Switch to omit markdown files from the diff and summary.
500.PARAMETER ExcludeExt
501File extensions to exclude from the diff.
502.PARAMETER ExcludePath
503Path prefixes to exclude from the diff.
504.PARAMETER OutputPath
505Custom output file path. When empty, defaults to
506.copilot-tracking/pr/pr-reference.xml relative to the repository root.
507.OUTPUTS
508System.IO.FileInfo
509#>
510 [OutputType([System.IO.FileInfo])]
511 param(
512 [Parameter(Mandatory = $true)]
513 [string]$BaseBranch,
514
515 [Parameter()]
516 [switch]$MergeBase,
517
518 [Parameter()]
519 [switch]$ExcludeMarkdownDiff,
520
521 [Parameter()]
522 [string[]]$ExcludeExt = @(),
523
524 [Parameter()]
525 [string[]]$ExcludePath = @(),
526
527 [Parameter()]
528 [string]$OutputPath = ""
529 )
530
531 Test-GitAvailability
532
533 $repoRoot = Get-RepositoryRoot -Strict
534
535 # Resolve auto base branch
536 if ($BaseBranch -eq 'auto') {
537 $BaseBranch = Resolve-DefaultBranch
538 }
539
540 if ($OutputPath) {
541 $prReferencePath = $OutputPath
542 } else {
543 $prReferencePath = Join-Path $repoRoot '.copilot-tracking/pr/pr-reference.xml'
544 }
545
546 $null = New-PrDirectory -OutputFilePath $prReferencePath
547
548 $diffSummary = '0 files changed'
549 $commitCount = 0
550 $comparisonInfo = $null
551 $baseCommit = ''
552
553 Push-Location $repoRoot
554 try {
555 $currentBranch = Get-CurrentBranchOrRef
556 $comparisonInfo = Resolve-ComparisonReference -BaseBranch $BaseBranch -UseMergeBase:$MergeBase
557 $baseCommit = Get-ShortCommitHash -Ref $comparisonInfo.Ref
558 $commitEntries = Get-CommitEntry -ComparisonRef $comparisonInfo.Ref
559 $commitCount = Get-CommitCount -ComparisonRef $comparisonInfo.Ref
560 $diffOutput = Get-DiffOutput -ComparisonRef $comparisonInfo.Ref -ExcludeMarkdownDiff:$ExcludeMarkdownDiff -ExcludeExt $ExcludeExt -ExcludePath $ExcludePath
561 $diffSummary = Get-DiffSummary -ComparisonRef $comparisonInfo.Ref -ExcludeMarkdownDiff:$ExcludeMarkdownDiff -ExcludeExt $ExcludeExt -ExcludePath $ExcludePath
562
563 $xmlContent = Get-PrXmlContent -CurrentBranch $currentBranch -BaseBranch $BaseBranch -CommitEntries $commitEntries -DiffOutput $diffOutput
564 $xmlContent | Set-Content -LiteralPath $prReferencePath
565 }
566 finally {
567 Pop-Location
568 }
569
570 $lineCount = (Get-Content -LiteralPath $prReferencePath).Count
571 $lineImpact = Get-LineImpact -DiffSummary $diffSummary
572
573 Write-Host "Created $prReferencePath"
574 if ($ExcludeMarkdownDiff) {
575 Write-Host 'Note: Markdown files were excluded from diff output'
576 }
577 if ($ExcludeExt.Count -gt 0) {
578 Write-Host "Note: Extensions excluded from diff: $($ExcludeExt -join ', ')"
579 }
580 if ($ExcludePath.Count -gt 0) {
581 Write-Host "Note: Paths excluded from diff: $($ExcludePath -join ', ')"
582 }
583 if ($MergeBase) {
584 Write-Host 'Comparison mode: merge-base'
585 }
586 Write-Host "Lines: $lineCount"
587 Write-Host "Base branch: $($comparisonInfo.Label) (@ $baseCommit)"
588 Write-Host "Commits compared: $commitCount"
589 Write-Host "Diff summary: $diffSummary"
590
591 if ($lineImpact -gt 1000) {
592 Write-Host 'Large diff detected. Rebase onto the intended base branch or narrow your changes if this scope is unexpected.'
593 }
594
595 return Get-Item -LiteralPath $prReferencePath
596}
597
598#region Main Execution
599if ($MyInvocation.InvocationName -ne '.') {
600 try {
601 Invoke-PrReferenceGeneration -BaseBranch $BaseBranch -MergeBase:$MergeBase -ExcludeMarkdownDiff:$ExcludeMarkdownDiff -ExcludeExt $ExcludeExt -ExcludePath $ExcludePath -OutputPath $OutputPath | Out-Null
602 exit 0
603 }
604 catch {
605 Write-Error -ErrorAction Continue "Generate PR Reference failed: $($_.Exception.Message)"
606 Write-Warning "PR reference generation failed: $($_.Exception.Message)"
607 exit 1
608 }
609}
610#endregion
611