microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v3.3.41

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/linting/Modules/LintingHelpers.psm1

238lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3
4# LintingHelpers.psm1
5#
6# Purpose: Shared helper functions for linting scripts and workflows
7# Author: HVE Core Team
8
9Import-Module (Join-Path $PSScriptRoot "../../lib/Modules/CIHelpers.psm1") -Force
10
11function Get-ChangedFilesFromGit {
12 <#
13 .SYNOPSIS
14 Gets changed files from git with intelligent fallback strategies.
15
16 .DESCRIPTION
17 Attempts to detect changed files using merge-base, with fallbacks for different scenarios.
18
19 .PARAMETER BaseBranch
20 The base branch to compare against (default: origin/main).
21
22 .PARAMETER FileExtensions
23 Array of file extensions to filter (e.g., @('*.ps1', '*.md')).
24
25 .OUTPUTS
26 Array of changed file paths.
27 #>
28 [CmdletBinding()]
29 param(
30 [Parameter(Mandatory = $false)]
31 [string]$BaseBranch = "origin/main",
32
33 [Parameter(Mandatory = $false)]
34 [string[]]$FileExtensions = @('*')
35 )
36
37 $changedFiles = @()
38
39 try {
40 # Try merge-base first (best for PRs)
41 $mergeBase = git merge-base HEAD $BaseBranch 2>$null
42
43 if ($LASTEXITCODE -eq 0 -and $mergeBase) {
44 Write-Verbose "Using merge-base: $mergeBase"
45 $changedFiles = git diff --name-only --diff-filter=ACMR $mergeBase HEAD 2>$null
46 }
47 elseif ((git rev-parse HEAD~1 2>$null)) {
48 Write-Verbose "Merge base failed, using HEAD~1"
49 $changedFiles = git diff --name-only --diff-filter=ACMR HEAD~1 HEAD 2>$null
50 }
51 else {
52 Write-Verbose "HEAD~1 failed, using staged/unstaged files"
53 $changedFiles = git diff --name-only HEAD 2>$null
54 }
55
56 if ($LASTEXITCODE -ne 0) {
57 Write-Warning "Unable to determine changed files from git"
58 return @()
59 }
60
61 # Filter by extensions and verify files exist
62 $filteredFiles = $changedFiles | Where-Object {
63 if ([string]::IsNullOrEmpty($_)) { return $false }
64
65 # Check if file matches any of the allowed extensions
66 $currentFile = $_
67 $matchesExtension = $false
68 foreach ($pattern in $FileExtensions) {
69 if ($currentFile -like $pattern) {
70 $matchesExtension = $true
71 break
72 }
73 }
74
75 $matchesExtension -and (Test-Path $currentFile -PathType Leaf)
76 }
77
78 Write-Verbose "Found $($filteredFiles.Count) changed files matching extensions: $($FileExtensions -join ', ')"
79 return $filteredFiles
80 }
81 catch {
82 Write-Warning "Error getting changed files: $($_.Exception.Message)"
83 return @()
84 }
85}
86
87function Get-FilesRecursive {
88 <#
89 .SYNOPSIS
90 Gets files recursively with gitignore filtering.
91
92 .DESCRIPTION
93 Recursively finds files by extension, respecting .gitignore patterns.
94
95 .PARAMETER Path
96 Root path to search from.
97
98 .PARAMETER Include
99 File patterns to include (e.g., @('*.ps1', '*.psm1')).
100
101 .PARAMETER GitIgnorePath
102 Path to .gitignore file for exclusion patterns.
103
104 .OUTPUTS
105 Array of FileInfo objects.
106 #>
107 [CmdletBinding()]
108 param(
109 [Parameter(Mandatory = $true)]
110 [string]$Path,
111
112 [Parameter(Mandatory = $true)]
113 [string[]]$Include,
114
115 [Parameter(Mandatory = $false)]
116 [string]$GitIgnorePath
117 )
118
119 # Determine whether $Path resides inside the current git repository
120 $sep = [System.IO.Path]::DirectorySeparatorChar
121 $repoRoot = git rev-parse --show-toplevel 2>$null
122 if ($repoRoot) { $repoRoot = $repoRoot.Replace('/', $sep) }
123 $resolved = Resolve-Path -Path $Path -ErrorAction SilentlyContinue
124 $resolvedPath = if ($resolved) { $resolved.Path.Replace('/', $sep) } else { $null }
125 $useGit = $repoRoot -and $resolvedPath -and (
126 $resolvedPath -eq $repoRoot -or
127 $resolvedPath.StartsWith("$repoRoot$sep")
128 )
129
130 if ($useGit) {
131 # git ls-files natively respects .gitignore via --exclude-standard
132 $relPath = if ($resolvedPath -eq $repoRoot) { '' }
133 else { $resolvedPath.Substring($repoRoot.Length + 1) }
134
135 $gitArgs = @('ls-files', '--cached', '--others', '--exclude-standard')
136 if ($relPath) {
137 $gitArgs += '--'
138 $gitArgs += "$relPath/"
139 }
140 else {
141 foreach ($pattern in $Include) {
142 $gitArgs += $pattern
143 }
144 }
145
146 $rawFiles = @(git @gitArgs | Where-Object { $_ })
147
148 # When scoped to a subdirectory, filter by Include patterns
149 if ($relPath) {
150 $rawFiles = @($rawFiles | Where-Object {
151 $name = [System.IO.Path]::GetFileName($_)
152 foreach ($p in $Include) {
153 if ($name -like $p) { return $true }
154 }
155 return $false
156 })
157 }
158
159 $files = @($rawFiles | ForEach-Object {
160 $fullPath = Join-Path $repoRoot $_
161 if (Test-Path $fullPath -PathType Leaf) {
162 Get-Item -LiteralPath $fullPath
163 }
164 })
165 }
166 else {
167 # Fallback for non-git contexts or paths outside the repository
168 $files = Get-ChildItem -Path $Path -Recurse -Include $Include -File -ErrorAction SilentlyContinue |
169 Where-Object { -not $_.LinkTarget }
170
171 if ($GitIgnorePath) {
172 $patterns = Get-GitIgnorePatterns -GitIgnorePath $GitIgnorePath
173 if ($patterns) {
174 $files = @($files | Where-Object {
175 $fullName = $_.FullName
176 foreach ($p in $patterns) {
177 if ($fullName -like $p) { return $false }
178 }
179 return $true
180 })
181 }
182 }
183 }
184
185 return $files
186}
187
188function Get-GitIgnorePatterns {
189 <#
190 .SYNOPSIS
191 Parses .gitignore into PowerShell wildcard patterns.
192
193 .PARAMETER GitIgnorePath
194 Path to .gitignore file.
195
196 .OUTPUTS
197 Array of wildcard patterns using platform-appropriate separators.
198 #>
199 [CmdletBinding()]
200 param(
201 [Parameter(Mandatory = $true)]
202 [string]$GitIgnorePath
203 )
204
205 if (-not (Test-Path $GitIgnorePath)) {
206 return @()
207 }
208
209 $sep = [System.IO.Path]::DirectorySeparatorChar
210
211 $patterns = Get-Content $GitIgnorePath | Where-Object {
212 $_ -and -not $_.StartsWith('#') -and $_.Trim() -ne ''
213 } | ForEach-Object {
214 $pattern = $_.Trim()
215
216 # Normalize to platform separator
217 $normalizedPattern = $pattern.Replace('/', $sep).Replace('\', $sep)
218
219 if ($pattern.EndsWith('/')) {
220 "*$sep$($normalizedPattern.TrimEnd($sep))$sep*"
221 }
222 elseif ($pattern.Contains('/') -or $pattern.Contains('\')) {
223 "*$sep$normalizedPattern*"
224 }
225 else {
226 "*$sep$normalizedPattern$sep*"
227 }
228 }
229
230 return $patterns
231}
232
233# Export local functions only - CIHelpers functions are used via direct import
234Export-ModuleMember -Function @(
235 'Get-ChangedFilesFromGit',
236 'Get-FilesRecursive',
237 'Get-GitIgnorePatterns'
238)
239