2020
2121// Integrate command - install git hooks for secrets scanning
2222
23- import { existsSync , mkdirSync , readFileSync , statSync } from 'node:fs' ;
24- import { isAbsolute , join } from 'node:path' ;
23+ import { existsSync , mkdirSync , readFileSync } from 'node:fs' ;
24+ import { join } from 'node:path' ;
2525import { platform } from 'node:os' ;
2626import { GLOBAL_HOOKS_DIR } from '../../../../lib/config-constants' ;
27- import logger from '../../../../lib/logger' ;
2827import { resolveAuth } from '../../../../lib/auth-resolver' ;
29- import { discoverProject } from '../../_common/discovery' ;
28+ import { findGitRoot } from '../../_common/discovery' ;
3029import { CommandFailedError , InvalidOptionError } from '../../_common/error' ;
3130import { performSecretInstall } from '../../install/secrets' ;
3231import { spawnProcess } from '../../../../lib/process' ;
32+ import * as fs from 'node:fs/promises' ;
3333import {
3434 blank ,
3535 confirmPrompt ,
@@ -41,6 +41,7 @@ import {
4141 text ,
4242 warn ,
4343} from '../../../../ui' ;
44+ import { GitRepo , resolveGitHooksDir } from '../../_common/git-repo' ;
4445import { HOOK_MARKER , getHookScript } from './git-shell-fragments' ;
4546import { installViaHusky } from './git-husky' ;
4647import {
@@ -81,47 +82,7 @@ interface HookInstallation {
8182 hooksDir : string ;
8283}
8384
84- export async function resolveGitHooksDir ( root : string ) : Promise < string > {
85- // core.hooksPath takes precedence over everything — it's what git actually uses to find hooks.
86- // Husky sets this to .husky; other tools (e.g. lefthook) may point elsewhere.
87- let configResult ;
88- try {
89- configResult = await spawnProcess ( 'git' , [ 'config' , 'core.hooksPath' ] , { cwd : root } ) ;
90- } catch {
91- configResult = null ;
92- }
93- if ( configResult ?. exitCode === 0 ) {
94- const configured = configResult . stdout . trim ( ) ;
95- if ( configured ) {
96- return isAbsolute ( configured ) ? configured : join ( root , configured ) ;
97- }
98- }
99-
100- const dotGit = join ( root , '.git' ) ;
101- try {
102- // Standard repo: .git is a directory — hooks live directly inside it, no subprocess needed
103- if ( statSync ( dotGit ) . isDirectory ( ) ) {
104- return join ( dotGit , 'hooks' ) ;
105- }
106- } catch {
107- // .git doesn't exist; fall through to git rev-parse
108- }
109- // Worktree or submodule: .git is a file pointer — ask git for the real hooks path
110- let result ;
111- try {
112- result = await spawnProcess ( 'git' , [ 'rev-parse' , '--git-path' , 'hooks' ] , { cwd : root } ) ;
113- } catch {
114- const errorMessage = 'git is not installed or not on PATH' ;
115- throw new CommandFailedError ( errorMessage ) ;
116- }
117- if ( result . exitCode !== 0 ) {
118- const detail = [ result . stderr , result . stdout ] . filter ( Boolean ) . join ( '\n' ) ;
119- const errorMessage = `Could not resolve git hooks directory (exit code ${ result . exitCode } ) ${ detail } ` ;
120- throw new CommandFailedError ( errorMessage ) ;
121- }
122- const resolved = result . stdout . trim ( ) ;
123- return isAbsolute ( resolved ) ? resolved : join ( root , resolved ) ;
124- }
85+ export { resolveGitHooksDir } from '../../_common/git-repo' ;
12586
12687export async function detectSonarHookInstallation ( root : string ) : Promise < HookInstallation > {
12788 let hooksDir : string ;
@@ -235,12 +196,11 @@ export async function installViaGitHooks(
235196) : Promise < void > {
236197 mkdirSync ( hooksDir , { recursive : true } ) ;
237198 const hookPath = join ( hooksDir , hook ) ;
238- const fs = await import ( 'node:fs/promises' ) ;
239199 if ( existsSync ( hookPath ) ) {
240200 const existing = await fs . readFile ( hookPath , 'utf-8' ) ;
241201 if ( ! existing . includes ( HOOK_MARKER ) && ! force ) {
242202 warn ( `A different ${ hook } hook already exists at ${ hookPath } .` ) ;
243- text ( ' Use --force to replace it, or add the secrets check manually .' ) ;
203+ text ( ' Use --force to replace it.' ) ;
244204 throw new CommandFailedError (
245205 `Refusing to overwrite existing ${ hook } hook at ${ hookPath } . Use --force to replace.` ,
246206 ) ;
@@ -291,14 +251,13 @@ async function integrateGitGlobal(options: IntegrateGitOptions): Promise<void> {
291251 'core.hooksPath' ,
292252 toForwardSlash ( GLOBAL_HOOKS_DIR ) ,
293253 ] ) ;
294- } catch {
295- const msg = 'git is not installed or not on PATH' ;
296- throw new CommandFailedError ( msg ) ;
254+ } catch ( error ) {
255+ const message = error instanceof Error ? error . message : String ( error ) ;
256+ throw new CommandFailedError ( `Failed to run git [ ${ message } ]` ) ;
297257 }
298258 if ( gitResult . exitCode !== 0 ) {
299259 const detail = [ gitResult . stderr , gitResult . stdout ] . filter ( Boolean ) . join ( '\n' ) ;
300260 const msg = `git config --global core.hooksPath failed (exit code ${ gitResult . exitCode } ): ${ detail } ` ;
301- logger . error ( msg ) ;
302261 throw new CommandFailedError ( msg ) ;
303262 }
304263
@@ -322,14 +281,14 @@ export async function integrateGit(options: IntegrateGitOptions): Promise<void>
322281 return integrateGitGlobal ( options ) ;
323282 }
324283
325- const projectInfo = await discoverProject ( process . cwd ( ) ) ;
326- if ( ! projectInfo . isGitRepo ) {
284+ const { gitRoot , isGit } = findGitRoot ( process . cwd ( ) ) ;
285+ if ( ! isGit ) {
327286 const errorMessage =
328287 'No git repository found. Please run this command from inside a git repository, or use --global to install a global hook.' ;
329288 throw new CommandFailedError ( errorMessage ) ;
330289 }
331290
332- text ( `We will install the hook in this repository: ${ projectInfo . rootDir } ` ) ;
291+ text ( `We will install the hook in this repository: ${ gitRoot } ` ) ;
333292 blank ( ) ;
334293
335294 if ( ! options . nonInteractive ) {
@@ -340,29 +299,22 @@ export async function integrateGit(options: IntegrateGitOptions): Promise<void>
340299 }
341300 blank ( ) ;
342301
343- const installation = await detectSonarHookInstallation ( projectInfo . rootDir ) ;
344- const useHusky = toForwardSlash ( installation . hooksDir ) . startsWith (
345- toForwardSlash ( join ( projectInfo . rootDir , '.husky' ) ) ,
346- ) ;
347- const usePreCommitConfig = existsSync ( join ( projectInfo . rootDir , PRE_COMMIT_CONFIG_FILE ) ) ;
348-
302+ const gitRepo = new GitRepo ( gitRoot ) ;
349303 const hook = await resolveHookType ( options ) ;
350304 text ( `Hook: ${ hook } ` ) ;
351305 blank ( ) ;
352306
353307 await ensureSonarSecrets ( ) ;
354308
355- const huskyHookPath = join ( projectInfo . rootDir , '.husky' , hook ) ;
356-
357- if ( usePreCommitConfig ) {
358- await installViaPreCommitFramework ( projectInfo . rootDir , hook ) ;
359- } else if ( useHusky ) {
360- await installViaHusky ( huskyHookPath , hook ) ;
309+ if ( gitRepo . usesPreCommitFramework ( ) ) {
310+ await installViaPreCommitFramework ( gitRepo . rootDir , hook ) ;
311+ } else if ( await gitRepo . usesHusky ( ) ) {
312+ await installViaHusky ( gitRepo . getHuskyHookPath ( hook ) , hook ) ;
361313 } else {
362- await installViaGitHooks ( installation . hooksDir , hook , options . force ) ;
314+ await installViaGitHooks ( await gitRepo . getHooksDir ( ) , hook , options . force ) ;
363315 }
364316
365317 showPostInstallInfo ( hook ) ;
366- await showInstallationStatus ( projectInfo . rootDir ) ;
318+ await showInstallationStatus ( gitRoot ) ;
367319 showVerificationGuide ( hook ) ;
368320}
0 commit comments