2 This module contains functions to manipulate gerrit queries.
14 # TODO: Haven't tested python 3
19 urlencode = urllib.urlencode
20 urljoin = urlparse.urljoin
21 urlparse = urlparse.urlparse
27 urlencode = urllib.parse.urlencode
28 urljoin = urllib.parse.urljoin
29 urlparse = urllib.parse.urlparse
33 logger = logging.getLogger("changes.gerritquery")
36 class GitReviewException(Exception):
40 class CommandFailed(GitReviewException):
41 """Command Failure Analysis"""
43 def __init__(self, *args):
44 Exception.__init__(self, *args)
45 (self.rc, self.output, self.argv, self.envp) = args
46 self.quickmsg = dict([
47 ("argv", " ".join(self.argv)),
49 ("output", self.output)])
52 return self.__doc__ + """
53 The following command failed with exit code %(rc)d
55 -----------------------
57 -----------------------""" % self.quickmsg
61 REMOTE_URL = 'ssh://git.opendaylight.org:29418'
65 remote_url = REMOTE_URL
67 query_limit = QUERY_LIMIT
69 def __init__(self, remote_url, branch, query_limit, verbose):
70 self.remote_url = remote_url
72 self.query_limit = query_limit
73 self.verbose = verbose
76 def print_safe_encoding(string):
77 if type(string) == unicode:
79 if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding:
80 encoding = sys.stdout.encoding
81 return string.encode(encoding or 'utf-8', 'replace')
85 def run_command_status(self, *argv, **kwargs):
86 logger.debug("%s Running: %s", datetime.datetime.now(), " ".join(argv))
88 # for python2 compatibility with shlex
89 if sys.version_info < (3,) and isinstance(argv[0], unicode):
90 argv = shlex.split(argv[0].encode('utf-8'))
92 argv = shlex.split(str(argv[0]))
93 stdin = kwargs.pop('stdin', None)
94 newenv = os.environ.copy()
96 newenv['LANGUAGE'] = 'C'
98 p = subprocess.Popen(argv,
99 stdin=subprocess.PIPE if stdin else None,
100 stdout=subprocess.PIPE,
101 stderr=subprocess.STDOUT,
103 (out, nothing) = p.communicate(stdin)
104 out = out.decode('utf-8', 'replace')
105 return p.returncode, out.strip()
107 def run_command(self, *argv, **kwargs):
108 (rc, output) = self.run_command_status(*argv, **kwargs)
111 def run_command_exc(self, klazz, *argv, **env):
113 Run command *argv, on failure raise klazz
115 klazz should be derived from CommandFailed
117 (rc, output) = self.run_command_status(*argv, **env)
119 raise klazz(rc, output, argv, env)
122 def parse_gerrit_ssh_params_from_git_url(self):
124 Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
125 real URLs or SCP-style addresses.
128 # The exact code for this in Git itself is a bit obtuse, so just do
129 # something sensible and pythonic here instead of copying the exact
132 # Handle real(ish) URLs
133 if "://" in self.remote_url:
134 parsed_url = urlparse(self.remote_url)
135 path = parsed_url.path
137 hostname = parsed_url.netloc
139 port = parsed_url.port
141 # Workaround bug in urlparse on OSX
142 if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
143 hostname = parsed_url.path[2:].split("/")[0]
146 (username, hostname) = hostname.split("@")
148 (hostname, port) = hostname.split(":")
153 # Handle SCP-style addresses
157 (hostname, path) = self.remote_url.split(":", 1)
159 (username, hostname) = hostname.split("@", 1)
161 # Strip leading slash and trailing .git from the path to form the project
163 project_name = re.sub(r"^/|(\.git$)", "", path)
165 return hostname, username, port, project_name
167 def gerrit_request(self, request):
169 Send a gerrit request and receive a response.
171 :param str request: A gerrit query
172 :return unicode: The JSON response
174 (hostname, username, port, project_name) = \
175 self.parse_gerrit_ssh_params_from_git_url()
177 port_data = "p%s" % port if port is not None else ""
181 userhost = "%s@%s" % (username, hostname)
183 logger.debug("gerrit request %s %s" % (self.remote_url, request))
184 output = self.run_command_exc(CommandFailed, "ssh", "-x" + port_data, userhost, request)
185 if logger.isEnabledFor(logging.DEBUG):
186 logger.debug("%s", self.print_safe_encoding(output))
189 def make_gerrit_query(self, project, changeid=None, limit=1, msg=None, status=None, comments=False, commitid=None):
191 Make a gerrit query by combining the given options.
193 :param str project: The project to search
194 :param str changeid: A Change-Id to search
195 :param int limit: The number of items to return
196 :param str msg or None: A commit-msg to search
197 :param str status or None: The gerrit status, i.e. merged
198 :param bool comments: If true include comments
199 :param commitid: A commit hash to search
200 :return str: A gerrit query
203 if project == "odlparent" or project == "yangtools":
204 query = "gerrit query --format=json limit:%d " \
208 query = "gerrit query --format=json limit:%d " \
209 "project:%s branch:%s" \
210 % (limit, project, self.branch)
212 query += " change:%s" % changeid
214 query += " message:{%s}" % msg
216 query += " commit:%s" % commitid
218 query += " status:%s --all-approvals" % status
220 query += " --comments"
223 def parse_gerrit(self, line, parse_exc=Exception):
225 Parse a single gerrit line and copy certain fields to a dictionary.
227 The merge time is found by looking for the Patch Set->Approval with
228 a SUBM type. Then use the grantedOn value.
230 :param str line: A single line from a previous gerrit query
231 :param parse_exc: The exception to except
232 :return dict: Pairs of gerrit items and their values
236 if line and line[0] == "{":
238 data = json.loads(line)
239 parsed['id'] = data['id']
240 parsed['number'] = data['number']
241 parsed['subject'] = data['subject']
242 parsed['url'] = data['url']
243 parsed['lastUpdated'] = data['lastUpdated']
244 parsed['grantedOn'] = 0
245 if "patchSets" in data:
246 patch_sets = data['patchSets']
247 for patch_set in reversed(patch_sets):
248 if "approvals" in patch_set:
249 approvals = patch_set['approvals']
250 for approval in approvals:
251 if 'type' in approval and approval['type'] == 'SUBM':
252 parsed['grantedOn'] = approval['grantedOn']
254 if parsed['grantedOn'] != 0:
256 if "comments" in data:
257 comments = data['comments']
258 for comment in reversed(comments):
259 if "message" in comment and "timestamp" in comment:
260 message = comment['message']
261 timestamp = comment['timestamp']
262 if "Build Started" in message and "patch-test" in message:
263 parsed['grantedOn'] = timestamp
266 logger.warn("Failed to decode JSON: %s", traceback.format_exc())
267 if logger.isEnabledFor(logging.DEBUG):
268 logger.warn(self.print_safe_encoding(line))
269 except Exception as err:
270 logger.warn("Exception: %s", traceback.format_exc())
274 def extract_lines_from_json(self, changes):
276 Extract a list of lines from the JSON gerrit query response.
280 :param unicode changes: The full JSON gerrit query response
281 :return list: Lines of the JSON
284 for line in changes.split("\n"):
285 if line.find('"type":"error","message"') != -1:
286 logger.warn("there was a query error")
288 if line.find('stats') == -1:
290 logger.debug("get_gerrit_lines: found %d lines", len(lines))
293 def get_gerrits(self, project, changeid=None, limit=1, msg=None, status=None, comments=False, commitid=None):
295 Get a list of gerrits from gerrit query request.
297 Gerrit returns queries in order of lastUpdated so resort based on merge time.
298 Also because gerrit returns them in lastUpdated order, it means all gerrits
299 merged after the one we are using will be returned, so the query limit needs to be
300 high enough to capture those extra merges plus the limit requested.
301 TODO: possibly add the before query to set a start time for the query around the change
303 :param str project: The project to search
304 :param str or None changeid: A Change-Id to search
305 :param int limit: The number of items to return
306 :param str or None msg: A commit-msg to search
307 :param str or None status: The gerrit status, i.e. merged
308 :param bool comments: If true include comments
309 :param commitid: A commit hash to search
310 :return str: List of gerrits sorted by merge time
312 logger.debug("get_gerrits: project: %s, changeid: %s, limit: %d, msg: %s, status: %s, comments: %s, " +
314 project, changeid, limit, msg, status, comments, commitid)
315 query = self.make_gerrit_query(project, changeid, limit, msg, status, comments, commitid)
316 changes = self.gerrit_request(query)
317 lines = self.extract_lines_from_json(changes)
321 gerrits.append(self.parse_gerrit(line))
323 from operator import itemgetter
325 logger.warn("No gerrits were found for %s", project)
328 sorted_gerrits = sorted(gerrits, key=itemgetter('grantedOn'), reverse=True)
330 logger.warn("KeyError exception in %s, %s", project, str(e))
331 return sorted_gerrits