checkmate5 4.0.67__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.
- checkmate/__init__.py +21 -0
- checkmate/__main__.py +25 -0
- checkmate/contrib/__init__.py +21 -0
- checkmate/contrib/plugins/__init__.py +0 -0
- checkmate/contrib/plugins/all/gptanalyzer/__init__.py +0 -0
- checkmate/contrib/plugins/all/gptanalyzer/analyzer.py +99 -0
- checkmate/contrib/plugins/all/gptanalyzer/issues_data.py +6 -0
- checkmate/contrib/plugins/all/gptanalyzer/setup.py +13 -0
- checkmate/contrib/plugins/cve/__init__.py +0 -0
- checkmate/contrib/plugins/cve/text4shell/__init__.py +0 -0
- checkmate/contrib/plugins/cve/text4shell/analyzer.py +64 -0
- checkmate/contrib/plugins/cve/text4shell/issues_data.py +8 -0
- checkmate/contrib/plugins/cve/text4shell/setup.py +13 -0
- checkmate/contrib/plugins/git/__init__.py +0 -0
- checkmate/contrib/plugins/git/commands/__init__.py +6 -0
- checkmate/contrib/plugins/git/commands/analyze.py +364 -0
- checkmate/contrib/plugins/git/commands/base.py +16 -0
- checkmate/contrib/plugins/git/commands/diff.py +199 -0
- checkmate/contrib/plugins/git/commands/init.py +59 -0
- checkmate/contrib/plugins/git/commands/update_stats.py +41 -0
- checkmate/contrib/plugins/git/hooks/__init__.py +0 -0
- checkmate/contrib/plugins/git/hooks/project.py +19 -0
- checkmate/contrib/plugins/git/lib/__init__.py +1 -0
- checkmate/contrib/plugins/git/lib/repository.py +557 -0
- checkmate/contrib/plugins/git/lib/repository_pygit2.py +531 -0
- checkmate/contrib/plugins/git/models.py +178 -0
- checkmate/contrib/plugins/git/setup.py +27 -0
- checkmate/contrib/plugins/golang/__init__.py +0 -0
- checkmate/contrib/plugins/golang/gostaticcheck/__init__.py +0 -0
- checkmate/contrib/plugins/golang/gostaticcheck/analyzer.py +94 -0
- checkmate/contrib/plugins/golang/gostaticcheck/issues_data.py +1246 -0
- checkmate/contrib/plugins/golang/gostaticcheck/setup.py +13 -0
- checkmate/contrib/plugins/iac/__init__.py +0 -0
- checkmate/contrib/plugins/iac/kubescape/__init__.py +0 -0
- checkmate/contrib/plugins/iac/kubescape/analyzer.py +115 -0
- checkmate/contrib/plugins/iac/kubescape/issues_data.py +636 -0
- checkmate/contrib/plugins/iac/kubescape/setup.py +14 -0
- checkmate/contrib/plugins/iac/tfsec/__init__.py +0 -0
- checkmate/contrib/plugins/iac/tfsec/analyzer.py +92 -0
- checkmate/contrib/plugins/iac/tfsec/issues_data.py +1917 -0
- checkmate/contrib/plugins/iac/tfsec/setup.py +13 -0
- checkmate/contrib/plugins/java/__init__.py +0 -0
- checkmate/contrib/plugins/java/semgrepjava/__init__.py +0 -0
- checkmate/contrib/plugins/java/semgrepjava/analyzer.py +96 -0
- checkmate/contrib/plugins/java/semgrepjava/issues_data.py +5 -0
- checkmate/contrib/plugins/java/semgrepjava/setup.py +13 -0
- checkmate/contrib/plugins/javascript/__init__.py +0 -0
- checkmate/contrib/plugins/javascript/semgrepeslint/__init__.py +0 -0
- checkmate/contrib/plugins/javascript/semgrepeslint/analyzer.py +95 -0
- checkmate/contrib/plugins/javascript/semgrepeslint/issues_data.py +6 -0
- checkmate/contrib/plugins/javascript/semgrepeslint/setup.py +13 -0
- checkmate/contrib/plugins/perl/__init__.py +0 -0
- checkmate/contrib/plugins/perl/graudit/__init__.py +0 -0
- checkmate/contrib/plugins/perl/graudit/analyzer.py +70 -0
- checkmate/contrib/plugins/perl/graudit/issues_data.py +8 -0
- checkmate/contrib/plugins/perl/graudit/setup.py +13 -0
- checkmate/contrib/plugins/python/__init__.py +0 -0
- checkmate/contrib/plugins/python/bandit/__init__.py +0 -0
- checkmate/contrib/plugins/python/bandit/analyzer.py +74 -0
- checkmate/contrib/plugins/python/bandit/issues_data.py +426 -0
- checkmate/contrib/plugins/python/bandit/setup.py +13 -0
- checkmate/contrib/plugins/ruby/__init__.py +0 -0
- checkmate/contrib/plugins/ruby/brakeman/__init__.py +0 -0
- checkmate/contrib/plugins/ruby/brakeman/analyzer.py +96 -0
- checkmate/contrib/plugins/ruby/brakeman/issues_data.py +518 -0
- checkmate/contrib/plugins/ruby/brakeman/setup.py +13 -0
- checkmate/helpers/__init__.py +0 -0
- checkmate/helpers/facts.py +26 -0
- checkmate/helpers/hashing.py +68 -0
- checkmate/helpers/issue.py +101 -0
- checkmate/helpers/settings.py +14 -0
- checkmate/lib/__init__.py +1 -0
- checkmate/lib/analysis/__init__.py +3 -0
- checkmate/lib/analysis/base.py +103 -0
- checkmate/lib/code/__init__.py +3 -0
- checkmate/lib/code/environment.py +809 -0
- checkmate/lib/models.py +515 -0
- checkmate/lib/stats/__init__.py +1 -0
- checkmate/lib/stats/helpers.py +19 -0
- checkmate/lib/stats/mapreduce.py +29 -0
- checkmate/management/__init__.py +1 -0
- checkmate/management/commands/__init__.py +18 -0
- checkmate/management/commands/alembic.py +32 -0
- checkmate/management/commands/analyze.py +42 -0
- checkmate/management/commands/analyzers.py +1 -0
- checkmate/management/commands/base.py +66 -0
- checkmate/management/commands/compare.py +0 -0
- checkmate/management/commands/export.py +0 -0
- checkmate/management/commands/info.py +0 -0
- checkmate/management/commands/init.py +103 -0
- checkmate/management/commands/issues.py +478 -0
- checkmate/management/commands/props/__init__.py +1 -0
- checkmate/management/commands/props/delete.py +29 -0
- checkmate/management/commands/props/get.py +30 -0
- checkmate/management/commands/props/set.py +29 -0
- checkmate/management/commands/reset.py +53 -0
- checkmate/management/commands/shell.py +19 -0
- checkmate/management/commands/snapshots.py +22 -0
- checkmate/management/commands/stats.py +21 -0
- checkmate/management/commands/summary.py +19 -0
- checkmate/management/commands/sync.py +63 -0
- checkmate/management/commands/trend.py +1 -0
- checkmate/management/commands/watch.py +27 -0
- checkmate/management/decorators.py +1 -0
- checkmate/management/helpers.py +140 -0
- checkmate/scripts/__init__.py +18 -0
- checkmate/scripts/manage.py +121 -0
- checkmate/settings/__init__.py +2 -0
- checkmate/settings/base.py +127 -0
- checkmate/settings/defaults.py +133 -0
- checkmate5-4.0.67.dist-info/LICENSE.txt +4095 -0
- checkmate5-4.0.67.dist-info/METADATA +15 -0
- checkmate5-4.0.67.dist-info/RECORD +116 -0
- checkmate5-4.0.67.dist-info/WHEEL +5 -0
- checkmate5-4.0.67.dist-info/entry_points.txt +2 -0
- checkmate5-4.0.67.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
NOTICE:
|
|
5
|
+
|
|
6
|
+
This version should use the git CLI only!
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import datetime
|
|
14
|
+
import re
|
|
15
|
+
import time
|
|
16
|
+
import logging
|
|
17
|
+
import traceback
|
|
18
|
+
import select
|
|
19
|
+
import fcntl
|
|
20
|
+
import shutil
|
|
21
|
+
import io
|
|
22
|
+
import tempfile
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GitException(BaseException):
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_first_date_for_group(start_date, group_type, n):
|
|
34
|
+
"""
|
|
35
|
+
:param start: start date
|
|
36
|
+
:n : how many groups we want to get
|
|
37
|
+
:group_type : daily, weekly, monthly
|
|
38
|
+
"""
|
|
39
|
+
current_date = start_date
|
|
40
|
+
if group_type == 'monthly':
|
|
41
|
+
current_year = start_date.year
|
|
42
|
+
current_month = start_date.month
|
|
43
|
+
for i in range(n-1):
|
|
44
|
+
current_month -= 1
|
|
45
|
+
if current_month == 0:
|
|
46
|
+
current_month = 12
|
|
47
|
+
current_year -= 1
|
|
48
|
+
first_date = datetime.datetime(current_year, current_month, 1)
|
|
49
|
+
elif group_type == 'weekly':
|
|
50
|
+
first_date = start_date - \
|
|
51
|
+
datetime.timedelta(days=start_date.weekday()+(n-1)*7)
|
|
52
|
+
elif group_type == 'daily':
|
|
53
|
+
first_date = start_date-datetime.timedelta(days=n-1)
|
|
54
|
+
first_date = datetime.datetime(
|
|
55
|
+
first_date.year, first_date.month, first_date.day, 0, 0, 0)
|
|
56
|
+
return first_date
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def group_snapshots_by_date(snapshots, period):
|
|
60
|
+
|
|
61
|
+
available_periods = {
|
|
62
|
+
'daily': lambda dt: dt.strftime("%Y-%m-%d"),
|
|
63
|
+
'weekly': lambda dt: dt.strftime("%Y-%W"),
|
|
64
|
+
'monthly': lambda dt: dt.strftime("%Y-%m")
|
|
65
|
+
}
|
|
66
|
+
formatter = available_periods[period]
|
|
67
|
+
|
|
68
|
+
grouped_snapshots = defaultdict(list)
|
|
69
|
+
|
|
70
|
+
for snapshot in snapshots:
|
|
71
|
+
dt = datetime.datetime.fromtimestamp(snapshot.committer_date_ts).date()
|
|
72
|
+
key = formatter(dt)
|
|
73
|
+
grouped_snapshots[key].append(snapshot)
|
|
74
|
+
|
|
75
|
+
return grouped_snapshots
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class Repository(object):
|
|
79
|
+
|
|
80
|
+
def __init__(self, path):
|
|
81
|
+
self._path = path
|
|
82
|
+
self.devnull = open(os.devnull, "w")
|
|
83
|
+
self.stderr = ''
|
|
84
|
+
self.stdout = ''
|
|
85
|
+
self.returncode = None
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def path(self):
|
|
89
|
+
return self._path
|
|
90
|
+
|
|
91
|
+
@path.setter
|
|
92
|
+
def set_path(self, path):
|
|
93
|
+
self._path = path
|
|
94
|
+
|
|
95
|
+
def _call(self, args, kwargs, capture_stderr=True, timeout=None):
|
|
96
|
+
|
|
97
|
+
if not 'cwd' in kwargs:
|
|
98
|
+
kwargs['cwd'] = self.path
|
|
99
|
+
|
|
100
|
+
if timeout:
|
|
101
|
+
|
|
102
|
+
# We write command output to temporary files, so that we are able to read it
|
|
103
|
+
# even if we terminate the command abruptly.
|
|
104
|
+
with tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr:
|
|
105
|
+
|
|
106
|
+
if capture_stderr:
|
|
107
|
+
stderr = stdout
|
|
108
|
+
|
|
109
|
+
p = subprocess.Popen(
|
|
110
|
+
*args, stdout=stdout, stderr=stderr, preexec_fn=os.setsid, **kwargs)
|
|
111
|
+
|
|
112
|
+
def read_output():
|
|
113
|
+
stdout.flush()
|
|
114
|
+
stderr.flush()
|
|
115
|
+
stdout.seek(0)
|
|
116
|
+
self.stdout = stdout.read()
|
|
117
|
+
stderr.seek(0)
|
|
118
|
+
self.stderr = stderr.read()
|
|
119
|
+
|
|
120
|
+
start_time = time.time()
|
|
121
|
+
|
|
122
|
+
while time.time() - start_time < timeout:
|
|
123
|
+
if p.poll() != None:
|
|
124
|
+
break
|
|
125
|
+
time.sleep(0.001)
|
|
126
|
+
|
|
127
|
+
timeout_occured = False
|
|
128
|
+
|
|
129
|
+
if p.poll() == None:
|
|
130
|
+
timeout_occured = True
|
|
131
|
+
stdout.flush()
|
|
132
|
+
stderr.flush()
|
|
133
|
+
p.terminate()
|
|
134
|
+
time.sleep(0.1)
|
|
135
|
+
if p.poll() == None:
|
|
136
|
+
p.kill()
|
|
137
|
+
|
|
138
|
+
read_output()
|
|
139
|
+
|
|
140
|
+
if timeout_occured:
|
|
141
|
+
self.stderr += "\n[process timed out after %d seconds]" % int(
|
|
142
|
+
timeout)
|
|
143
|
+
|
|
144
|
+
self.returncode = p.returncode
|
|
145
|
+
return p.returncode, self.stdout
|
|
146
|
+
else:
|
|
147
|
+
if capture_stderr:
|
|
148
|
+
stderr = subprocess.STDOUT
|
|
149
|
+
else:
|
|
150
|
+
stderr = subprocess.PIPE
|
|
151
|
+
p = subprocess.Popen(
|
|
152
|
+
*args, stdout=subprocess.PIPE, stderr=stderr, **kwargs)
|
|
153
|
+
stdout, stderr = p.communicate()
|
|
154
|
+
return p.returncode, stdout
|
|
155
|
+
|
|
156
|
+
def call(self, *args, **kwargs):
|
|
157
|
+
if 'timeout' in kwargs:
|
|
158
|
+
timeout = kwargs['timeout']
|
|
159
|
+
del kwargs['timeout']
|
|
160
|
+
return self._call(args, kwargs, timeout=timeout)
|
|
161
|
+
else:
|
|
162
|
+
return self._call(args, kwargs)
|
|
163
|
+
|
|
164
|
+
def check_output(self, *args, **kwargs):
|
|
165
|
+
code = os.getenv('CODE_DIR')
|
|
166
|
+
os.chdir(code)
|
|
167
|
+
returncode, stdout = self._call(args, kwargs, capture_stderr=False)
|
|
168
|
+
subprocess.CalledProcessError(returncode, args[0], stdout)
|
|
169
|
+
return stdout
|
|
170
|
+
|
|
171
|
+
def add_remote(self, name, url):
|
|
172
|
+
return_code, stdout = self.call(["git", "remote", "add", name, url])
|
|
173
|
+
return return_code
|
|
174
|
+
|
|
175
|
+
def remove_remote(self, name):
|
|
176
|
+
return_code, stdout = self.call(["git", "remote", "remove", name])
|
|
177
|
+
return return_code
|
|
178
|
+
|
|
179
|
+
def get_remotes(self):
|
|
180
|
+
remotes = {}
|
|
181
|
+
return_code, stdout = self.call(["git", "remote", "-v"])
|
|
182
|
+
if return_code != 0:
|
|
183
|
+
raise subprocess.CalledProcessError(returncode, args[0], stdout)
|
|
184
|
+
for line in stdout.split(b'\n'):
|
|
185
|
+
match = re.match(r'([^\s]+)\s+([^\s]+)', str(line))
|
|
186
|
+
if not match:
|
|
187
|
+
continue
|
|
188
|
+
name = match.group(1)
|
|
189
|
+
remotes[name] = {
|
|
190
|
+
'name': name,
|
|
191
|
+
'url': match.group(2)
|
|
192
|
+
}
|
|
193
|
+
return list(remotes.values())
|
|
194
|
+
|
|
195
|
+
def update_remote_url(self, remote, url):
|
|
196
|
+
return_code, stdout = self.call(
|
|
197
|
+
["git", "remote", "set-url", remote, url])
|
|
198
|
+
return return_code
|
|
199
|
+
|
|
200
|
+
def update_remote_name(self, remote, name):
|
|
201
|
+
return_code, stdout = self.call(
|
|
202
|
+
["git", "remote", "rename", remote, name])
|
|
203
|
+
return return_code
|
|
204
|
+
|
|
205
|
+
def init(self):
|
|
206
|
+
return_code, stdout = self.call(["git", "init"])
|
|
207
|
+
return return_code
|
|
208
|
+
|
|
209
|
+
def checkout(self, branch, B=False):
|
|
210
|
+
if B:
|
|
211
|
+
return_code, stdout = self.call(["git", "checkout", "-B", branch])
|
|
212
|
+
else:
|
|
213
|
+
return_code, stdout = self.call(["git", "checkout", branch])
|
|
214
|
+
return return_code
|
|
215
|
+
|
|
216
|
+
def pull(self, remote="origin", branch="master"):
|
|
217
|
+
return_code, stdout = self.call(["git", "pull", remote, branch])
|
|
218
|
+
return return_code
|
|
219
|
+
|
|
220
|
+
def _get_ssh_wrapper(self):
|
|
221
|
+
# Get the directory of the current file
|
|
222
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
223
|
+
|
|
224
|
+
# Construct the full path to the 'ssh' file
|
|
225
|
+
wrapper = os.path.join(current_dir, 'ssh')
|
|
226
|
+
|
|
227
|
+
return wrapper
|
|
228
|
+
|
|
229
|
+
def _get_ssh_config(self, identity_file):
|
|
230
|
+
return """Host *
|
|
231
|
+
StrictHostKeyChecking no
|
|
232
|
+
IdentityFile "%s"
|
|
233
|
+
IdentitiesOnly yes
|
|
234
|
+
""" % identity_file
|
|
235
|
+
|
|
236
|
+
def fetch(self, remote="origin", branch=None, ssh_identity_file=None, git_config=None, git_credentials=None):
|
|
237
|
+
if not re.match(r"^[\w\d]+$", remote):
|
|
238
|
+
raise ValueError("Invalid remote: %s" % remote)
|
|
239
|
+
try:
|
|
240
|
+
directory = tempfile.mkdtemp()
|
|
241
|
+
env = {'HOME': directory}
|
|
242
|
+
if ssh_identity_file:
|
|
243
|
+
# To Do: Security audit
|
|
244
|
+
logger.debug("Fetching with SSH key")
|
|
245
|
+
|
|
246
|
+
env.update({'CONFIG_FILE': directory+"/ssh_config",
|
|
247
|
+
'GIT_SSH': self._get_ssh_wrapper()})
|
|
248
|
+
|
|
249
|
+
with open(directory+"/ssh_config", "w") as ssh_config_file:
|
|
250
|
+
ssh_config_file.write(
|
|
251
|
+
self._get_ssh_config(ssh_identity_file))
|
|
252
|
+
|
|
253
|
+
if git_config:
|
|
254
|
+
env.update({'GIT_CONFIG_NOSYSTEM': '1'})
|
|
255
|
+
|
|
256
|
+
with open(directory+"/.gitconfig", "w") as git_config_file:
|
|
257
|
+
git_config_file.write(git_config)
|
|
258
|
+
|
|
259
|
+
if git_credentials:
|
|
260
|
+
|
|
261
|
+
with open(directory+"/.git-credentials", "w") as git_credentials_file:
|
|
262
|
+
git_credentials_file.write(git_credentials)
|
|
263
|
+
|
|
264
|
+
extra_args = []
|
|
265
|
+
if branch is not None:
|
|
266
|
+
extra_args.append(branch)
|
|
267
|
+
|
|
268
|
+
return_code, stdout = self.call(
|
|
269
|
+
["git", "fetch", remote]+extra_args, env=env, timeout=120)
|
|
270
|
+
finally:
|
|
271
|
+
shutil.rmtree(directory)
|
|
272
|
+
|
|
273
|
+
return return_code
|
|
274
|
+
|
|
275
|
+
def reset(self, branch):
|
|
276
|
+
return_code, stdout = self.call(["git", "reset", branch])
|
|
277
|
+
|
|
278
|
+
def get_branches(self, include_remote=True):
|
|
279
|
+
if include_remote:
|
|
280
|
+
extra_args = ['-a']
|
|
281
|
+
else:
|
|
282
|
+
extra_args = []
|
|
283
|
+
raw_output = self.check_output(
|
|
284
|
+
["git", "branch", "--list"]+extra_args).decode("utf-8", 'ignore')
|
|
285
|
+
branches = [re.sub(r"^remotes\/", "", ss) for ss in [re.sub(r"[^\~\w\d\-\:\/\.\\]*", "", s.strip())
|
|
286
|
+
for s in raw_output.split("\n")] if ss]
|
|
287
|
+
return branches
|
|
288
|
+
|
|
289
|
+
def set_branch(self, branch):
|
|
290
|
+
return self.call(["git", "checkout", branch])[0]
|
|
291
|
+
|
|
292
|
+
def filter_commits_by_branch(self, commits, branch="master"):
|
|
293
|
+
|
|
294
|
+
since = min([commit['committer_date_ts'] for commit in commits])
|
|
295
|
+
until = max([commit['committer_date_ts'] for commit in commits])
|
|
296
|
+
|
|
297
|
+
branch_commits = self.get_commits(
|
|
298
|
+
branch=branch, since=since, until=until)
|
|
299
|
+
branch_shas = [commit['sha'] for commit in branch_commits]
|
|
300
|
+
|
|
301
|
+
filtered_commits = [
|
|
302
|
+
commit for commit in commits if commit['sha'] in branch_shas]
|
|
303
|
+
|
|
304
|
+
return filtered_commits
|
|
305
|
+
|
|
306
|
+
def summarize_commits(self, commits, include_limit=6):
|
|
307
|
+
|
|
308
|
+
sorted_commits = sorted(
|
|
309
|
+
commits, key=lambda commit: commit['committer_date'])
|
|
310
|
+
summary = {'count': len(commits)}
|
|
311
|
+
|
|
312
|
+
if len(commits) <= include_limit:
|
|
313
|
+
summary['commits'] = commits
|
|
314
|
+
summary['slices'] = [(None, None)]
|
|
315
|
+
else:
|
|
316
|
+
summary['commits'] = commits[:include_limit/2:] + \
|
|
317
|
+
commits[-include_limit/2:]
|
|
318
|
+
summary['slices'] = [(None, include_limit/2),
|
|
319
|
+
(-include_limit/2, None)]
|
|
320
|
+
|
|
321
|
+
summary['authors'] = {}
|
|
322
|
+
|
|
323
|
+
for commit in commits:
|
|
324
|
+
author_name = commit['author_name']
|
|
325
|
+
if author_name in summary['authors']:
|
|
326
|
+
author_summary = summary['authors'][author_name]
|
|
327
|
+
author_summary['count'] += 1
|
|
328
|
+
if not commit['author_email'] in author_summary['emails']:
|
|
329
|
+
author_summary['emails'].append(commit['author_email'])
|
|
330
|
+
else:
|
|
331
|
+
summary['authors'][author_name] = {'emails': [commit['author_email']],
|
|
332
|
+
'count': 1,
|
|
333
|
+
'name': author_name}
|
|
334
|
+
|
|
335
|
+
summary['authors'] = list(summary['authors'].values())
|
|
336
|
+
|
|
337
|
+
return summary
|
|
338
|
+
|
|
339
|
+
def get_parents(self, commit_sha):
|
|
340
|
+
base_args = ["git",
|
|
341
|
+
"rev-list",
|
|
342
|
+
"--parents",
|
|
343
|
+
"-n",
|
|
344
|
+
"1"]
|
|
345
|
+
return self.check_output(base_args+[commit_sha]).decode('utf-8', 'ignore').split()[1:]
|
|
346
|
+
|
|
347
|
+
def get_commits(self, branch=None, offset=0, limit=0, shas=None, params=None,
|
|
348
|
+
from_to=None, args=None, **kwargs):
|
|
349
|
+
|
|
350
|
+
split_sequence = '---a4337bc45a8fc544c03f52dc550cd6e1e87021bc896588bd79e901e2---'
|
|
351
|
+
try:
|
|
352
|
+
base_args = ["git",
|
|
353
|
+
"--no-pager",
|
|
354
|
+
"log",
|
|
355
|
+
"--date=raw",
|
|
356
|
+
"--pretty=format:%H:-:%ct:-:%cn:-:%ce:-:%at:-:%an:-:%ae:-:%P:-:%T:-:%n%B%n"+split_sequence+"%n"]
|
|
357
|
+
extra_args = []
|
|
358
|
+
if params:
|
|
359
|
+
extra_args.extend(params)
|
|
360
|
+
if args:
|
|
361
|
+
extra_args.extend(args)
|
|
362
|
+
for key, value in list(kwargs.items()):
|
|
363
|
+
if key in ('before', 'until') and isinstance(value, datetime.datetime):
|
|
364
|
+
value = (value+datetime.timedelta(days=1)).ctime()
|
|
365
|
+
elif key in ('after', 'since') and isinstance(value, datetime.datetime):
|
|
366
|
+
value = (value-datetime.timedelta(days=1)).ctime()
|
|
367
|
+
base_args.append("--%s" % key.replace('_', '-'))
|
|
368
|
+
if value:
|
|
369
|
+
base_args.append("%s" % value)
|
|
370
|
+
if shas:
|
|
371
|
+
raw_log_output = ''
|
|
372
|
+
for sha in shas:
|
|
373
|
+
sha_extra_args = extra_args+['-n', '1', sha]
|
|
374
|
+
raw_log_output += self.check_output(
|
|
375
|
+
base_args+sha_extra_args).decode('utf-8', 'ignore')
|
|
376
|
+
else:
|
|
377
|
+
if branch:
|
|
378
|
+
extra_args.extend([branch])
|
|
379
|
+
if from_to:
|
|
380
|
+
extra_args.extend([from_to[0]+".."+from_to[1]])
|
|
381
|
+
if offset != 0:
|
|
382
|
+
extra_args.extend(["--skip", "%d" % offset])
|
|
383
|
+
if limit != 0:
|
|
384
|
+
extra_args.extend(["--max-count", "%d" % limit])
|
|
385
|
+
raw_log_output = self.check_output(
|
|
386
|
+
base_args+extra_args).decode('utf-8', 'ignore')
|
|
387
|
+
except subprocess.CalledProcessError as e:
|
|
388
|
+
if not e.returncode in (141, 128):
|
|
389
|
+
raise
|
|
390
|
+
raw_log_output = e.output.decode("utf-8", "ignore")
|
|
391
|
+
headers_and_logs_str = list(map(lambda x: x.lstrip().split(
|
|
392
|
+
"\n", 1), raw_log_output.split(split_sequence)))[:-1]
|
|
393
|
+
headers_and_logs = [(x[0].split(":-:"), x[1])
|
|
394
|
+
for x in headers_and_logs_str]
|
|
395
|
+
|
|
396
|
+
def get_initials(name):
|
|
397
|
+
parts = re.sub(r"[^\s\w\d]+", "", name).split()
|
|
398
|
+
if len(parts) <= 3:
|
|
399
|
+
return "".join([s[0].upper() for s in parts])
|
|
400
|
+
else:
|
|
401
|
+
return "".join([s[0].upper() for s in parts[:3]+parts[-1:]])
|
|
402
|
+
|
|
403
|
+
def decode_entry(x):
|
|
404
|
+
return {
|
|
405
|
+
'sha': x[0][0],
|
|
406
|
+
'committer_date': datetime.datetime.fromtimestamp(int(x[0][1])) if x[0][1] else None,
|
|
407
|
+
'committer_date_ts': int(x[0][1]) if x[0][1] else None,
|
|
408
|
+
'committer_name': x[0][2],
|
|
409
|
+
'committer_email': x[0][3],
|
|
410
|
+
'committer_initials': get_initials(x[0][2]),
|
|
411
|
+
'author_initials': get_initials(x[0][5]),
|
|
412
|
+
'author_date': datetime.datetime.fromtimestamp(int(x[0][4])) if x[0][4] else None,
|
|
413
|
+
'author_date_ts': int(x[0][4]) if x[0][4] else None,
|
|
414
|
+
'author_name': x[0][5],
|
|
415
|
+
'author_email': x[0][6],
|
|
416
|
+
'parents': x[0][7].split(),
|
|
417
|
+
'tree_sha': x[0][8],
|
|
418
|
+
'log': x[1],
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
commits = sorted(map(decode_entry, headers_and_logs),
|
|
422
|
+
key=lambda x: x['committer_date_ts'])
|
|
423
|
+
|
|
424
|
+
# Workaround to achieve precise datetime matching in the 'before' and 'since' fields.
|
|
425
|
+
for key in ('before', 'until'):
|
|
426
|
+
if key in kwargs and isinstance(kwargs[key], datetime.datetime):
|
|
427
|
+
commits = [
|
|
428
|
+
commit for commit in commits if commit['committer_date'] < kwargs[key]]
|
|
429
|
+
for key in ('since', 'after'):
|
|
430
|
+
if key in kwargs and isinstance(kwargs[key], datetime.datetime):
|
|
431
|
+
commits = [
|
|
432
|
+
commit for commit in commits if commit['committer_date'] > kwargs[key]]
|
|
433
|
+
|
|
434
|
+
return commits
|
|
435
|
+
|
|
436
|
+
def get_submodules(self):
|
|
437
|
+
submodules = map(lambda x: x.split(" ")[1],
|
|
438
|
+
self.check_output(["git", "submodule"]).split("\n")[:-1]).decode("utf-8", 'ignore')
|
|
439
|
+
return submodules
|
|
440
|
+
|
|
441
|
+
def get_modifications_by_author(self, commits):
|
|
442
|
+
modifications_by_author = defaultdict(lambda: defaultdict(lambda: {'lines_added': 0,
|
|
443
|
+
'lines_removed': 0,
|
|
444
|
+
'commits': 0}))
|
|
445
|
+
for commit in commits:
|
|
446
|
+
author_email = commit['author_email']
|
|
447
|
+
sha = commit['sha']
|
|
448
|
+
try:
|
|
449
|
+
modified_files = [(int(v[0]), int(v[1]), v[2])
|
|
450
|
+
for v in [s.split("\t")
|
|
451
|
+
for s in self.check_output(["git",
|
|
452
|
+
"diff",
|
|
453
|
+
r"--numstat",
|
|
454
|
+
"{sha}^..{sha}"
|
|
455
|
+
.format(sha=sha)
|
|
456
|
+
.decode("utf-8", 'ignore')])
|
|
457
|
+
.strip()
|
|
458
|
+
.split("\n")]
|
|
459
|
+
if len(v) == 3
|
|
460
|
+
and v[2] != ''
|
|
461
|
+
and v[0] != '-'
|
|
462
|
+
and v[1] != '-']
|
|
463
|
+
except subprocess.CalledProcessError:
|
|
464
|
+
continue
|
|
465
|
+
for (lines_added, lines_removed, path) in modified_files:
|
|
466
|
+
d = modifications_by_author[author_email][path]
|
|
467
|
+
d['lines_added'] += lines_added
|
|
468
|
+
d['lines_removed'] += lines_removed
|
|
469
|
+
d['commits'] += 1
|
|
470
|
+
return modifications_by_author
|
|
471
|
+
|
|
472
|
+
def get_contributors(self, branch=None):
|
|
473
|
+
args = ["-se"]
|
|
474
|
+
if branch:
|
|
475
|
+
args += [branch]
|
|
476
|
+
else:
|
|
477
|
+
args += ["--all"]
|
|
478
|
+
lines = self.check_output(
|
|
479
|
+
["git", "shortlog"]+args).decode("utf-8", 'ignore').split("\n")
|
|
480
|
+
contributors = []
|
|
481
|
+
for line in lines:
|
|
482
|
+
match = re.match("^.*?(\d+)\s+(.*?)\s*\<(.*)\>\s*$", line)
|
|
483
|
+
if match:
|
|
484
|
+
(n_commits, name, email) = match.groups()
|
|
485
|
+
contributors.append(
|
|
486
|
+
{'name': name, 'email': email, 'n_commits': int(n_commits)})
|
|
487
|
+
return contributors
|
|
488
|
+
|
|
489
|
+
def get_number_of_commits(self, branch=None):
|
|
490
|
+
if branch:
|
|
491
|
+
command = ["git", "rev-list", branch, "--count"]
|
|
492
|
+
else:
|
|
493
|
+
command = ["git", "rev-list", "HEAD", "--count"]
|
|
494
|
+
n_commits = int(self.check_output(
|
|
495
|
+
command).strip().decode("utf-8", 'ignore'))
|
|
496
|
+
return n_commits
|
|
497
|
+
|
|
498
|
+
def get_files_in_commit(self, commit_sha, path=None):
|
|
499
|
+
if path == None:
|
|
500
|
+
opts = ["--full-tree", "-r", commit_sha]
|
|
501
|
+
else:
|
|
502
|
+
opts = ["%s:%s" % (commit_sha, path)]
|
|
503
|
+
files = [dict(list(zip(['mode', 'type', 'sha', 'path'], f.strip().split())))
|
|
504
|
+
for f in self.check_output(["git", "ls-tree"]+opts)
|
|
505
|
+
.decode("utf-8", 'ignore')
|
|
506
|
+
.split("\n") if f]
|
|
507
|
+
return files
|
|
508
|
+
|
|
509
|
+
def get_file_details(self, commit_sha, path):
|
|
510
|
+
(file_mode, file_type, file_sha, file_path) = self.check_output(["git",
|
|
511
|
+
"ls-tree",
|
|
512
|
+
commit_sha,
|
|
513
|
+
path])\
|
|
514
|
+
.decode("utf-8", 'ignore').split()
|
|
515
|
+
return {'mode': file_mode, 'type': file_type, 'sha': file_sha, 'path': file_path}
|
|
516
|
+
|
|
517
|
+
def get_diffs(self, commit_sha_a, commit_sha_b=None):
|
|
518
|
+
if not commit_sha_b:
|
|
519
|
+
files = self.get_files_in_commit(commit_sha_a)
|
|
520
|
+
return [['A', file['path']] for file in files]
|
|
521
|
+
else:
|
|
522
|
+
diffs = self.check_output(["git",
|
|
523
|
+
"diff",
|
|
524
|
+
"--name-status",
|
|
525
|
+
commit_sha_a,
|
|
526
|
+
commit_sha_b])\
|
|
527
|
+
.decode("utf-8", 'ignore').split("\n")
|
|
528
|
+
diffs = [x.split("\t") for x in diffs]
|
|
529
|
+
return [x for x in diffs if len(x) == 2]
|
|
530
|
+
|
|
531
|
+
def _decode_file_content(self, content):
|
|
532
|
+
try:
|
|
533
|
+
return content.decode('utf-8', errors='ignore')
|
|
534
|
+
except UnicodeDecodeError:
|
|
535
|
+
return content.decode('latin1', errors='ignore')
|
|
536
|
+
|
|
537
|
+
def get_file_content(self, commit_sha, path, decode=False):
|
|
538
|
+
|
|
539
|
+
try:
|
|
540
|
+
file_content = self.check_output(
|
|
541
|
+
["git","show", "%s:%s" % (commit_sha, path)])
|
|
542
|
+
except subprocess.CalledProcessError:
|
|
543
|
+
raise IOError
|
|
544
|
+
if decode:
|
|
545
|
+
return self._decode_file_content(file_content)
|
|
546
|
+
return file_content
|
|
547
|
+
|
|
548
|
+
def get_file_content_by_sha(self, sha, decode=False):
|
|
549
|
+
try:
|
|
550
|
+
file_content = self.check_output(
|
|
551
|
+
["git", "cat-file", "blob", "%s" % (sha,)])
|
|
552
|
+
|
|
553
|
+
except subprocess.CalledProcessError:
|
|
554
|
+
raise IOError
|
|
555
|
+
if decode:
|
|
556
|
+
return self._decode_file_content(file_content)
|
|
557
|
+
return file_content
|