Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions cycode/cli/commands/scan/code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from uuid import UUID, uuid4

import click
from git import NULL_TREE, Repo

from cycode.cli import consts
from cycode.cli.config import configuration_manager
Expand All @@ -28,9 +27,8 @@
from cycode.cli.models import CliError, Document, DocumentDetections, LocalScanResult, Severity
from cycode.cli.printers import ConsolePrinter
from cycode.cli.utils import scan_utils
from cycode.cli.utils.path_utils import (
get_path_by_os,
)
from cycode.cli.utils.git_proxy import git_proxy
from cycode.cli.utils.path_utils import get_path_by_os
from cycode.cli.utils.progress_bar import ScanProgressBarSection
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
from cycode.cli.utils.scan_utils import set_issue_detected
Expand Down Expand Up @@ -244,7 +242,7 @@ def scan_commit_range(
documents_to_scan = []
commit_ids_to_scan = []

repo = Repo(path)
repo = git_proxy.get_repo(path)
total_commits_count = int(repo.git.rev_list('--count', commit_range))
logger.debug('Calculating diffs for %s commits in the commit range %s', total_commits_count, commit_range)

Expand All @@ -261,7 +259,7 @@ def scan_commit_range(

commit_id = commit.hexsha
commit_ids_to_scan.append(commit_id)
parent = commit.parents[0] if commit.parents else NULL_TREE
parent = commit.parents[0] if commit.parents else git_proxy.get_null_tree()
diff = commit.diff(parent, create_patch=True, R=True)
commit_documents_to_scan = []
for blob in diff:
Expand Down Expand Up @@ -688,7 +686,7 @@ def get_scan_parameters(context: click.Context, paths: Tuple[str]) -> dict:

def try_get_git_remote_url(path: str) -> Optional[str]:
try:
remote_url = Repo(path).remotes[0].config_reader.get('url')
remote_url = git_proxy.get_repo(path).remotes[0].config_reader.get('url')
logger.debug('Found Git remote URL, %s', {'remote_url': remote_url, 'path': path})
return remote_url
except Exception as e:
Expand Down
4 changes: 2 additions & 2 deletions cycode/cli/commands/scan/pre_commit/pre_commit_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from typing import List

import click
from git import Repo

from cycode.cli import consts
from cycode.cli.commands.scan.code_scanner import scan_documents, scan_sca_pre_commit
Expand All @@ -12,6 +11,7 @@
get_diff_file_path,
)
from cycode.cli.models import Document
from cycode.cli.utils.git_proxy import git_proxy
from cycode.cli.utils.path_utils import (
get_path_by_os,
)
Expand All @@ -31,7 +31,7 @@ def pre_commit_command(context: click.Context, ignored_args: List[str]) -> None:
scan_sca_pre_commit(context)
return

diff_files = Repo(os.getcwd()).index.diff('HEAD', create_patch=True, R=True)
diff_files = git_proxy.get_repo(os.getcwd()).index.diff('HEAD', create_patch=True, R=True)

progress_bar.set_section_length(ScanProgressBarSection.PREPARE_LOCAL_FILES, len(diff_files))

Expand Down
13 changes: 8 additions & 5 deletions cycode/cli/exceptions/handle_scan_errors.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
from typing import Optional

import click
from git import InvalidGitRepositoryError

from cycode.cli.exceptions import custom_exceptions
from cycode.cli.models import CliError, CliErrors
from cycode.cli.printers import ConsolePrinter
from cycode.cli.utils.git_proxy import git_proxy


def handle_scan_exception(
context: click.Context, e: Exception, *, return_exception: bool = False
) -> Optional[CliError]:
context.obj['did_fail'] = True

ConsolePrinter(context).print_exception()
ConsolePrinter(context).print_exception(e)

errors: CliErrors = {
custom_exceptions.NetworkError: CliError(
Expand Down Expand Up @@ -49,7 +49,7 @@ def handle_scan_exception(
'Please make sure that your file is well formed '
'and execute the scan again',
),
InvalidGitRepositoryError: CliError(
git_proxy.get_invalid_git_repository_error(): CliError(
soft_fail=False,
code='invalid_git_error',
message='The path you supplied does not correlate to a git repository. '
Expand All @@ -69,10 +69,13 @@ def handle_scan_exception(
ConsolePrinter(context).print_error(error)
return None

unknown_error = CliError(code='unknown_error', message=str(e))

if return_exception:
return CliError(code='unknown_error', message=str(e))
return unknown_error

if isinstance(e, click.ClickException):
raise e

raise click.ClickException(str(e))
ConsolePrinter(context).print_error(unknown_error)
exit(1)
14 changes: 7 additions & 7 deletions cycode/cli/files_collector/repository_documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from cycode.cli import consts
from cycode.cli.files_collector.sca import sca_code_scanner
from cycode.cli.models import Document
from cycode.cli.utils.git_proxy import git_proxy
from cycode.cli.utils.path_utils import get_file_content, get_path_by_os

if TYPE_CHECKING:
Expand All @@ -13,8 +14,6 @@

from cycode.cli.utils.progress_bar import BaseProgressBar, ProgressBarSection

from git import Repo


def should_process_git_object(obj: 'Blob', _: int) -> bool:
return obj.type == 'blob' and obj.size > 0
Expand All @@ -23,14 +22,14 @@ def should_process_git_object(obj: 'Blob', _: int) -> bool:
def get_git_repository_tree_file_entries(
path: str, branch: str
) -> Union[Iterator['IndexObjUnion'], Iterator['TraversedTreeTup']]:
return Repo(path).tree(branch).traverse(predicate=should_process_git_object)
return git_proxy.get_repo(path).tree(branch).traverse(predicate=should_process_git_object)


def parse_commit_range(commit_range: str, path: str) -> Tuple[str, str]:
from_commit_rev = None
to_commit_rev = None

for commit in Repo(path).iter_commits(rev=commit_range):
for commit in git_proxy.get_repo(path).iter_commits(rev=commit_range):
if not to_commit_rev:
to_commit_rev = commit.hexsha
from_commit_rev = commit.hexsha
Expand All @@ -52,7 +51,7 @@ def get_pre_commit_modified_documents(
git_head_documents = []
pre_committed_documents = []

repo = Repo(os.getcwd())
repo = git_proxy.get_repo(os.getcwd())
diff_files = repo.index.diff(consts.GIT_HEAD_COMMIT_REV, create_patch=True, R=True)
progress_bar.set_section_length(progress_bar_section, len(diff_files))
for file in diff_files:
Expand Down Expand Up @@ -82,7 +81,7 @@ def get_commit_range_modified_documents(
from_commit_documents = []
to_commit_documents = []

repo = Repo(path)
repo = git_proxy.get_repo(path)
diff = repo.commit(from_commit_rev).diff(to_commit_rev)

modified_files_diff = [
Expand Down Expand Up @@ -131,7 +130,8 @@ def _get_end_commit_from_branch_update_details(update_details: str) -> str:
def _get_oldest_unupdated_commit_for_branch(commit: str) -> Optional[str]:
# get a list of commits by chronological order that are not in the remote repository yet
# more info about rev-list command: https://git-scm.com/docs/git-rev-list
not_updated_commits = Repo(os.getcwd()).git.rev_list(commit, '--topo-order', '--reverse', '--not', '--all')
repo = git_proxy.get_repo(os.getcwd())
not_updated_commits = repo.git.rev_list(commit, '--topo-order', '--reverse', '--not', '--all')

commits = not_updated_commits.splitlines()
if not commits:
Expand Down
16 changes: 9 additions & 7 deletions cycode/cli/files_collector/sca/sca_code_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
from typing import TYPE_CHECKING, Dict, List, Optional

import click
from git import GitCommandError, Repo

from cycode.cli import consts
from cycode.cli.files_collector.sca.maven.restore_gradle_dependencies import RestoreGradleDependencies
from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import RestoreMavenDependencies
from cycode.cli.models import Document
from cycode.cli.utils.git_proxy import git_proxy
from cycode.cli.utils.path_utils import get_file_content, get_file_dir, join_paths
from cycode.cyclient import logger

if TYPE_CHECKING:
from git import Repo

from cycode.cli.files_collector.sca.maven.base_restore_maven_dependencies import BaseRestoreMavenDependencies

BUILD_GRADLE_FILE_NAME = 'build.gradle'
Expand All @@ -27,21 +29,21 @@ def perform_pre_commit_range_scan_actions(
to_commit_documents: List[Document],
to_commit_rev: str,
) -> None:
repo = Repo(path)
repo = git_proxy.get_repo(path)
add_ecosystem_related_files_if_exists(from_commit_documents, repo, from_commit_rev)
add_ecosystem_related_files_if_exists(to_commit_documents, repo, to_commit_rev)


def perform_pre_hook_range_scan_actions(
git_head_documents: List[Document], pre_committed_documents: List[Document]
) -> None:
repo = Repo(os.getcwd())
repo = git_proxy.get_repo(os.getcwd())
add_ecosystem_related_files_if_exists(git_head_documents, repo, consts.GIT_HEAD_COMMIT_REV)
add_ecosystem_related_files_if_exists(pre_committed_documents)


def add_ecosystem_related_files_if_exists(
documents: List[Document], repo: Optional[Repo] = None, commit_rev: Optional[str] = None
documents: List[Document], repo: Optional['Repo'] = None, commit_rev: Optional[str] = None
) -> None:
documents_to_add: List[Document] = []
for doc in documents:
Expand All @@ -56,7 +58,7 @@ def add_ecosystem_related_files_if_exists(


def get_doc_ecosystem_related_project_files(
doc: Document, documents: List[Document], ecosystem: str, commit_rev: Optional[str], repo: Optional[Repo]
doc: Document, documents: List[Document], ecosystem: str, commit_rev: Optional[str], repo: Optional['Repo']
) -> List[Document]:
documents_to_add: List[Document] = []
for ecosystem_project_file in consts.PROJECT_FILES_BY_ECOSYSTEM_MAP.get(ecosystem):
Expand Down Expand Up @@ -136,10 +138,10 @@ def get_manifest_file_path(document: Document, is_monitor_action: bool, project_
return join_paths(project_path, document.path) if is_monitor_action else document.path


def get_file_content_from_commit(repo: Repo, commit: str, file_path: str) -> Optional[str]:
def get_file_content_from_commit(repo: 'Repo', commit: str, file_path: str) -> Optional[str]:
try:
return repo.git.show(f'{commit}:{file_path}')
except GitCommandError:
except git_proxy.get_git_command_error():
return None


Expand Down
2 changes: 1 addition & 1 deletion cycode/cli/printers/printer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def print_exception(self, e: Optional[BaseException] = None) -> None:
# gets the most recent exception caught by an except clause
message = f'Error: {traceback.format_exc()}'
else:
traceback_message = ''.join(traceback.format_exception(e))
traceback_message = ''.join(traceback.format_exception(None, e, e.__traceback__))
message = f'Error: {traceback_message}'

click.secho(message, err=True, fg=self.RED_COLOR_NAME)
Expand Down
76 changes: 76 additions & 0 deletions cycode/cli/utils/git_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import types
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Optional, Type

_GIT_ERROR_MESSAGE = """
Cycode CLI needs the git executable to be installed on the system.
Git executable must be available in the PATH.
Git 1.7.x or newer is required.
You can help Cycode CLI to locate the Git executable
by setting the GIT_PYTHON_GIT_EXECUTABLE=<path/to/git> environment variable.
""".strip().replace('\n', ' ')

try:
import git
except ImportError:
git = None

if TYPE_CHECKING:
from git import PathLike, Repo


class GitProxyError(Exception):
pass


class _AbstractGitProxy(ABC):
@abstractmethod
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
...

@abstractmethod
def get_null_tree(self) -> object:
...

@abstractmethod
def get_invalid_git_repository_error(self) -> Type[BaseException]:
...

@abstractmethod
def get_git_command_error(self) -> Type[BaseException]:
...


class _DummyGitProxy(_AbstractGitProxy):
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
raise RuntimeError(_GIT_ERROR_MESSAGE)

def get_null_tree(self) -> object:
raise RuntimeError(_GIT_ERROR_MESSAGE)

def get_invalid_git_repository_error(self) -> Type[BaseException]:
return GitProxyError

def get_git_command_error(self) -> Type[BaseException]:
return GitProxyError


class _GitProxy(_AbstractGitProxy):
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
return git.Repo(path, *args, **kwargs)

def get_null_tree(self) -> object:
return git.NULL_TREE

def get_invalid_git_repository_error(self) -> Type[BaseException]:
return git.InvalidGitRepositoryError

def get_git_command_error(self) -> Type[BaseException]:
return git.GitCommandError


def get_git_proxy(git_module: Optional[types.ModuleType]) -> _AbstractGitProxy:
return _GitProxy() if git_module else _DummyGitProxy()


git_proxy = get_git_proxy(git)
14 changes: 8 additions & 6 deletions tests/cli/exceptions/test_handle_scan_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import click
import pytest
from click import ClickException
from git import InvalidGitRepositoryError
from requests import Response

from cycode.cli.exceptions import custom_exceptions
from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception
from cycode.cli.utils.git_proxy import git_proxy

if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch
Expand All @@ -26,7 +26,7 @@ def ctx() -> click.Context:
(custom_exceptions.HttpUnauthorizedError('msg', Response()), True),
(custom_exceptions.ZipTooLargeError(1000), True),
(custom_exceptions.TfplanKeyError('msg'), True),
(InvalidGitRepositoryError(), None),
(git_proxy.get_invalid_git_repository_error()(), None),
],
)
def test_handle_exception_soft_fail(
Expand All @@ -40,7 +40,7 @@ def test_handle_exception_soft_fail(


def test_handle_exception_unhandled_error(ctx: click.Context) -> None:
with ctx, pytest.raises(ClickException):
with ctx, pytest.raises(SystemExit):
handle_scan_exception(ctx, ValueError('test'))

assert ctx.obj.get('did_fail') is True
Expand All @@ -58,10 +58,12 @@ def test_handle_exception_click_error(ctx: click.Context) -> None:
def test_handle_exception_verbose(monkeypatch: 'MonkeyPatch') -> None:
ctx = click.Context(click.Command('path'), obj={'verbose': True, 'output': 'text'})

error_text = 'test'

def mock_secho(msg: str, *_, **__) -> None:
assert 'Error:' in msg or 'Correlation ID:' in msg
assert error_text in msg or 'Correlation ID:' in msg

monkeypatch.setattr(click, 'secho', mock_secho)

with ctx, pytest.raises(ClickException):
handle_scan_exception(ctx, ValueError('test'))
with pytest.raises(SystemExit):
handle_scan_exception(ctx, ValueError(error_text))
Loading