conviso-ast 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- conviso_ast-3.0.0.data/scripts/flow_bash_completer.sh +21 -0
- conviso_ast-3.0.0.data/scripts/flow_fish_completer.fish +1 -0
- conviso_ast-3.0.0.data/scripts/flow_zsh_completer.sh +32 -0
- conviso_ast-3.0.0.dist-info/METADATA +37 -0
- conviso_ast-3.0.0.dist-info/RECORD +128 -0
- conviso_ast-3.0.0.dist-info/WHEEL +5 -0
- conviso_ast-3.0.0.dist-info/entry_points.txt +3 -0
- conviso_ast-3.0.0.dist-info/top_level.txt +1 -0
- convisoappsec/__init__.py +0 -0
- convisoappsec/common/__init__.py +5 -0
- convisoappsec/common/box.py +251 -0
- convisoappsec/common/cleaner.py +78 -0
- convisoappsec/common/docker.py +399 -0
- convisoappsec/common/exceptions.py +8 -0
- convisoappsec/common/git_data_parser.py +76 -0
- convisoappsec/common/graphql/__init__.py +0 -0
- convisoappsec/common/graphql/error_handlers.py +75 -0
- convisoappsec/common/graphql/errors.py +16 -0
- convisoappsec/common/graphql/low_client.py +51 -0
- convisoappsec/common/retry_handler.py +40 -0
- convisoappsec/common/strings.py +8 -0
- convisoappsec/flow/__init__.py +3 -0
- convisoappsec/flow/api.py +104 -0
- convisoappsec/flow/cleaner.py +118 -0
- convisoappsec/flow/graphql_api/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/client.py +18 -0
- convisoappsec/flow/graphql_api/beta/models/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/models/issues/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/models/issues/container.py +72 -0
- convisoappsec/flow/graphql_api/beta/models/issues/iac.py +6 -0
- convisoappsec/flow/graphql_api/beta/models/issues/normalize.py +13 -0
- convisoappsec/flow/graphql_api/beta/models/issues/sast.py +53 -0
- convisoappsec/flow/graphql_api/beta/models/issues/sca.py +78 -0
- convisoappsec/flow/graphql_api/beta/resources_api.py +142 -0
- convisoappsec/flow/graphql_api/beta/schemas/__init__.py +0 -0
- convisoappsec/flow/graphql_api/beta/schemas/mutations/__init__.py +61 -0
- convisoappsec/flow/graphql_api/beta/schemas/resolvers/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/client.py +46 -0
- convisoappsec/flow/graphql_api/v1/models/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/models/asset.py +14 -0
- convisoappsec/flow/graphql_api/v1/models/issues.py +16 -0
- convisoappsec/flow/graphql_api/v1/models/project.py +35 -0
- convisoappsec/flow/graphql_api/v1/resources_api.py +489 -0
- convisoappsec/flow/graphql_api/v1/schemas/__init__.py +0 -0
- convisoappsec/flow/graphql_api/v1/schemas/mutations/__init__.py +212 -0
- convisoappsec/flow/graphql_api/v1/schemas/resolvers/__init__.py +180 -0
- convisoappsec/flow/source_code_scanner/__init__.py +9 -0
- convisoappsec/flow/source_code_scanner/exceptions.py +2 -0
- convisoappsec/flow/source_code_scanner/scc.py +68 -0
- convisoappsec/flow/source_code_scanner/source_code_scanner.py +177 -0
- convisoappsec/flow/util/__init__.py +7 -0
- convisoappsec/flow/util/ci_provider.py +99 -0
- convisoappsec/flow/util/metrics.py +16 -0
- convisoappsec/flow/util/source_code_compressor.py +22 -0
- convisoappsec/flow/version_control_system_adapter.py +528 -0
- convisoappsec/flow/version_searchers/__init__.py +9 -0
- convisoappsec/flow/version_searchers/sorted_by_versioning_style.py +85 -0
- convisoappsec/flow/version_searchers/timebased_version_seacher.py +39 -0
- convisoappsec/flow/version_searchers/version_searcher_result.py +33 -0
- convisoappsec/flow/versioning_style/__init__.py +0 -0
- convisoappsec/flow/versioning_style/semantic_versioning.py +44 -0
- convisoappsec/flowcli/__init__.py +3 -0
- convisoappsec/flowcli/__main__.py +4 -0
- convisoappsec/flowcli/assets/__init__.py +4 -0
- convisoappsec/flowcli/assets/create.py +88 -0
- convisoappsec/flowcli/assets/entrypoint.py +20 -0
- convisoappsec/flowcli/assets/ls.py +63 -0
- convisoappsec/flowcli/ast/__init__.py +3 -0
- convisoappsec/flowcli/ast/entrypoint.py +427 -0
- convisoappsec/flowcli/common.py +175 -0
- convisoappsec/flowcli/companies/__init__.py +0 -0
- convisoappsec/flowcli/companies/ls.py +25 -0
- convisoappsec/flowcli/container/__init__.py +3 -0
- convisoappsec/flowcli/container/entrypoint.py +17 -0
- convisoappsec/flowcli/container/run.py +306 -0
- convisoappsec/flowcli/context.py +49 -0
- convisoappsec/flowcli/deploy/__init__.py +0 -0
- convisoappsec/flowcli/deploy/create/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/context.py +12 -0
- convisoappsec/flowcli/deploy/create/entrypoint.py +31 -0
- convisoappsec/flowcli/deploy/create/with_/__init__.py +3 -0
- convisoappsec/flowcli/deploy/create/with_/entrypoint.py +20 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/context.py +11 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/entrypoint.py +30 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/__init__.py +4 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/entrypoint.py +21 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/time_.py +84 -0
- convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/versioning_style.py +115 -0
- convisoappsec/flowcli/deploy/create/with_/values.py +133 -0
- convisoappsec/flowcli/entrypoint.py +103 -0
- convisoappsec/flowcli/environment_checker.py +45 -0
- convisoappsec/flowcli/findings/__init__.py +4 -0
- convisoappsec/flowcli/findings/create/__init__.py +4 -0
- convisoappsec/flowcli/findings/create/entrypoint.py +18 -0
- convisoappsec/flowcli/findings/create/with_/__init__.py +3 -0
- convisoappsec/flowcli/findings/create/with_/entrypoint.py +19 -0
- convisoappsec/flowcli/findings/create/with_/version_tracker.py +93 -0
- convisoappsec/flowcli/findings/entrypoint.py +19 -0
- convisoappsec/flowcli/findings/import_sarif/__init__.py +4 -0
- convisoappsec/flowcli/findings/import_sarif/entrypoint.py +430 -0
- convisoappsec/flowcli/help_option.py +18 -0
- convisoappsec/flowcli/iac/__init__.py +3 -0
- convisoappsec/flowcli/iac/entrypoint.py +17 -0
- convisoappsec/flowcli/iac/run.py +328 -0
- convisoappsec/flowcli/requirements_verifier.py +132 -0
- convisoappsec/flowcli/sast/__init__.py +3 -0
- convisoappsec/flowcli/sast/entrypoint.py +17 -0
- convisoappsec/flowcli/sast/run.py +485 -0
- convisoappsec/flowcli/sbom/__init__.py +3 -0
- convisoappsec/flowcli/sbom/entrypoint.py +17 -0
- convisoappsec/flowcli/sbom/generate.py +235 -0
- convisoappsec/flowcli/sca/__init__.py +3 -0
- convisoappsec/flowcli/sca/entrypoint.py +17 -0
- convisoappsec/flowcli/sca/run.py +479 -0
- convisoappsec/flowcli/vulnerability/__init__.py +3 -0
- convisoappsec/flowcli/vulnerability/assert_security_rules.py +201 -0
- convisoappsec/flowcli/vulnerability/container_vulnerability_manager.py +175 -0
- convisoappsec/flowcli/vulnerability/entrypoint.py +18 -0
- convisoappsec/flowcli/vulnerability/rules_schema.json +53 -0
- convisoappsec/flowcli/vulnerability/run.py +487 -0
- convisoappsec/logger.py +29 -0
- convisoappsec/sast/__init__.py +0 -0
- convisoappsec/sast/decision.py +45 -0
- convisoappsec/sast/sastbox.py +296 -0
- convisoappsec/version.py +1 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from functools import reduce
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CIProvider(Enum):
|
|
6
|
+
AWS_CODEBUILD = {
|
|
7
|
+
'_env_vars': [
|
|
8
|
+
'CODEBUILD_BUILD_ARN',
|
|
9
|
+
'CODEBUILD_BUILD_ID',
|
|
10
|
+
'CODEBUILD_BUILD_NUMBER'
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
AZURE_PIPELINES = {
|
|
14
|
+
'_env_vars': [
|
|
15
|
+
'SYSTEM_JOBDISPLAYNAME',
|
|
16
|
+
'SYSTEM_JOBID',
|
|
17
|
+
'SYSTEM_TEAMPROJECT'
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
BITBUCKET = {
|
|
21
|
+
'_env_vars': [
|
|
22
|
+
'BITBUCKET_PROJECT_KEY',
|
|
23
|
+
'BITBUCKET_PROJECT_UUID',
|
|
24
|
+
'BITBUCKET_PIPELINE_UUID'
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
CIRCLECI = {
|
|
28
|
+
'_env_vars': [
|
|
29
|
+
'CIRCLECI',
|
|
30
|
+
'CIRCLE_JOB',
|
|
31
|
+
'CIRCLE_USERNAME'
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
CODEFRESH = {
|
|
35
|
+
'_env_vars': [
|
|
36
|
+
'CF_REPO_NAME',
|
|
37
|
+
'CF_REVISION',
|
|
38
|
+
'CF_BRANCH'
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
GITLAB = {
|
|
42
|
+
'_env_vars': [
|
|
43
|
+
'GITLAB_CI',
|
|
44
|
+
'CI_PROJECT_ID',
|
|
45
|
+
'CI_SERVER_NAME'
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
GITHUB = {
|
|
49
|
+
'_env_vars': [
|
|
50
|
+
'GITHUB_REPOSITORY',
|
|
51
|
+
'GITHUB_REF',
|
|
52
|
+
'GITHUB_JOB'
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
JENKINS = {
|
|
56
|
+
'_env_vars': [
|
|
57
|
+
'BUILD_NUMBER',
|
|
58
|
+
'BUILD_TAG',
|
|
59
|
+
'JOB_NAME'
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
OTHER = {
|
|
63
|
+
'_env_vars': []
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def names(cls):
|
|
69
|
+
return [provider.name for provider in cls]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def env_vars_exists(self, env):
|
|
73
|
+
provider_vars = self.__provider_vars
|
|
74
|
+
|
|
75
|
+
if not provider_vars:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
compute_found_env_vars = ComputeFoundEnvVars(env)
|
|
79
|
+
|
|
80
|
+
found_vars = reduce(compute_found_env_vars, provider_vars, 0)
|
|
81
|
+
vars_length = len(provider_vars)
|
|
82
|
+
|
|
83
|
+
return found_vars == vars_length
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def __provider_vars(self):
|
|
88
|
+
return self.value['_env_vars']
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ComputeFoundEnvVars:
|
|
92
|
+
def __init__(self, environment):
|
|
93
|
+
self.__environment = environment
|
|
94
|
+
|
|
95
|
+
def __call__(self, found_vars, env_var_name):
|
|
96
|
+
if self.__environment.get(env_var_name):
|
|
97
|
+
return found_vars + 1
|
|
98
|
+
else:
|
|
99
|
+
0
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from convisoappsec.flow.source_code_scanner import SCC
|
|
2
|
+
from convisoappsec.logger import LOGGER
|
|
3
|
+
import docker
|
|
4
|
+
|
|
5
|
+
def project_metrics(source_code_dir):
|
|
6
|
+
try:
|
|
7
|
+
scanner = SCC(source_code_dir, create_source_code_volume=False)
|
|
8
|
+
scanner.scan()
|
|
9
|
+
return {
|
|
10
|
+
'total_lines': scanner.total_source_code_lines
|
|
11
|
+
}
|
|
12
|
+
except docker.errors.APIError as e:
|
|
13
|
+
LOGGER.error('Error on fetch project metrics')
|
|
14
|
+
LOGGER.exception(e)
|
|
15
|
+
return {}
|
|
16
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import tarfile
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SourceCodeCompressor(object):
|
|
5
|
+
TAR_WRITE_MODE = 'w|'
|
|
6
|
+
TAR_ROOT_DIR = '.'
|
|
7
|
+
|
|
8
|
+
def __init__(self, source_code_dir="."):
|
|
9
|
+
self.source_code_dir = source_code_dir
|
|
10
|
+
|
|
11
|
+
def write_to(self, fileobj):
|
|
12
|
+
tarball_filehandler = tarfile.open(
|
|
13
|
+
mode=self.TAR_WRITE_MODE,
|
|
14
|
+
fileobj=fileobj
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
tarball_filehandler.add(
|
|
18
|
+
name=self.source_code_dir,
|
|
19
|
+
arcname=self.TAR_ROOT_DIR,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
tarball_filehandler.close()
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import re
|
|
4
|
+
import yaml
|
|
5
|
+
import git
|
|
6
|
+
import os
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from convisoappsec.logger import LOGGER
|
|
9
|
+
from git.exc import GitCommandError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitAdapter(object):
|
|
13
|
+
LIST_OPTION = '--list'
|
|
14
|
+
SORT_OPTION = '--sort'
|
|
15
|
+
FORMAT_OPTION = '--format'
|
|
16
|
+
ANCESTRY_PATH_OPTION = '--ancestry-path'
|
|
17
|
+
HEAD_COMMIT = 'HEAD'
|
|
18
|
+
OPTION_WITH_ARG_FMT = '{option}={value}'
|
|
19
|
+
EMPTY_REPOSITORY_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'
|
|
20
|
+
|
|
21
|
+
def __init__(self, repository_dir='.', load_remote_repositories_heads=False, unshallow_repository=False):
|
|
22
|
+
LOGGER.debug('Unshallow: {}'.format(unshallow_repository))
|
|
23
|
+
LOGGER.debug('Load remote: {}'.format(load_remote_repositories_heads))
|
|
24
|
+
|
|
25
|
+
self._git_client = git.cmd.Git(repository_dir)
|
|
26
|
+
self._first_commit = None
|
|
27
|
+
self._repo = git.Repo(repository_dir)
|
|
28
|
+
|
|
29
|
+
if load_remote_repositories_heads:
|
|
30
|
+
self.__load_remote_repositories_heads()
|
|
31
|
+
|
|
32
|
+
if unshallow_repository:
|
|
33
|
+
self.__unshallow_repository()
|
|
34
|
+
|
|
35
|
+
def repo_url(self):
|
|
36
|
+
"""
|
|
37
|
+
Function to get the repository URL and convert it to an HTTPS format if necessary.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: The repository URL in HTTPS format, or None if the URL cannot be determined.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
repos_url = self._repo.remotes.origin.url
|
|
44
|
+
except AttributeError:
|
|
45
|
+
if self._repo.remotes:
|
|
46
|
+
repos_url = self._repo.remotes[0].url
|
|
47
|
+
else:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
if repos_url.startswith('git@'):
|
|
51
|
+
return repos_url.replace(':', '/').replace('git@', 'https://').replace('.git', '')
|
|
52
|
+
|
|
53
|
+
elif repos_url.startswith('ssh://git@ssh.dev.azure.com'):
|
|
54
|
+
parts = repos_url.split('/')
|
|
55
|
+
if len(parts) >= 7:
|
|
56
|
+
organization = parts[4]
|
|
57
|
+
project = parts[5]
|
|
58
|
+
repo = parts[6].replace('.git', '')
|
|
59
|
+
return f"https://dev.azure.com/{organization}/{project}/_git/{repo}"
|
|
60
|
+
|
|
61
|
+
return repos_url
|
|
62
|
+
|
|
63
|
+
def get_branch_name(self):
|
|
64
|
+
"""retrieves the branch name"""
|
|
65
|
+
try:
|
|
66
|
+
return self._repo.active_branch.name
|
|
67
|
+
except TypeError:
|
|
68
|
+
return 'HEAD'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_commit_history(self):
|
|
72
|
+
"""
|
|
73
|
+
Retrieves the commit history (including stashes) of the repository.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
list: A list of commit information.
|
|
77
|
+
"""
|
|
78
|
+
commits = self._repo.iter_commits()
|
|
79
|
+
|
|
80
|
+
commit_info_list = [
|
|
81
|
+
{
|
|
82
|
+
'commit': commit.hexsha,
|
|
83
|
+
'author': "{author_name} <{author_email}>".format(
|
|
84
|
+
author_name=commit.author.name, author_email=commit.author.email
|
|
85
|
+
),
|
|
86
|
+
'date': commit.authored_datetime.strftime("%Y-%m-%d %H:%M:%S"),
|
|
87
|
+
'message': commit.message
|
|
88
|
+
}
|
|
89
|
+
for commit in commits
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Include information about stashes
|
|
93
|
+
stashes = self._repo.git.stash("list", "--format=%H|%gd|%ci|%P|%gs", "--date=iso").splitlines()
|
|
94
|
+
stash_info_list = [
|
|
95
|
+
{
|
|
96
|
+
'commit': stash_info.split('|')[0],
|
|
97
|
+
'stash_ref': stash_info.split('|')[1],
|
|
98
|
+
'date': stash_info.split('|')[2],
|
|
99
|
+
'parent_commits': stash_info.split('|')[3].split(),
|
|
100
|
+
'message': stash_info.split('|')[4],
|
|
101
|
+
'contributors': self.get_contributors_for_stash_merge(stash_info.split('|')[0])
|
|
102
|
+
}
|
|
103
|
+
for stash_info in stashes
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
commit_info_list.extend(stash_info_list)
|
|
107
|
+
|
|
108
|
+
return commit_info_list
|
|
109
|
+
|
|
110
|
+
def get_contributors_for_stash_merge(self, stash_commit):
|
|
111
|
+
"""
|
|
112
|
+
Get contributors involved in a stash merge.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
stash_commit (str): The commit hash of the stash.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list: List of contributors involved in the stash merge.
|
|
119
|
+
"""
|
|
120
|
+
contributors = set()
|
|
121
|
+
stash_diff = self._repo.git.diff(stash_commit + "^", stash_commit, "--name-only").splitlines()
|
|
122
|
+
|
|
123
|
+
for file_path in stash_diff:
|
|
124
|
+
try:
|
|
125
|
+
blame_output = self._repo.git.blame(stash_commit, "--", file_path, p=True).splitlines()
|
|
126
|
+
author_info = {}
|
|
127
|
+
for line in blame_output:
|
|
128
|
+
if line.startswith('author '):
|
|
129
|
+
author_info['name'] = line[len('author '):]
|
|
130
|
+
elif line.startswith('author-mail '):
|
|
131
|
+
author_info['email'] = line[len('author-mail '):].strip('<>')
|
|
132
|
+
|
|
133
|
+
if 'name' in author_info and 'email' in author_info:
|
|
134
|
+
contributors.add("{name} {email}".format(name=author_info['name'], email=author_info['email']))
|
|
135
|
+
author_info = {}
|
|
136
|
+
|
|
137
|
+
except GitCommandError as e:
|
|
138
|
+
LOGGER.warning(f"Could not process git blame for {file_path}: {e}")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
return list(contributors)
|
|
142
|
+
|
|
143
|
+
def tags(self, sort='-committerdate'):
|
|
144
|
+
sort_option = self.OPTION_WITH_ARG_FMT.format(
|
|
145
|
+
option=self.SORT_OPTION,
|
|
146
|
+
value=sort,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
args = (self.LIST_OPTION, sort_option)
|
|
150
|
+
client_output = self._git_client.tag(args)
|
|
151
|
+
tags = client_output.splitlines()
|
|
152
|
+
return tags
|
|
153
|
+
|
|
154
|
+
def diff(self, version, another_version):
|
|
155
|
+
version = version or self.EMPTY_REPOSITORY_HASH
|
|
156
|
+
|
|
157
|
+
if version == self.EMPTY_REPOSITORY_HASH:
|
|
158
|
+
msg_fmt = """Creating diff comparing revision[{0}] and the repository beginning"""
|
|
159
|
+
LOGGER.warning(msg_fmt.format(another_version))
|
|
160
|
+
|
|
161
|
+
diff_file = tempfile.TemporaryFile()
|
|
162
|
+
self._git_client.diff(version, another_version, output_stream=diff_file)
|
|
163
|
+
|
|
164
|
+
return diff_file
|
|
165
|
+
|
|
166
|
+
def diff_stats(self, version, another_version):
|
|
167
|
+
version = version or self.EMPTY_REPOSITORY_HASH
|
|
168
|
+
|
|
169
|
+
if version == self.EMPTY_REPOSITORY_HASH:
|
|
170
|
+
msg_fmt = """Creating diff stats comparing revision[{0}] and the repository beginning"""
|
|
171
|
+
LOGGER.warning(msg_fmt.format(another_version))
|
|
172
|
+
|
|
173
|
+
stats_output = tempfile.TemporaryFile()
|
|
174
|
+
self._git_client.diff(version, another_version, '--numstat', output_stream=stats_output)
|
|
175
|
+
|
|
176
|
+
stats_summary = GitDiffNumStatSummary.load(stats_output)
|
|
177
|
+
|
|
178
|
+
return stats_summary
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def first_commit(self):
|
|
182
|
+
if self._first_commit:
|
|
183
|
+
return self._first_commit
|
|
184
|
+
|
|
185
|
+
command_output = tempfile.TemporaryFile()
|
|
186
|
+
|
|
187
|
+
args = [
|
|
188
|
+
'--reverse',
|
|
189
|
+
"--pretty=%H",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
self._git_client.log(args, output_stream=command_output)
|
|
193
|
+
command_output.seek(0)
|
|
194
|
+
first_in_bytes = command_output.readline()
|
|
195
|
+
command_output.close()
|
|
196
|
+
first = first_in_bytes.decode()
|
|
197
|
+
|
|
198
|
+
return first.strip()
|
|
199
|
+
|
|
200
|
+
def commit_is_first(self, commit):
|
|
201
|
+
return commit == self.first_commit
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def head_commit(self):
|
|
205
|
+
client_output = self._git_client.rev_parse(self.HEAD_COMMIT)
|
|
206
|
+
return client_output.strip()
|
|
207
|
+
|
|
208
|
+
@property
|
|
209
|
+
def current_commit(self):
|
|
210
|
+
return self.head_commit
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def previous_commit(self):
|
|
214
|
+
return self.previous_commit_from(self.current_commit)
|
|
215
|
+
|
|
216
|
+
def previous_commit_from(self, commit, offset=1):
|
|
217
|
+
if self.commit_is_first(commit):
|
|
218
|
+
return self.EMPTY_REPOSITORY_HASH
|
|
219
|
+
|
|
220
|
+
command_fmt = "{commit}~{offset}"
|
|
221
|
+
|
|
222
|
+
command = command_fmt.format(
|
|
223
|
+
commit=commit,
|
|
224
|
+
offset=offset,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
client_output = self._git_client.rev_parse(
|
|
228
|
+
command
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
return client_output.strip()
|
|
232
|
+
|
|
233
|
+
def show_commit_refs(self, commit):
|
|
234
|
+
with tempfile.TemporaryFile() as client_output:
|
|
235
|
+
self._git_client.show_ref(
|
|
236
|
+
"--head", "--heads", "--tags", output_stream=client_output
|
|
237
|
+
)
|
|
238
|
+
refs = _read_file_lines_generator(client_output)
|
|
239
|
+
refs = list(
|
|
240
|
+
filter(
|
|
241
|
+
lambda ref: re.search(commit, ref),
|
|
242
|
+
refs,
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return refs
|
|
247
|
+
|
|
248
|
+
def show_commit_from_tag(self, tag):
|
|
249
|
+
client_output = self._git_client.rev_parse(
|
|
250
|
+
tag
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return client_output.strip()
|
|
254
|
+
|
|
255
|
+
def get_commit_author(self, commit_hash):
|
|
256
|
+
if self.EMPTY_REPOSITORY_HASH == commit_hash:
|
|
257
|
+
default_commit = {
|
|
258
|
+
'name': 'Default',
|
|
259
|
+
'email': 'Default',
|
|
260
|
+
'commit': commit_hash
|
|
261
|
+
}
|
|
262
|
+
return default_commit
|
|
263
|
+
|
|
264
|
+
delimiter = '|;|'
|
|
265
|
+
fmt_author_name = '%an'
|
|
266
|
+
fmt_author_email = '%ae'
|
|
267
|
+
fmt_long_commit_hash = '%H'
|
|
268
|
+
|
|
269
|
+
row_fmt = '{name}{delimiter}{email}{delimiter}{commit}'.format(
|
|
270
|
+
name=fmt_author_name,
|
|
271
|
+
delimiter=delimiter,
|
|
272
|
+
email=fmt_author_email,
|
|
273
|
+
commit=fmt_long_commit_hash
|
|
274
|
+
)
|
|
275
|
+
format_option = self.OPTION_WITH_ARG_FMT.format(
|
|
276
|
+
option=self.FORMAT_OPTION,
|
|
277
|
+
value=row_fmt,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
author = self._git_client.show(
|
|
281
|
+
'-s', format_option, commit_hash
|
|
282
|
+
).split(delimiter)
|
|
283
|
+
|
|
284
|
+
author_data = {
|
|
285
|
+
'name': author[0],
|
|
286
|
+
'email': author[1],
|
|
287
|
+
'commit': author[2],
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return author_data
|
|
291
|
+
|
|
292
|
+
def get_commits_by_range(self, start_commit, end_commit):
|
|
293
|
+
tmp_commits = tempfile.TemporaryFile()
|
|
294
|
+
|
|
295
|
+
self._git_client.rev_list(
|
|
296
|
+
self.ANCESTRY_PATH_OPTION, start_commit + '..' + end_commit, output_stream=tmp_commits
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return _read_file_lines_generator(tmp_commits)
|
|
300
|
+
|
|
301
|
+
def get_commit_authors_by_range(self, start_commit, end_commit):
|
|
302
|
+
start_commit_range = start_commit
|
|
303
|
+
authors = []
|
|
304
|
+
|
|
305
|
+
if self.EMPTY_REPOSITORY_HASH == start_commit_range:
|
|
306
|
+
start_commit_range = self.first_commit
|
|
307
|
+
|
|
308
|
+
commit_range = f"{start_commit_range}..{end_commit}"
|
|
309
|
+
log_output = self._git_client.log(commit_range, "--pretty=tformat:%H|%an|%ae").splitlines()
|
|
310
|
+
|
|
311
|
+
for line in log_output:
|
|
312
|
+
commit_hash, name, email = line.split('|')
|
|
313
|
+
author_data = {
|
|
314
|
+
'name': name,
|
|
315
|
+
'email': email,
|
|
316
|
+
'commit': commit_hash,
|
|
317
|
+
}
|
|
318
|
+
authors.append(author_data)
|
|
319
|
+
|
|
320
|
+
return authors
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def empty_repository_tree_commit(self):
|
|
324
|
+
return self.EMPTY_REPOSITORY_HASH
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def remote_repositories_name(self):
|
|
328
|
+
args = ("show")
|
|
329
|
+
client_output = self._git_client.remote(args)
|
|
330
|
+
repositories = client_output.splitlines()
|
|
331
|
+
return repositories
|
|
332
|
+
|
|
333
|
+
def __load_remote_repositories_heads(self):
|
|
334
|
+
heads_refspec_format = "refs/heads/*:refs/remotes/{remote_repository_name}/*"
|
|
335
|
+
|
|
336
|
+
for remote_repository_name in self.remote_repositories_name:
|
|
337
|
+
try:
|
|
338
|
+
heads_refspec = heads_refspec_format.format(
|
|
339
|
+
remote_repository_name=remote_repository_name
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
args = (remote_repository_name, heads_refspec)
|
|
343
|
+
self._git_client.fetch(args)
|
|
344
|
+
|
|
345
|
+
except GitCommandError:
|
|
346
|
+
raw_msg = "We can\'t ensure that the refspec refs/heads/* from repository {repository} were loaded."
|
|
347
|
+
msg = raw_msg.format(repository=remote_repository_name)
|
|
348
|
+
LOGGER.warning(msg)
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def is_shallow_repository(self):
|
|
352
|
+
import os.path
|
|
353
|
+
|
|
354
|
+
args = ('--git-dir')
|
|
355
|
+
git_dir = self._git_client.rev_parse(args)
|
|
356
|
+
|
|
357
|
+
working_dir = self._git_client.working_dir
|
|
358
|
+
shallow_file = os.path.join(working_dir, git_dir, 'shallow')
|
|
359
|
+
|
|
360
|
+
return os.path.isfile(shallow_file)
|
|
361
|
+
|
|
362
|
+
def __unshallow_repository(self):
|
|
363
|
+
if not self.is_shallow_repository:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
args = ('--unshallow')
|
|
367
|
+
self._git_client.fetch(args)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _read_file_lines_generator(file):
|
|
371
|
+
file.seek(0)
|
|
372
|
+
|
|
373
|
+
while True:
|
|
374
|
+
line = file.readline()
|
|
375
|
+
line = line.decode().strip()
|
|
376
|
+
|
|
377
|
+
if line:
|
|
378
|
+
yield line
|
|
379
|
+
else:
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class InvalidGitDiffNumStatLineValueException(ValueError):
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class GitDiffNumStatLine(object):
|
|
388
|
+
ADDED_LINES_POSITION = 1
|
|
389
|
+
DELETED_LINES_POSITION = 2
|
|
390
|
+
FILE_PATH_POSITION = 3
|
|
391
|
+
|
|
392
|
+
# (added_lines) (deleted_lines) (file_path)
|
|
393
|
+
SRC_LINE_REGEX = r'(\d+)\s+(\d+)\s+(.*)'
|
|
394
|
+
BIN_LINE_REGEX = r'(-)\s+(-)\s+(.*)'
|
|
395
|
+
|
|
396
|
+
def __init__(self, added_lines, deleted_lines, file_path):
|
|
397
|
+
self.added_lines = added_lines
|
|
398
|
+
self.deleted_lines = deleted_lines
|
|
399
|
+
self.file_path = file_path
|
|
400
|
+
|
|
401
|
+
@classmethod
|
|
402
|
+
def parse(cls, raw_line):
|
|
403
|
+
with suppress(AttributeError):
|
|
404
|
+
match = re.match(cls.SRC_LINE_REGEX, raw_line)
|
|
405
|
+
group = match.group
|
|
406
|
+
|
|
407
|
+
added_lines_str = group(cls.ADDED_LINES_POSITION)
|
|
408
|
+
deleted_lines_str = group(cls.DELETED_LINES_POSITION)
|
|
409
|
+
file_path = group(cls.FILE_PATH_POSITION)
|
|
410
|
+
|
|
411
|
+
added_lines = int(added_lines_str)
|
|
412
|
+
deleted_lines = int(deleted_lines_str)
|
|
413
|
+
|
|
414
|
+
return cls(added_lines, deleted_lines, file_path)
|
|
415
|
+
|
|
416
|
+
with suppress(AttributeError):
|
|
417
|
+
match = re.match(cls.BIN_LINE_REGEX, raw_line)
|
|
418
|
+
|
|
419
|
+
file_path = match.group(cls.FILE_PATH_POSITION)
|
|
420
|
+
|
|
421
|
+
return cls(0, 0, file_path)
|
|
422
|
+
|
|
423
|
+
error_msg_fmt = '\n'.join([
|
|
424
|
+
'The expected git diff numstat line format are:',
|
|
425
|
+
'Expected format: {src_fmt}',
|
|
426
|
+
'Expected format: {bin_fmt}',
|
|
427
|
+
'Given value: {given}',
|
|
428
|
+
])
|
|
429
|
+
|
|
430
|
+
msg = error_msg_fmt.format(
|
|
431
|
+
src_fmt=cls.SRC_LINE_REGEX,
|
|
432
|
+
bin_fmt=cls.BIN_LINE_REGEX,
|
|
433
|
+
given=raw_line
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
raise InvalidGitDiffNumStatLineValueException(msg)
|
|
437
|
+
|
|
438
|
+
@classmethod
|
|
439
|
+
def load(cls, numstat_fh):
|
|
440
|
+
numstat_fh.seek(0)
|
|
441
|
+
|
|
442
|
+
while True:
|
|
443
|
+
line = numstat_fh.readline()
|
|
444
|
+
|
|
445
|
+
with suppress(AttributeError):
|
|
446
|
+
line = line.decode()
|
|
447
|
+
|
|
448
|
+
if line:
|
|
449
|
+
yield cls.parse(line)
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
break
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class GitDiffNumStatSummary(object):
|
|
456
|
+
|
|
457
|
+
def __init__(self):
|
|
458
|
+
self.added_lines = 0
|
|
459
|
+
self.deleted_lines = 0
|
|
460
|
+
self.changed_files = []
|
|
461
|
+
|
|
462
|
+
def _add_numstat_lines(self, numstat_lines):
|
|
463
|
+
for numstat_line in numstat_lines:
|
|
464
|
+
self._add_numstat_line(numstat_line)
|
|
465
|
+
|
|
466
|
+
def _add_numstat_line(self, numstat_line):
|
|
467
|
+
self._add_added_lines(numstat_line.added_lines)
|
|
468
|
+
self._add_deleted_lines(numstat_line.deleted_lines)
|
|
469
|
+
self._add_changed_files(numstat_line.file_path)
|
|
470
|
+
|
|
471
|
+
def _add_added_lines(self, added_lines):
|
|
472
|
+
self.added_lines += added_lines
|
|
473
|
+
|
|
474
|
+
def _add_deleted_lines(self, deleted_lines):
|
|
475
|
+
self.deleted_lines += deleted_lines
|
|
476
|
+
|
|
477
|
+
def _add_changed_files(self, changed_file):
|
|
478
|
+
self.changed_files.append(
|
|
479
|
+
changed_file
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
@property
|
|
483
|
+
def changed_lines(self):
|
|
484
|
+
return self.added_lines + self.deleted_lines
|
|
485
|
+
|
|
486
|
+
@classmethod
|
|
487
|
+
def load(cls, numstat_fh):
|
|
488
|
+
git_diffnumstat_lines = GitDiffNumStatLine.load(numstat_fh)
|
|
489
|
+
summary = cls()
|
|
490
|
+
summary._add_numstat_lines(git_diffnumstat_lines)
|
|
491
|
+
|
|
492
|
+
return summary
|
|
493
|
+
|
|
494
|
+
@property
|
|
495
|
+
def dict(self):
|
|
496
|
+
field_names = [
|
|
497
|
+
'added_lines',
|
|
498
|
+
'deleted_lines',
|
|
499
|
+
'changed_lines',
|
|
500
|
+
'changed_files',
|
|
501
|
+
]
|
|
502
|
+
|
|
503
|
+
fields = {
|
|
504
|
+
name: getattr(self, name) for name in field_names
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return fields
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def quoted_presenter(dumper, data):
|
|
511
|
+
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"')
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
yaml.add_representer(str, quoted_presenter)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class CommitAuthorFile:
|
|
518
|
+
def __init__(self):
|
|
519
|
+
self._tmp_authors = tempfile.NamedTemporaryFile()
|
|
520
|
+
|
|
521
|
+
def add_author(self, author):
|
|
522
|
+
yaml_str = yaml.dump(author, explicit_start=True)
|
|
523
|
+
yaml_bytes = yaml_str.encode()
|
|
524
|
+
self._tmp_authors.write(yaml_bytes)
|
|
525
|
+
|
|
526
|
+
def get_file_descriptor(self):
|
|
527
|
+
self._tmp_authors.seek(0)
|
|
528
|
+
return self._tmp_authors
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .version_searcher_result import VersionSearcherResult
|
|
2
|
+
from .timebased_version_seacher import TimeBasedVersionSearcher
|
|
3
|
+
from .sorted_by_versioning_style import SortedByVersioningStyle
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'VersionSearcherResult',
|
|
7
|
+
'TimeBasedVersionSearcher',
|
|
8
|
+
'SortedByVersioningStyle',
|
|
9
|
+
]
|