diff --git a/changelog.d/20241014_131933_aurelien.gateau_use_ui_verbose.md b/changelog.d/20241014_131933_aurelien.gateau_use_ui_verbose.md new file mode 100644 index 0000000000..24e6e5be72 --- /dev/null +++ b/changelog.d/20241014_131933_aurelien.gateau_use_ui_verbose.md @@ -0,0 +1,3 @@ +### Changed + +- Adding the `--debug` argument now automatically turns on verbose mode. diff --git a/ggshield/__main__.py b/ggshield/__main__.py index ae18210709..be3fef13a8 100644 --- a/ggshield/__main__.py +++ b/ggshield/__main__.py @@ -28,7 +28,7 @@ from ggshield.core.config import Config from ggshield.core.env_utils import load_dot_env from ggshield.core.errors import ExitCode -from ggshield.core.ui import log_utils +from ggshield.core.ui import ensure_level, log_utils from ggshield.core.ui.rich import RichGGShieldUI from ggshield.utils.click import RealPath from ggshield.utils.os import getenv_bool @@ -57,19 +57,6 @@ def exit_code(ctx: click.Context, exit_code: int, **kwargs: Any) -> int: return exit_code -def config_path_callback( - ctx: click.Context, param: click.Parameter, value: Optional[Path] -) -> Optional[Path]: - # The --config option is marked as "is_eager" to ensure it's called before all the - # others. This makes it the right place to create the configuration object. - if not ctx.obj: - ctx.obj = ContextObj() - ctx.obj.cache = Cache() - - ctx.obj.config = Config(value) - return value - - @click.group( context_settings={"help_option_names": ["-h", "--help"]}, commands={ @@ -91,24 +78,38 @@ def config_path_callback( type=RealPath(exists=True, resolve_path=True, file_okay=True, dir_okay=False), is_eager=True, help="Set a custom config file. Ignores local and global config files.", - callback=config_path_callback, ) @add_common_options() @click.version_option(version=__version__) @click.pass_context def cli( ctx: click.Context, + *, + allow_self_signed: Optional[bool], + config_path: Optional[Path], **kwargs: Any, ) -> None: - load_dot_env() + # Create ContextObj, load config + ctx.obj = ctx_obj = ContextObj() + ctx_obj.cache = Cache() + ctx_obj.config = Config(config_path) + user_config = ctx_obj.config.user_config + + # If the config wants a higher UI level, set it now + if user_config.debug and ui.get_level() < ui.Level.DEBUG: + setup_debug_mode() + elif user_config.verbose and ui.get_level() < ui.Level.VERBOSE: + ensure_level(ui.Level.VERBOSE) - config = ContextObj.get(ctx).config + # Update allow_self_signed in the config + # TODO: this should be reworked: if a command which writes the config is called with + # --allow-self-signed, the config will contain `allow_self_signed: true`. + if allow_self_signed: + user_config.allow_self_signed = allow_self_signed - _set_color(ctx) + load_dot_env() - if config.user_config.debug: - # if `debug` is set in the configuration file, then setup debug mode now. - setup_debug_mode() + _set_color(ctx) def _set_color(ctx: click.Context): diff --git a/ggshield/cmd/hmsl/check_secret_manager/hashicorp_vault.py b/ggshield/cmd/hmsl/check_secret_manager/hashicorp_vault.py index 10aa6b2159..a37e2268df 100644 --- a/ggshield/cmd/hmsl/check_secret_manager/hashicorp_vault.py +++ b/ggshield/cmd/hmsl/check_secret_manager/hashicorp_vault.py @@ -13,7 +13,6 @@ json_option, text_json_format_option, ) -from ggshield.cmd.utils.context_obj import ContextObj from ggshield.core import ui from ggshield.core.errors import UnexpectedError from ggshield.core.text_utils import pluralize @@ -156,8 +155,7 @@ def check_hashicorp_vault_cmd( f"Could not fetch {len(result.not_fetched_paths)} paths. " "Make sure your token has access to all the secrets in your vault." ) - config = ContextObj.get(ctx).config - if config.user_config.verbose: + if ui.is_verbose(): ui.display_error("> The following paths could not be fetched:") for path in result.not_fetched_paths: ui.display_error(f"- {path}") diff --git a/ggshield/cmd/iac/scan/all.py b/ggshield/cmd/iac/scan/all.py index fdbff41789..41f408c328 100644 --- a/ggshield/cmd/iac/scan/all.py +++ b/ggshield/cmd/iac/scan/all.py @@ -84,10 +84,10 @@ def iac_scan_all( root = get_project_root_dir(directory) relative_paths = [str(x.resolve().relative_to(root)) for x in paths] - if config.user_config.verbose: - ui.display_info("> Scanned files") + if ui.is_verbose(): + ui.display_verbose("> Scanned files") for filepath in relative_paths: - ui.display_info(f"- {click.format_filename(filepath)}") + ui.display_verbose(f"- {click.format_filename(filepath)}") client = ctx_obj.client diff --git a/ggshield/cmd/iac/scan/ci.py b/ggshield/cmd/iac/scan/ci.py index 7e8a098c86..5f65adebc3 100644 --- a/ggshield/cmd/iac/scan/ci.py +++ b/ggshield/cmd/iac/scan/ci.py @@ -13,6 +13,7 @@ from ggshield.cmd.utils.common_decorators import display_beta_warning, exception_wrapper from ggshield.cmd.utils.common_options import directory_argument from ggshield.cmd.utils.context_obj import ContextObj +from ggshield.core import ui from ggshield.core.git_hooks.ci.get_scan_ci_parameters import ( NotAMergeRequestError, get_scan_ci_parameters, @@ -54,14 +55,9 @@ def scan_ci_cmd( # we will work with branch names and deep commits, so we run a git fetch to ensure the # branch names and commit sha are locally available git(["fetch"], cwd=directory) - params = get_scan_ci_parameters( - ci_mode, wd=directory, verbose=config.user_config.verbose - ) + params = get_scan_ci_parameters(ci_mode, wd=directory) if params is None: - click.echo( - "No commit found in merge request, skipping scan.", - err=True, - ) + ui.display_info("No commit found in merge request, skipping scan.") return 0 current_commit, reference_commit = params @@ -78,13 +74,10 @@ def scan_ci_cmd( augment_unignored_issues(config.user_config, result) return display_iac_scan_diff_result(ctx, directory, result) except NotAMergeRequestError: - click.echo( - ( - "WARNING: scan ci expects to be run in a merge-request pipeline.\n" - "No target branch could be identified, will perform a scan all instead.\n" - "This is a fallback behaviour, that will be removed in a future version." - ), - err=True, + ui.display_warning( + "scan ci expects to be run in a merge-request pipeline.\n" + "No target branch could be identified, will perform a scan all instead.\n" + "This is a fallback behaviour, that will be removed in a future version." ) result = iac_scan_all( ctx, directory, scan_mode=ScanMode.CI_ALL, ci_mode=ci_mode diff --git a/ggshield/cmd/iac/scan/diff.py b/ggshield/cmd/iac/scan/diff.py index 79ca062914..a739e663de 100644 --- a/ggshield/cmd/iac/scan/diff.py +++ b/ggshield/cmd/iac/scan/diff.py @@ -115,31 +115,32 @@ def iac_scan_diff( check_directory_not_ignored(directory, exclusion_regexes) - verbose = config.user_config.verbose if config and config.user_config else False + verbose = ui.is_verbose() + if verbose: if previous_ref is None: - ui.display_info("> No file to scan in reference.") + ui.display_verbose("> No file to scan in reference.") else: - ui.display_info(f"> Scanned files in reference {previous_ref}") + ui.display_verbose(f"> Scanned files in reference {previous_ref}") filepaths = filter_iac_filepaths( directory, get_git_filepaths(directory, previous_ref) ) for filepath in filepaths: - ui.display_info(f"- {click.format_filename(filepath)}") - ui.display_info("") + ui.display_verbose(f"- {click.format_filename(filepath)}") + ui.display_verbose("") if current_ref is None: current_ref = INDEX_REF if include_staged else "HEAD" if verbose: if include_staged: - ui.display_info("> Scanned files in current state (staged)") + ui.display_verbose("> Scanned files in current state (staged)") else: - ui.display_info("> Scanned files in current state") + ui.display_verbose("> Scanned files in current state") filepaths = filter_iac_filepaths( directory, get_git_filepaths(directory=directory, ref=current_ref) ) for filepath in filepaths: - ui.display_info(f"- {click.format_filename(filepath)}") + ui.display_verbose(f"- {click.format_filename(filepath)}") modified_iac_files = [] diff --git a/ggshield/cmd/iac/scan/iac_scan_utils.py b/ggshield/cmd/iac/scan/iac_scan_utils.py index ed2394a370..3c7efecb01 100644 --- a/ggshield/cmd/iac/scan/iac_scan_utils.py +++ b/ggshield/cmd/iac/scan/iac_scan_utils.py @@ -38,7 +38,7 @@ def create_output_handler(ctx: click.Context) -> IaCOutputHandler: output_handler_cls = IaCJSONOutputHandler else: output_handler_cls = IaCTextOutputHandler - return output_handler_cls(verbose=ctx_obj.config.user_config.verbose) + return output_handler_cls(verbose=ui.is_verbose()) def handle_scan_error(client: GGClient, detail: Detail) -> None: diff --git a/ggshield/cmd/sca/scan/ci.py b/ggshield/cmd/sca/scan/ci.py index 69181afa64..3083bbb254 100644 --- a/ggshield/cmd/sca/scan/ci.py +++ b/ggshield/cmd/sca/scan/ci.py @@ -14,7 +14,7 @@ ) from ggshield.cmd.utils.common_decorators import exception_wrapper from ggshield.cmd.utils.common_options import directory_argument -from ggshield.cmd.utils.context_obj import ContextObj +from ggshield.core import ui from ggshield.core.git_hooks.ci.get_scan_ci_parameters import ( NotAMergeRequestError, get_scan_ci_parameters, @@ -63,7 +63,6 @@ def scan_ci_cmd( ignore_not_fixable, ) - config = ContextObj.get(ctx).config ci_mode = SupportedCI.from_ci_env() output_handler = create_output_handler(ctx) @@ -71,14 +70,9 @@ def scan_ci_cmd( # we will work with branch names and deep commits, so we run a git fetch to ensure the # branch names and commit sha are locally available git(["fetch"], cwd=directory) - params = get_scan_ci_parameters( - ci_mode, wd=directory, verbose=config.user_config.verbose - ) + params = get_scan_ci_parameters(ci_mode, wd=directory) if params is None: - click.echo( - "No commit found in merge request, skipping scan.", - err=True, - ) + ui.display_info("No commit found in merge request, skipping scan.") return 0 current_commit, reference_commit = params @@ -94,13 +88,10 @@ def scan_ci_cmd( scan = SCAScanDiffVulnerabilityCollection(id=str(directory), result=result) return output_handler.process_scan_diff_result(scan) except NotAMergeRequestError: - click.echo( - ( - "WARNING: scan ci expects to be run in a merge-request pipeline.\n" - "No target branch could be identified, will perform a scan all instead.\n" - "This is a fallback behaviour, that will be removed in a future version." - ), - err=True, + ui.display_warning( + "scan ci expects to be run in a merge-request pipeline.\n" + "No target branch could be identified, will perform a scan all instead.\n" + "This is a fallback behaviour, that will be removed in a future version." ) result = sca_scan_all(ctx, directory=directory, scan_mode=ScanMode.CI_ALL) scan = SCAScanAllVulnerabilityCollection(id=str(directory), result=result) diff --git a/ggshield/cmd/sca/scan/sca_scan_utils.py b/ggshield/cmd/sca/scan/sca_scan_utils.py index 6faf9c510f..847dbf77ca 100644 --- a/ggshield/cmd/sca/scan/sca_scan_utils.py +++ b/ggshield/cmd/sca/scan/sca_scan_utils.py @@ -64,10 +64,7 @@ def sca_scan_all( check_directory_not_ignored(directory, exclusion_regexes) sca_filepaths, sca_filter_status_code = get_sca_scan_all_filepaths( - directory=directory, - exclusion_regexes=exclusion_regexes, - verbose=config.user_config.verbose, - client=client, + directory=directory, exclusion_regexes=exclusion_regexes, client=client ) if len(sca_filepaths) == 0: @@ -109,10 +106,7 @@ def sca_scan_all( def get_sca_scan_all_filepaths( - directory: Path, - exclusion_regexes: Set[Pattern[str]], - verbose: bool, - client: GGClient, + directory: Path, exclusion_regexes: Set[Pattern[str]], client: GGClient ) -> Tuple[List[str], int]: """ Retrieve SCA related files of a directory. @@ -140,10 +134,10 @@ def get_sca_scan_all_filepaths( # Only sca_files field is useful in the case of a full_scan, # all the potential files already exist in `all_filepaths` sca_files = response.sca_files - if verbose: - ui.display_info("> Scanned files:") + if ui.is_verbose(): + ui.display_verbose("> Scanned files:") for filename in sca_files: - ui.display_info(f"- {click.format_filename(filename)}") + ui.display_verbose(f"- {click.format_filename(filename)}") return sca_files, response.status_code @@ -159,7 +153,7 @@ def create_output_handler(ctx: click.Context) -> SCAOutputHandler: output_handler_cls = SCATextOutputHandler config = ctx_obj.config return output_handler_cls( - verbose=config.user_config.verbose, exit_zero=config.user_config.exit_zero + verbose=ui.is_verbose(), exit_zero=config.user_config.exit_zero ) @@ -204,19 +198,11 @@ def sca_scan_diff( previous_files = [] else: previous_files = sca_files_from_git_repo( - directory, - previous_ref, - client, - exclusion_regexes=exclusion_regexes, - verbose=config.user_config.verbose, + directory, previous_ref, client, exclusion_regexes=exclusion_regexes ) current_files = sca_files_from_git_repo( - directory, - current_ref, - client, - exclusion_regexes=exclusion_regexes, - verbose=config.user_config.verbose, + directory, current_ref, client, exclusion_regexes=exclusion_regexes ) if len(previous_files) == 0 and len(current_files) == 0: diff --git a/ggshield/cmd/secret/scan/archive.py b/ggshield/cmd/secret/scan/archive.py index fa859968d4..f07d2e0017 100644 --- a/ggshield/cmd/secret/scan/archive.py +++ b/ggshield/cmd/secret/scan/archive.py @@ -44,17 +44,15 @@ def archive_cmd( ctx_obj = ContextObj.get(ctx) config = ctx_obj.config - verbose = config.user_config.verbose files, binary_paths = create_files_from_paths( paths=[temp_path], exclusion_regexes=ctx_obj.exclusion_regexes, list_files_mode=ListFilesMode.ALL, ) - if verbose: - print_file_list(files, binary_paths) + print_file_list(files, binary_paths) ui.display_heading("Starting scan") - with ui.create_scanner_ui(len(files), verbose=verbose) as scanner_ui: + with ui.create_scanner_ui(len(files)) as scanner_ui: scan_context = ScanContext( scan_mode=ScanMode.ARCHIVE, command_path=ctx.command_path, diff --git a/ggshield/cmd/secret/scan/changes.py b/ggshield/cmd/secret/scan/changes.py index 2628a368f1..8c91ffe6be 100644 --- a/ggshield/cmd/secret/scan/changes.py +++ b/ggshield/cmd/secret/scan/changes.py @@ -35,10 +35,9 @@ def changes_cmd(ctx: click.Context, **kwargs: Any) -> int: default_branch = get_default_branch() commit_list = get_list_commit_SHA(f"{default_branch}..HEAD") - if config.user_config.verbose: - ui.display_info( - f"Scan staged changes and {len(commit_list)} new {pluralize('commit', len(commit_list))}" - ) + ui.display_verbose( + f"Scan staged changes and {len(commit_list)} new {pluralize('commit', len(commit_list))}" + ) scan_context = ScanContext( scan_mode=ScanMode.CHANGE, @@ -54,5 +53,4 @@ def changes_cmd(ctx: click.Context, **kwargs: Any) -> int: exclusion_regexes=ctx_obj.exclusion_regexes, secret_config=config.user_config.secret, scan_context=scan_context, - verbose=config.user_config.verbose, ) diff --git a/ggshield/cmd/secret/scan/ci.py b/ggshield/cmd/secret/scan/ci.py index e0218fe57f..fb37dd7128 100644 --- a/ggshield/cmd/secret/scan/ci.py +++ b/ggshield/cmd/secret/scan/ci.py @@ -11,6 +11,7 @@ ) from ggshield.cmd.utils.common_decorators import exception_wrapper from ggshield.cmd.utils.context_obj import ContextObj +from ggshield.core import ui from ggshield.core.cache import ReadOnlyCache from ggshield.core.git_hooks.ci import collect_commit_range_from_ci_env from ggshield.core.scan import ScanContext, ScanMode @@ -32,11 +33,10 @@ def ci_cmd(ctx: click.Context, **kwargs: Any) -> int: if not (os.getenv("CI") or os.getenv("JENKINS_HOME") or os.getenv("BUILD_BUILDID")): raise UsageError("`secret scan ci` should only be used in a CI environment.") - commit_list, ci_mode = collect_commit_range_from_ci_env(config.user_config.verbose) + commit_list, ci_mode = collect_commit_range_from_ci_env() mode_header = f"{ScanMode.CI.value}/{ci_mode.value}" - if config.user_config.verbose: - click.echo(f"Commits to scan: {len(commit_list)}", err=True) + ui.display_verbose(f"Commits to scan: {len(commit_list)}") scan_context = ScanContext( scan_mode=mode_header, diff --git a/ggshield/cmd/secret/scan/docker.py b/ggshield/cmd/secret/scan/docker.py index 6289ce7d4c..8dc3046dc5 100644 --- a/ggshield/cmd/secret/scan/docker.py +++ b/ggshield/cmd/secret/scan/docker.py @@ -59,7 +59,6 @@ def docker_name_cmd( cache=ctx_obj.cache, secret_config=config.user_config.secret, scan_context=scan_context, - verbose=config.user_config.verbose, ) return output_handler.process_scan(scan) diff --git a/ggshield/cmd/secret/scan/docset.py b/ggshield/cmd/secret/scan/docset.py index 70f3147884..c7d7aa8066 100644 --- a/ggshield/cmd/secret/scan/docset.py +++ b/ggshield/cmd/secret/scan/docset.py @@ -25,10 +25,7 @@ def generate_files_from_docsets(file: TextIO) -> Iterator[Scannable]: def create_scans_from_docset_files( - scanner: SecretScanner, - input_files: Iterable[TextIO], - progress: GGShieldProgress, - verbose: bool = False, + scanner: SecretScanner, input_files: Iterable[TextIO], progress: GGShieldProgress ) -> List[SecretScanCollection]: scans: List[SecretScanCollection] = [] @@ -36,7 +33,7 @@ def create_scans_from_docset_files( ui.display_verbose(f"- {click.format_filename(input_file.name)}") files = generate_files_from_docsets(input_file) - with ui.create_message_only_scanner_ui(verbose=verbose) as scanner_ui: + with ui.create_message_only_scanner_ui() as scanner_ui: results = scanner.scan(files, scanner_ui=scanner_ui) scans.append( SecretScanCollection(id=input_file.name, type="docset", results=results) @@ -80,10 +77,7 @@ def docset_cmd( scan_context=scan_context, ) scans = create_scans_from_docset_files( - scanner=scanner, - input_files=files, - progress=progress, - verbose=config.user_config.verbose, + scanner=scanner, input_files=files, progress=progress ) return output_handler.process_scan( diff --git a/ggshield/cmd/secret/scan/path.py b/ggshield/cmd/secret/scan/path.py index 4e3f15cfa5..7fdcfc4a90 100644 --- a/ggshield/cmd/secret/scan/path.py +++ b/ggshield/cmd/secret/scan/path.py @@ -43,7 +43,6 @@ def path_cmd( ctx_obj = ContextObj.get(ctx) config = ctx_obj.config output_handler = create_output_handler(ctx) - verbose = config.user_config.verbose for path in paths: check_directory_not_ignored(path, ctx_obj.exclusion_regexes) @@ -62,16 +61,15 @@ def path_cmd( ListFilesMode.ALL_BUT_GITIGNORED if use_gitignore else ListFilesMode.ALL ), ) - if verbose: - print_file_list(files, binary_paths) + print_file_list(files, binary_paths) if not yes: confirm_scan(files) - if verbose: + if ui.is_verbose(): ui.display_heading("Starting scan") target = paths[0] if len(paths) == 1 else Path.cwd() target_path = target if target.is_dir() else target.parent - with ui.create_scanner_ui(len(files), verbose=verbose) as scanner_ui: + with ui.create_scanner_ui(len(files)) as scanner_ui: scan_context = ScanContext( scan_mode=ScanMode.PATH, command_path=ctx.command_path, diff --git a/ggshield/cmd/secret/scan/precommit.py b/ggshield/cmd/secret/scan/precommit.py index b8cf7dddd4..ae14dce96c 100644 --- a/ggshield/cmd/secret/scan/precommit.py +++ b/ggshield/cmd/secret/scan/precommit.py @@ -60,13 +60,12 @@ def precommit_cmd( """ ctx_obj = ContextObj.get(ctx) config = ctx_obj.config - verbose = config.user_config.verbose if check_user_requested_skip(): return 0 output_handler = SecretTextOutputHandler( - verbose=verbose, + verbose=ui.is_verbose(), client=ctx_obj.client, output=None, secret_config=config.user_config.secret, @@ -94,15 +93,12 @@ def precommit_cmd( scan_context=scan_context, secret_config=config.user_config.secret, ) - with ui.create_scanner_ui(len(commit.urls), verbose=verbose) as scanner_ui: + with ui.create_scanner_ui(len(commit.urls)) as scanner_ui: results = scanner.scan(commit.get_files(), scanner_ui) return_code = output_handler.process_scan( SecretScanCollection(id="cached", type="pre-commit", results=results) ) if return_code: - click.echo( - ctx_obj.client.remediation_messages.pre_commit, - err=True, - ) + ui.display_info(ctx_obj.client.remediation_messages.pre_commit) return return_code diff --git a/ggshield/cmd/secret/scan/prepush.py b/ggshield/cmd/secret/scan/prepush.py index c989486065..b25452e73e 100644 --- a/ggshield/cmd/secret/scan/prepush.py +++ b/ggshield/cmd/secret/scan/prepush.py @@ -11,6 +11,7 @@ from ggshield.cmd.utils.common_decorators import exception_wrapper from ggshield.cmd.utils.context_obj import ContextObj from ggshield.cmd.utils.hooks import check_user_requested_skip +from ggshield.core import ui from ggshield.core.git_hooks.prepush import collect_commits_refs from ggshield.core.scan import ScanContext, ScanMode from ggshield.utils.git_shell import ( @@ -44,13 +45,12 @@ def prepush_cmd(ctx: click.Context, prepush_args: List[str], **kwargs: Any) -> i logger.debug("refs=(%s, %s)", local_commit, remote_commit) if local_commit == EMPTY_SHA: - click.echo("Deletion event or nothing to scan.", err=True) + ui.display_info("Deletion event or nothing to scan.") return 0 if remote_commit == EMPTY_SHA: - click.echo( - f"New tree event. Scanning last {config.user_config.max_commits_for_hook} commits.", - err=True, + ui.display_info( + f"New tree event. Scanning last {config.user_config.max_commits_for_hook} commits." ) before = EMPTY_TREE after = local_commit @@ -65,24 +65,21 @@ def prepush_cmd(ctx: click.Context, prepush_args: List[str], **kwargs: Any) -> i ) if not commit_list: - click.echo( + ui.display_warning( "Unable to get commit range.\n" f" before: {before}\n" f" after: {after}\n" "Skipping pre-push hook\n", - err=True, ) return 0 if len(commit_list) > config.user_config.max_commits_for_hook: - click.echo( - f"Too many commits. Scanning last {config.user_config.max_commits_for_hook} commits\n", - err=True, + ui.display_info( + f"Too many commits. Scanning last {config.user_config.max_commits_for_hook} commits\n" ) commit_list = commit_list[-config.user_config.max_commits_for_hook :] - if config.user_config.verbose: - click.echo(f"Commits to scan: {len(commit_list)}", err=True) + ui.display_verbose(f"Commits to scan: {len(commit_list)}") check_git_dir() @@ -102,8 +99,5 @@ def prepush_cmd(ctx: click.Context, prepush_args: List[str], **kwargs: Any) -> i scan_context=scan_context, ) if return_code: - click.echo( - ctx_obj.client.remediation_messages.pre_push, - err=True, - ) + ui.display_info(ctx_obj.client.remediation_messages.pre_push) return return_code diff --git a/ggshield/cmd/secret/scan/prereceive.py b/ggshield/cmd/secret/scan/prereceive.py index 67ff0c3efd..5dd961a8d6 100644 --- a/ggshield/cmd/secret/scan/prereceive.py +++ b/ggshield/cmd/secret/scan/prereceive.py @@ -59,16 +59,13 @@ def _execute_prereceive( scan_context=scan_context, ) if return_code: - click.echo( - ( - config.user_config.secret.prereceive_remediation_message - or client.remediation_messages.pre_receive - ), - err=True, + ui.display_info( + config.user_config.secret.prereceive_remediation_message + or client.remediation_messages.pre_receive ) sys.exit(return_code) except Exception as error: - sys.exit(handle_exception(error, config.user_config.verbose)) + sys.exit(handle_exception(error)) @click.command() @@ -113,14 +110,12 @@ def prereceive_cmd( assert commit_list, "Commit list should not be empty at this point" if len(commit_list) > config.user_config.max_commits_for_hook: - click.echo( + ui.display_info( f"Too many commits. Scanning last {config.user_config.max_commits_for_hook} commits\n", - err=True, ) commit_list = commit_list[-config.user_config.max_commits_for_hook :] - if config.user_config.verbose: - click.echo(f"Commits to scan: {len(commit_list)}", err=True) + ui.display_verbose(f"Commits to scan: {len(commit_list)}") process = multiprocessing.Process( target=_execute_prereceive, diff --git a/ggshield/cmd/secret/scan/pypi.py b/ggshield/cmd/secret/scan/pypi.py index 5177655dd8..58a7ce41e4 100644 --- a/ggshield/cmd/secret/scan/pypi.py +++ b/ggshield/cmd/secret/scan/pypi.py @@ -98,7 +98,6 @@ def pypi_cmd( ctx_obj = ContextObj.get(ctx) config = ctx_obj.config output_handler = create_output_handler(ctx) - verbose = config.user_config.verbose with tempfile.TemporaryDirectory(suffix="ggshield") as temp_dir: temp_path = Path(temp_dir) @@ -109,11 +108,10 @@ def pypi_cmd( package_name=package_name, exclusion_regexes=ctx_obj.exclusion_regexes, ) - if verbose: - print_file_list(files, binary_paths) + print_file_list(files, binary_paths) ui.display_heading("Starting scan") - with ui.create_scanner_ui(len(files), verbose=verbose) as scanner_ui: + with ui.create_scanner_ui(len(files)) as scanner_ui: scan_context = ScanContext( scan_mode=ScanMode.PYPI, command_path=ctx.command_path, diff --git a/ggshield/cmd/secret/scan/range.py b/ggshield/cmd/secret/scan/range.py index 857e433444..7ae3d4e355 100644 --- a/ggshield/cmd/secret/scan/range.py +++ b/ggshield/cmd/secret/scan/range.py @@ -10,6 +10,7 @@ ) from ggshield.cmd.utils.common_decorators import exception_wrapper from ggshield.cmd.utils.context_obj import ContextObj +from ggshield.core import ui from ggshield.core.scan import ScanContext, ScanMode from ggshield.utils.git_shell import get_list_commit_SHA from ggshield.verticals.secret.repo import scan_commit_range @@ -37,8 +38,7 @@ def range_cmd( commit_list = get_list_commit_SHA(commit_range) if not commit_list: raise UsageError("invalid commit range") - if config.user_config.verbose: - click.echo(f"Commits to scan: {len(commit_list)}", err=True) + ui.display_verbose(f"Commits to scan: {len(commit_list)}") scan_context = ScanContext( scan_mode=ScanMode.COMMIT_RANGE, @@ -54,5 +54,4 @@ def range_cmd( exclusion_regexes=ctx_obj.exclusion_regexes, secret_config=config.user_config.secret, scan_context=scan_context, - verbose=config.user_config.verbose, ) diff --git a/ggshield/cmd/secret/scan/secret_scan_common_options.py b/ggshield/cmd/secret/scan/secret_scan_common_options.py index cc4a42dea9..e27d023c77 100644 --- a/ggshield/cmd/secret/scan/secret_scan_common_options.py +++ b/ggshield/cmd/secret/scan/secret_scan_common_options.py @@ -14,6 +14,7 @@ ) from ggshield.cmd.utils.context_obj import ContextObj from ggshield.cmd.utils.output_format import OutputFormat +from ggshield.core import ui from ggshield.core.config.user_config import SecretConfig from ggshield.core.filter import init_exclusion_regexes from ggshield.utils.click import RealPath @@ -159,7 +160,7 @@ def create_output_handler(ctx: click.Context) -> SecretOutputHandler: output_handler_cls = OUTPUT_HANDLER_CLASSES[ctx_obj.output_format] config = ctx_obj.config return output_handler_cls( - verbose=config.user_config.verbose, + verbose=ui.is_verbose(), client=ctx_obj.client, output=ctx_obj.output, secret_config=config.user_config.secret, diff --git a/ggshield/cmd/secret/scan/ui_utils.py b/ggshield/cmd/secret/scan/ui_utils.py index 7bbb2a80d5..d5806e7224 100644 --- a/ggshield/cmd/secret/scan/ui_utils.py +++ b/ggshield/cmd/secret/scan/ui_utils.py @@ -6,10 +6,12 @@ def print_file_list(files: List[Scannable], binary_paths: List[Path]) -> None: + if not ui.is_verbose(): + return if binary_paths: ui.display_heading("Ignored binary files") for path in binary_paths: - ui.display_info(f"- {path}") + ui.display_verbose(f"- {path}") ui.display_heading("Files to scan") for f in files: - ui.display_info(f"- {f.path}") + ui.display_verbose(f"- {f.path}") diff --git a/ggshield/cmd/utils/common_decorators.py b/ggshield/cmd/utils/common_decorators.py index b08f9809e9..4f0fc9a909 100644 --- a/ggshield/cmd/utils/common_decorators.py +++ b/ggshield/cmd/utils/common_decorators.py @@ -1,10 +1,8 @@ from functools import wraps from typing import Callable, TypeVar -import click from typing_extensions import ParamSpec -from ggshield.cmd.utils.context_obj import ContextObj from ggshield.core import ui from ggshield.core.errors import handle_exception @@ -19,9 +17,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> int: try: return func(*args, **kwargs) except Exception as error: - ctx = next(arg for arg in args if isinstance(arg, click.Context)) - config = ContextObj.get(ctx).config - return handle_exception(error, config.user_config.verbose) + return handle_exception(error) return wrapper diff --git a/ggshield/cmd/utils/common_options.py b/ggshield/cmd/utils/common_options.py index ddfe8dc0de..f129769a81 100644 --- a/ggshield/cmd/utils/common_options.py +++ b/ggshield/cmd/utils/common_options.py @@ -66,7 +66,13 @@ def create_config_callback(*option_names: str) -> ClickCallback[ArgT]: def callback( ctx: click.Context, param: click.Parameter, value: Optional[ArgT] ) -> Optional[ArgT]: - if value is not None: + if value is not None and ctx.obj is not None: + # If ctx.obj has not been defined yet, then it means we are in the top-level + # cli() function and the config has not been loaded yet, so we can't change + # it. + # + # cli() takes care of applying the few config-related options it receives + # itself. obj = get_config_from_context(ctx) for name in option_names[:-1]: obj = getattr(obj, name) @@ -76,13 +82,21 @@ def callback( return callback +def verbose_callback( + ctx: click.Context, param: click.Parameter, value: Optional[bool] +) -> Optional[bool]: + if value is not None: + ui.ensure_level(ui.Level.VERBOSE) + return value + + _verbose_option = click.option( "-v", "--verbose", is_flag=True, default=None, help="Verbose display mode.", - callback=create_config_callback("verbose"), + callback=verbose_callback, ) @@ -90,19 +104,14 @@ def debug_callback( ctx: click.Context, param: click.Parameter, value: Optional[bool] ) -> Optional[bool]: if value is not None: - ui.set_level(ui.Level.DEBUG) setup_debug_mode() return value -# The --debug option is marked as "is_eager" so that we can setup logs as soon as -# possible. If we don't then log commands for the creation of the Config instance -# are ignored. _debug_option = click.option( "--debug", is_flag=True, default=None, - is_eager=True, help="Send log output to stderr. Equivalent to `--log-file -`.", callback=debug_callback, ) @@ -112,17 +121,13 @@ def log_file_callback( ctx: click.Context, param: click.Parameter, value: Optional[str] ) -> Optional[str]: if value is not None: - setup_debug_mode(filename=None if value == "-" else value) + setup_debug_mode(filename=value) return value -# The --log-file option is marked as "is_eager" so that we can setup logs as soon as -# possible. If we don't then log commands for the creation of the Config instance -# are ignored. _log_file_option = click.option( "--log-file", metavar="FILE", - is_eager=True, help="Send log output to FILE. Use '-' to redirect to stderr.", envvar="GITGUARDIAN_LOG_FILE", callback=log_file_callback, diff --git a/ggshield/cmd/utils/context_obj.py b/ggshield/cmd/utils/context_obj.py index 20053942e3..e9f4e61790 100644 --- a/ggshield/cmd/utils/context_obj.py +++ b/ggshield/cmd/utils/context_obj.py @@ -81,4 +81,5 @@ def cache(self, value: Cache) -> None: def get(ctx: click.Context) -> "ContextObj": """The recommended way to get a ContextObj instance, see the class docstring for details""" + assert ctx.obj return cast(ContextObj, ctx.obj) diff --git a/ggshield/cmd/utils/debug.py b/ggshield/cmd/utils/debug.py index 7d107307e1..f2d9ac9d75 100644 --- a/ggshield/cmd/utils/debug.py +++ b/ggshield/cmd/utils/debug.py @@ -33,11 +33,3 @@ def setup_debug_mode(*, filename: Optional[str] = None) -> None: logger.debug("args=%s", sys.argv) logger.debug("py-gitguardian=%s", pygitguardian.__version__) - - -def reset_debug_mode() -> None: - """ - This function is used by unit-tests. - """ - log_utils.reset_log_handler() - ui.set_level(ui.Level.INFO) diff --git a/ggshield/core/errors.py b/ggshield/core/errors.py index c8d58ff565..aafd92eafb 100644 --- a/ggshield/core/errors.py +++ b/ggshield/core/errors.py @@ -154,7 +154,7 @@ def format_items(dct: Dict[str, Any], indent: int) -> None: return "\n".join(lines) -def handle_exception(exc: Exception, verbose: bool) -> int: +def handle_exception(exc: Exception) -> int: """ Take an exception, print information about it and return the exit code to use """ @@ -183,7 +183,7 @@ def handle_exception(exc: Exception, verbose: bool) -> int: if not isinstance(exc, (click.ClickException, GitError)): click.echo() - if verbose: + if ui.is_verbose(): traceback.print_exc() else: ui.display_info("Re-run the command with --verbose to get a stack trace.") diff --git a/ggshield/core/git_hooks/ci/commit_range.py b/ggshield/core/git_hooks/ci/commit_range.py index 65576172ff..d49f931f1a 100644 --- a/ggshield/core/git_hooks/ci/commit_range.py +++ b/ggshield/core/git_hooks/ci/commit_range.py @@ -1,11 +1,10 @@ import os from typing import List, Tuple -import click - from ggshield.core.errors import UnexpectedError from ggshield.utils.git_shell import EMPTY_SHA, get_list_commit_SHA +from ... import ui from .previous_commit import ( github_pull_request_previous_commit_sha, github_push_previous_commit_sha, @@ -14,27 +13,23 @@ from .supported_ci import SupportedCI -def collect_commit_range_from_ci_env( - verbose: bool, -) -> Tuple[List[str], SupportedCI]: +def collect_commit_range_from_ci_env() -> Tuple[List[str], SupportedCI]: supported_ci = SupportedCI.from_ci_env() try: fcn = COLLECT_COMMIT_RANGE_FUNCTIONS[supported_ci] except KeyError: raise UnexpectedError(f"Not implemented for {supported_ci.value}") - return fcn(verbose), supported_ci + return fcn(), supported_ci -def jenkins_range(verbose: bool) -> List[str]: # pragma: no cover +def jenkins_range() -> List[str]: # pragma: no cover head_commit = os.getenv("GIT_COMMIT") previous_commit = os.getenv("GIT_PREVIOUS_COMMIT") - if verbose: - click.echo( - f"\tGIT_COMMIT: {head_commit}" f"\nGIT_PREVIOUS_COMMIT: {previous_commit}", - err=True, - ) + ui.display_verbose( + f"\tGIT_COMMIT: {head_commit}" f"\nGIT_PREVIOUS_COMMIT: {previous_commit}" + ) if previous_commit: commit_list = get_list_commit_SHA(f"{previous_commit}...{head_commit}") @@ -53,15 +48,13 @@ def jenkins_range(verbose: bool) -> List[str]: # pragma: no cover ) -def travis_range(verbose: bool) -> List[str]: # pragma: no cover +def travis_range() -> List[str]: # pragma: no cover commit_range = os.getenv("TRAVIS_COMMIT_RANGE") commit_sha = os.getenv("TRAVIS_COMMIT", "HEAD") - if verbose: - click.echo( - f"TRAVIS_COMMIT_RANGE: {commit_range}" f"\nTRAVIS_COMMIT: {commit_sha}", - err=True, - ) + ui.display_verbose( + f"TRAVIS_COMMIT_RANGE: {commit_range}" f"\nTRAVIS_COMMIT: {commit_sha}" + ) if commit_range: commit_list = get_list_commit_SHA(commit_range) @@ -80,10 +73,9 @@ def travis_range(verbose: bool) -> List[str]: # pragma: no cover ) -def bitbucket_pipelines_range(verbose: bool) -> List[str]: # pragma: no cover +def bitbucket_pipelines_range() -> List[str]: # pragma: no cover commit_sha = os.getenv("BITBUCKET_COMMIT", "HEAD") - if verbose: - click.echo(f"BITBUCKET_COMMIT: {commit_sha}", err=True) + ui.display_verbose(f"BITBUCKET_COMMIT: {commit_sha}") commit_list = get_list_commit_SHA(f"{commit_sha}~1...") if commit_list: @@ -96,7 +88,7 @@ def bitbucket_pipelines_range(verbose: bool) -> List[str]: # pragma: no cover ) -def circle_ci_range(verbose: bool) -> List[str]: # pragma: no cover +def circle_ci_range() -> List[str]: # pragma: no cover """ # Extract commit range (or single commit) COMMIT_RANGE=$(echo "${CIRCLE_COMPARE_URL}" | cut -d/ -f7) @@ -109,10 +101,7 @@ def circle_ci_range(verbose: bool) -> List[str]: # pragma: no cover compare_range = os.getenv("CIRCLE_RANGE") commit_sha = os.getenv("CIRCLE_SHA1", "HEAD") - if verbose: - click.echo( - f"CIRCLE_RANGE: {compare_range}\nCIRCLE_SHA1: {commit_sha}", err=True - ) + ui.display_verbose(f"CIRCLE_RANGE: {compare_range}\nCIRCLE_SHA1: {commit_sha}") if compare_range and not compare_range.startswith("..."): commit_list = get_list_commit_SHA(compare_range) @@ -131,18 +120,16 @@ def circle_ci_range(verbose: bool) -> List[str]: # pragma: no cover ) -def gitlab_ci_range(verbose: bool) -> List[str]: # pragma: no cover +def gitlab_ci_range() -> List[str]: # pragma: no cover before_sha = gitlab_push_previous_commit_sha() commit_sha = os.getenv("CI_COMMIT_SHA", "HEAD") merge_request_target_branch = os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME") - if verbose: - click.echo( - f"CI_MERGE_REQUEST_TARGET_BRANCH_NAME: {merge_request_target_branch}\n" - f"CI_COMMIT_BEFORE_SHA: {before_sha}\n" - f"CI_COMMIT_SHA: {commit_sha}", - err=True, - ) + ui.display_verbose( + f"CI_MERGE_REQUEST_TARGET_BRANCH_NAME: {merge_request_target_branch}\n" + f"CI_COMMIT_BEFORE_SHA: {before_sha}\n" + f"CI_COMMIT_SHA: {commit_sha}" + ) if before_sha and before_sha != EMPTY_SHA: commit_list = get_list_commit_SHA(f"{before_sha}~1...") @@ -167,22 +154,20 @@ def gitlab_ci_range(verbose: bool) -> List[str]: # pragma: no cover ) -def github_actions_range(verbose: bool) -> List[str]: # pragma: no cover +def github_actions_range() -> List[str]: # pragma: no cover push_before_sha = github_push_previous_commit_sha() push_base_sha = os.getenv("GITHUB_PUSH_BASE_SHA") pull_req_base_sha = github_pull_request_previous_commit_sha() default_branch = os.getenv("GITHUB_DEFAULT_BRANCH") head_sha = os.getenv("GITHUB_SHA", "HEAD") - if verbose: - click.echo( - f"github_push_before_sha: {push_before_sha}\n" - f"github_push_base_sha: {push_base_sha}\n" - f"github_pull_base_sha: {pull_req_base_sha}\n" - f"github_default_branch: {default_branch}\n" - f"github_head_sha: {head_sha}", - err=True, - ) + ui.display_verbose( + f"github_push_before_sha: {push_before_sha}\n" + f"github_push_base_sha: {push_base_sha}\n" + f"github_pull_base_sha: {pull_req_base_sha}\n" + f"github_default_branch: {default_branch}\n" + f"github_head_sha: {head_sha}" + ) # The PR base sha has to be checked before the push_before_sha # because the first one is only populated in case of PR @@ -224,11 +209,10 @@ def github_actions_range(verbose: bool) -> List[str]: # pragma: no cover ) -def drone_range(verbose: bool) -> List[str]: # pragma: no cover +def drone_range() -> List[str]: # pragma: no cover before_sha = os.getenv("DRONE_COMMIT_BEFORE") - if verbose: - click.echo(f"DRONE_COMMIT_BEFORE: {before_sha}\n", err=True) + ui.display_verbose(f"DRONE_COMMIT_BEFORE: {before_sha}\n") if before_sha and before_sha != EMPTY_SHA: commit_list = get_list_commit_SHA(f"{before_sha}..") @@ -242,11 +226,10 @@ def drone_range(verbose: bool) -> List[str]: # pragma: no cover ) -def azure_range(verbose: bool) -> List[str]: # pragma: no cover +def azure_range() -> List[str]: # pragma: no cover head_commit = os.getenv("BUILD_SOURCEVERSION") - if verbose: - click.echo(f"BUILD_SOURCEVERSION: {head_commit}\n", err=True) + ui.display_verbose(f"BUILD_SOURCEVERSION: {head_commit}\n") if head_commit: commit_list = get_list_commit_SHA(f"{head_commit}~1...") diff --git a/ggshield/core/git_hooks/ci/current_and_previous_state.py b/ggshield/core/git_hooks/ci/current_and_previous_state.py index 4e2179a7c9..10b83fe666 100644 --- a/ggshield/core/git_hooks/ci/current_and_previous_state.py +++ b/ggshield/core/git_hooks/ci/current_and_previous_state.py @@ -1,35 +1,32 @@ -import click - from ggshield.utils.git_shell import check_git_dir +from ... import ui from .commit_range import collect_commit_range_from_ci_env from .previous_commit import get_previous_commit_from_ci_env -def get_current_and_previous_state_from_ci_env(verbose: bool): +def get_current_and_previous_state_from_ci_env(): """ Returns the current commit sha and the previous commit sha of the targeted branch in a CI env """ check_git_dir() - new_commits, _ = collect_commit_range_from_ci_env(verbose) - previous_commit = get_previous_commit_from_ci_env(verbose) + new_commits, _ = collect_commit_range_from_ci_env() + previous_commit = get_previous_commit_from_ci_env() if not new_commits: current_commit = "HEAD" else: current_commit = new_commits[-1] - if verbose: - if new_commits: - click.echo("List of new commits: ", err=True) - for commit in new_commits: - click.echo(f"- {commit}", err=True) + if new_commits: + ui.display_verbose("List of new commits: ") + for commit in new_commits: + ui.display_verbose(f"- {commit}") - click.echo( - f"Comparing commit {current_commit} to commit {previous_commit}", - err=True, - ) + ui.display_verbose( + f"Comparing commit {current_commit} to commit {previous_commit}", + ) return current_commit, previous_commit diff --git a/ggshield/core/git_hooks/ci/get_scan_ci_parameters.py b/ggshield/core/git_hooks/ci/get_scan_ci_parameters.py index f34a5980af..a5e1c3f483 100644 --- a/ggshield/core/git_hooks/ci/get_scan_ci_parameters.py +++ b/ggshield/core/git_hooks/ci/get_scan_ci_parameters.py @@ -2,8 +2,7 @@ from pathlib import Path from typing import Dict, Optional, Tuple, Union -import click - +from ggshield.core import ui from ggshield.core.errors import NotAMergeRequestError, UnexpectedError from ggshield.utils.git_shell import get_commits_not_in_branch, get_remotes @@ -34,9 +33,7 @@ def travis_scan_ci_args() -> Tuple[str, str]: def get_scan_ci_parameters( - current_ci: SupportedCI, - wd: Optional[Union[str, Path]] = None, - verbose: bool = False, + current_ci: SupportedCI, wd: Optional[Union[str, Path]] = None ) -> Union[Tuple[str, str], None]: """ Function used to gather current commit and reference commit, for the SCA/IaC scan @@ -48,11 +45,7 @@ def get_scan_ci_parameters( Note: this function will not work (i.e. probably raise) if the git directory is a shallow clone """ - if verbose: - click.echo( - f"\tIdentified current ci as {current_ci.value}", - err=True, - ) + ui.display_verbose(f"\tIdentified current ci as {current_ci.value}") if current_ci == SupportedCI.TRAVIS: return travis_scan_ci_args() @@ -60,18 +53,10 @@ def get_scan_ci_parameters( remotes = get_remotes(wd=wd) if len(remotes) == 0: # note: this should not happen in practice, esp. in a CI job - if verbose: - click.echo( - "\tNo remote found.", - err=True, - ) + ui.display_verbose("\tNo remote found.") remote_prefix = "" else: - if verbose: - click.echo( - f"\tUsing first remote {remotes[0]}.", - err=True, - ) + ui.display_verbose(f"\tUsing first remote {remotes[0]}.") remote_prefix = f"{remotes[0]}/" target_branch_var = CI_TARGET_BRANCH_ASSOC.get(current_ci) @@ -100,13 +85,11 @@ def get_scan_ci_parameters( current_commit = mr_commits[0] reference_commit = mr_commits[-1] + "~1" - if verbose: - click.echo( - ( - f"\tIdentified current commit as {current_commit}\n" - f"\tIdentified reference commit as {reference_commit}" - ), - err=True, + ui.display_verbose( + ( + f"\tIdentified current commit as {current_commit}\n" + f"\tIdentified reference commit as {reference_commit}" ) + ) return current_commit, reference_commit diff --git a/ggshield/core/git_hooks/ci/previous_commit.py b/ggshield/core/git_hooks/ci/previous_commit.py index 61eb3922c1..6cf13e378c 100644 --- a/ggshield/core/git_hooks/ci/previous_commit.py +++ b/ggshield/core/git_hooks/ci/previous_commit.py @@ -3,8 +3,7 @@ from pathlib import Path from typing import Optional -import click - +from ggshield.core import ui from ggshield.core.errors import UnexpectedError from ggshield.utils.git_shell import ( EMPTY_SHA, @@ -18,9 +17,7 @@ from .supported_ci import SupportedCI -def get_previous_commit_from_ci_env( - verbose: bool, -) -> Optional[str]: +def get_previous_commit_from_ci_env() -> Optional[str]: """ Returns the previous HEAD sha of the targeted branch. Returns None if there was no commit before. @@ -31,21 +28,19 @@ def get_previous_commit_from_ci_env( except KeyError: raise UnexpectedError(f"Not implemented for {supported_ci.value}") - return fcn(verbose) + return fcn() -def github_previous_commit_sha(verbose: bool) -> Optional[str]: +def github_previous_commit_sha() -> Optional[str]: push_before_sha = github_push_previous_commit_sha() pull_req_base_sha = github_pull_request_previous_commit_sha() head_sha = os.getenv("GITHUB_SHA", "HEAD") event_name = os.getenv("GITHUB_EVENT_NAME") - if verbose: - click.echo( - f"github_push_before_sha: {push_before_sha}\n" - f"github_pull_base_sha: {pull_req_base_sha}\n", - err=True, - ) + ui.display_verbose( + f"github_push_before_sha: {push_before_sha}\n" + f"github_pull_base_sha: {pull_req_base_sha}\n" + ) # The PR base sha has to be checked before the push_before_sha # because the first one is only populated in case of PR @@ -55,19 +50,18 @@ def github_previous_commit_sha(verbose: bool) -> Optional[str]: return pull_req_base_sha if push_before_sha and push_before_sha != EMPTY_SHA: - return get_commit_except_forced_push(push_before_sha, verbose) + return get_commit_except_forced_push(push_before_sha) if head_sha and event_name == "push": # New branch pushed, try to get sha from git state ref_type = os.getenv("GITHUB_REF_TYPE") ref_name = os.getenv("GITHUB_REF_NAME") if ref_type == "branch" and ref_name: - return get_new_branch_parent_commit(ref_name, verbose) + return get_new_branch_parent_commit(ref_name) - if verbose: - click.echo("Could not find previous commit for current branch.") - click.echo("Current branch may have been just pushed.") - click.echo("Only scan last commit.") + ui.display_verbose("Could not find previous commit for current branch.") + ui.display_verbose("Current branch may have been just pushed.") + ui.display_verbose("Only scan last commit.") last_commits = get_list_commit_SHA(f"{head_sha}~1", max_count=1) if len(last_commits) == 1: return last_commits[0] @@ -99,17 +93,15 @@ def github_pull_request_previous_commit_sha() -> Optional[str]: return get_last_commit_sha_of_branch(f"remotes/origin/{targeted_branch}") -def gitlab_previous_commit_sha(verbose: bool) -> Optional[str]: +def gitlab_previous_commit_sha() -> Optional[str]: push_before_sha = gitlab_push_previous_commit_sha() - merge_req_base_sha = gitlab_merge_request_previous_commit_sha(verbose) + merge_req_base_sha = gitlab_merge_request_previous_commit_sha() is_merge_req = bool(os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")) - if verbose: - click.echo( - f"gitlab_push_before_sha: {push_before_sha}\n" - f"gitlab_merge_base_sha: {merge_req_base_sha}\n", - err=True, - ) + ui.display_verbose( + f"gitlab_push_before_sha: {push_before_sha}\n" + f"gitlab_merge_base_sha: {merge_req_base_sha}\n" + ) # push_before_sha is always EMPTY_SHA in MR pipeline according with # https://docs.gitlab.com/ee/ci/variables/predefined_variables.html @@ -120,12 +112,12 @@ def gitlab_previous_commit_sha(verbose: bool) -> Optional[str]: return merge_req_base_sha if push_before_sha and push_before_sha != EMPTY_SHA: - return get_commit_except_forced_push(push_before_sha, verbose) + return get_commit_except_forced_push(push_before_sha) # push_before_sha is also always EMPTY_SHA for the first commit of a new branch current_branch = os.getenv("CI_COMMIT_BRANCH") if current_branch: - return get_new_branch_parent_commit(current_branch, verbose) + return get_new_branch_parent_commit(current_branch) raise UnexpectedError( "Unable to get previous commit. Please submit an issue with the following info:\n" @@ -139,7 +131,7 @@ def gitlab_push_previous_commit_sha() -> Optional[str]: return os.getenv("CI_COMMIT_BEFORE_SHA") -def gitlab_merge_request_previous_commit_sha(verbose: bool) -> Optional[str]: +def gitlab_merge_request_previous_commit_sha() -> Optional[str]: targeted_branch = os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME") # Not in a pull request workflow @@ -156,46 +148,41 @@ def gitlab_merge_request_previous_commit_sha(verbose: bool) -> Optional[str]: # "CI_MERGE_REQUEST_DIFF_BASE_SHA" # This is not the current state of the target branch but the initial state # of current branch - if verbose: - click.echo(f"Failed to get {targeted_branch} HEAD.") - click.echo( - f"Fallback on commit {os.getenv('CI_MERGE_REQUEST_DIFF_BASE_SHA')}" - ) + ui.display_verbose(f"Failed to get {targeted_branch} HEAD.") + ui.display_verbose( + f"Fallback on commit {os.getenv('CI_MERGE_REQUEST_DIFF_BASE_SHA')}" + ) return os.getenv("CI_MERGE_REQUEST_DIFF_BASE_SHA") return last_commit -def jenkins_previous_commit_sha(verbose: bool) -> Optional[str]: +def jenkins_previous_commit_sha() -> Optional[str]: previous_commit = os.getenv("GIT_PREVIOUS_COMMIT") target_branch = os.getenv("CHANGE_TARGET") current_commit = os.getenv("GIT_COMMIT") current_branch = os.getenv("GIT_BRANCH") - if verbose: - click.echo( - f"GIT_PREVIOUS_COMMIT: {previous_commit}\n" - f"CHANGE_TARGET: {target_branch}\n", - err=True, - ) + ui.display_verbose( + f"GIT_PREVIOUS_COMMIT: {previous_commit}\n" f"CHANGE_TARGET: {target_branch}\n" + ) if target_branch: return get_last_commit_sha_of_branch(f"origin/{target_branch}") if previous_commit and previous_commit != EMPTY_SHA: - return get_commit_except_forced_push(previous_commit, verbose) + return get_commit_except_forced_push(previous_commit) if current_branch: try: - return get_new_branch_parent_commit(current_branch, verbose) + return get_new_branch_parent_commit(current_branch) except subprocess.CalledProcessError: - click.echo("Failed to retrieve initial commit from git history.") + ui.display_error("Failed to retrieve initial commit from git history.") if current_commit: - if verbose: - click.echo("Could not find previous commit for current branch.") - click.echo("Current branch may have been just pushed.") - click.echo("Only scan last commit.") + ui.display_verbose("Could not find previous commit for current branch.") + ui.display_verbose("Current branch may have been just pushed.") + ui.display_verbose("Only scan last commit.") last_commits = get_list_commit_SHA(f"{current_commit}~1", max_count=1) if len(last_commits) == 1: return last_commits[0] @@ -208,47 +195,42 @@ def jenkins_previous_commit_sha(verbose: bool) -> Optional[str]: ) -def azure_previous_commit_sha(verbose: bool) -> Optional[str]: +def azure_previous_commit_sha() -> Optional[str]: push_before_sha = azure_push_previous_commit_sha() - pull_req_base_sha = azure_pull_request_previous_commit_sha(verbose) + pull_req_base_sha = azure_pull_request_previous_commit_sha() - if verbose: - click.echo( - f"PUSH_PREVIOUS_COMMIT: {push_before_sha}\n" - f"PR_TARGET_COMMIT: {pull_req_base_sha}\n", - err=True, - ) + ui.display_verbose( + f"PUSH_PREVIOUS_COMMIT: {push_before_sha}\n" + f"PR_TARGET_COMMIT: {pull_req_base_sha}\n" + ) if pull_req_base_sha is not None: return pull_req_base_sha if push_before_sha is not None and push_before_sha != EMPTY_SHA: - if verbose: - click.echo( - "The number of commits of a push event is not available in Azure pipelines." - ) - click.echo("Scanning only last commit.") - return get_commit_except_forced_push(push_before_sha, verbose) + ui.display_verbose( + "The number of commits of a push event is not available in Azure pipelines." + ) + ui.display_verbose("Scanning only last commit.") + return get_commit_except_forced_push(push_before_sha) # New branch push current_branch = os.getenv("BUILD_SOURCEBRANCHNAME") if current_branch: - return get_new_branch_parent_commit(current_branch, verbose) + return get_new_branch_parent_commit(current_branch) # Can't find previous SHA, return last commit head_commit = os.getenv("BUILD_SOURCEVERSION") last_commits = get_list_commit_SHA(f"{head_commit}~1", max_count=1) if len(last_commits) == 0: - if verbose: - click.echo("Unable to find commit HEAD~1.") + ui.display_verbose("Unable to find commit HEAD~1.") return None - if verbose: - click.echo( - "The number of commits of a push event is not available in Azure pipelines." - ) - click.echo("Scanning only last commit.") + ui.display_verbose( + "The number of commits of a push event is not available in Azure pipelines." + ) + ui.display_verbose("Scanning only last commit.") if last_commits[0] is not None: return last_commits[0] @@ -270,7 +252,7 @@ def azure_push_previous_commit_sha() -> Optional[str]: return push_before_sha -def azure_pull_request_previous_commit_sha(verbose: bool) -> Optional[str]: +def azure_pull_request_previous_commit_sha() -> Optional[str]: """ Returns commit to compare with in a PR environment in Azure. If env variable SYSTEM_PULLREQUEST_TARGETBRANCHNAME is not defined, not in a PR. @@ -290,20 +272,18 @@ def azure_pull_request_previous_commit_sha(verbose: bool) -> Optional[str]: if last_commit is not None: return last_commit - if verbose: - click.echo("Unable to find HEAD commit sha of target branch.") - click.echo("Scans only last commit of current branch.") + ui.display_verbose("Unable to find HEAD commit sha of target branch.") + ui.display_verbose("Scans only last commit of current branch.") last_commit = get_last_commit_sha_of_branch("HEAD~1") if last_commit is not None: return last_commit - if verbose: - click.echo("Unable to find commit HEAD~1.") + ui.display_verbose("Unable to find commit HEAD~1.") return None -def get_new_branch_parent_commit(branch_name: str, verbose: bool) -> Optional[str]: +def get_new_branch_parent_commit(branch_name: str) -> Optional[str]: """ Assuming `branch_name` is a branch newly pushed, this returns a reference to the parent commit of all commits pushed on that branch. @@ -314,23 +294,17 @@ def get_new_branch_parent_commit(branch_name: str, verbose: bool) -> Optional[st ref = f"refs/remotes/origin/{branch_name}" return ref if is_valid_git_commit_ref(ref) else "HEAD" new_branch_before_sha = f"{new_commits[-1]}^1" - if verbose: - click.echo( - f"new_branch_before_sha: {new_branch_before_sha}\n", - err=True, - ) + ui.display_verbose(f"new_branch_before_sha: {new_branch_before_sha}\n") if is_valid_git_commit_ref(new_branch_before_sha): return new_branch_before_sha else: - if verbose: - click.echo("> This might be a new repository.", err=True) + ui.display_verbose("> This might be a new repository.") return None -def get_commit_except_forced_push(commit: str, verbose: bool) -> Optional[str]: +def get_commit_except_forced_push(commit: str) -> Optional[str]: if not is_valid_git_commit_ref(commit): - if verbose: - click.echo("> This might be a forced push.", err=True) + ui.display_verbose("> This might be a forced push.") return None return commit diff --git a/ggshield/core/git_hooks/prereceive.py b/ggshield/core/git_hooks/prereceive.py index fabae5ff4e..8cb5905d0d 100644 --- a/ggshield/core/git_hooks/prereceive.py +++ b/ggshield/core/git_hooks/prereceive.py @@ -3,8 +3,6 @@ import sys from typing import Optional, Tuple -import click - from ggshield.core import ui from ggshield.core.errors import UnexpectedError from ggshield.utils.git_shell import EMPTY_SHA, git @@ -36,9 +34,8 @@ def get_breakglass_option() -> bool: if option_count is not None: for option in range(option_count): if os.getenv(f"GIT_PUSH_OPTION_{option}", "") == "breakglass": - click.echo( - "SKIP: breakglass detected. Skipping GitGuardian pre-receive hook.", - err=True, + ui.display_info( + "SKIP: breakglass detected. Skipping GitGuardian pre-receive hook." ) return True @@ -78,7 +75,7 @@ def parse_stdin() -> Optional[Tuple[str, str]]: if new_commit == EMPTY_SHA: # Deletion event, nothing to do - click.echo("Deletion event or nothing to scan.", err=True) + ui.display_info("Deletion event or nothing to scan.") return None # ignore _old_commit because in case of a force-push, it is going to be overwritten @@ -93,10 +90,7 @@ def parse_stdin() -> Optional[Tuple[str, str]]: assert old_commit != EMPTY_SHA assert new_commit != EMPTY_SHA if old_commit == new_commit: - click.echo( - "Pushed branch does not contain any new commit.", - err=True, - ) + ui.display_info("Pushed branch does not contain any new commit.") return None return (old_commit, new_commit) diff --git a/ggshield/core/ui/__init__.py b/ggshield/core/ui/__init__.py index 30331fdb01..eaa7a61309 100644 --- a/ggshield/core/ui/__init__.py +++ b/ggshield/core/ui/__init__.py @@ -12,7 +12,7 @@ from .scanner_ui import ScannerUI -# GGShielUI instance to which top-level function forward their output. +# GGShieldUI instance to which top-level functions forward their output. # Can be changed using set_ui(). _ui: GGShieldUI = PlainTextGGShieldUI() @@ -21,6 +21,26 @@ def set_level(level: Level) -> None: _ui.level = level +def get_level() -> Level: + return _ui.level + + +def ensure_level(level: Level): + """ + Make sure the verbosity level is at least set to `level` + """ + if _ui.level < level: + set_level(level) + + +def is_verbose() -> bool: + """ + Convenient function to check if verbose messages are visible. Use this if displaying + verbose messages is costly (for example displaying a list of files) + """ + return _ui.level >= Level.VERBOSE + + def set_ui(ui: GGShieldUI) -> None: """Change the GGShieldUI instance used to output messages. Takes care of carrying existing settings from the old instance to the new one.""" @@ -61,9 +81,15 @@ def create_progress(total: int) -> GGShieldProgress: return _ui.create_progress(total) -def create_scanner_ui(total: int, verbose: bool = False) -> ScannerUI: - return _ui.create_scanner_ui(total, verbose=verbose) +def create_scanner_ui(total: int) -> ScannerUI: + return _ui.create_scanner_ui(total) + +def create_message_only_scanner_ui() -> ScannerUI: + return _ui.create_message_only_scanner_ui() -def create_message_only_scanner_ui(verbose: bool = False) -> ScannerUI: - return _ui.create_message_only_scanner_ui(verbose=verbose) + +def _reset_ui(): + """Reset the module to its startup state. Used by reset.reset().""" + global _ui + _ui = PlainTextGGShieldUI() diff --git a/ggshield/core/ui/ggshield_ui.py b/ggshield/core/ui/ggshield_ui.py index bc97a84a8d..3ed93e0648 100644 --- a/ggshield/core/ui/ggshield_ui.py +++ b/ggshield/core/ui/ggshield_ui.py @@ -57,11 +57,7 @@ def __init__(self): self.level = Level.INFO @abstractmethod - def create_scanner_ui( - self, - total: int, - verbose: bool = False, - ) -> ScannerUI: + def create_scanner_ui(self, total: int) -> ScannerUI: """ Creates a ScannerUI instance. This is used to show progress on scanning Scannables. @@ -69,10 +65,7 @@ def create_scanner_ui( ... @abstractmethod - def create_message_only_scanner_ui( - self, - verbose: bool = False, - ) -> ScannerUI: + def create_message_only_scanner_ui(self) -> ScannerUI: """ Creates a ScannerUI instance without a progress bar. This is used when the scan itself is part of a larger scan. For example when scanning a commit range, each diff --git a/ggshield/core/ui/log_utils.py b/ggshield/core/ui/log_utils.py index ec0857e39f..654f0375d2 100644 --- a/ggshield/core/ui/log_utils.py +++ b/ggshield/core/ui/log_utils.py @@ -44,8 +44,8 @@ def set_log_handler(filename: Optional[str] = None) -> None: logging.basicConfig(level=logging.DEBUG, force=True, handlers=[_log_handler]) -def reset_log_handler(): - """Remove our log handler. Used by unit tests.""" +def _reset_log_handler(): + """Remove our log handler. Used by reset.reset().""" global _log_handler if _log_handler: logging.getLogger().removeHandler(_log_handler) diff --git a/ggshield/core/ui/plain_text/plain_text_ggshield_ui.py b/ggshield/core/ui/plain_text/plain_text_ggshield_ui.py index c8711f460b..0578f1583e 100644 --- a/ggshield/core/ui/plain_text/plain_text_ggshield_ui.py +++ b/ggshield/core/ui/plain_text/plain_text_ggshield_ui.py @@ -59,17 +59,10 @@ def log(self, record: logging.LogRecord) -> None: self.display_warning(f"Unsupported log level {level}") self.display_error(msg) - def create_scanner_ui( - self, - total: int, - verbose: bool = False, - ) -> ScannerUI: + def create_scanner_ui(self, total: int) -> ScannerUI: return PlainTextScannerUI() - def create_message_only_scanner_ui( - self, - verbose: bool = False, - ) -> ScannerUI: + def create_message_only_scanner_ui(self) -> ScannerUI: return PlainTextScannerUI() def create_progress(self, total: int) -> GGShieldProgress: diff --git a/ggshield/core/ui/reset.py b/ggshield/core/ui/reset.py new file mode 100644 index 0000000000..9b771d738e --- /dev/null +++ b/ggshield/core/ui/reset.py @@ -0,0 +1,10 @@ +from . import _reset_ui +from .log_utils import _reset_log_handler + + +def reset(): + """ + This function is only used by unit-tests to reset the UI + """ + _reset_log_handler() + _reset_ui() diff --git a/ggshield/core/ui/rich/rich_ggshield_ui.py b/ggshield/core/ui/rich/rich_ggshield_ui.py index 67870620ee..a86f93a89a 100644 --- a/ggshield/core/ui/rich/rich_ggshield_ui.py +++ b/ggshield/core/ui/rich/rich_ggshield_ui.py @@ -63,18 +63,11 @@ def __init__(self): self.console = Console(file=sys.stderr) self._previous_timestamp = "" - def create_scanner_ui( - self, - total: int, - verbose: bool = False, - ) -> ScannerUI: - return RichProgressScannerUI(self, total, verbose) - - def create_message_only_scanner_ui( - self, - verbose: bool = False, - ) -> ScannerUI: - return RichMessageOnlyScannerUI(self, verbose) + def create_scanner_ui(self, total: int) -> ScannerUI: + return RichProgressScannerUI(self, total) + + def create_message_only_scanner_ui(self) -> ScannerUI: + return RichMessageOnlyScannerUI(self) def create_progress(self, total: int) -> GGShieldProgress: return RichGGShieldProgress(self.console, total) diff --git a/ggshield/core/ui/rich/rich_scanner_ui.py b/ggshield/core/ui/rich/rich_scanner_ui.py index 43b984554a..1f7fed080a 100644 --- a/ggshield/core/ui/rich/rich_scanner_ui.py +++ b/ggshield/core/ui/rich/rich_scanner_ui.py @@ -12,13 +12,8 @@ class RichMessageOnlyScannerUI(ScannerUI): Basic UI, only supports showing messages when `on_*()` methods are called """ - def __init__( - self, - ui: GGShieldUI, - verbose: bool = False, - ): + def __init__(self, ui: GGShieldUI): self.ui = ui - self.verbose = verbose def __enter__(self) -> Self: return self @@ -27,9 +22,8 @@ def __exit__(self, *args: Any) -> None: pass def on_scanned(self, scannables: Sequence[Scannable]) -> None: - if self.verbose: - for scannable in scannables: - self.ui.display_info(f"Scanned {scannable.path}") + for scannable in scannables: + self.ui.display_verbose(f"Scanned {scannable.path}") def on_skipped(self, scannable: Scannable, reason: str) -> None: if reason: @@ -42,8 +36,8 @@ class RichProgressScannerUI(RichMessageOnlyScannerUI): Show a progress bar in addition to messages when `on_*()` methods are called """ - def __init__(self, ui: GGShieldUI, total: int, verbose: bool = False): - super().__init__(ui, verbose) + def __init__(self, ui: GGShieldUI, total: int): + super().__init__(ui) self.progress = ui.create_progress(total) def __enter__(self) -> Self: diff --git a/ggshield/verticals/sca/file_selection.py b/ggshield/verticals/sca/file_selection.py index 0215a7c36d..ad914e2c92 100644 --- a/ggshield/verticals/sca/file_selection.py +++ b/ggshield/verticals/sca/file_selection.py @@ -63,7 +63,6 @@ def sca_files_from_git_repo( ref: str, client: GGClient, exclusion_regexes: Optional[Set[Pattern[str]]] = None, - verbose: bool = False, ) -> Set[Path]: """Returns SCA files from the git repository at the given directory, for the given ref. Empty string denotes selection @@ -90,9 +89,9 @@ def sca_files_from_git_repo( raise UnexpectedError("Failed to select SCA files") sca_files = sca_files_result.sca_files - if verbose: - ui.display_info(f"> Scanned files from {ref}:") + if ui.is_verbose(): + ui.display_verbose(f"> Scanned files from {ref}:") for filename in sca_files: - ui.display_info(f"- {click.format_filename(filename)}") + ui.display_verbose(f"- {click.format_filename(filename)}") return set(map(Path, sca_files_result.sca_files)) diff --git a/ggshield/verticals/secret/docker.py b/ggshield/verticals/secret/docker.py index 64e150edf3..28fad3217d 100644 --- a/ggshield/verticals/secret/docker.py +++ b/ggshield/verticals/secret/docker.py @@ -327,7 +327,6 @@ def docker_scan_archive( cache: Cache, secret_config: SecretConfig, scan_context: ScanContext, - verbose: bool = False, ) -> SecretScanCollection: scanner = SecretScanner( client=client, diff --git a/ggshield/verticals/secret/repo.py b/ggshield/verticals/secret/repo.py index e8142d67fe..36c1857cd6 100644 --- a/ggshield/verticals/secret/repo.py +++ b/ggshield/verticals/secret/repo.py @@ -48,11 +48,10 @@ def scan_repo_path( output_handler=output_handler, exclusion_regexes=exclusion_regexes, scan_context=scan_context, - verbose=config.user_config.verbose, secret_config=config.user_config.secret, ) except Exception as error: - return handle_exception(error, config.user_config.verbose) + return handle_exception(error) def scan_commits_content( @@ -156,7 +155,6 @@ def scan_commit_range( scan_context: ScanContext, secret_config: SecretConfig, include_staged: bool = False, - verbose: bool = False, ) -> ExitCode: """ Scan every commit in a range. @@ -187,11 +185,10 @@ def scan_commit_range( scans: List[SecretScanCollection] = [] def commit_scanned_callback(commit: Commit): - if verbose: - if include_staged and commit.sha is None: - ui.display_info("Scanned staged changes") - else: - ui.display_info(f"Scanned {commit.sha}") + if include_staged and commit.sha is None: + ui.display_verbose("Scanned staged changes") + else: + ui.display_verbose(f"Scanned {commit.sha}") with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: futures = [] diff --git a/tests/unit/cmd/sca/test_ci.py b/tests/unit/cmd/sca/test_ci.py index f5cf4ccae9..390256551d 100644 --- a/tests/unit/cmd/sca/test_ci.py +++ b/tests/unit/cmd/sca/test_ci.py @@ -80,4 +80,4 @@ def test_sca_scan_ci_non_mr_env( sca_scan_all_mock.call_args.kwargs.items() ) assert result.exit_code == ExitCode.SUCCESS - assert "WARNING: " in result.stderr + assert "Warning: " in result.stderr diff --git a/tests/unit/cmd/sca/test_scan.py b/tests/unit/cmd/sca/test_scan.py index 48e9c73fae..acf3d35e45 100644 --- a/tests/unit/cmd/sca/test_scan.py +++ b/tests/unit/cmd/sca/test_scan.py @@ -63,10 +63,7 @@ def test_get_sca_scan_all_filepaths(client: GGClient, tmp_path) -> None: write_text(filename=str(tmp_path / ".venv" / "Pipfile"), content="") result = get_sca_scan_all_filepaths( - directory=tmp_path, - exclusion_regexes=set(), - verbose=False, - client=client, + directory=tmp_path, exclusion_regexes=set(), client=client ) assert result == (["Pipfile"], 200) diff --git a/tests/unit/cmd/scan/test_ci.py b/tests/unit/cmd/scan/test_ci.py index c021ed176e..b6cc81a1ff 100644 --- a/tests/unit/cmd/scan/test_ci.py +++ b/tests/unit/cmd/scan/test_ci.py @@ -5,6 +5,7 @@ import pytest from ggshield.__main__ import cli +from ggshield.core import ui from ggshield.core.errors import ExitCode from ggshield.core.git_hooks.ci.commit_range import gitlab_ci_range from ggshield.utils.git_shell import EMPTY_SHA @@ -69,11 +70,14 @@ def test_gitlab_ci_range( ): """ GIVEN a GitLab CI environment - WHEN gitlab_ci_range(verbose=True) is called + AND verbose mode has been activated + WHEN gitlab_ci_range() is called THEN the correct commit range is requested AND stdout is empty (to avoid polluting redirections) AND stderr is not empty """ + ui.set_level(ui.Level.VERBOSE) + monkeypatch.setenv("CI", "1") monkeypatch.setenv("GITLAB_CI", "1") for k, v in env.items(): @@ -81,7 +85,7 @@ def test_gitlab_ci_range( get_list_mock.return_value = ["a"] * 51 - gitlab_ci_range(verbose=True) + gitlab_ci_range() get_list_mock.assert_called_once_with(expected_parameter) captured = capsys.readouterr() diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f31ba07395..b021020578 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -14,8 +14,8 @@ from pygitguardian.models import ScanResult, SecretIncident from requests.utils import DEFAULT_CA_BUNDLE_PATH, extract_zipped_paths -from ggshield.cmd.utils.debug import reset_debug_mode from ggshield.core.cache import Cache +from ggshield.core.ui.reset import reset from ggshield.core.url_utils import dashboard_to_api_url from ggshield.utils.git_shell import ( _get_git_path, @@ -706,15 +706,12 @@ def clear_cache(): @pytest.fixture(autouse=True) -def _reset_debug_mode(): +def _reset_ui_fixture(): """ Enabling debug mode has global side effects. Reset it to ensure a test touching the log or debug configuration does not affect other tests. """ - try: - yield - finally: - reset_debug_mode() + reset() SECRET_INCIDENT_MOCK = SecretIncident.from_dict( diff --git a/tests/unit/core/git_hooks/ci/test_get_scan_ci_parameters.py b/tests/unit/core/git_hooks/ci/test_get_scan_ci_parameters.py index 20eee14ef0..01a80ffce4 100644 --- a/tests/unit/core/git_hooks/ci/test_get_scan_ci_parameters.py +++ b/tests/unit/core/git_hooks/ci/test_get_scan_ci_parameters.py @@ -47,7 +47,7 @@ def test_regular_pipeline(self, ci, monkeypatch): monkeypatch.setenv(CI_TARGET_BRANCH_ASSOC[ci], "main") first_commit = repo.create_commit() last_commit = repo.create_commit() - params = get_scan_ci_parameters(ci, wd=repo.path, verbose=False) + params = get_scan_ci_parameters(ci, wd=repo.path) assert params == (last_commit, f"{first_commit}~1") def test_gitlab_ci(self, monkeypatch): @@ -61,7 +61,7 @@ def test_gitlab_ci(self, monkeypatch): monkeypatch.setenv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "mr_branch") first_commit = repo.create_commit() last_commit = repo.create_commit() - params = get_scan_ci_parameters(SupportedCI.GITLAB, wd=repo.path, verbose=False) + params = get_scan_ci_parameters(SupportedCI.GITLAB, wd=repo.path) assert params == (last_commit, f"{first_commit}~1") def test_github_ci(self, monkeypatch): @@ -78,7 +78,7 @@ def test_github_ci(self, monkeypatch): repo.create_branch("simulate_merge_commit") repo.create_commit() - params = get_scan_ci_parameters(SupportedCI.GITHUB, wd=repo.path, verbose=False) + params = get_scan_ci_parameters(SupportedCI.GITHUB, wd=repo.path) assert params == (last_commit, f"{first_commit}~1") def test_travis_ci(self, monkeypatch): @@ -92,7 +92,7 @@ def test_travis_ci(self, monkeypatch): last_commit = repo.create_commit() monkeypatch.setenv("TRAVIS_PULL_REQUEST", "1") monkeypatch.setenv("TRAVIS_COMMIT_RANGE", f"{first_commit}..{last_commit}") - params = get_scan_ci_parameters(SupportedCI.TRAVIS, wd=repo.path, verbose=False) + params = get_scan_ci_parameters(SupportedCI.TRAVIS, wd=repo.path) assert params == (last_commit, f"{first_commit}~1") @pytest.mark.parametrize( @@ -109,7 +109,7 @@ def test_not_a_merge_request_error_is_raised(self, ci, monkeypatch): monkeypatch.delenv("GITHUB_BASE_REF", raising=False) monkeypatch.delenv("GITHUB_HEAD_REF", raising=False) with pytest.raises(NotAMergeRequestError): - get_scan_ci_parameters(ci, wd=self.repo.path, verbose=False) + get_scan_ci_parameters(ci, wd=self.repo.path) def test_circleci_not_supported(self): """ @@ -120,6 +120,4 @@ def test_circleci_not_supported(self): with pytest.raises( UnexpectedError, match="Using scan ci is not supported for CIRCLECI" ): - get_scan_ci_parameters( - SupportedCI.CIRCLECI, wd=self.repo.path, verbose=False - ) + get_scan_ci_parameters(SupportedCI.CIRCLECI, wd=self.repo.path) diff --git a/tests/unit/core/git_hooks/ci/test_previous_commit.py b/tests/unit/core/git_hooks/ci/test_previous_commit.py index b9877f52ea..8c4648ccf0 100644 --- a/tests/unit/core/git_hooks/ci/test_previous_commit.py +++ b/tests/unit/core/git_hooks/ci/test_previous_commit.py @@ -99,7 +99,7 @@ def test_get_previous_commit_from_ci_env_new_branch( with cd(repository.path), mock.patch.dict(os.environ, clear=True): # Simulate CI env setup_ci_env(monkeypatch, "new-branch", head_sha) - found_sha = get_previous_commit_from_ci_env(False) + found_sha = get_previous_commit_from_ci_env() assert found_sha is not None, "No previous commit SHA found" found_sha_evaluated = git(["rev-parse", found_sha], cwd=repository.path) @@ -126,7 +126,7 @@ def test_get_previous_commit_from_ci_env_new_repo( with cd(repository.path), mock.patch.dict(os.environ, clear=True): # Simulate CI env setup_ci_env(monkeypatch, "new-branch", "HEAD") - assert get_previous_commit_from_ci_env(False) is None + assert get_previous_commit_from_ci_env() is None @parametrized_ci_provider @@ -165,7 +165,7 @@ def test_get_previous_commit_from_ci_env_sub_branch_forced_push( with cd(repository.path), mock.patch.dict(os.environ, clear=True): # Simulate CI env setup_ci_env(monkeypatch, branch_name, "HEAD", before_sha) - found_sha = get_previous_commit_from_ci_env(False) + found_sha = get_previous_commit_from_ci_env() assert found_sha is None @@ -197,7 +197,7 @@ def test_get_previous_commit_from_ci_env_default_branch_forced_push( with cd(repository.path), mock.patch.dict(os.environ, clear=True): # Simulate CI env setup_ci_env(monkeypatch, "new-branch", "HEAD", before_sha) - assert get_previous_commit_from_ci_env(False) is None + assert get_previous_commit_from_ci_env() is None @parametrized_ci_provider @@ -225,4 +225,4 @@ def test_get_previous_commit_from_ci_env_all_commits_forced_push( with cd(repository.path), mock.patch.dict(os.environ, clear=True): # Simulate CI env setup_ci_env(monkeypatch, "new-branch", "HEAD", before_sha) - assert get_previous_commit_from_ci_env(False) is None + assert get_previous_commit_from_ci_env() is None diff --git a/tests/unit/core/ui/test_ui.py b/tests/unit/core/ui/test_ui.py new file mode 100644 index 0000000000..5329b900f4 --- /dev/null +++ b/tests/unit/core/ui/test_ui.py @@ -0,0 +1,28 @@ +from typing import Optional + +import pytest + +from ggshield.core import ui +from ggshield.core.ui import Level + + +def test_level(): + assert ui.get_level() == Level.INFO + + ui.set_level(Level.VERBOSE) + assert ui.get_level() == Level.VERBOSE + + +@pytest.mark.parametrize( + ("level", "expected"), + ( + (None, False), + (Level.VERBOSE, True), + (Level.DEBUG, True), + (Level.ERROR, False), + ), +) +def test_is_verbose(level: Optional[Level], expected: bool): + if level: + ui.set_level(level) + assert ui.is_verbose() == expected