Skip to content

Commit 1281605

Browse files
CLI-9 PR review
1 parent 29b2b02 commit 1281605

File tree

12 files changed

+168
-116
lines changed

12 files changed

+168
-116
lines changed

.npmrc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
save-exact=true
2-
registry=https://repox.jfrog.io/artifactory/api/npm/npm/

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@
7171
"istanbul-lib-instrument": "6.0.3",
7272
"istanbul-lib-report": "3.0.1",
7373
"istanbul-reports": "3.2.0",
74-
"js-yaml": "^4.1.1",
7574
"lcov-result-merger": "5.0.1",
7675
"@types/js-yaml": "^4.0.9",
7776
"lint-staged": "16.2.7",

src/cli/command-tree.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ integrateCommand
109109
'--global',
110110
'Install hook globally for all repositories (sets git config --global core.hooksPath)',
111111
)
112-
.action((options: IntegrateGitOptions) => runCommand(() => integrateGit(options)));
112+
.authenticatedAction((auth, options: IntegrateGitOptions) => integrateGit(options, auth));
113113

114114
// List Sonar resources
115115
const list = COMMAND_TREE.command('list').description('List Sonar resources');

src/cli/commands/_common/discovery.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ export async function discoverProject(startDir: string): Promise<DiscoveredProje
162162
return config;
163163
}
164164

165-
function findGitRoot(startDir: string): { gitRoot: string; isGit: boolean } {
165+
export function findGitRoot(startDir: string): { gitRoot: string; isGit: boolean } {
166166
let dir = startDir;
167167

168168
for (;;) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* SonarQube CLI
3+
* Copyright (C) 2026 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
// Git repository abstraction for hook installation: root dir, hooks path, and framework detection.
22+
23+
import { existsSync, statSync } from 'node:fs';
24+
import { isAbsolute, join } from 'node:path';
25+
import { CommandFailedError } from './error';
26+
import { spawnProcess } from '../../../lib/process';
27+
28+
const PRE_COMMIT_CONFIG_FILE = '.pre-commit-config.yaml';
29+
export const toForwardSlash = (p: string) => p.replaceAll('\\', '/');
30+
31+
/**
32+
* Resolves the directory git uses for hooks (core.hooksPath or .git/hooks).
33+
*/
34+
export async function resolveGitHooksDir(root: string): Promise<string> {
35+
let configResult;
36+
try {
37+
configResult = await spawnProcess('git', ['config', 'core.hooksPath'], { cwd: root });
38+
} catch {
39+
configResult = null;
40+
}
41+
if (configResult?.exitCode === 0) {
42+
const configured = configResult.stdout.trim();
43+
if (configured) {
44+
return isAbsolute(configured) ? configured : join(root, configured);
45+
}
46+
}
47+
48+
const dotGit = join(root, '.git');
49+
try {
50+
if (statSync(dotGit).isDirectory()) {
51+
return join(dotGit, 'hooks');
52+
}
53+
} catch {
54+
// .git is a file (worktree) or missing — use git rev-parse
55+
}
56+
57+
let result;
58+
try {
59+
result = await spawnProcess('git', ['rev-parse', '--git-path', 'hooks'], { cwd: root });
60+
} catch (error) {
61+
const message = error instanceof Error ? error.message : String(error);
62+
throw new CommandFailedError(`Failed to run git [${message}]`);
63+
}
64+
if (result.exitCode !== 0) {
65+
const detail = [result.stderr, result.stdout].filter((s) => s.length > 0).join('\n');
66+
throw new CommandFailedError(
67+
`Could not resolve git hooks directory (exit code ${result.exitCode}) ${detail}`,
68+
);
69+
}
70+
const resolved = result.stdout.trim();
71+
return isAbsolute(resolved) ? resolved : join(root, resolved);
72+
}
73+
74+
/**
75+
* Represents a git repository at a given root. Use to decide hook installation strategy
76+
* without resolving all state up front (e.g. only resolve hooks dir when not using pre-commit).
77+
*/
78+
export class GitRepo {
79+
readonly rootDir: string;
80+
private _hooksDir: Promise<string> | null = null;
81+
82+
constructor(rootDir: string) {
83+
this.rootDir = rootDir;
84+
}
85+
86+
/** True if the repo uses the pre-commit framework (.pre-commit-config.yaml). */
87+
usesPreCommitFramework(): boolean {
88+
return existsSync(join(this.rootDir, PRE_COMMIT_CONFIG_FILE));
89+
}
90+
91+
private async getHooksDirOnce(): Promise<string> {
92+
this._hooksDir ??= resolveGitHooksDir(this.rootDir);
93+
return this._hooksDir;
94+
}
95+
96+
/** True if git's hooks path points to .husky (Husky is in use). */
97+
async usesHusky(): Promise<boolean> {
98+
const hooksDir = await this.getHooksDirOnce();
99+
return toForwardSlash(hooksDir).startsWith(toForwardSlash(join(this.rootDir, '.husky')));
100+
}
101+
102+
/** Resolved git hooks directory (core.hooksPath or .git/hooks). */
103+
async getHooksDir(): Promise<string> {
104+
return this.getHooksDirOnce();
105+
}
106+
107+
/** Path to the Husky hook file for the given hook name (e.g. 'pre-commit', 'pre-push'). */
108+
getHuskyHookPath(hook: string): string {
109+
return join(this.rootDir, '.husky', hook);
110+
}
111+
}

src/cli/commands/analyze/secrets.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,9 @@ function handleScanFailure(
311311
}
312312

313313
if (exitCode === EXIT_CODE_SECRETS_FOUND) {
314-
error(`Secrets found (${scanDurationMs}ms)`);
315-
logger.error(`Secrets found, exit code: ${exitCode}`);
316314
throw new CommandFailedError(`Secrets found (${scanDurationMs}ms)`, exitCode);
317315
}
318316

319-
error(`Scan error (exit code ${exitCode}, ${scanDurationMs}ms)`);
320-
logger.error(`Scan failed with exit code: ${exitCode}`);
321317
throw new CommandFailedError(`Scan error (exit code ${exitCode})`, exitCode);
322318
}
323319

src/cli/commands/integrate/git/git-husky.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,25 @@
2323
import { info, success } from '../../../../ui';
2424
import { HOOK_MARKER, getHuskySnippet } from './git-shell-fragments';
2525
import type { GitHookType } from '.';
26+
import { readFile, writeFile } from 'node:fs/promises';
2627

2728
export async function installViaHusky(huskyHookPath: string, hook: GitHookType): Promise<void> {
28-
const fs = await import('node:fs/promises');
2929
let content: string;
3030
try {
31-
content = await fs.readFile(huskyHookPath, 'utf-8');
32-
} catch {
33-
content = '';
31+
content = await readFile(huskyHookPath, 'utf-8');
32+
} catch (error) {
33+
const err = error as NodeJS.ErrnoException;
34+
if (err.code === 'ENOENT') {
35+
content = '';
36+
} else {
37+
throw error;
38+
}
3439
}
3540
if (content.includes(HOOK_MARKER)) {
3641
info(`Secrets check already present in .husky/${hook}.`);
3742
return;
3843
}
3944
const newContent = content ? content.trimEnd() + getHuskySnippet(hook) : getHuskySnippet(hook);
40-
await fs.writeFile(huskyHookPath, newContent, { encoding: 'utf-8', mode: 0o755 });
45+
await writeFile(huskyHookPath, newContent, { encoding: 'utf-8', mode: 0o755 });
4146
success(`${hook} hook installed (Husky detected: added to .husky/${hook}).`);
4247
}

src/cli/commands/integrate/git/git-precommit-framework.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { join } from 'node:path';
2525
import yaml from 'js-yaml';
2626
import { spawnProcess } from '../../../../lib/process';
2727
import { CommandFailedError } from '../../_common/error';
28-
import { error, success } from '../../../../ui';
28+
import { success } from '../../../../ui';
2929
import type { GitHookType } from '.';
3030

3131
export const PRE_COMMIT_CONFIG_FILE = '.pre-commit-config.yaml';
@@ -113,10 +113,18 @@ export function upsertPreCommitConfig(root: string, stage: GitHookType): void {
113113
}
114114

115115
async function runPreCommitCommand(args: string[], cwd: string): Promise<void> {
116-
const result = await spawnProcess('pre-commit', args, { cwd });
116+
let result;
117+
try {
118+
result = await spawnProcess('pre-commit', args, { cwd });
119+
} catch (error) {
120+
const message = error instanceof Error ? error.message : String(error);
121+
throw new CommandFailedError(`Failed to run pre-commit [${message}]`);
122+
}
117123
if (result.exitCode !== 0) {
118124
const detail = [result.stderr, result.stdout].filter(Boolean).join('\n');
119-
throw new Error(`pre-commit ${args.join(' ')} failed (exit code ${result.exitCode}) ${detail}`);
125+
throw new CommandFailedError(
126+
`pre-commit ${args.join(' ')} failed (exit code ${result.exitCode}) ${detail}`,
127+
);
120128
}
121129
}
122130

@@ -144,7 +152,6 @@ export async function installViaPreCommitFramework(root: string, hook: GitHookTy
144152
await runPreCommitInstall(root, hook);
145153
} catch {
146154
const errorMessage = `Updated ${PRE_COMMIT_CONFIG_FILE} but pre-commit commands failed. Install the pre-commit framework (e.g. pip install pre-commit) and run: pre-commit uninstall && pre-commit clean && pre-commit install${hook === 'pre-push' ? ' && pre-commit install --hook-type pre-push' : ''}`;
147-
error(errorMessage);
148155
throw new CommandFailedError(errorMessage);
149156
}
150157
success(`${hook} hook installed (pre-commit framework: added to ${PRE_COMMIT_CONFIG_FILE}).`);

0 commit comments

Comments
 (0)