2 This module contains functions to manipulate gerrit queries.
14 # TODO: Haven't tested python 3
18 urlencode = urllib.urlencode
19 urljoin = urlparse.urljoin
20 urlparse = urlparse.urlparse
25 urlencode = urllib.parse.urlencode
26 urljoin = urllib.parse.urljoin
27 urlparse = urllib.parse.urlparse
31 class GitReviewException(Exception):
35 class CommandFailed(GitReviewException):
37 def __init__(self, *args):
38 Exception.__init__(self, *args)
39 (self.rc, self.output, self.argv, self.envp) = args
40 self.quickmsg = dict([
41 ("argv", " ".join(self.argv)),
43 ("output", self.output)])
46 return self.__doc__ + """
47 The following command failed with exit code %(rc)d
49 -----------------------
51 -----------------------""" % self.quickmsg
55 REMOTE_URL = 'ssh://git.opendaylight.org:29418'
59 remote_url = REMOTE_URL
61 query_limit = QUERY_LIMIT
64 def __init__(self, remote_url, branch, query_limit, verbose):
65 self.remote_url = remote_url
67 self.query_limit = query_limit
68 self.verbose = verbose
70 def set_verbose(self, verbose):
71 self.verbose = verbose
74 def print_safe_encoding(string):
75 if sys.stdout.encoding is None:
78 print(string.encode(sys.stdout.encoding, 'replace'))
80 def run_command_status(self, *argv, **kwargs):
83 print(datetime.datetime.now(), "Running:", " ".join(argv))
85 # for python2 compatibility with shlex
86 if sys.version_info < (3,) and isinstance(argv[0], unicode):
87 argv = shlex.split(argv[0].encode('utf-8'))
89 argv = shlex.split(str(argv[0]))
90 stdin = kwargs.pop('stdin', None)
91 newenv = os.environ.copy()
93 newenv['LANGUAGE'] = 'C'
95 p = subprocess.Popen(argv,
96 stdin=subprocess.PIPE if stdin else None,
97 stdout=subprocess.PIPE,
98 stderr=subprocess.STDOUT,
100 (out, nothing) = p.communicate(stdin)
101 out = out.decode('utf-8', 'replace')
102 return (p.returncode, out.strip())
104 def run_command(self, *argv, **kwargs):
105 (rc, output) = self.run_command_status(*argv, **kwargs)
108 def run_command_exc(self, klazz, *argv, **env):
110 Run command *argv, on failure raise klazz
112 klazz should be derived from CommandFailed
114 (rc, output) = self.run_command_status(*argv, **env)
116 raise klazz(rc, output, argv, env)
119 def parse_gerrit_ssh_params_from_git_url(self):
121 Parse a given Git "URL" into Gerrit parameters. Git "URLs" are either
122 real URLs or SCP-style addresses.
125 # The exact code for this in Git itself is a bit obtuse, so just do
126 # something sensible and pythonic here instead of copying the exact
129 # Handle real(ish) URLs
130 if "://" in self.remote_url:
131 parsed_url = urlparse(self.remote_url)
132 path = parsed_url.path
134 hostname = parsed_url.netloc
136 port = parsed_url.port
138 # Workaround bug in urlparse on OSX
139 if parsed_url.scheme == "ssh" and parsed_url.path[:2] == "//":
140 hostname = parsed_url.path[2:].split("/")[0]
143 (username, hostname) = hostname.split("@")
145 (hostname, port) = hostname.split(":")
150 # Handle SCP-style addresses
154 (hostname, path) = self.remote_url_url.split(":", 1)
156 (username, hostname) = hostname.split("@", 1)
158 # Strip leading slash and trailing .git from the path to form the project
160 project_name = re.sub(r"^/|(\.git$)", "", path)
162 return (hostname, username, port, project_name)
164 def gerrit_request(self, request):
166 Send a gerrit request and receive a response.
168 :param str request: A gerrit query
169 :return unicode: The JSON response
171 (hostname, username, port, project_name) = \
172 self.parse_gerrit_ssh_params_from_git_url()
174 port_data = "p%s" % port if port is not None else ""
178 userhost = "%s@%s" % (username, hostname)
180 if self.verbose >= 2:
181 print("gerrit request %s %s" % (self.remote_url, request))
182 output = self.run_command_exc(
184 "ssh", "-x" + port_data, userhost,
186 if self.verbose >= 3:
187 self.print_safe_encoding(output)
190 def make_gerrit_query(self, project, changeid=None, limit=1, msg=None):
192 Make a gerrit query by combining the given options.
194 :param str project: The project to search
195 :param str changeid: A Change-Id to search
196 :param int limit: The number of items to return
197 :param str msg: A commit-msg to search
198 :return str: A gerrit query
200 query = "gerrit query --format=json limit:%d status:merged --all-approvals " \
201 "project:%s branch:%s" \
202 % (limit, project, self.branch)
204 query += " change:%s" % changeid
206 query += " message:%s" % msg
209 def parse_gerrit(self, line, parse_exc=Exception):
211 Parse a single gerrit line and copy certain fields to a dictionary.
213 The merge time is found by looking for the Patch Set->Approval with
214 a SUBM type. Then use the grantedOn value.
216 :param str line: A single line from a previous gerrit query
217 :param parse_exc: The exception to except
218 :return dict: Pairs of gerrit items and their values
222 if line and line[0] == "{":
224 data = json.loads(line)
225 parsed['id'] = data['id']
226 parsed['number'] = data['number']
227 parsed['subject'] = data['subject']
228 parsed['url'] = data['url']
229 parsed['lastUpdated'] = data['lastUpdated']
230 if "patchSets" in data:
231 patch_sets = data['patchSets']
232 for patch_set in reversed(patch_sets):
233 if "approvals" in patch_set:
234 approvals = patch_set['approvals']
235 for approval in approvals:
236 if 'type' in approval and approval['type'] == 'SUBM':
237 parsed['grantedOn'] = approval['grantedOn']
239 if parsed['grantedOn']:
243 print("Failed to decode JSON: %s" % traceback.format_exc())
244 self.print_safe_encoding(line)
245 except Exception as err:
246 print("Exception: %s" % traceback.format_exc())
250 def extract_lines_from_json(self, changes):
252 Extract a list of lines from the JSON gerrit query response.
256 :param unicode changes: The full JSON gerrit query response
257 :return list: Lines of the JSON
260 for line in changes.split("\n"):
261 if line.find('stats') == -1:
263 if self.verbose >= 2:
264 print("get_gerrit_lines: found %d lines" % len(lines))
267 def get_gerrits(self, project, changeid=None, limit=1, msg=None):
269 Get a list of gerrits from gerrit query request.
271 Gerrit returns queries in order of lastUpdated so resort based on merge time.
272 Also because gerrit returns them in lastUpdated order, it means all gerrits
273 merged after the one we are using will be returned, so the query limit needs to be
274 high enough to capture those extra merges plus the limit requested.
275 TODO: possibly add the before query to set a start time for the query around the change
277 :param str project: The project to search
278 :param str or None changeid: A Change-Id to search
279 :param int limit: The number of items to return
280 :param str msg: A commit-msg to search
281 :return str: A gerrit query
283 query = self.make_gerrit_query(project, changeid, limit, msg)
284 changes = self.gerrit_request(query)
285 lines = self.extract_lines_from_json(changes)
288 gerrits.append(self.parse_gerrit(line))
290 from operator import itemgetter
291 sorted_gerrits = sorted(gerrits, key=itemgetter('grantedOn'), reverse=True)
292 return sorted_gerrits