auto-commit.ps1 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. #!/usr/bin/env pwsh
  2. # Git extension: auto-commit.ps1
  3. # Automatically commit changes after a Spec Kit command completes.
  4. # Checks per-command config keys in git-config.yml before committing.
  5. #
  6. # Usage: auto-commit.ps1 <event_name>
  7. # e.g.: auto-commit.ps1 after_specify
  8. param(
  9. [Parameter(Position = 0, Mandatory = $true)]
  10. [string]$EventName
  11. )
  12. $ErrorActionPreference = 'Stop'
  13. function Find-ProjectRoot {
  14. param([string]$StartDir)
  15. $current = Resolve-Path $StartDir
  16. while ($true) {
  17. foreach ($marker in @('.specify', '.git')) {
  18. if (Test-Path (Join-Path $current $marker)) {
  19. return $current
  20. }
  21. }
  22. $parent = Split-Path $current -Parent
  23. if ($parent -eq $current) { return $null }
  24. $current = $parent
  25. }
  26. }
  27. $repoRoot = Find-ProjectRoot -StartDir $PSScriptRoot
  28. if (-not $repoRoot) { $repoRoot = Get-Location }
  29. Set-Location $repoRoot
  30. # Check if git is available
  31. if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
  32. Write-Warning "[specify] Warning: Git not found; skipped auto-commit"
  33. exit 0
  34. }
  35. # Temporarily relax ErrorActionPreference so git stderr warnings
  36. # (e.g. CRLF notices on Windows) do not become terminating errors.
  37. $savedEAP = $ErrorActionPreference
  38. $ErrorActionPreference = 'Continue'
  39. try {
  40. git rev-parse --is-inside-work-tree 2>$null | Out-Null
  41. $isRepo = $LASTEXITCODE -eq 0
  42. } finally {
  43. $ErrorActionPreference = $savedEAP
  44. }
  45. if (-not $isRepo) {
  46. Write-Warning "[specify] Warning: Not a Git repository; skipped auto-commit"
  47. exit 0
  48. }
  49. # Read per-command config from git-config.yml
  50. $configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml"
  51. $enabled = $false
  52. $commitMsg = ""
  53. if (Test-Path $configFile) {
  54. # Parse YAML to find auto_commit section
  55. $inAutoCommit = $false
  56. $inEvent = $false
  57. $defaultEnabled = $false
  58. foreach ($line in Get-Content $configFile) {
  59. # Detect auto_commit: section
  60. if ($line -match '^auto_commit:') {
  61. $inAutoCommit = $true
  62. $inEvent = $false
  63. continue
  64. }
  65. # Exit auto_commit section on next top-level key
  66. if ($inAutoCommit -and $line -match '^[a-z]') {
  67. break
  68. }
  69. if ($inAutoCommit) {
  70. # Check default key
  71. if ($line -match '^\s+default:\s*(.+)$') {
  72. $val = $matches[1].Trim().ToLower()
  73. if ($val -eq 'true') { $defaultEnabled = $true }
  74. }
  75. # Detect our event subsection
  76. if ($line -match "^\s+${EventName}:") {
  77. $inEvent = $true
  78. continue
  79. }
  80. # Inside our event subsection
  81. if ($inEvent) {
  82. # Exit on next sibling key (2-space indent, not 4+)
  83. if ($line -match '^\s{2}[a-z]' -and $line -notmatch '^\s{4}') {
  84. $inEvent = $false
  85. continue
  86. }
  87. if ($line -match '\s+enabled:\s*(.+)$') {
  88. $val = $matches[1].Trim().ToLower()
  89. if ($val -eq 'true') { $enabled = $true }
  90. if ($val -eq 'false') { $enabled = $false }
  91. }
  92. if ($line -match '\s+message:\s*(.+)$') {
  93. $commitMsg = $matches[1].Trim() -replace '^["'']' -replace '["'']$'
  94. }
  95. }
  96. }
  97. }
  98. # If event-specific key not found, use default
  99. if (-not $enabled -and $defaultEnabled) {
  100. $hasEventKey = Select-String -Path $configFile -Pattern "^\s*${EventName}:" -Quiet
  101. if (-not $hasEventKey) {
  102. $enabled = $true
  103. }
  104. }
  105. } else {
  106. # No config file — auto-commit disabled by default
  107. exit 0
  108. }
  109. if (-not $enabled) {
  110. exit 0
  111. }
  112. # Check if there are changes to commit
  113. # Relax ErrorActionPreference so CRLF warnings on stderr do not terminate.
  114. $savedEAP = $ErrorActionPreference
  115. $ErrorActionPreference = 'Continue'
  116. try {
  117. git diff --quiet HEAD 2>$null; $d1 = $LASTEXITCODE
  118. git diff --cached --quiet 2>$null; $d2 = $LASTEXITCODE
  119. $untracked = git ls-files --others --exclude-standard 2>$null
  120. } finally {
  121. $ErrorActionPreference = $savedEAP
  122. }
  123. if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
  124. Write-Host "[specify] No changes to commit after $EventName" -ForegroundColor DarkGray
  125. exit 0
  126. }
  127. # Derive a human-readable command name from the event
  128. $commandName = $EventName -replace '^after_', '' -replace '^before_', ''
  129. $phase = if ($EventName -match '^before_') { 'before' } else { 'after' }
  130. # Use custom message if configured, otherwise default
  131. if (-not $commitMsg) {
  132. $commitMsg = "[Spec Kit] Auto-commit $phase $commandName"
  133. }
  134. # Stage and commit
  135. # Relax ErrorActionPreference so CRLF warnings on stderr do not terminate,
  136. # while still allowing redirected error output to be captured for diagnostics.
  137. $savedEAP = $ErrorActionPreference
  138. $ErrorActionPreference = 'Continue'
  139. try {
  140. $out = git add . 2>&1 | Out-String
  141. if ($LASTEXITCODE -ne 0) { throw "git add failed: $out" }
  142. $out = git commit -q -m $commitMsg 2>&1 | Out-String
  143. if ($LASTEXITCODE -ne 0) { throw "git commit failed: $out" }
  144. } catch {
  145. Write-Warning "[specify] Error: $_"
  146. exit 1
  147. } finally {
  148. $ErrorActionPreference = $savedEAP
  149. }
  150. Write-Host "[OK] Changes committed $phase $commandName"