| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- #!/usr/bin/env pwsh
- # Git extension: auto-commit.ps1
- # Automatically commit changes after a Spec Kit command completes.
- # Checks per-command config keys in git-config.yml before committing.
- #
- # Usage: auto-commit.ps1 <event_name>
- # e.g.: auto-commit.ps1 after_specify
- param(
- [Parameter(Position = 0, Mandatory = $true)]
- [string]$EventName
- )
- $ErrorActionPreference = 'Stop'
- function Find-ProjectRoot {
- param([string]$StartDir)
- $current = Resolve-Path $StartDir
- while ($true) {
- foreach ($marker in @('.specify', '.git')) {
- if (Test-Path (Join-Path $current $marker)) {
- return $current
- }
- }
- $parent = Split-Path $current -Parent
- if ($parent -eq $current) { return $null }
- $current = $parent
- }
- }
- $repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot
- if (-not $repoRoot) { $repoRoot = Get-Location }
- Set-Location $repoRoot
- # Check if git is available
- if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
- Write-Warning "[specify] Warning: Git not found; skipped auto-commit"
- exit 0
- }
- # Temporarily relax ErrorActionPreference so git stderr warnings
- # (e.g. CRLF notices on Windows) do not become terminating errors.
- $savedEAP = $ErrorActionPreference
- $ErrorActionPreference = 'Continue'
- try {
- git rev-parse --is-inside-work-tree 2>$null | Out-Null
- $isRepo = $LASTEXITCODE -eq 0
- } finally {
- $ErrorActionPreference = $savedEAP
- }
- if (-not $isRepo) {
- Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
- exit 0
- }
- # Read per-command config from git-config.yml
- $configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml"
- $enabled = $false
- $commitMsg = ""
- if (Test-Path $configFile) {
- # Parse YAML to find auto_commit section
- $inAutoCommit = $false
- $inEvent = $false
- $defaultEnabled = $false
- foreach ($line in Get-Content $configFile) {
- # Detect auto_commit: section
- if ($line -match '^auto_commit:') {
- $inAutoCommit = $true
- $inEvent = $false
- continue
- }
- # Exit auto_commit section on next top-level key
- if ($inAutoCommit -and $line -match '^[a-z]') {
- break
- }
- if ($inAutoCommit) {
- # Check default key
- if ($line -match '^\s+default:\s*(.+)$') {
- $val = $matches[1].Trim().ToLower()
- if ($val -eq 'true') { $defaultEnabled = $true }
- }
- # Detect our event subsection
- if ($line -match "^\s+${EventName}:") {
- $inEvent = $true
- continue
- }
- # Inside our event subsection
- if ($inEvent) {
- # Exit on next sibling key (2-space indent, not 4+)
- if ($line -match '^\s{2}[a-z]' -and $line -notmatch '^\s{4}') {
- $inEvent = $false
- continue
- }
- if ($line -match '\s+enabled:\s*(.+)$') {
- $val = $matches[1].Trim().ToLower()
- if ($val -eq 'true') { $enabled = $true }
- if ($val -eq 'false') { $enabled = $false }
- }
- if ($line -match '\s+message:\s*(.+)$') {
- $commitMsg = $matches[1].Trim() -replace '^["'']' -replace '["'']$'
- }
- }
- }
- }
- # If event-specific key not found, use default
- if (-not $enabled -and $defaultEnabled) {
- $hasEventKey = Select-String -Path $configFile -Pattern "^\s*${EventName}:" -Quiet
- if (-not $hasEventKey) {
- $enabled = $true
- }
- }
- } else {
- # No config file — auto-commit disabled by default
- exit 0
- }
- if (-not $enabled) {
- exit 0
- }
- # Check if there are changes to commit
- # Relax ErrorActionPreference so CRLF warnings on stderr do not terminate.
- $savedEAP = $ErrorActionPreference
- $ErrorActionPreference = 'Continue'
- try {
- git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
- git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
- $untracked = git ls-files --others --exclude-standard 2>$null
- } finally {
- $ErrorActionPreference = $savedEAP
- }
- if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
- Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
- exit 0
- }
- # Derive a human-readable command name from the event
- $commandName = $EventName -replace '^after_', '' -replace '^before_', ''
- $phase = if ($EventName -match '^before_') { 'before' } else { 'after' }
- # Use custom message if configured, otherwise default
- if (-not $commitMsg) {
- $commitMsg = "[Spec Kit] Auto-commit $phase $commandName"
- }
- # Stage and commit
- # Relax ErrorActionPreference so CRLF warnings on stderr do not terminate,
- # while still allowing redirected error output to be captured for diagnostics.
- $savedEAP = $ErrorActionPreference
- $ErrorActionPreference = 'Continue'
- try {
- $out = git add . 2>&1 | Out-String
- if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
- $out = git commit -q -m $commitMsg 2>&1 | Out-String
- if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" }
- } catch {
- Write-Warning "[specify] Error: $_"
- exit 1
- } finally {
- $ErrorActionPreference = $savedEAP
- }
- Write-Host "[OK] Changes committed $phase $commandName"
|