Skip to content

Commit e63c1db

Browse files
authored
CM-30526 - Make Git executable optional (#230)
1 parent 8b91279 commit e63c1db

File tree

9 files changed

+169
-35
lines changed

9 files changed

+169
-35
lines changed

cycode/cli/commands/scan/code_scanner.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from uuid import UUID, uuid4
88

99
import click
10-
from git import NULL_TREE, Repo
1110

1211
from cycode.cli import consts
1312
from cycode.cli.config import configuration_manager
@@ -28,9 +27,8 @@
2827
from cycode.cli.models import CliError, Document, DocumentDetections, LocalScanResult, Severity
2928
from cycode.cli.printers import ConsolePrinter
3029
from cycode.cli.utils import scan_utils
31-
from cycode.cli.utils.path_utils import (
32-
get_path_by_os,
33-
)
30+
from cycode.cli.utils.git_proxy import git_proxy
31+
from cycode.cli.utils.path_utils import get_path_by_os
3432
from cycode.cli.utils.progress_bar import ScanProgressBarSection
3533
from cycode.cli.utils.scan_batch import run_parallel_batched_scan
3634
from cycode.cli.utils.scan_utils import set_issue_detected
@@ -244,7 +242,7 @@ def scan_commit_range(
244242
documents_to_scan = []
245243
commit_ids_to_scan = []
246244

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

@@ -261,7 +259,7 @@ def scan_commit_range(
261259

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

689687
def try_get_git_remote_url(path: str) -> Optional[str]:
690688
try:
691-
remote_url = Repo(path).remotes[0].config_reader.get('url')
689+
remote_url = git_proxy.get_repo(path).remotes[0].config_reader.get('url')
692690
logger.debug('Found Git remote URL, %s', {'remote_url': remote_url, 'path': path})
693691
return remote_url
694692
except Exception as e:

cycode/cli/commands/scan/pre_commit/pre_commit_command.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from typing import List
33

44
import click
5-
from git import Repo
65

76
from cycode.cli import consts
87
from cycode.cli.commands.scan.code_scanner import scan_documents, scan_sca_pre_commit
@@ -12,6 +11,7 @@
1211
get_diff_file_path,
1312
)
1413
from cycode.cli.models import Document
14+
from cycode.cli.utils.git_proxy import git_proxy
1515
from cycode.cli.utils.path_utils import (
1616
get_path_by_os,
1717
)
@@ -31,7 +31,7 @@ def pre_commit_command(context: click.Context, ignored_args: List[str]) -> None:
3131
scan_sca_pre_commit(context)
3232
return
3333

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

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

cycode/cli/exceptions/handle_scan_errors.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from typing import Optional
22

33
import click
4-
from git import InvalidGitRepositoryError
54

65
from cycode.cli.exceptions import custom_exceptions
76
from cycode.cli.models import CliError, CliErrors
87
from cycode.cli.printers import ConsolePrinter
8+
from cycode.cli.utils.git_proxy import git_proxy
99

1010

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

16-
ConsolePrinter(context).print_exception()
16+
ConsolePrinter(context).print_exception(e)
1717

1818
errors: CliErrors = {
1919
custom_exceptions.NetworkError: CliError(
@@ -49,7 +49,7 @@ def handle_scan_exception(
4949
'Please make sure that your file is well formed '
5050
'and execute the scan again',
5151
),
52-
InvalidGitRepositoryError: CliError(
52+
git_proxy.get_invalid_git_repository_error(): CliError(
5353
soft_fail=False,
5454
code='invalid_git_error',
5555
message='The path you supplied does not correlate to a git repository. '
@@ -69,10 +69,13 @@ def handle_scan_exception(
6969
ConsolePrinter(context).print_error(error)
7070
return None
7171

72+
unknown_error = CliError(code='unknown_error', message=str(e))
73+
7274
if return_exception:
73-
return CliError(code='unknown_error', message=str(e))
75+
return unknown_error
7476

7577
if isinstance(e, click.ClickException):
7678
raise e
7779

78-
raise click.ClickException(str(e))
80+
ConsolePrinter(context).print_error(unknown_error)
81+
exit(1)

cycode/cli/files_collector/repository_documents.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from cycode.cli import consts
55
from cycode.cli.files_collector.sca import sca_code_scanner
66
from cycode.cli.models import Document
7+
from cycode.cli.utils.git_proxy import git_proxy
78
from cycode.cli.utils.path_utils import get_file_content, get_path_by_os
89

910
if TYPE_CHECKING:
@@ -13,8 +14,6 @@
1314

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

16-
from git import Repo
17-
1817

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

2827

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

33-
for commit in Repo(path).iter_commits(rev=commit_range):
32+
for commit in git_proxy.get_repo(path).iter_commits(rev=commit_range):
3433
if not to_commit_rev:
3534
to_commit_rev = commit.hexsha
3635
from_commit_rev = commit.hexsha
@@ -52,7 +51,7 @@ def get_pre_commit_modified_documents(
5251
git_head_documents = []
5352
pre_committed_documents = []
5453

55-
repo = Repo(os.getcwd())
54+
repo = git_proxy.get_repo(os.getcwd())
5655
diff_files = repo.index.diff(consts.GIT_HEAD_COMMIT_REV, create_patch=True, R=True)
5756
progress_bar.set_section_length(progress_bar_section, len(diff_files))
5857
for file in diff_files:
@@ -82,7 +81,7 @@ def get_commit_range_modified_documents(
8281
from_commit_documents = []
8382
to_commit_documents = []
8483

85-
repo = Repo(path)
84+
repo = git_proxy.get_repo(path)
8685
diff = repo.commit(from_commit_rev).diff(to_commit_rev)
8786

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

136136
commits = not_updated_commits.splitlines()
137137
if not commits:

cycode/cli/files_collector/sca/sca_code_scanner.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,18 @@
22
from typing import TYPE_CHECKING, Dict, List, Optional
33

44
import click
5-
from git import GitCommandError, Repo
65

76
from cycode.cli import consts
87
from cycode.cli.files_collector.sca.maven.restore_gradle_dependencies import RestoreGradleDependencies
98
from cycode.cli.files_collector.sca.maven.restore_maven_dependencies import RestoreMavenDependencies
109
from cycode.cli.models import Document
10+
from cycode.cli.utils.git_proxy import git_proxy
1111
from cycode.cli.utils.path_utils import get_file_content, get_file_dir, join_paths
1212
from cycode.cyclient import logger
1313

1414
if TYPE_CHECKING:
15+
from git import Repo
16+
1517
from cycode.cli.files_collector.sca.maven.base_restore_maven_dependencies import BaseRestoreMavenDependencies
1618

1719
BUILD_GRADLE_FILE_NAME = 'build.gradle'
@@ -27,21 +29,21 @@ def perform_pre_commit_range_scan_actions(
2729
to_commit_documents: List[Document],
2830
to_commit_rev: str,
2931
) -> None:
30-
repo = Repo(path)
32+
repo = git_proxy.get_repo(path)
3133
add_ecosystem_related_files_if_exists(from_commit_documents, repo, from_commit_rev)
3234
add_ecosystem_related_files_if_exists(to_commit_documents, repo, to_commit_rev)
3335

3436

3537
def perform_pre_hook_range_scan_actions(
3638
git_head_documents: List[Document], pre_committed_documents: List[Document]
3739
) -> None:
38-
repo = Repo(os.getcwd())
40+
repo = git_proxy.get_repo(os.getcwd())
3941
add_ecosystem_related_files_if_exists(git_head_documents, repo, consts.GIT_HEAD_COMMIT_REV)
4042
add_ecosystem_related_files_if_exists(pre_committed_documents)
4143

4244

4345
def add_ecosystem_related_files_if_exists(
44-
documents: List[Document], repo: Optional[Repo] = None, commit_rev: Optional[str] = None
46+
documents: List[Document], repo: Optional['Repo'] = None, commit_rev: Optional[str] = None
4547
) -> None:
4648
documents_to_add: List[Document] = []
4749
for doc in documents:
@@ -56,7 +58,7 @@ def add_ecosystem_related_files_if_exists(
5658

5759

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

138140

139-
def get_file_content_from_commit(repo: Repo, commit: str, file_path: str) -> Optional[str]:
141+
def get_file_content_from_commit(repo: 'Repo', commit: str, file_path: str) -> Optional[str]:
140142
try:
141143
return repo.git.show(f'{commit}:{file_path}')
142-
except GitCommandError:
144+
except git_proxy.get_git_command_error():
143145
return None
144146

145147

cycode/cli/printers/printer_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def print_exception(self, e: Optional[BaseException] = None) -> None:
4343
# gets the most recent exception caught by an except clause
4444
message = f'Error: {traceback.format_exc()}'
4545
else:
46-
traceback_message = ''.join(traceback.format_exception(e))
46+
traceback_message = ''.join(traceback.format_exception(None, e, e.__traceback__))
4747
message = f'Error: {traceback_message}'
4848

4949
click.secho(message, err=True, fg=self.RED_COLOR_NAME)

cycode/cli/utils/git_proxy.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import types
2+
from abc import ABC, abstractmethod
3+
from typing import TYPE_CHECKING, Optional, Type
4+
5+
_GIT_ERROR_MESSAGE = """
6+
Cycode CLI needs the git executable to be installed on the system.
7+
Git executable must be available in the PATH.
8+
Git 1.7.x or newer is required.
9+
You can help Cycode CLI to locate the Git executable
10+
by setting the GIT_PYTHON_GIT_EXECUTABLE=<path/to/git> environment variable.
11+
""".strip().replace('\n', ' ')
12+
13+
try:
14+
import git
15+
except ImportError:
16+
git = None
17+
18+
if TYPE_CHECKING:
19+
from git import PathLike, Repo
20+
21+
22+
class GitProxyError(Exception):
23+
pass
24+
25+
26+
class _AbstractGitProxy(ABC):
27+
@abstractmethod
28+
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
29+
...
30+
31+
@abstractmethod
32+
def get_null_tree(self) -> object:
33+
...
34+
35+
@abstractmethod
36+
def get_invalid_git_repository_error(self) -> Type[BaseException]:
37+
...
38+
39+
@abstractmethod
40+
def get_git_command_error(self) -> Type[BaseException]:
41+
...
42+
43+
44+
class _DummyGitProxy(_AbstractGitProxy):
45+
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
46+
raise RuntimeError(_GIT_ERROR_MESSAGE)
47+
48+
def get_null_tree(self) -> object:
49+
raise RuntimeError(_GIT_ERROR_MESSAGE)
50+
51+
def get_invalid_git_repository_error(self) -> Type[BaseException]:
52+
return GitProxyError
53+
54+
def get_git_command_error(self) -> Type[BaseException]:
55+
return GitProxyError
56+
57+
58+
class _GitProxy(_AbstractGitProxy):
59+
def get_repo(self, path: Optional['PathLike'] = None, *args, **kwargs) -> 'Repo':
60+
return git.Repo(path, *args, **kwargs)
61+
62+
def get_null_tree(self) -> object:
63+
return git.NULL_TREE
64+
65+
def get_invalid_git_repository_error(self) -> Type[BaseException]:
66+
return git.InvalidGitRepositoryError
67+
68+
def get_git_command_error(self) -> Type[BaseException]:
69+
return git.GitCommandError
70+
71+
72+
def get_git_proxy(git_module: Optional[types.ModuleType]) -> _AbstractGitProxy:
73+
return _GitProxy() if git_module else _DummyGitProxy()
74+
75+
76+
git_proxy = get_git_proxy(git)

tests/cli/exceptions/test_handle_scan_errors.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import click
44
import pytest
55
from click import ClickException
6-
from git import InvalidGitRepositoryError
76
from requests import Response
87

98
from cycode.cli.exceptions import custom_exceptions
109
from cycode.cli.exceptions.handle_scan_errors import handle_scan_exception
10+
from cycode.cli.utils.git_proxy import git_proxy
1111

1212
if TYPE_CHECKING:
1313
from _pytest.monkeypatch import MonkeyPatch
@@ -26,7 +26,7 @@ def ctx() -> click.Context:
2626
(custom_exceptions.HttpUnauthorizedError('msg', Response()), True),
2727
(custom_exceptions.ZipTooLargeError(1000), True),
2828
(custom_exceptions.TfplanKeyError('msg'), True),
29-
(InvalidGitRepositoryError(), None),
29+
(git_proxy.get_invalid_git_repository_error()(), None),
3030
],
3131
)
3232
def test_handle_exception_soft_fail(
@@ -40,7 +40,7 @@ def test_handle_exception_soft_fail(
4040

4141

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

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

61+
error_text = 'test'
62+
6163
def mock_secho(msg: str, *_, **__) -> None:
62-
assert 'Error:' in msg or 'Correlation ID:' in msg
64+
assert error_text in msg or 'Correlation ID:' in msg
6365

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

66-
with ctx, pytest.raises(ClickException):
67-
handle_scan_exception(ctx, ValueError('test'))
68+
with pytest.raises(SystemExit):
69+
handle_scan_exception(ctx, ValueError(error_text))

0 commit comments

Comments
 (0)