param( [Parameter(Mandatory = $true)] [string]$Message, [string]$RepositoryRoot = '' ) $ErrorActionPreference = 'Stop' if ([string]::IsNullOrWhiteSpace($RepositoryRoot)) { $RepositoryRoot = (Get-Location).Path } $RepositoryRoot = [System.IO.Path]::GetFullPath($RepositoryRoot) $gitRoot = & git -C $RepositoryRoot rev-parse --show-toplevel 2>$null | Select-Object -First 1 if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($gitRoot)) { Write-Error "Git repository was not found: $RepositoryRoot" exit 1 } $RepositoryRoot = [System.IO.Path]::GetFullPath($gitRoot) $mappingFile = Join-Path $PSScriptRoot 'commit-module-mappings.json' $mapping = Get-Content -LiteralPath $mappingFile -Raw -Encoding UTF8 | ConvertFrom-Json function Get-CommitFiles { param([string]$Root) $staged = @(& git -C $Root -c core.quotepath=false diff --cached --name-only --diff-filter=ACMRD) $staged = @($staged | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($staged.Count -gt 0) { return $staged } $files = @() $statusLines = & git -C $Root -c core.quotepath=false status --porcelain=v1 --untracked-files=all foreach ($line in $statusLines) { if ([string]::IsNullOrWhiteSpace($line) -or $line.Length -lt 4) { continue } $path = $line.Substring(3).Trim() if ($path.Contains(' -> ')) { $path = ($path -split ' -> ')[-1] } $files += $path.Replace('\', '/') } return @($files | Select-Object -Unique) } function Get-Modules { param([string[]]$Files) $modules = @() foreach ($file in $Files) { foreach ($entry in $mapping.mappings) { if ($file -match $entry.pattern) { $modules += $entry.module break } } } return @($modules | Select-Object -Unique) } if ($Message -notmatch '^\[([^\]]+)\]\[([^\]]+)\]:\s+(.+)$') { Write-Error 'Commit message format must be: [module][type]: description' exit 1 } $messageModule = $Matches[1] $messageType = $Matches[2] $description = $Matches[3] if ([string]::IsNullOrWhiteSpace($description)) { Write-Error 'Commit message description is empty.' exit 1 } if ($mapping.validTypes -notcontains $messageType) { Write-Error ("Invalid commit type '{0}'. Allowed types: {1}" -f $messageType, ($mapping.validTypes -join ', ')) exit 1 } $files = @(Get-CommitFiles $RepositoryRoot) $modules = @(Get-Modules $files) Write-Output ("Repository: {0}" -f $RepositoryRoot) if ($files.Count -eq 0) { Write-Warning 'No staged, unstaged, or untracked files were found. Only the message was validated.' } else { Write-Output 'Commit scope files:' foreach ($file in $files) { Write-Output (" - {0}" -f $file) } } if ($modules.Count -gt 0) { Write-Output ("Detected modules: {0}" -f ($modules -join ', ')) if ($modules -notcontains $messageModule) { Write-Warning ("Message module [{0}] does not match detected modules [{1}]. Review the commit scope." -f $messageModule, ($modules -join ', ')) } } else { Write-Warning 'No path mapping matched. Review the commit module manually.' } Write-Output 'Commit message is valid.' exit 0