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.
Files changed (128) hide show
  1. conviso_ast-3.0.0.data/scripts/flow_bash_completer.sh +21 -0
  2. conviso_ast-3.0.0.data/scripts/flow_fish_completer.fish +1 -0
  3. conviso_ast-3.0.0.data/scripts/flow_zsh_completer.sh +32 -0
  4. conviso_ast-3.0.0.dist-info/METADATA +37 -0
  5. conviso_ast-3.0.0.dist-info/RECORD +128 -0
  6. conviso_ast-3.0.0.dist-info/WHEEL +5 -0
  7. conviso_ast-3.0.0.dist-info/entry_points.txt +3 -0
  8. conviso_ast-3.0.0.dist-info/top_level.txt +1 -0
  9. convisoappsec/__init__.py +0 -0
  10. convisoappsec/common/__init__.py +5 -0
  11. convisoappsec/common/box.py +251 -0
  12. convisoappsec/common/cleaner.py +78 -0
  13. convisoappsec/common/docker.py +399 -0
  14. convisoappsec/common/exceptions.py +8 -0
  15. convisoappsec/common/git_data_parser.py +76 -0
  16. convisoappsec/common/graphql/__init__.py +0 -0
  17. convisoappsec/common/graphql/error_handlers.py +75 -0
  18. convisoappsec/common/graphql/errors.py +16 -0
  19. convisoappsec/common/graphql/low_client.py +51 -0
  20. convisoappsec/common/retry_handler.py +40 -0
  21. convisoappsec/common/strings.py +8 -0
  22. convisoappsec/flow/__init__.py +3 -0
  23. convisoappsec/flow/api.py +104 -0
  24. convisoappsec/flow/cleaner.py +118 -0
  25. convisoappsec/flow/graphql_api/__init__.py +0 -0
  26. convisoappsec/flow/graphql_api/beta/__init__.py +0 -0
  27. convisoappsec/flow/graphql_api/beta/client.py +18 -0
  28. convisoappsec/flow/graphql_api/beta/models/__init__.py +0 -0
  29. convisoappsec/flow/graphql_api/beta/models/issues/__init__.py +0 -0
  30. convisoappsec/flow/graphql_api/beta/models/issues/container.py +72 -0
  31. convisoappsec/flow/graphql_api/beta/models/issues/iac.py +6 -0
  32. convisoappsec/flow/graphql_api/beta/models/issues/normalize.py +13 -0
  33. convisoappsec/flow/graphql_api/beta/models/issues/sast.py +53 -0
  34. convisoappsec/flow/graphql_api/beta/models/issues/sca.py +78 -0
  35. convisoappsec/flow/graphql_api/beta/resources_api.py +142 -0
  36. convisoappsec/flow/graphql_api/beta/schemas/__init__.py +0 -0
  37. convisoappsec/flow/graphql_api/beta/schemas/mutations/__init__.py +61 -0
  38. convisoappsec/flow/graphql_api/beta/schemas/resolvers/__init__.py +0 -0
  39. convisoappsec/flow/graphql_api/v1/__init__.py +0 -0
  40. convisoappsec/flow/graphql_api/v1/client.py +46 -0
  41. convisoappsec/flow/graphql_api/v1/models/__init__.py +0 -0
  42. convisoappsec/flow/graphql_api/v1/models/asset.py +14 -0
  43. convisoappsec/flow/graphql_api/v1/models/issues.py +16 -0
  44. convisoappsec/flow/graphql_api/v1/models/project.py +35 -0
  45. convisoappsec/flow/graphql_api/v1/resources_api.py +489 -0
  46. convisoappsec/flow/graphql_api/v1/schemas/__init__.py +0 -0
  47. convisoappsec/flow/graphql_api/v1/schemas/mutations/__init__.py +212 -0
  48. convisoappsec/flow/graphql_api/v1/schemas/resolvers/__init__.py +180 -0
  49. convisoappsec/flow/source_code_scanner/__init__.py +9 -0
  50. convisoappsec/flow/source_code_scanner/exceptions.py +2 -0
  51. convisoappsec/flow/source_code_scanner/scc.py +68 -0
  52. convisoappsec/flow/source_code_scanner/source_code_scanner.py +177 -0
  53. convisoappsec/flow/util/__init__.py +7 -0
  54. convisoappsec/flow/util/ci_provider.py +99 -0
  55. convisoappsec/flow/util/metrics.py +16 -0
  56. convisoappsec/flow/util/source_code_compressor.py +22 -0
  57. convisoappsec/flow/version_control_system_adapter.py +528 -0
  58. convisoappsec/flow/version_searchers/__init__.py +9 -0
  59. convisoappsec/flow/version_searchers/sorted_by_versioning_style.py +85 -0
  60. convisoappsec/flow/version_searchers/timebased_version_seacher.py +39 -0
  61. convisoappsec/flow/version_searchers/version_searcher_result.py +33 -0
  62. convisoappsec/flow/versioning_style/__init__.py +0 -0
  63. convisoappsec/flow/versioning_style/semantic_versioning.py +44 -0
  64. convisoappsec/flowcli/__init__.py +3 -0
  65. convisoappsec/flowcli/__main__.py +4 -0
  66. convisoappsec/flowcli/assets/__init__.py +4 -0
  67. convisoappsec/flowcli/assets/create.py +88 -0
  68. convisoappsec/flowcli/assets/entrypoint.py +20 -0
  69. convisoappsec/flowcli/assets/ls.py +63 -0
  70. convisoappsec/flowcli/ast/__init__.py +3 -0
  71. convisoappsec/flowcli/ast/entrypoint.py +427 -0
  72. convisoappsec/flowcli/common.py +175 -0
  73. convisoappsec/flowcli/companies/__init__.py +0 -0
  74. convisoappsec/flowcli/companies/ls.py +25 -0
  75. convisoappsec/flowcli/container/__init__.py +3 -0
  76. convisoappsec/flowcli/container/entrypoint.py +17 -0
  77. convisoappsec/flowcli/container/run.py +306 -0
  78. convisoappsec/flowcli/context.py +49 -0
  79. convisoappsec/flowcli/deploy/__init__.py +0 -0
  80. convisoappsec/flowcli/deploy/create/__init__.py +4 -0
  81. convisoappsec/flowcli/deploy/create/context.py +12 -0
  82. convisoappsec/flowcli/deploy/create/entrypoint.py +31 -0
  83. convisoappsec/flowcli/deploy/create/with_/__init__.py +3 -0
  84. convisoappsec/flowcli/deploy/create/with_/entrypoint.py +20 -0
  85. convisoappsec/flowcli/deploy/create/with_/tag_tracker/__init__.py +4 -0
  86. convisoappsec/flowcli/deploy/create/with_/tag_tracker/context.py +11 -0
  87. convisoappsec/flowcli/deploy/create/with_/tag_tracker/entrypoint.py +30 -0
  88. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/__init__.py +4 -0
  89. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/entrypoint.py +21 -0
  90. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/time_.py +84 -0
  91. convisoappsec/flowcli/deploy/create/with_/tag_tracker/sort_by/versioning_style.py +115 -0
  92. convisoappsec/flowcli/deploy/create/with_/values.py +133 -0
  93. convisoappsec/flowcli/entrypoint.py +103 -0
  94. convisoappsec/flowcli/environment_checker.py +45 -0
  95. convisoappsec/flowcli/findings/__init__.py +4 -0
  96. convisoappsec/flowcli/findings/create/__init__.py +4 -0
  97. convisoappsec/flowcli/findings/create/entrypoint.py +18 -0
  98. convisoappsec/flowcli/findings/create/with_/__init__.py +3 -0
  99. convisoappsec/flowcli/findings/create/with_/entrypoint.py +19 -0
  100. convisoappsec/flowcli/findings/create/with_/version_tracker.py +93 -0
  101. convisoappsec/flowcli/findings/entrypoint.py +19 -0
  102. convisoappsec/flowcli/findings/import_sarif/__init__.py +4 -0
  103. convisoappsec/flowcli/findings/import_sarif/entrypoint.py +430 -0
  104. convisoappsec/flowcli/help_option.py +18 -0
  105. convisoappsec/flowcli/iac/__init__.py +3 -0
  106. convisoappsec/flowcli/iac/entrypoint.py +17 -0
  107. convisoappsec/flowcli/iac/run.py +328 -0
  108. convisoappsec/flowcli/requirements_verifier.py +132 -0
  109. convisoappsec/flowcli/sast/__init__.py +3 -0
  110. convisoappsec/flowcli/sast/entrypoint.py +17 -0
  111. convisoappsec/flowcli/sast/run.py +485 -0
  112. convisoappsec/flowcli/sbom/__init__.py +3 -0
  113. convisoappsec/flowcli/sbom/entrypoint.py +17 -0
  114. convisoappsec/flowcli/sbom/generate.py +235 -0
  115. convisoappsec/flowcli/sca/__init__.py +3 -0
  116. convisoappsec/flowcli/sca/entrypoint.py +17 -0
  117. convisoappsec/flowcli/sca/run.py +479 -0
  118. convisoappsec/flowcli/vulnerability/__init__.py +3 -0
  119. convisoappsec/flowcli/vulnerability/assert_security_rules.py +201 -0
  120. convisoappsec/flowcli/vulnerability/container_vulnerability_manager.py +175 -0
  121. convisoappsec/flowcli/vulnerability/entrypoint.py +18 -0
  122. convisoappsec/flowcli/vulnerability/rules_schema.json +53 -0
  123. convisoappsec/flowcli/vulnerability/run.py +487 -0
  124. convisoappsec/logger.py +29 -0
  125. convisoappsec/sast/__init__.py +0 -0
  126. convisoappsec/sast/decision.py +45 -0
  127. convisoappsec/sast/sastbox.py +296 -0
  128. 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
+ ]