[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()] [string[]]$Contract, [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) $contracts = @($Contract | ForEach-Object { $_.Trim() } | Where-Object { $_ } | Select-Object -Unique) if ($plannedPaths.Count -eq 0 -or $contracts.Count -eq 0) { throw '必须同时提供预计修改路径和公共契约关键词,不能在范围不明确时开始开发。' } 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 = @() 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" $contractHits = @( $contracts | Where-Object { $branch -match [Regex]::Escape($_) ` -or $subject -match [Regex]::Escape($_) ` -or $diffText -match [Regex]::Escape($_) } ) if ($pathHits.Count -gt 0 -or $contractHits.Count -gt 0) { $branchFindings += [pscustomobject]@{ Branch = $branch Subject = $subject PathHits = @($pathHits | Select-Object -Unique) ContractHits = @($contractHits | Select-Object -Unique) } } } $existingFindings = @() $searchRoots = @('app', 'config', 'database', 'modules', 'resources', 'routes', 'tests') foreach ($keyword in $contracts) { $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) } } } Write-Host "Codex 任务开工检查:$Task" -ForegroundColor Cyan Write-Host "开发者:$Developer;Git 身份:$gitUser;基线:$BaseRef" Write-Host "预计路径:$($plannedPaths -join ', ')" Write-Host "公共契约:$($contracts -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 ($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.ContractHits.Count -gt 0) { Write-Host " 重叠契约:$($finding.ContractHits -join ', ')" } } exit 2 } Write-Host '通过:未发现远程任务分支与当前计划重叠。可以创建或继续独立任务分支。' -ForegroundColor Green exit 0