Skip to content

Commit

Permalink
validate that git is installed before executing any command. (#3568)
Browse files Browse the repository at this point in the history
* validate that git is installed

* pre-commit

* add ruff rule

* update to gitutil - first phase

* use only GitUtil phase2

* exit if git is not installed

* try to fix uts

* add gitutil UT and remove cirucilar import

* git content return repo

* test

* update

* try to fix validations

* fix content artifcats tests

* pre-commit + linter fixes

* pre-commit

* try to fix uts now

* update more uts

* fix lint uts

* update final tests

* changelog

* fix last uts

* fixes

* fix more unit-tests

* update

* ruff

* remove from git_util

* update err with click

* pre-commit

* make sure git is imported at top

* ruff

* update name of repo cls

* cr fixes

* change git method to git_util

* readme

* update

* fix tests

* fix tests

* fix uts

* changelog

* update logger

* use error with click

* pre-commit
  • Loading branch information
GuyAfik committed Sep 10, 2023
1 parent fee1876 commit d0c24d2
Show file tree
Hide file tree
Showing 30 changed files with 107 additions and 139 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
@@ -1,5 +1,6 @@
# Changelog
## Unreleased
* Demisto-SDK will now exit gracefully with an appropriate error message when *git* is not installed.

## 1.20.2
* Updated the **pre-commit** command to run on all python versions in one run.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Expand Up @@ -9,6 +9,8 @@
The Demisto SDK library can be used to manage your Cortex XSOAR content with ease and efficiency.
The library supports Python 3.8-3.10. _Python 3.8 support will be removed soon._

In order to function properly, the Demisto SDK requires git to be installed. If git isn't installed, an appropriate message will be raised.

## Usage

### Installation
Expand Down
12 changes: 9 additions & 3 deletions demisto_sdk/__main__.py
@@ -1,14 +1,20 @@
# Site packages
import sys

import click

try:
import git
except ImportError:
sys.exit(click.style("Git executable cannot be found, or is invalid", fg="red"))

import copy
import functools
import logging
import os
import sys
from pathlib import Path
from typing import IO, Any, Dict, Iterable, Tuple, Union

import click
import git
import typer
from pkg_resources import DistributionNotFound, get_distribution

Expand Down
34 changes: 14 additions & 20 deletions demisto_sdk/commands/common/content/content.py
Expand Up @@ -3,7 +3,7 @@
import os
from typing import Any, Iterator

from git import InvalidGitRepositoryError, Repo
from git import InvalidGitRepositoryError
from wcmatch.pathlib import Path

from demisto_sdk.commands.common.constants import (
Expand All @@ -24,6 +24,7 @@
Documentation,
)
from demisto_sdk.commands.common.content.objects_factory import path_to_pack_object
from demisto_sdk.commands.common.git_util import GitUtil
from demisto_sdk.commands.common.logger import logger


Expand Down Expand Up @@ -53,17 +54,14 @@ def from_cwd(cls) -> Content:
TODO:
1. Add attribute which init only changed objects by git.
"""
repo = cls.git()
if repo:
content = Content(repo.working_tree_dir) # type: ignore
else:
content = Content(Path.cwd())

return content
try:
return Content(str(cls.git_util().repo.working_tree_dir))
except InvalidGitRepositoryError:
return Content(Path.cwd())

@staticmethod
def git() -> Repo | None:
"""Git repository object.
def git_util() -> GitUtil:
"""Git Util object.
Returns:
Repo: Repo object of content repo if exists else retun None.
Expand All @@ -74,17 +72,13 @@ def git() -> Repo | None:
Notes:
1. Should be called when cwd inside content repository.
"""
try:
if content_path := os.getenv("DEMISTO_SDK_CONTENT_PATH"):
repo = Repo(content_path)
logger.debug(f"Using content path: {content_path}")
else:
repo = Repo(Path.cwd(), search_parent_directories=True)
except InvalidGitRepositoryError:
logger.debug("Git repo was not found.")
repo = None
if content_path := os.getenv("DEMISTO_SDK_CONTENT_PATH"):
git_util = GitUtil(Path(content_path))
logger.debug(f"Using content path: {content_path}")
else:
git_util = GitUtil(search_parent_directories=True)

return repo
return git_util

@property
def path(self) -> Path:
Expand Down
37 changes: 25 additions & 12 deletions demisto_sdk/commands/common/git_util.py
@@ -1,30 +1,43 @@
import os
import re
from pathlib import Path
from typing import Set, Tuple, Union
from typing import Optional, Set, Tuple, Union

import click
import gitdb
from git import InvalidGitRepositoryError, Repo
from git import (
InvalidGitRepositoryError,
Repo, # noqa: TID251: required to create GitUtil
)
from git.diff import Lit_change_type
from git.remote import Remote

from demisto_sdk.commands.common.constants import PACKS_FOLDER


class GitUtil:
repo: Repo
# in order to use Repo class/static methods
REPO_CLS = Repo

def __init__(self, repo: Repo = None):
if not repo:
try:
self.repo = Repo(Path.cwd(), search_parent_directories=True)
except InvalidGitRepositoryError:
raise InvalidGitRepositoryError(
"Unable to find Repository from current working directory - aborting"
)
def __init__(
self,
path: Optional[Path] = None,
search_parent_directories: bool = True,
):

if isinstance(path, str):
repo_path = Path(path)
else:
self.repo = repo
repo_path = path or Path.cwd()

try:
self.repo = Repo(
repo_path, search_parent_directories=search_parent_directories
)
except InvalidGitRepositoryError:
raise InvalidGitRepositoryError(
f"Unable to find Repository from current {repo_path.absolute()} - aborting"
)

def get_all_files(self) -> Set[Path]:
return set(map(Path, self.repo.git.ls_files().split("\n")))
Expand Down
Expand Up @@ -28,7 +28,6 @@
from demisto_sdk.commands.common.content import Content
from demisto_sdk.commands.common.content_constant_paths import CONF_PATH, CONTENT_PATH
from demisto_sdk.commands.common.errors import Errors
from demisto_sdk.commands.common.git_util import GitUtil
from demisto_sdk.commands.common.handlers import DEFAULT_JSON_HANDLER as json
from demisto_sdk.commands.common.handlers import DEFAULT_YAML_HANDLER as yaml
from demisto_sdk.commands.common.hook_validations.base_validator import (
Expand Down Expand Up @@ -303,7 +302,7 @@ def is_release_branch() -> bool:
Returns:
(bool): is release branch
"""
git_util = GitUtil(repo=Content.git())
git_util = Content.git_util()
main_branch = git_util.handle_prev_ver()[1]
if not main_branch.startswith("origin"):
main_branch = "origin/" + main_branch
Expand Down
Expand Up @@ -5,7 +5,6 @@
from demisto_sdk.commands.common.constants import PACKS_DIR
from demisto_sdk.commands.common.content.content import Content
from demisto_sdk.commands.common.errors import Errors
from demisto_sdk.commands.common.git_util import GitUtil
from demisto_sdk.commands.common.hook_validations.base_validator import (
BaseValidator,
error_codes,
Expand Down Expand Up @@ -251,7 +250,7 @@ def validate_deprecated_items_usage(self):
For existing content, a warning is raised.
"""
is_valid = True
new_files = GitUtil(repo=Content.git()).added_files()
new_files = Content.git_util().added_files()
items: List[dict] = self.graph.find_items_using_deprecated_items(
self.file_paths
)
Expand Down
Expand Up @@ -9,7 +9,7 @@
from typing import Dict, List, Set, Tuple

from dateutil import parser
from git import GitCommandError, Repo
from git import GitCommandError
from packaging.version import Version, parse

from demisto_sdk.commands.common import tools
Expand Down Expand Up @@ -135,7 +135,7 @@ def __init__(
self.metadata_content: Dict = dict()

if not prev_ver:
git_util = GitUtil(repo=Content.git())
git_util = Content.git_util()
main_branch = git_util.handle_prev_ver()[1]
self.prev_ver = (
f"origin/{main_branch}"
Expand Down Expand Up @@ -981,7 +981,7 @@ def is_right_usage_of_usecase_tag(self):
return True

def get_master_private_repo_meta_file(self, metadata_file_path: str):
current_repo = Repo(Path.cwd(), search_parent_directories=True)
current_repo = GitUtil().repo

# if running on master branch in private repo - do not run the test
if current_repo.active_branch == "master":
Expand Down
2 changes: 1 addition & 1 deletion demisto_sdk/commands/common/tests/git_config_test.py
Expand Up @@ -3,7 +3,7 @@
from typing import NamedTuple

import pytest
from git import Repo
from git import Repo # noqa: TID251

from demisto_sdk.commands.common.git_content_config import (
GitContentConfig,
Expand Down
20 changes: 4 additions & 16 deletions demisto_sdk/commands/common/tests/pack_unique_files_test.py
Expand Up @@ -760,7 +760,7 @@ class MyRepo:
active_branch = "master"

mocker.patch(
"demisto_sdk.commands.common.hook_validations.pack_unique_files.Repo",
"demisto_sdk.commands.common.git_util.Repo",
return_value=MyRepo,
)
res = self.validator.get_master_private_repo_meta_file(
Expand Down Expand Up @@ -810,12 +810,10 @@ def rev_parse(self, var):
git = gitClass()

mocker.patch(
"demisto_sdk.commands.common.hook_validations.pack_unique_files.Repo",
"demisto_sdk.commands.common.git_util.Repo",
return_value=MyRepo(),
)
mocker.patch(
"demisto_sdk.commands.common.tools.git.Repo", return_value=MyRepo()
)

with ChangeCWD(repo.path):
res = self.validator.get_master_private_repo_meta_file(
str(pack.pack_metadata.path)
Expand Down Expand Up @@ -864,12 +862,9 @@ def rev_parse(self, var):
git = gitClass()

mocker.patch(
"demisto_sdk.commands.common.hook_validations.pack_unique_files.Repo",
"demisto_sdk.commands.common.git_util.Repo",
return_value=MyRepo(),
)
mocker.patch(
"demisto_sdk.commands.common.tools.git.Repo", return_value=MyRepo()
)
res = self.validator.get_master_private_repo_meta_file(
str(pack.pack_metadata.path)
)
Expand Down Expand Up @@ -925,13 +920,6 @@ def remote(self):

git = gitClass()

mocker.patch(
"demisto_sdk.commands.common.hook_validations.pack_unique_files.Repo",
return_value=MyRepo(),
)
mocker.patch(
"demisto_sdk.commands.common.tools.git.Repo", return_value=MyRepo()
)
mocker.patch("demisto_sdk.commands.common.git_util.Repo", return_value=MyRepo())

with ChangeCWD(repo.path):
Expand Down
5 changes: 2 additions & 3 deletions demisto_sdk/commands/common/tests/tools_test.py
Expand Up @@ -6,7 +6,6 @@
from tempfile import NamedTemporaryFile, TemporaryDirectory
from typing import Callable, List, Optional, Tuple, Union

import git
import pytest
import requests

Expand Down Expand Up @@ -626,12 +625,12 @@ class TestGetRemoteFileLocally:
FILE_NAME = "somefile.json"
FILE_CONTENT = '{"id": "some_file"}'

git_util = GitUtil(repo=Content.git())
git_util = Content.git_util()
main_branch = git_util.handle_prev_ver()[1]

def setup_method(self):
# create local git repo
example_repo = git.Repo.init(self.REPO_NAME)
example_repo = GitUtil.REPO_CLS.init(self.REPO_NAME)
origin_branch = self.main_branch
if not origin_branch.startswith("origin"):
origin_branch = "origin/" + origin_branch
Expand Down
17 changes: 7 additions & 10 deletions demisto_sdk/commands/common/tools.py
Expand Up @@ -339,7 +339,7 @@ def src_root() -> Path:
Returns:
Path: src root path.
"""
git_dir = git.Repo(Path.cwd(), search_parent_directories=True).working_tree_dir
git_dir = GitUtil().repo.working_tree_dir

return Path(git_dir) / "demisto_sdk" # type: ignore

Expand Down Expand Up @@ -436,10 +436,7 @@ def get_local_remote_file(
tag: str = "master",
return_content: bool = False,
):
repo = git.Repo(
search_parent_directories=True
) # the full file path could be a git file path
repo_git_util = GitUtil(repo)
repo_git_util = GitUtil()
git_path = repo_git_util.get_local_remote_file_path(full_file_path, tag)
file_content = repo_git_util.get_local_remote_file_content(git_path)
if return_content:
Expand Down Expand Up @@ -1958,7 +1955,7 @@ def is_external_repository() -> bool:
"""
try:
git_repo = git.Repo(os.getcwd(), search_parent_directories=True)
git_repo = GitUtil().repo
private_settings_path = os.path.join(git_repo.working_dir, ".private-repo-settings") # type: ignore
return Path(private_settings_path).exists()
except git.InvalidGitRepositoryError:
Expand Down Expand Up @@ -2016,10 +2013,10 @@ def get_content_path(relative_path: Optional[Path] = None) -> Path:
)
try:
if content_path := os.getenv("DEMISTO_SDK_CONTENT_PATH"):
git_repo = git.Repo(content_path)
git_repo = GitUtil(Path(content_path), search_parent_directories=False).repo
logger.debug(f"Using content path: {content_path}")
else:
git_repo = git.Repo(Path.cwd(), search_parent_directories=True)
git_repo = GitUtil().repo

remote_url = git_repo.remote().urls.__next__()
is_fork_repo = "content" in remote_url
Expand Down Expand Up @@ -2122,7 +2119,7 @@ def is_file_from_content_repo(file_path: str) -> Tuple[bool, str]:
str: relative path of file in content repo.
"""
try:
git_repo = git.Repo(os.getcwd(), search_parent_directories=True)
git_repo = GitUtil().repo
remote_url = git_repo.remote().urls.__next__()
is_fork_repo = "content" in remote_url
is_external_repo = is_external_repository()
Expand Down Expand Up @@ -3150,7 +3147,7 @@ def extract_docker_image_from_text(text: str, with_no_tag: bool = False):

def get_current_repo() -> Tuple[str, str, str]:
try:
git_repo = git.Repo(os.getcwd(), search_parent_directories=True)
git_repo = GitUtil().repo
parsed_git = giturlparse.parse(git_repo.remotes.origin.url)
host = parsed_git.host
if "@" in host:
Expand Down
Expand Up @@ -1446,7 +1446,7 @@ def content_files_handler(
and content_object.code_path.name == "CommonServerPython.py"
):
# Modify CommonServerPython.py global variables
repo = artifact_manager.content.git()
repo = artifact_manager.content.git_util().repo
modify_common_server_constants(
content_object.code_path,
artifact_manager.content_version,
Expand Down
Expand Up @@ -134,8 +134,8 @@ def mock_git(mocker):
from demisto_sdk.commands.common.content import Content

# Mock git working directory
mocker.patch.object(Content, "git")
Content.git().working_tree_dir = TEST_CONTENT_REPO
mocker.patch.object(Content, "git_util")
Content.git_util().repo.working_tree_dir = TEST_CONTENT_REPO
yield


Expand Down Expand Up @@ -407,8 +407,10 @@ def mock_single_pack_git(mocker):
from demisto_sdk.commands.common.content import Content

# Mock git working directory
mocker.patch.object(Content, "git")
Content.git().working_tree_dir = TEST_DATA / "content_repo_with_alternative_fields"
mocker.patch.object(Content, "git_util")
Content.git_util().repo.working_tree_dir = (
TEST_DATA / "content_repo_with_alternative_fields"
)
yield


Expand Down

0 comments on commit d0c24d2

Please sign in to comment.