@@ -46,9 +46,6 @@ type GitContext = {
4646 projectPath : string ;
4747} ;
4848
49- type GeneratedDirectoryState =
50- { kind : 'empty' } | { kind : 'foreign' } | { kind : 'owned' ; project : string } ;
51-
5249const fail = ( reason : string , message : string ) : FailedInstallResult => ( {
5350 status : 'failed' ,
5451 reason,
@@ -116,42 +113,32 @@ const resolveGitContext = (cwd: string): GitContext | InstallResult => {
116113
117114 const [
118115 insideWorkTree = '' ,
119- gitRoot ,
120- repositoryPrefix ,
121- gitCommonDirectory ,
122- effectiveHooksDirectory ,
116+ gitRoot = '' ,
117+ repositoryPrefix = '' ,
118+ gitCommonDirectory = '' ,
119+ effectiveHooksDirectory = '' ,
123120 ] = removeLineEnding ( repository . stdout ) . split ( / \r ? \n / u) ;
124121
125- if ( repository . status !== 0 ) {
126- if ( insideWorkTree . trim ( ) === 'true' ) {
127- return fail (
128- 'git-command-failed' ,
129- `Failed to resolve the Git repository paths: ${ repository . stderr . trim ( ) } ` ,
130- ) ;
131- }
122+ if ( insideWorkTree !== 'true' ) {
132123 return skip ( 'not-git-repository' ) ;
133124 }
134125
135- if ( insideWorkTree . trim ( ) !== 'true' ) {
136- return skip ( 'not-git-repository' ) ;
126+ if ( repository . status !== 0 ) {
127+ return fail (
128+ 'git-command-failed' ,
129+ `Failed to resolve the Git repository paths: ${ repository . stderr . trim ( ) } ` ,
130+ ) ;
137131 }
138132
139- if (
140- gitRoot === undefined ||
141- repositoryPrefix === undefined ||
142- gitCommonDirectory === undefined ||
143- effectiveHooksDirectory === undefined
144- ) {
133+ if ( ! gitRoot || ! gitCommonDirectory || ! effectiveHooksDirectory ) {
145134 return fail ( 'git-command-failed' , 'Failed to resolve the Git repository paths.' ) ;
146135 }
147136
148- const normalizedPrefix = repositoryPrefix . replaceAll ( '\\' , '/' ) . replace ( / \/ $ / u, '' ) ;
149-
150137 return {
151138 defaultHooksDirectory : path . join ( gitCommonDirectory , 'hooks' ) ,
152139 effectiveHooksDirectory,
153140 gitRoot,
154- projectPath : normalizedPrefix || '.' ,
141+ projectPath : repositoryPrefix . replaceAll ( '\\' , '/' ) . replace ( / \/ $ / u , '' ) || '.' ,
155142 } ;
156143} ;
157144
@@ -170,29 +157,16 @@ const isCurrentFile = (filePath: string, content: string, executable = false): b
170157const isSamePath = ( first : string , second : string ) : boolean =>
171158 path . resolve ( first ) === path . resolve ( second ) ;
172159
173- const ownerContent = ( project : string ) : string => `${ project } \n` ;
174-
175- const readGeneratedDirectoryState = ( directory : string ) : GeneratedDirectoryState => {
176- let entries : string [ ] ;
160+ const readOwner = ( directory : string ) : string | undefined => {
177161 try {
178- entries = readdirSync ( directory ) ;
162+ const content = readFileSync ( path . join ( directory , ownerFileName ) , 'utf8' ) ;
163+ const owner = removeLineEnding ( content ) ;
164+ return content === `${ owner } \n` && owner . length > 0 && ! / [ \r \n ] / u. test ( owner )
165+ ? owner
166+ : undefined ;
179167 } catch {
180- return { kind : 'empty' } ;
181- }
182-
183- if ( entries . includes ( ownerFileName ) ) {
184- try {
185- const content = readFileSync ( path . join ( directory , ownerFileName ) , 'utf8' ) ;
186- const project = removeLineEnding ( content ) ;
187- return content === ownerContent ( project ) && project . length > 0 && ! / [ \r \n ] / u. test ( project )
188- ? { kind : 'owned' , project }
189- : { kind : 'foreign' } ;
190- } catch {
191- return { kind : 'foreign' } ;
192- }
168+ return undefined ;
193169 }
194-
195- return entries . every ( ( entry ) => entry === '.gitignore' ) ? { kind : 'empty' } : { kind : 'foreign' } ;
196170} ;
197171
198172const displayPath = ( gitRoot : string , filePath : string ) : string => {
@@ -215,37 +189,30 @@ const claimOwner = (
215189 project : string ,
216190) : SkippedInstallResult | undefined => {
217191 const ownerPath = path . join ( directory , ownerFileName ) ;
218- const content = ownerContent ( project ) ;
219- const state = readGeneratedDirectoryState ( directory ) ;
192+ const owner = readOwner ( directory ) ;
220193
221- if ( state . kind === 'owned' && state . project !== project ) {
222- return ownerConflict ( state . project ) ;
223- }
224- if ( state . kind === 'foreign' ) {
225- return directoryConflict ( gitRoot , directory ) ;
194+ if ( owner ) {
195+ return owner === project ? undefined : ownerConflict ( owner ) ;
226196 }
227197
228- if ( state . kind = == 'owned' ) {
229- return undefined ;
198+ if ( readdirSync ( directory ) . some ( ( entry ) => entry ! == '.gitignore' ) ) {
199+ return directoryConflict ( gitRoot , directory ) ;
230200 }
231201
232202 try {
233203 // Exclusive creation makes concurrent prepare scripts agree on one owner.
234- writeFileSync ( ownerPath , content , { flag : 'wx' } ) ;
204+ writeFileSync ( ownerPath , ` ${ project } \n` , { flag : 'wx' } ) ;
235205 } catch ( error ) {
236206 const code = error instanceof Error && 'code' in error ? error . code : undefined ;
237207 if ( code !== 'EEXIST' ) {
238208 throw error ;
239209 }
240210
241- const concurrentState = readGeneratedDirectoryState ( directory ) ;
242- if ( concurrentState . kind === 'owned' && concurrentState . project === project ) {
243- return undefined ;
211+ const concurrentOwner = readOwner ( directory ) ;
212+ if ( ! concurrentOwner ) {
213+ return directoryConflict ( gitRoot , directory ) ;
244214 }
245- if ( concurrentState . kind === 'owned' ) {
246- return ownerConflict ( concurrentState . project ) ;
247- }
248- return directoryConflict ( gitRoot , directory ) ;
215+ return concurrentOwner === project ? undefined : ownerConflict ( concurrentOwner ) ;
249216 }
250217
251218 return undefined ;
@@ -280,17 +247,16 @@ export const installHooks = ({
280247 const usesDefaultHooks = isSamePath ( effectiveHooksDirectory , defaultHooksDirectory ) ;
281248
282249 if ( ! hooksPathMatches && ! usesDefaultHooks ) {
283- const activeState = readGeneratedDirectoryState ( effectiveHooksDirectory ) ;
284- if ( activeState . kind === 'owned' ) {
285- if ( activeState . project !== projectPath ) {
286- return ownerConflict ( activeState . project ) ;
287- }
288- } else {
250+ const activeOwner = readOwner ( effectiveHooksDirectory ) ;
251+ if ( ! activeOwner ) {
289252 return skip (
290253 'hooks-path-conflict' ,
291254 `Git hooks are already configured at "${ displayPath ( gitRoot , effectiveHooksDirectory ) } "` ,
292255 ) ;
293256 }
257+ if ( activeOwner !== projectPath ) {
258+ return ownerConflict ( activeOwner ) ;
259+ }
294260 }
295261
296262 if ( usesDefaultHooks ) {
@@ -304,25 +270,22 @@ export const installHooks = ({
304270 }
305271
306272 const files = Object . entries ( createHookFiles ( ) ) ;
307- const expectedOwner = ownerContent ( projectPath ) ;
308- // Skip all writes only when the config, owner, generated content, and executable modes match.
309- const unchanged =
310- hooksPathMatches &&
311- isCurrentFile ( path . join ( directory , ownerFileName ) , expectedOwner ) &&
312- isCurrentFile ( path . join ( directory , '.gitignore' ) , gitignore ) &&
313- files . every ( ( [ name , content ] ) => isCurrentFile ( path . join ( directory , name ) , content , true ) ) ;
314-
315- if ( unchanged ) {
316- return { status : 'unchanged' , hooksPath } ;
317- }
318-
319273 try {
320274 mkdirSync ( directory , { recursive : true } ) ;
321275 const ownerResult = claimOwner ( directory , gitRoot , projectPath ) ;
322276 if ( ownerResult ) {
323277 return ownerResult ;
324278 }
325279
280+ // Skip generated file writes when their content and executable modes match.
281+ const unchanged =
282+ hooksPathMatches &&
283+ isCurrentFile ( path . join ( directory , '.gitignore' ) , gitignore ) &&
284+ files . every ( ( [ name , content ] ) => isCurrentFile ( path . join ( directory , name ) , content , true ) ) ;
285+ if ( unchanged ) {
286+ return { status : 'unchanged' , hooksPath } ;
287+ }
288+
326289 writeFileSync ( path . join ( directory , '.gitignore' ) , gitignore ) ;
327290
328291 for ( const [ name , content ] of files ) {
0 commit comments