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