Skip to content

Commit 00ab3cb

Browse files
committed
feat: enhance commit and pre-commit hooks with environment variable support and improved logging
1 parent 8b7d35f commit 00ab3cb

File tree

3 files changed

+102
-87
lines changed

3 files changed

+102
-87
lines changed

booster/tools/git-hooks/hooks/commit-msg.ts

Lines changed: 30 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@
77
* - Branch name validation using validate-branch-name
88
* - Commit message linting with commitlint
99
* - Automatic ticket footer appending when required
10+
*
11+
* Environment Variables:
12+
* - SKIP_COMMITMSG=1: Skip the entire commit-msg hook
13+
* - COMMITMSG_VERBOSE=1: Enable verbose output for debugging
1014
*/
1115

1216
import { $, fs, path } from 'zx'
17+
import validateBranchNameConfig from '../../../validate-branch-name.config.cjs'
1318
import {
19+
getCurrentBranch,
1420
log,
15-
shouldSkipDuringMerge,
1621
runTool,
1722
runWithRunner,
18-
getCurrentBranch,
23+
shouldSkipDuringMerge,
1924
} from '../shared/utils.ts'
2025

2126
// Configure zx
22-
$.verbose = false
27+
$.verbose = process.env.COMMITMSG_VERBOSE === '1' || process.env.COMMITMSG_VERBOSE === 'true'
28+
29+
// Fix locale issues that can occur in VS Code
30+
process.env.LC_ALL = 'C'
31+
process.env.LANG = 'C'
2332

2433
/**
2534
* Branch validation configuration interface
@@ -43,51 +52,21 @@ interface ProcessedConfig {
4352
config: BranchConfig
4453
}
4554

46-
/**
47-
* Options for running commands with runner
48-
*/
49-
interface RunnerOptions {
50-
quiet?: boolean
51-
showCommand?: boolean
52-
}
53-
5455
/**
5556
* Load and parse validate-branch-name configuration
5657
*/
57-
async function loadConfig(): Promise<BranchConfig> {
58-
const configPath = path.resolve('./validate-branch-name.config.cjs')
58+
function loadConfig(): BranchConfig {
59+
const config = validateBranchNameConfig.config
5960

60-
if (!(await fs.pathExists(configPath))) {
61-
throw new Error(`Configuration file not found: ${configPath}`)
62-
}
63-
64-
try {
65-
const content = await fs.readFile(configPath, 'utf8')
66-
67-
// Extract the config object from the CommonJS module
68-
const configMatch = content.match(/const config = ({.*?});/s)
69-
if (!configMatch) {
70-
throw new Error('Could not find config object in config file')
71-
}
72-
73-
const configStr = configMatch[1]
74-
75-
// Safely evaluate the JavaScript object
76-
const config = eval(`(${configStr})`)
77-
78-
// Validate required properties and apply defaults
79-
return {
80-
types: Array.isArray(config.types) ? config.types : [],
81-
ticketIdPrefix: config.ticketIdPrefix || null,
82-
ticketNumberPattern: config.ticketNumberPattern || null,
83-
commitFooterLabel: config.commitFooterLabel || 'Closes',
84-
requireTickets: Boolean(config.requireTickets),
85-
skipped: Array.isArray(config.skipped) ? config.skipped : [],
86-
namePattern: config.namePattern || null,
87-
}
88-
} catch (error: unknown) {
89-
const errorMessage = error instanceof Error ? error.message : String(error)
90-
throw new Error(`Failed to load branch validation config: ${errorMessage}`)
61+
// Validate required properties and apply defaults
62+
return {
63+
types: Array.isArray(config.types) ? config.types : [],
64+
ticketIdPrefix: config.ticketIdPrefix || null,
65+
ticketNumberPattern: config.ticketNumberPattern || null,
66+
commitFooterLabel: config.commitFooterLabel || 'Closes',
67+
requireTickets: Boolean(config.requireTickets),
68+
skipped: Array.isArray(config.skipped) ? config.skipped : [],
69+
namePattern: config.namePattern || null,
9170
}
9271
}
9372

@@ -106,10 +85,6 @@ function processConfig(config: BranchConfig): ProcessedConfig {
10685
// Use explicit requireTickets flag, but validate that patterns exist if tickets are required
10786
const needTicket =
10887
config.requireTickets && !!(config.ticketIdPrefix && config.ticketNumberPattern)
109-
console.log('Need ticket:', needTicket)
110-
console.log('Ticket ID Prefix:', config.ticketIdPrefix)
111-
console.log('Ticket Number Pattern:', config.ticketNumberPattern)
112-
console.log('config.requireTickets:', config.requireTickets)
11388

11489
let footerLabel = String(config.commitFooterLabel || 'Closes').trim()
11590

@@ -216,7 +191,7 @@ async function lintCommitMessage(commitFile: string): Promise<boolean> {
216191
*/
217192
async function appendTicketFooter(commitFile: string): Promise<void> {
218193
// Load and process configuration
219-
const config = await loadConfig()
194+
const config = loadConfig()
220195
const branchName = await getCurrentBranch()
221196

222197
// Check if current branch is skipped (exempt from all validation)
@@ -272,6 +247,12 @@ async function main(): Promise<void> {
272247

273248
log.step('Starting commit-msg validation...')
274249

250+
// Check if we should skip the entire hook
251+
if (process.env.SKIP_COMMITMSG === '1' || process.env.SKIP_COMMITMSG === 'true') {
252+
log.info('Skipping commit-msg validation (SKIP_COMMITMSG environment variable set)')
253+
process.exit(0)
254+
}
255+
275256
// Check if we should skip all checks (during merge)
276257
if (await shouldSkipDuringMerge()) {
277258
log.info('Skipping commit-msg checks during merge')

booster/tools/git-hooks/hooks/pre-commit.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,43 @@
99
* - ECS (code style auto-fixes)
1010
* - PHPStan (static analysis)
1111
* - Psalm (static analysis)
12+
*
13+
* Environment Variables:
14+
* - SKIP_PRECOMMIT=1: Skip the entire pre-commit hook
15+
* - FORCE_COMMIT=1: Continue with commit even if PHPStan or Psalm fail
16+
* - PRECOMMIT_VERBOSE=1: Enable verbose output for debugging
1217
*/
1318

1419
import { $, fs } from 'zx'
1520
import {
16-
log,
21+
checkPhpSyntax,
22+
exitIfChecksFailed,
1723
getStagedPhpFiles,
1824
hasVendorBin,
19-
shouldSkipDuringMerge,
25+
log,
2026
runTool,
21-
checkPhpSyntax,
22-
stageFiles,
2327
runVendorBin,
2428
runWithRunner,
25-
exitIfChecksFailed,
29+
shouldSkipDuringMerge,
30+
stageFiles,
2631
} from '../shared/utils.ts'
2732

2833
// Configure zx
29-
$.verbose = false
34+
$.verbose = process.env.PRECOMMIT_VERBOSE === '1' || process.env.PRECOMMIT_VERBOSE === 'true'
35+
36+
// Fix locale issues that can occur in VS Code
37+
process.env.LC_ALL = 'C'
38+
process.env.LANG = 'C'
3039

3140
async function main(): Promise<void> {
3241
log.step('Starting pre-commit checks...')
3342

43+
// Check if we should skip the entire hook
44+
if (process.env.SKIP_PRECOMMIT === '1' || process.env.SKIP_PRECOMMIT === 'true') {
45+
log.info('Skipping pre-commit checks (SKIP_PRECOMMIT environment variable set)')
46+
process.exit(0)
47+
}
48+
3449
// Check if we should skip all checks
3550
if (await shouldSkipDuringMerge()) {
3651
log.info('Skipping pre-commit checks during merge')
@@ -55,6 +70,13 @@ async function main(): Promise<void> {
5570
// Track overall success
5671
let allSuccessful = true
5772

73+
// Check if we should force commit even if static analysis fails
74+
const forceCommit = process.env.FORCE_COMMIT === '1' || process.env.FORCE_COMMIT === 'true'
75+
76+
if (forceCommit) {
77+
log.info('Force commit mode enabled (FORCE_COMMIT environment variable set)')
78+
}
79+
5880
// Run Rector if available
5981
if (await hasVendorBin('rector')) {
6082
const success = await runTool('Rector', async () => {
@@ -100,7 +122,11 @@ async function main(): Promise<void> {
100122
})
101123

102124
if (!success) {
103-
allSuccessful = false
125+
if (forceCommit) {
126+
log.warn('PHPStan failed, but continuing due to FORCE_COMMIT flag')
127+
} else {
128+
allSuccessful = false
129+
}
104130
}
105131
} else {
106132
log.skip('PHPStan not found in vendor/bin. Skipping...')
@@ -144,7 +170,11 @@ async function main(): Promise<void> {
144170
})
145171

146172
if (!success) {
147-
allSuccessful = false
173+
if (forceCommit) {
174+
log.warn('Psalm failed, but continuing due to FORCE_COMMIT flag')
175+
} else {
176+
allSuccessful = false
177+
}
148178
}
149179
} else {
150180
log.skip('Psalm not found in vendor/bin. Skipping...')

booster/tools/git-hooks/shared/utils.ts

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Provides consistent logging, file operations, and tool detection
66
*/
77

8-
import { $, fs, path, chalk } from 'zx'
8+
import { $, chalk, fs, path } from 'zx'
99

1010
// Configure zx behavior
1111
$.verbose = false
@@ -17,21 +17,6 @@ interface RunOptions {
1717
quiet?: boolean
1818
}
1919

20-
/**
21-
* Detect if we're in a DDEV environment and DDEV is available
22-
*/
23-
export async function isDdevRunning(): Promise<boolean> {
24-
try {
25-
// Check if DDEV is available and we're in a DDEV project
26-
await $`ddev --version`.quiet()
27-
28-
// Check if .ddev directory exists (indicates DDEV project)
29-
return await fs.pathExists('.ddev')
30-
} catch (error) {
31-
return false
32-
}
33-
}
34-
3520
/**
3621
* Check if we're already inside a DDEV container
3722
*/
@@ -56,31 +41,33 @@ export async function runWithRunner(command: string[], options: RunOptions = {})
5641
let fullCommand: string[]
5742
let contextLabel: string
5843

59-
const isDdev = await isDdevRunning()
60-
const isContainer = await isInsideDdevContainer()
44+
const isInsideContainer = await isInsideDdevContainer()
6145

62-
if (isDdev && !isContainer) {
63-
// We're in a DDEV project but not inside container - run via DDEV
46+
if (!isInsideContainer) {
47+
// We're not inside a container - run via DDEV
6448
fullCommand = ['ddev', 'exec', ...command]
6549
contextLabel = 'via DDEV'
6650
} else {
67-
// Run directly (either no DDEV or already inside container)
51+
// Run directly (already inside container)
6852
fullCommand = command
69-
contextLabel = isContainer ? 'inside DDEV container' : 'direct'
53+
contextLabel = isInsideContainer ? 'inside DDEV container' : 'direct'
7054
}
7155

7256
// Log command execution if not quiet
7357
if (!quiet) {
7458
const commandStr = command.join(' ')
75-
console.log(chalk.cyan(`🔧 Executing (${contextLabel}): ${commandStr}`))
59+
log.info(`Executing (${contextLabel}): ${commandStr}`)
7660
}
7761

78-
// Execute with appropriate stdio handling
79-
if (quiet) {
80-
return await $({ stdio: 'pipe' })`${fullCommand}`
81-
} else {
82-
return await $({ stdio: 'inherit' })`${fullCommand}`
62+
// Set clean environment to avoid locale warnings
63+
const cleanEnv = {
64+
...process.env,
65+
LC_ALL: 'C',
66+
LANG: 'C',
8367
}
68+
69+
// Execute with appropriate stdio handling and clean environment
70+
return await $({ stdio: quiet ? 'pipe' : 'inherit', env: cleanEnv })`${fullCommand}`
8471
}
8572

8673
/**
@@ -112,7 +99,7 @@ export async function getStagedPhpFiles(): Promise<string[]> {
11299
// Filter for PHP files that actually exist
113100
const phpFiles: string[] = []
114101
for (const file of allFiles) {
115-
if (file.endsWith('.php') && (await fs.pathExists(file))) {
102+
if (file.endsWith('.php') && !file.includes('/vendor/') && (await fs.pathExists(file))) {
116103
phpFiles.push(file)
117104
}
118105
}
@@ -176,7 +163,24 @@ export async function getCurrentBranch(): Promise<string> {
176163
const result = await runWithRunner(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], {
177164
quiet: true,
178165
})
179-
return result.toString().trim()
166+
167+
// Clean the result to handle any locale warnings or extra output
168+
const branchName = result.toString().trim()
169+
170+
// Extract just the last line if there are multiple lines (locale warnings, etc.)
171+
const lines = branchName.split('\n').filter((line: string) => line.trim() !== '')
172+
const cleanBranchName = lines[lines.length - 1].trim()
173+
174+
// Additional validation to ensure we have a valid branch name
175+
if (
176+
!cleanBranchName ||
177+
cleanBranchName.includes('warning:') ||
178+
cleanBranchName.includes('error:')
179+
) {
180+
throw new Error(`Invalid branch name detected: "${cleanBranchName}"`)
181+
}
182+
183+
return cleanBranchName
180184
} catch (error: unknown) {
181185
throw new Error(
182186
`Failed to get current branch: ${error instanceof Error ? error.message : String(error)}`,

0 commit comments

Comments
 (0)