Fix PEP8 issues
[integration/test.git] / tools / distchanges / gerritquery.py
1 """
2 This module contains functions to manipulate gerrit queries.
3 """
4 import datetime
5 import json
6 import os
7 import re
8 import shlex
9 import subprocess
10 import traceback
11 import sys
12
13
14 # TODO: Haven't tested python 3
15 if sys.version < '3':
16     import urllib
17     import urlparse
18     urlencode = urllib.urlencode
19     urljoin = urlparse.urljoin
20     urlparse = urlparse.urlparse
21     do_input = raw_input
22 else:
23     import urllib.parse
24     import urllib.request
25     urlencode = urllib.parse.urlencode
26     urljoin = urllib.parse.urljoin
27     urlparse = urllib.parse.urlparse
28     do_input = input
29
30
31 class GitReviewException(Exception):
32     EXIT_CODE = 127
33
34
35 class CommandFailed(GitReviewException):
36     """Command Failure Analysis"""
37
38     def __init__(self, *args):
39         Exception.__init__(self, *args)
40         (self.rc, self.output, self.argv, self.envp) = args
41         self.quickmsg = dict([
42             ("argv", " ".join(self.argv)),
43             ("rc", self.rc),
44             ("output", self.output)])
45
46     def __str__(self):
47         return self.__doc__ + """
48 The following command failed with exit code %(rc)d
49     "%(argv)s"
50 -----------------------
51 %(output)s
52 -----------------------""" % self.quickmsg
53
54
55 class GerritQuery:
56     REMOTE_URL = 'ssh://git.opendaylight.org:29418'
57     BRANCH = 'master'
58     QUERY_LIMIT = 50
59
60     remote_url = REMOTE_URL
61     branch = BRANCH
62     query_limit = QUERY_LIMIT
63     verbose = 0
64
65     def __init__(self, remote_url, branch, query_limit, verbose):
66         self.remote_url = remote_url
67         self.branch = branch
68         self.query_limit = query_limit
69         self.verbose = verbose
70
71     def set_verbose(self, verbose):
72         self.verbose = verbose
73
74     @staticmethod
75     def print_safe_encoding(string):
76         if sys.stdout.encoding is None:
77             # just print(string) could still throw a UnicodeEncodeError sometimes so casting string to unicode
78             print(unicode(string))
79         else:
80             print(string.encode(sys.stdout.encoding, 'replace'))
81
82     def run_command_status(self, *argv, **kwargs):
83
84         if self.verbose >= 2:
85             print(datetime.datetime.now(), "Running:", " ".join(argv))
86         if len(argv) == 1:
87             # for python2 compatibility with shlex
88             if sys.version_info < (3,) and isinstance(argv[0], unicode):
89                 argv = shlex.split(argv[0].encode('utf-8'))
90             else:
91                 argv = shlex.split(str(argv[0]))
92         stdin = kwargs.pop('stdin', None)
93         newenv = os.environ.copy()
94         newenv['LANG'] = 'C'
95         newenv['LANGUAGE'] = 'C'
96         newenv.update(kwargs)
97         p = subprocess.Popen(argv,
98                              stdin=subprocess.PIPE if stdin else None,
99                              stdout=subprocess.PIPE,
100                              stderr=subprocess.STDOUT,
101                              env=newenv)
102         (out, nothing) = p.communicate(stdin)
103         out = out.decode('utf-8', 'replace')
104         return p.returncode, out.strip()
105
106     def run_command(self, *argv, **kwargs):
107         (rc, output) = self.run_command_status(*argv, **kwargs)
108         return output
109
110     def run_command_exc(self, klazz, *argv, **env):
111         """
112         Run command *argv, on failure raise klazz
113
114         klazz should be derived from CommandFailed
115         """
116         (rc, output) = self.run_command_status(*argv, **env)
117         if rc != 0:
118             raise klazz(rc, output, argv, env)
119         return output
120
121     def parse_gerrit_ssh_params_from_git_url(self):
122         """
123         Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
124         real URLs or SCP-style addresses.
125         """
126
127         # The exact code for this in Git itself is a bit obtuse, so just do
128         # something sensible and pythonic here instead of copying the exact
129         # minutiae from Git.
130
131         # Handle real(ish) URLs
132         if "://" in self.remote_url:
133             parsed_url = urlparse(self.remote_url)
134             path = parsed_url.path
135
136             hostname = parsed_url.netloc
137             username = None
138             port = parsed_url.port
139
140             # Workaround bug in urlparse on OSX
141             if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
142                 hostname = parsed_url.path[2:].split("/")[0]
143
144             if "@" in hostname:
145                 (username, hostname) = hostname.split("@")
146             if ":" in hostname:
147                 (hostname, port) = hostname.split(":")
148
149             if port is not None:
150                 port = str(port)
151
152         # Handle SCP-style addresses
153         else:
154             username = None
155             port = None
156             (hostname, path) = self.remote_url.split(":", 1)
157             if "@" in hostname:
158                 (username, hostname) = hostname.split("@", 1)
159
160         # Strip leading slash and trailing .git from the path to form the project
161         # name.
162         project_name = re.sub(r"^/|(\.git$)", "", path)
163
164         return hostname, username, port, project_name
165
166     def gerrit_request(self, request):
167         """
168         Send a gerrit request and receive a response.
169
170         :param str request: A gerrit query
171         :return unicode: The JSON response
172         """
173         (hostname, username, port, project_name) = \
174             self.parse_gerrit_ssh_params_from_git_url()
175
176         port_data = "p%s" % port if port is not None else ""
177         if username is None:
178             userhost = hostname
179         else:
180             userhost = "%s@%s" % (username, hostname)
181
182         if self.verbose >= 2:
183             print("gerrit request %s %s" % (self.remote_url, request))
184         output = self.run_command_exc(
185             CommandFailed,
186             "ssh", "-x" + port_data, userhost,
187             request)
188         if self.verbose >= 3:
189             self.print_safe_encoding(output)
190         return output
191
192     def make_gerrit_query(self, project, changeid=None, limit=1, msg=None):
193         """
194         Make a gerrit query by combining the given options.
195
196         :param str project: The project to search
197         :param str changeid: A Change-Id to search
198         :param int limit: The number of items to return
199         :param str msg: A commit-msg to search
200         :return str: A gerrit query
201         """
202         query = "gerrit query --format=json limit:%d status:merged --all-approvals " \
203                 "project:%s branch:%s" \
204                 % (limit, project, self.branch)
205         if changeid:
206             query += " change:%s" % changeid
207         if msg:
208             query += " message:%s" % msg
209         return query
210
211     def parse_gerrit(self, line, parse_exc=Exception):
212         """
213         Parse a single gerrit line and copy certain fields to a dictionary.
214
215         The merge time is found by looking for the Patch Set->Approval with
216         a SUBM type. Then use the grantedOn value.
217
218         :param str line: A single line from a previous gerrit query
219         :param parse_exc: The exception to except
220         :return dict: Pairs of gerrit items and their values
221         """
222         parsed = {}
223         try:
224             if line and line[0] == "{":
225                 try:
226                     data = json.loads(line)
227                     parsed['id'] = data['id']
228                     parsed['number'] = data['number']
229                     parsed['subject'] = data['subject']
230                     parsed['url'] = data['url']
231                     parsed['lastUpdated'] = data['lastUpdated']
232                     if "patchSets" in data:
233                         patch_sets = data['patchSets']
234                         for patch_set in reversed(patch_sets):
235                             if "approvals" in patch_set:
236                                 approvals = patch_set['approvals']
237                                 for approval in approvals:
238                                     if 'type' in approval and approval['type'] == 'SUBM':
239                                         parsed['grantedOn'] = approval['grantedOn']
240                                         break
241                                 if parsed['grantedOn']:
242                                     break
243                 except Exception:
244                     if self.verbose:
245                         print("Failed to decode JSON: %s" % traceback.format_exc())
246                         self.print_safe_encoding(line)
247         except Exception as err:
248             print("Exception: %s" % traceback.format_exc())
249             raise parse_exc(err)
250         return parsed
251
252     def extract_lines_from_json(self, changes):
253         """
254         Extract a list of lines from the JSON gerrit query response.
255
256         Drop the stats line.
257
258         :param unicode changes: The full JSON gerrit query response
259         :return list: Lines of the JSON
260         """
261         lines = []
262         for line in changes.split("\n"):
263             if line.find('"type":"error","message"') != -1:
264                 print("there was a query error")
265                 continue
266             if line.find('stats') == -1:
267                 lines.append(line)
268         if self.verbose >= 2:
269             print("get_gerrit_lines: found %d lines" % len(lines))
270         return lines
271
272     def get_gerrits(self, project, changeid=None, limit=1, msg=None):
273         """
274         Get a list of gerrits from gerrit query request.
275
276         Gerrit returns queries in order of lastUpdated so resort based on merge time.
277         Also because gerrit returns them in lastUpdated order, it means all gerrits
278         merged after the one we are using will be returned, so the query limit needs to be
279         high enough to capture those extra merges plus the limit requested.
280         TODO: possibly add the before query to set a start time for the query around the change
281
282         :param str project: The project to search
283         :param str or None changeid: A Change-Id to search
284         :param int limit: The number of items to return
285         :param str msg: A commit-msg to search
286         :return str: A gerrit query
287         """
288         query = self.make_gerrit_query(project, changeid, limit, msg)
289         changes = self.gerrit_request(query)
290         lines = self.extract_lines_from_json(changes)
291         gerrits = []
292         for line in lines:
293             gerrits.append(self.parse_gerrit(line))
294
295         from operator import itemgetter
296         sorted_gerrits = sorted(gerrits, key=itemgetter('grantedOn'), reverse=True)
297         return sorted_gerrits