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
47 [("argv", " ".join(self.argv)), ("rc", self.rc), ("output", self.output)]
54 The following command failed with exit code %(rc)d
56 -----------------------
58 -----------------------"""
64 REMOTE_URL = "ssh://git.opendaylight.org:29418"
68 remote_url = REMOTE_URL
70 query_limit = QUERY_LIMIT
72 def __init__(self, remote_url, branch, query_limit, verbose):
73 self.remote_url = remote_url
75 self.query_limit = query_limit
76 self.verbose = verbose
79 def print_safe_encoding(string):
81 # FIXME: Python3 does not have 'unicode'
82 if isinstance(string, unicode):
84 if hasattr(sys.stdout, "encoding") and sys.stdout.encoding:
85 encoding = sys.stdout.encoding
86 return string.encode(encoding or "utf-8", "replace")
92 def run_command_status(self, *argv, **kwargs):
93 logger.debug("%s Running: %s", datetime.datetime.now(), " ".join(argv))
95 # for python2 compatibility with shlex
96 if sys.version_info < (3,) and isinstance(argv[0], unicode):
97 argv = shlex.split(argv[0].encode("utf-8"))
99 argv = shlex.split(str(argv[0]))
100 stdin = kwargs.pop("stdin", None)
101 newenv = os.environ.copy()
103 newenv["LANGUAGE"] = "C"
104 newenv.update(kwargs)
105 p = subprocess.Popen(
107 stdin=subprocess.PIPE if stdin else None,
108 stdout=subprocess.PIPE,
109 stderr=subprocess.STDOUT,
112 (out, nothing) = p.communicate(stdin)
113 out = out.decode("utf-8", "replace")
114 return p.returncode, out.strip()
116 def run_command(self, *argv, **kwargs):
117 (rc, output) = self.run_command_status(*argv, **kwargs)
120 def run_command_exc(self, klazz, *argv, **env):
122 Run command *argv, on failure raise klazz
124 klazz should be derived from CommandFailed
126 (rc, output) = self.run_command_status(*argv, **env)
128 raise klazz(rc, output, argv, env)
131 def parse_gerrit_ssh_params_from_git_url(self):
133 Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
134 real URLs or SCP-style addresses.
137 # The exact code for this in Git itself is a bit obtuse, so just do
138 # something sensible and pythonic here instead of copying the exact
141 # Handle real(ish) URLs
142 if "://" in self.remote_url:
143 parsed_url = urlparse(self.remote_url)
144 path = parsed_url.path
146 hostname = parsed_url.netloc
148 port = parsed_url.port
150 # Workaround bug in urlparse on OSX
151 if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
152 hostname = parsed_url.path[2:].split("/")[0]
155 (username, hostname) = hostname.split("@")
157 (hostname, port) = hostname.split(":")
162 # Handle SCP-style addresses
166 (hostname, path) = self.remote_url.split(":", 1)
168 (username, hostname) = hostname.split("@", 1)
170 # Strip leading slash and trailing .git from the path to form the project
172 project_name = re.sub(r"^/|(\.git$)", "", path)
174 return hostname, username, port, project_name
176 def gerrit_request(self, request):
178 Send a gerrit request and receive a response.
180 :param str request: A gerrit query
181 :return unicode: The JSON response
188 ) = self.parse_gerrit_ssh_params_from_git_url()
190 port_data = "p%s" % port if port is not None else ""
194 userhost = "%s@%s" % (username, hostname)
196 logger.debug("gerrit request %s %s" % (self.remote_url, request))
197 output = self.run_command_exc(
198 CommandFailed, "ssh", "-x" + port_data, userhost, request
200 if logger.isEnabledFor(logging.DEBUG):
201 logger.debug("%s", self.print_safe_encoding(output))
204 def make_gerrit_query(
215 Make a gerrit query by combining the given options.
217 :param str project: The project to search
218 :param str changeid: A Change-Id to search
219 :param int limit: The number of items to return
220 :param str msg or None: A commit-msg to search
221 :param str status or None: The gerrit status, i.e. merged
222 :param bool comments: If true include comments
223 :param commitid: A commit hash to search
224 :return str: A gerrit query
227 if project == "odlparent" or project == "yangtools":
228 query = "gerrit query --format=json limit:%d " "project:%s" % (
233 query = "gerrit query --format=json limit:%d " "project:%s branch:%s" % (
239 query += " change:%s" % changeid
241 query += " message:{%s}" % msg
243 query += " commit:%s" % commitid
245 query += " status:%s --all-approvals" % status
247 query += " --comments"
250 def parse_gerrit(self, line, parse_exc=Exception):
252 Parse a single gerrit line and copy certain fields to a dictionary.
254 The merge time is found by looking for the Patch Set->Approval with
255 a SUBM type. Then use the grantedOn value.
257 :param str line: A single line from a previous gerrit query
258 :param parse_exc: The exception to except
259 :return dict: Pairs of gerrit items and their values
263 if line and line[0] == "{":
265 data = json.loads(line)
266 parsed["id"] = data["id"]
267 parsed["number"] = data["number"]
268 parsed["subject"] = data["subject"]
269 parsed["url"] = data["url"]
270 parsed["lastUpdated"] = data["lastUpdated"]
271 parsed["grantedOn"] = 0
272 if "patchSets" in data:
273 patch_sets = data["patchSets"]
274 for patch_set in reversed(patch_sets):
275 if "approvals" in patch_set:
276 approvals = patch_set["approvals"]
277 for approval in approvals:
280 and approval["type"] == "SUBM"
282 parsed["grantedOn"] = approval["grantedOn"]
284 if parsed["grantedOn"] != 0:
286 if "comments" in data:
287 comments = data["comments"]
288 for comment in reversed(comments):
289 if "message" in comment and "timestamp" in comment:
290 message = comment["message"]
291 timestamp = comment["timestamp"]
293 "Build Started" in message
294 and "patch-test" in message
296 parsed["grantedOn"] = timestamp
299 logger.warn("Failed to decode JSON: %s", traceback.format_exc())
300 if logger.isEnabledFor(logging.DEBUG):
301 logger.warn(self.print_safe_encoding(line))
302 except Exception as err:
303 logger.warn("Exception: %s", traceback.format_exc())
307 def extract_lines_from_json(self, changes):
309 Extract a list of lines from the JSON gerrit query response.
313 :param unicode changes: The full JSON gerrit query response
314 :return list: Lines of the JSON
318 for i, line in enumerate(changes.split("\n")):
319 if line.find('"grantedOn":') != -1:
322 logger.debug("skipping: {}".format(line))
325 "get_gerrit_lines: found {} lines, skipped: {}".format(len(lines), skipped)
340 Get a list of gerrits from gerrit query request.
342 Gerrit returns queries in order of lastUpdated so resort based on merge time.
343 Also because gerrit returns them in lastUpdated order, it means all gerrits
344 merged after the one we are using will be returned, so the query limit needs to be
345 high enough to capture those extra merges plus the limit requested.
346 TODO: possibly add the before query to set a start time for the query around the change
348 :param str project: The project to search
349 :param str or None changeid: A Change-Id to search
350 :param int limit: The number of items to return
351 :param str or None msg: A commit-msg to search
352 :param str or None status: The gerrit status, i.e. merged
353 :param bool comments: If true include comments
354 :param commitid: A commit hash to search
355 :return str: List of gerrits sorted by merge time
358 "get_gerrits: project: %s, changeid: %s, limit: %d, msg: %s, status: %s, comments: %s, "
368 query = self.make_gerrit_query(
369 project, changeid, limit, msg, status, comments, commitid
371 changes = self.gerrit_request(query)
372 lines = self.extract_lines_from_json(changes)
376 gerrits.append(self.parse_gerrit(line))
378 from operator import itemgetter
381 logger.warn("No gerrits were found for %s", project)
384 sorted_gerrits = sorted(gerrits, key=itemgetter("grantedOn"), reverse=True)
385 except KeyError as e:
386 logger.warn("KeyError exception in %s, %s", project, str(e))
387 return sorted_gerrits