11import { spawnSync } from 'node:child_process' ;
22import {
3+ chmodSync ,
34 existsSync ,
45 mkdirSync ,
56 mkdtempSync ,
@@ -34,6 +35,16 @@ const runSetup = (args: string[], runCwd: string = cwd) =>
3435 env,
3536 } ) ;
3637
38+ const runSetupSuccessfully = ( args : string [ ] , runCwd : string = cwd ) : string => {
39+ const result = runSetup ( args , runCwd ) ;
40+ if ( result . status !== 0 ) {
41+ throw new Error (
42+ result . stderr || result . error ?. message || `Exited with ${ result . status } ` ,
43+ ) ;
44+ }
45+ return `${ result . stdout } ${ result . stderr } ` ;
46+ } ;
47+
3748beforeEach ( ( ) => {
3849 cwd = mkdtempSync ( path . join ( import . meta. dirname , 'test-temp-rstack setup ' ) ) ;
3950 env = {
@@ -108,6 +119,65 @@ test('installs hooks silently without loading Rstack config', ({
108119 expect ( execCli ( 'setup' , { cwd, env } ) ) . toBe ( '' ) ;
109120} ) ;
110121
122+ test ( 'guides and forces setup while preserving existing hooks' , ( {
123+ expect,
124+ } ) => {
125+ initRepository ( ) ;
126+ const existingHook = path . join ( cwd , '.git' , 'hooks' , 'pre-commit' ) ;
127+ writeFileSync (
128+ existingHook ,
129+ "#!/usr/bin/env sh\nprintf 'ran\\n' > old-hook-ran\n" ,
130+ ) ;
131+ chmodSync ( existingHook , 0o755 ) ;
132+
133+ const skippedOutput = runSetupSuccessfully ( [ ] ) ;
134+ expect ( skippedOutput ) . toContain (
135+ 'Git hooks setup skipped: existing Git hooks were found: pre-commit.' ,
136+ ) ;
137+ expect ( skippedOutput ) . toContain (
138+ 'To continue, run rs setup --force. Existing hook files will be preserved but become inactive.' ,
139+ ) ;
140+
141+ const forcedOutput = runSetupSuccessfully ( [ '--force' ] ) ;
142+ expect ( forcedOutput ) . toContain (
143+ 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.' ,
144+ ) ;
145+ expect ( forcedOutput ) . toContain (
146+ 'The existing files were preserved and will become active again if core.hooksPath is unset.' ,
147+ ) ;
148+ expect ( git ( [ 'config' , '--local' , '--get' , 'core.hooksPath' ] ) ) . toBe ( hooksPath ) ;
149+
150+ git ( [ 'hook' , 'run' , 'pre-commit' ] ) ;
151+ expect ( existsSync ( path . join ( cwd , 'old-hook-ran' ) ) ) . toBe ( false ) ;
152+
153+ git ( [ 'config' , '--local' , '--unset' , 'core.hooksPath' ] ) ;
154+ git ( [ 'hook' , 'run' , 'pre-commit' ] ) ;
155+ expect ( existsSync ( path . join ( cwd , 'old-hook-ran' ) ) ) . toBe ( true ) ;
156+
157+ expect ( runSetupSuccessfully ( [ '-f' ] ) ) . toContain (
158+ 'The previous Git hooks path ".git/hooks" is now inactive: pre-commit.' ,
159+ ) ;
160+ } ) ;
161+
162+ test ( 'reports how to restore a replaced hooks path' , ( { expect } ) => {
163+ initRepository ( ) ;
164+ const existingDirectory = path . join ( cwd , '.husky' , '_' ) ;
165+ mkdirSync ( existingDirectory , { recursive : true } ) ;
166+ writeFileSync (
167+ path . join ( existingDirectory , 'pre-commit' ) ,
168+ '#!/usr/bin/env sh\n' ,
169+ ) ;
170+ git ( [ 'config' , '--local' , 'core.hooksPath' , '.husky/_' ] ) ;
171+
172+ const output = runSetupSuccessfully ( [ '--force' ] ) ;
173+ expect ( output ) . toContain (
174+ 'The previous Git hooks path ".husky/_" is now inactive: pre-commit.' ,
175+ ) ;
176+ expect ( output ) . toContain (
177+ 'The existing files were preserved. Set core.hooksPath back to this path to use them again.' ,
178+ ) ;
179+ } ) ;
180+
111181test ( 'installs root-relative hooks and reports owner conflicts' , ( {
112182 execCli,
113183 expect,
@@ -126,9 +196,7 @@ test('installs root-relative hooks and reports owner conflicts', ({
126196 ) ;
127197 expect ( existsSync ( path . join ( cwd , 'custom hooks' , '_' , 'runner' ) ) ) . toBe ( true ) ;
128198
129- const conflict = runSetup ( [ '--hooks-dir' , 'custom hooks' ] , docs ) ;
130- expect ( conflict . status ) . toBe ( 0 ) ;
131- expect ( `${ conflict . stdout } ${ conflict . stderr } ` ) . toContain (
199+ expect ( runSetupSuccessfully ( [ '--hooks-dir' , 'custom hooks' ] , docs ) ) . toContain (
132200 'Git hooks are already managed by Rstack project "frontend"' ,
133201 ) ;
134202} ) ;
0 commit comments