Skip to content

Commit 3e64a50

Browse files
committed
refactor(setup): simplify hook installation
1 parent 2a5eae3 commit 3e64a50

1 file changed

Lines changed: 43 additions & 80 deletions

File tree

‎packages/rstack/src/setup/install.ts‎

Lines changed: 43 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ type GitContext = {
4646
projectPath: string;
4747
};
4848

49-
type GeneratedDirectoryState =
50-
{ kind: 'empty' } | { kind: 'foreign' } | { kind: 'owned'; project: string };
51-
5249
const 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
170157
const 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

198172
const 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

Comments
 (0)