275 lines
9.4 KiB
PowerShell
275 lines
9.4 KiB
PowerShell
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[ValidateNotNullOrEmpty()]
|
||
[string]$Task,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[ValidatePattern('^[A-Za-z0-9_-]+$')]
|
||
[string]$Developer,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[ValidateNotNullOrEmpty()]
|
||
[string[]]$PlannedPath,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[ValidateNotNullOrEmpty()]
|
||
[Alias('WriteContract')]
|
||
[string[]]$Contract,
|
||
|
||
[string[]]$ReuseContract = @(),
|
||
|
||
[string]$Remote = 'origin',
|
||
[string]$BaseRef = 'origin/main'
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
function Invoke-Git {
|
||
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments)
|
||
|
||
$output = & git @Arguments 2>&1
|
||
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "Git 命令执行失败:git $($Arguments -join ' ')`n$($output -join "`n")"
|
||
}
|
||
|
||
return @($output)
|
||
}
|
||
|
||
function Normalize-RepoPath {
|
||
param([string]$Path)
|
||
|
||
return ($Path.Trim() -replace '\\', '/').TrimStart('.', '/').TrimEnd('/')
|
||
}
|
||
|
||
function Test-PathOverlap {
|
||
param(
|
||
[string]$Planned,
|
||
[string]$Changed
|
||
)
|
||
|
||
return $Planned -eq $Changed `
|
||
-or $Changed.StartsWith($Planned + '/', [StringComparison]::OrdinalIgnoreCase) `
|
||
-or $Planned.StartsWith($Changed + '/', [StringComparison]::OrdinalIgnoreCase)
|
||
}
|
||
|
||
$repoRoot = (Invoke-Git rev-parse --show-toplevel | Select-Object -First 1).Trim()
|
||
$repoName = Split-Path -Leaf $repoRoot
|
||
$originUrl = (Invoke-Git remote get-url $Remote | Select-Object -First 1).Trim()
|
||
$gitUser = (Invoke-Git config user.name | Select-Object -First 1).Trim()
|
||
|
||
if ($repoName -ne 'CaiWuXiTong') {
|
||
throw "当前目录不是 CaiWuXiTong:$repoRoot"
|
||
}
|
||
|
||
if ($originUrl -notmatch 'WuYongJie/CaiWuXiTong(?:\.git)?$') {
|
||
throw "远程仓库不是 WuYongJie/CaiWuXiTong:$originUrl"
|
||
}
|
||
|
||
if ([string]::IsNullOrWhiteSpace($gitUser)) {
|
||
throw 'Git user.name 未配置。'
|
||
}
|
||
|
||
$plannedPaths = @($PlannedPath | ForEach-Object { Normalize-RepoPath $_ } | Where-Object { $_ } | Select-Object -Unique)
|
||
$writeContracts = @($Contract | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique)
|
||
$reuseContracts = @($ReuseContract | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique)
|
||
|
||
if ($plannedPaths.Count -eq 0 -or $writeContracts.Count -eq 0) {
|
||
throw '必须同时提供预计修改路径和将写入的公共契约关键词,不能在范围不明确时开始开发。'
|
||
}
|
||
|
||
$broadPathRoots = @('app', 'bootstrap', 'config', 'database', 'deploy', 'docs', 'modules', 'public', 'resources', 'routes', 'scripts', 'tests')
|
||
$broadPlannedPaths = @($plannedPaths | Where-Object { $broadPathRoots -contains $_ })
|
||
|
||
if ($broadPlannedPaths.Count -gt 0) {
|
||
throw "预计修改路径不能使用宽泛根目录:$($broadPlannedPaths -join ', ')。请填写精确文件或最小子目录,例如 tests/Feature/Business/OrderDataNavigationTest.php。"
|
||
}
|
||
|
||
Invoke-Git fetch --prune $Remote | Out-Null
|
||
Invoke-Git rev-parse --verify $BaseRef | Out-Null
|
||
|
||
$status = @(Invoke-Git status --porcelain=v1)
|
||
$trackedChanges = @($status | Where-Object { $_ -and -not $_.StartsWith('?? ') })
|
||
$untrackedChanges = @($status | Where-Object { $_ -and $_.StartsWith('?? ') })
|
||
|
||
if ($trackedChanges.Count -gt 0) {
|
||
Write-Host '阻断:工作区已有受控文件改动,不能开始新任务。' -ForegroundColor Red
|
||
$trackedChanges | ForEach-Object { Write-Host " $_" }
|
||
exit 2
|
||
}
|
||
|
||
$currentBranch = (Invoke-Git branch --show-current | Select-Object -First 1).Trim()
|
||
$remoteBranches = @(
|
||
Invoke-Git for-each-ref '--format=%(refname:short)' "refs/remotes/$Remote" |
|
||
Where-Object {
|
||
$_ -and
|
||
$_ -ne $Remote -and
|
||
$_ -ne "$Remote/HEAD" -and
|
||
$_ -ne $BaseRef -and
|
||
$_ -ne "$Remote/$currentBranch"
|
||
}
|
||
)
|
||
|
||
$branchFindings = @()
|
||
$reuseFindings = @()
|
||
|
||
foreach ($branch in $remoteBranches) {
|
||
$counts = ((Invoke-Git rev-list --left-right --count "$BaseRef...$branch") | Select-Object -First 1).Trim() -split '\s+'
|
||
$branchOnly = [int]$counts[1]
|
||
|
||
if ($branchOnly -eq 0) {
|
||
continue
|
||
}
|
||
|
||
& git merge-base --is-ancestor $branch $BaseRef
|
||
if ($LASTEXITCODE -eq 0) {
|
||
continue
|
||
}
|
||
|
||
$changedFiles = @(Invoke-Git diff --name-only "$BaseRef...$branch" | ForEach-Object { Normalize-RepoPath $_ })
|
||
$pathHits = @()
|
||
|
||
foreach ($planned in $plannedPaths) {
|
||
foreach ($changed in $changedFiles) {
|
||
if (Test-PathOverlap -Planned $planned -Changed $changed) {
|
||
$pathHits += $changed
|
||
}
|
||
}
|
||
}
|
||
|
||
$subject = (Invoke-Git log -1 '--format=%s' $branch | Select-Object -First 1).Trim()
|
||
$diffText = (Invoke-Git diff --unified=0 "$BaseRef...$branch") -join "`n"
|
||
$writeContractHits = @(
|
||
$writeContracts | Where-Object {
|
||
$branch -match [Regex]::Escape($_) `
|
||
-or $subject -match [Regex]::Escape($_) `
|
||
-or $diffText -match [Regex]::Escape($_)
|
||
}
|
||
)
|
||
|
||
$reuseContractHits = @(
|
||
$reuseContracts | Where-Object {
|
||
$branch -match [Regex]::Escape($_) `
|
||
-or $subject -match [Regex]::Escape($_) `
|
||
-or $diffText -match [Regex]::Escape($_)
|
||
}
|
||
)
|
||
|
||
if ($pathHits.Count -gt 0 -or $writeContractHits.Count -gt 0) {
|
||
$branchFindings += [pscustomobject]@{
|
||
Branch = $branch
|
||
Subject = $subject
|
||
PathHits = @($pathHits | Select-Object -Unique)
|
||
WriteContractHits = @($writeContractHits | Select-Object -Unique)
|
||
ReuseContractHits = @($reuseContractHits | Select-Object -Unique)
|
||
}
|
||
} elseif ($reuseContractHits.Count -gt 0) {
|
||
$reuseFindings += [pscustomobject]@{
|
||
Branch = $branch
|
||
Subject = $subject
|
||
ReuseContractHits = @($reuseContractHits | Select-Object -Unique)
|
||
}
|
||
}
|
||
}
|
||
|
||
$existingFindings = @()
|
||
$existingReuseFindings = @()
|
||
$searchRoots = @('app', 'config', 'database', 'modules', 'resources', 'routes', 'tests')
|
||
|
||
foreach ($keyword in $writeContracts) {
|
||
$matches = @(& git grep -n -I -F -e $keyword $BaseRef -- @searchRoots 2>$null)
|
||
|
||
if ($LASTEXITCODE -notin @(0, 1)) {
|
||
throw "检查主线公共契约失败:$keyword"
|
||
}
|
||
|
||
if ($matches.Count -gt 0) {
|
||
$existingFindings += [pscustomobject]@{
|
||
Keyword = $keyword
|
||
Matches = @($matches | Select-Object -First 8)
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($keyword in $reuseContracts) {
|
||
$matches = @(& git grep -n -I -F -e $keyword $BaseRef -- @searchRoots 2>$null)
|
||
|
||
if ($LASTEXITCODE -notin @(0, 1)) {
|
||
throw "检查主线仅复用公共契约失败:$keyword"
|
||
}
|
||
|
||
if ($matches.Count -gt 0) {
|
||
$existingReuseFindings += [pscustomobject]@{
|
||
Keyword = $keyword
|
||
Matches = @($matches | Select-Object -First 8)
|
||
}
|
||
}
|
||
}
|
||
|
||
Write-Host "Codex 任务开工检查:$Task" -ForegroundColor Cyan
|
||
Write-Host "开发者:$Developer;Git 身份:$gitUser;基线:$BaseRef"
|
||
Write-Host "预计路径:$($plannedPaths -join ', ')"
|
||
Write-Host "将写入的公共契约:$($writeContracts -join ', ')"
|
||
|
||
if ($reuseContracts.Count -gt 0) {
|
||
Write-Host "仅复用、不修改的公共契约:$($reuseContracts -join ', ')"
|
||
}
|
||
|
||
if ($untrackedChanges.Count -gt 0) {
|
||
Write-Host '提醒:存在未跟踪文件,Codex 必须确认归属且不得顺带提交:' -ForegroundColor Yellow
|
||
$untrackedChanges | ForEach-Object { Write-Host " $_" }
|
||
}
|
||
|
||
if ($existingFindings.Count -gt 0) {
|
||
Write-Host '主线已存在相同关键词,请先复用或说明差异:' -ForegroundColor Yellow
|
||
|
||
foreach ($finding in $existingFindings) {
|
||
Write-Host " [$($finding.Keyword)]"
|
||
$finding.Matches | ForEach-Object { Write-Host " $_" }
|
||
}
|
||
}
|
||
|
||
if ($existingReuseFindings.Count -gt 0) {
|
||
Write-Host '主线已存在下列仅复用契约;这属于预期复用,不单独阻断:' -ForegroundColor Cyan
|
||
|
||
foreach ($finding in $existingReuseFindings) {
|
||
Write-Host " [$($finding.Keyword)]"
|
||
$finding.Matches | ForEach-Object { Write-Host " $_" }
|
||
}
|
||
}
|
||
|
||
if ($reuseFindings.Count -gt 0) {
|
||
Write-Host '提示:以下远程任务分支也在使用仅复用契约;当前任务已声明不修改它们,因此不单独阻断。若实施中需要改该契约实现,必须停止、更新 WIP PR 并重新预检:' -ForegroundColor Yellow
|
||
|
||
foreach ($finding in $reuseFindings) {
|
||
Write-Host " $($finding.Branch) — $($finding.Subject)"
|
||
Write-Host " 仅复用契约:$($finding.ReuseContractHits -join ', ')"
|
||
}
|
||
}
|
||
|
||
if ($branchFindings.Count -gt 0) {
|
||
Write-Host '阻断:以下远程任务分支与当前计划存在实际写入重叠。停止修改,由相关开发者先完成协调:' -ForegroundColor Red
|
||
|
||
foreach ($finding in $branchFindings) {
|
||
Write-Host " $($finding.Branch) — $($finding.Subject)"
|
||
|
||
if ($finding.PathHits.Count -gt 0) {
|
||
Write-Host " 重叠路径:$($finding.PathHits -join ', ')"
|
||
}
|
||
|
||
if ($finding.WriteContractHits.Count -gt 0) {
|
||
Write-Host " 重叠写入契约:$($finding.WriteContractHits -join ', ')"
|
||
}
|
||
|
||
if ($finding.ReuseContractHits.Count -gt 0) {
|
||
Write-Host " 同时仅复用:$($finding.ReuseContractHits -join ', ')"
|
||
}
|
||
}
|
||
|
||
exit 2
|
||
}
|
||
|
||
Write-Host '通过:未发现远程任务分支与当前计划的实际写入重叠。可以创建或继续独立任务分支。' -ForegroundColor Green
|
||
exit 0
|