14 1. What about the time between when a merge is submitted and
15 the patch is in the distribution? Should we look at the other
16 events and see when the merge job finished?
17 2. Use the git query option to request records in multiple queries
18 rather than grabbing all 50 in one shot. Keep going until the requested
19 number is found. Verify if this is better than just doing all 50 in one
20 shot since multiple requests are ssh round trips per request.
23 # This file started as an exact copy of git-review so including it"s copyright
26 Copyright (C) 2011-2017 OpenStack LLC.
28 Licensed under the Apache License, Version 2.0 (the "License");
29 you may not use this file except in compliance with the License.
30 You may obtain a copy of the License at
32 http://www.apache.org/licenses/LICENSE-2.0
34 Unless required by applicable law or agreed to in writing, software
35 distributed under the License is distributed on an "AS IS" BASIS,
36 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
39 See the License for the specific language governing permissions and
40 limitations under the License."""
43 logger = logging.getLogger("changes")
44 logger.setLevel(logging.DEBUG)
45 formatter = logging.Formatter('%(asctime)s - %(levelname).4s - %(name)s - %(lineno)04d - %(message)s')
46 ch = logging.StreamHandler()
47 ch.setLevel(logging.INFO)
48 ch.setFormatter(formatter)
50 fh = logging.FileHandler("/tmp/changes.txt", "w")
51 fh.setLevel(logging.DEBUG)
52 fh.setFormatter(formatter)
56 class ChangeId(object):
57 def __init__(self, changeid, merged):
58 self.changeid = changeid
62 class Changes(object):
63 # NETVIRT_PROJECTS, as taken from autorelease dependency info [0]
64 # TODO: it would be nice to fetch the dependency info on the fly in case it changes down the road
65 # [0] https://logs.opendaylight.org/releng/jenkins092/autorelease-release-carbon/127/archives/dependencies.log.gz
66 NETVIRT_PROJECTS = ["netvirt", "controller", "dlux", "dluxapps", "genius", "infrautils", "mdsal", "netconf",
67 "neutron", "odlparent", "openflowplugin", "ovsdb", "sfc", "yangtools"]
68 PROJECT_NAMES = NETVIRT_PROJECTS
69 VERBOSE = logging.INFO
70 DISTRO_PATH = "/tmp/distribution-karaf"
72 REMOTE_URL = gerritquery.GerritQuery.REMOTE_URL
78 distro_path = DISTRO_PATH
79 distro_url = DISTRO_URL
80 project_names = PROJECT_NAMES
84 remote_url = REMOTE_URL
91 def __init__(self, branch=BRANCH, distro_path=DISTRO_PATH,
92 limit=LIMIT, qlimit=QUERY_LIMIT,
93 project_names=PROJECT_NAMES, remote_url=REMOTE_URL,
96 self.distro_path = distro_path
99 self.project_names = project_names
100 self.remote_url = remote_url
101 self.verbose = verbose
103 self.set_log_level(verbose)
104 self.regex_changeid = re.compile(r'\bI([a-f0-9]{40})\b|\bI([a-f0-9]{8})\b')
105 # self.regex_shortmsg = re.compile(r'"([^"]*)"|(git.commit.message.short=(.*))')
106 self.regex_shortmsg1 = re.compile(r'(git.commit.message.short=.*"([^"]*)")')
107 self.regex_shortmsg2 = re.compile(r'(git.commit.message.short=(.*))')
108 self.regex_longmsg = re.compile(r'git.commit.message.full=(.*)')
109 self.regex_commitid = re.compile(r'(git.commit.id=(.*))')
112 def set_log_level(level):
115 def epoch_to_utc(self, epoch):
116 utc = time.gmtime(epoch)
118 return time.strftime("%Y-%m-%d %H:%M:%S", utc)
120 def pretty_print_gerrits(self, project, gerrits):
122 print("%s" % project)
123 print("i grantedOn lastUpdatd chang subject")
124 print("-- ------------------- ------------------- ----- -----------------------------------------")
126 print("gerrit is under review")
128 for i, gerrit in enumerate(gerrits):
129 if isinstance(gerrit, dict):
130 print("%02d %19s %19s %5s %s"
132 self.epoch_to_utc(gerrit["grantedOn"]) if "grantedOn" in gerrit else 0,
133 self.epoch_to_utc(gerrit["lastUpdated"]) if "lastUpdated" in gerrit else 0,
134 gerrit["number"] if "number" in gerrit else "00000",
135 gerrit["subject"].encode('ascii', 'replace') if "subject" in gerrit else "none"))
137 def pretty_print_projects(self, projects):
138 if isinstance(projects, dict):
139 for project_name, values in sorted(projects.items()):
140 if "includes" in values:
141 self.pretty_print_gerrits(project_name, values["includes"])
143 def set_projects(self, project_names=PROJECT_NAMES):
144 for project in project_names:
145 self.projects[project] = {"commit": [], "includes": []}
147 def download_distro(self):
149 Download the distribution from self.distro_url and extract it to self.distro_path
151 logger.info("attempting to download distribution from %s and extract to %s", self.distro_url, self.distro_path)
153 tmp_distro_zip = '/tmp/distro.zip'
154 tmp_unzipped_location = '/tmp/distro_unzipped'
155 downloader = urllib3.PoolManager(cert_reqs='CERT_NONE')
157 # disabling warnings to prevent scaring the user with InsecureRequestWarning
158 urllib3.disable_warnings()
160 downloaded_distro = downloader.request('GET', self.distro_url)
161 with open(tmp_distro_zip, 'wb') as f:
162 f.write(downloaded_distro.data)
164 downloaded_distro.release_conn()
166 # after the .zip is extracted we want to rename it to be the distro_path which may have
167 # been given by the user
168 distro_zip = zipfile.ZipFile(tmp_distro_zip, 'r')
169 distro_zip.extractall(tmp_unzipped_location)
170 unzipped_distro_folder = os.listdir(tmp_unzipped_location)
172 # if the distro_path already exists, we wont overwrite it and just continue hoping what's
173 # there is relevant (and maybe already put there by this tool earlier)
175 os.rename(tmp_unzipped_location + "/" + unzipped_distro_folder[0], self.distro_path)
178 logger.warn("Unable to move extracted files from %s to %s. Using whatever bits are already there",
179 tmp_unzipped_location, self.distro_path)
181 def get_includes(self, project, changeid=None, msg=None, merged=True):
183 Get the gerrits that would be included before the change merge time.
185 :param str project: The project to search
186 :param str or None changeid: The Change-Id of the gerrit to use for the merge time
187 :param str or None msg: The commit message of the gerrit to use for the merge time
188 :param bool merged: The requested gerrit was merged
189 :return list: includes[0] is the gerrit requested, [1 to limit] are the gerrits found.
192 includes = self.gerritquery.get_gerrits(project, changeid, 1, msg, status="merged")
194 includes = self.gerritquery.get_gerrits(project, changeid, 1, None, None, True)
196 logger.info("Review %s in %s:%s was not found", changeid, project, self.gerritquery.branch)
199 gerrits = self.gerritquery.get_gerrits(project, changeid=None, limit=self.qlimit, msg=msg, status="merged")
200 for gerrit in gerrits:
201 # don"t include the same change in the list
202 if gerrit["id"] == changeid:
205 # TODO: should the check be < or <=?
206 if gerrit["grantedOn"] <= includes[0]["grantedOn"]:
207 includes.append(gerrit)
209 # break out if we have the number requested
210 if len(includes) == self.limit + 1:
213 if len(includes) != self.limit + 1:
214 logger.info("%s query limit was not large enough to capture %d gerrits", project, self.limit)
219 def extract_gitproperties_file(fullpath):
221 Extract a git.properties from a jar archive.
223 :param str fullpath: Path to the jar
224 :return str: Containing git.properties or None if not found
226 if zipfile.is_zipfile(fullpath):
227 zf = zipfile.ZipFile(fullpath, "r")
229 pfile = zf.open("META-INF/git.properties")
230 return str(pfile.read())
235 def get_changeid_from_properties(self, project, pfile):
237 Parse the git.properties file to find a Change-Id.
239 There are a few different forms that we know of so far:
240 - I0123456789012345678901234567890123456789
242 - no Change-Id at all. There is a commit message and commit hash.
243 In this example the commit hash cannot be found because it was a merge
244 so you must use the message. Note spaces need to be replaced with 's.
245 - a patch that has not been merged. For these we look at the gerrit comment
246 for when the patch-test job starts.
248 :param str project: The project to search
249 :param str pfile: String containing the content of the git.properties file
250 :return ChangeId: The Change-Id with a valid Change-Id or None if not found
252 logger.info("trying Change-Id from git.properties in %s", project)
253 # match a 40 or 8 char Change-Id hash. both start with I
254 changeid = self.regex_changeid.search(pfile)
256 logger.info("trying Change-Id from git.properties as merged in %s: %s", project, changeid.group())
258 gerrits = self.gerritquery.get_gerrits(project, changeid.group(), 1, None, status="merged")
260 logger.info("found Change-Id from git.properties as merged in %s", project)
261 return ChangeId(changeid.group(), True)
263 # Maybe this is a patch that has not merged yet
264 logger.info("did not find Change-Id from git.properties as merged in %s, trying as unmerged: %s",
265 project, changeid.group())
267 gerrits = self.gerritquery.get_gerrits(project, changeid.group(), 1, None, status=None, comments=True)
269 logger.info("found Change-Id from git.properties as unmerged in %s", project)
270 return ChangeId(gerrits[0]["id"], False)
272 logger.info("did not find Change-Id from git.properties in %s, trying commitid", project)
274 # match a 40 or 8 char Change-Id hash. both start with I
275 commitid = self.regex_commitid.search(pfile)
276 if commitid and commitid.group(2):
277 logger.info("trying commitid from git.properties in %s: %s", project, commitid.group(2))
279 gerrits = self.gerritquery.get_gerrits(project, commitid=commitid.group(2))
281 logger.info("found Change-Id from git.properties as unmerged in %s", project)
282 return ChangeId(gerrits[0]["id"], True)
284 logger.info("did not find Change-Id from commitid from git.properties in %s, trying short commit message1",
287 # Didn't find a Change-Id so try to get a commit message
288 # match on "blah" but only keep the blah
289 msg = self.regex_shortmsg1.search(pfile)
290 if msg and msg.group(2):
291 # logger.info("msg.groups 0: %s, 1: %s, 2: %s", msg.group(), msg.group(1), msg.group(2))
292 logger.info("trying with short commit-msg 1 from git.properties in %s: %s", project, msg.group(2))
294 gerrits = self.gerritquery.get_gerrits(project, msg=msg.group(2))
296 logger.info("found Change-Id from git.properties short commit-msg 1 in %s", project)
297 return ChangeId(gerrits[0]["id"], True)
299 msg_no_spaces = msg.group(2).replace(" ", "+")
300 logger.info("did not find Change-Id in %s, trying with commit-msg 1 (no spaces): %s",
301 project, msg_no_spaces)
303 gerrits = self.gerritquery.get_gerrits(project, msg=msg_no_spaces)
305 logger.info("found Change-Id from git.properties short commit-msg 1 (no spaces) in %s", project)
306 return ChangeId(gerrits[0]["id"], True)
308 logger.info("did not find Change-Id from short commit message1 from git.properties in %s", project)
310 # Didn't find a Change-Id so try to get a commit message
311 # match on "blah" but only keep the blah
312 msg = self.regex_shortmsg2.search(pfile)
313 if msg and msg.group(2):
314 logger.info("trying with short commit-msg 2 from git.properties in %s: %s", project, msg.group(2))
316 gerrits = self.gerritquery.get_gerrits(project, msg=msg.group(2))
318 logger.info("found Change-Id from git.properties short commit-msg 2 in %s", project)
319 return ChangeId(gerrits[0]["id"], True)
321 msg_no_spaces = msg.group(2).replace(" ", "+")
322 logger.info("did not find Change-Id in %s, trying with commit-msg 2 (no spaces): %s",
323 project, msg_no_spaces)
325 gerrits = self.gerritquery.get_gerrits(project, msg=msg_no_spaces)
327 logger.info("found Change-Id from git.properties short commit-msg 2 (no spaces) in %s", project)
328 return ChangeId(gerrits[0]["id"], True)
330 logger.info("did not find Change-Id from short commit message2 from git.properties in %s", project)
332 # Maybe one of the monster 'merge the world' gerrits
333 msg = self.regex_longmsg.search(pfile)
336 lines = str(msg.group()).split("\\n")
337 cli = next((i for i, line in enumerate(lines[:-1]) if '* changes\\:' in line), None)
338 first_msg = lines[cli + 1] if cli else None
340 logger.info("did not find Change-Id or short commit-msg in %s, trying with merge commit-msg: %s",
342 gerrits = self.gerritquery.get_gerrits(project, None, 1, first_msg)
344 logger.info("found Change-Id from git.properties merge commit-msg in %s", project)
345 return ChangeId(gerrits[0]["id"], True)
347 logger.warn("did not find Change-Id for %s" % project)
349 return ChangeId(None, False)
351 def find_distro_changeid(self, project):
353 Find a distribution Change-Id by finding a project jar in
354 the distribution and parsing it's git.properties.
356 :param str project: The project to search
357 :return ChangeId: The Change-Id with a valid Change-Id or None if not found
359 project_dir = os.path.join(self.distro_path, "system", "org", "opendaylight", project)
361 for root, dirs, files in os.walk(project_dir):
363 if file_.endswith(".jar"):
364 fullpath = os.path.join(root, file_)
365 pfile = self.extract_gitproperties_file(fullpath)
367 changeid = self.get_changeid_from_properties(project, pfile)
368 if changeid.changeid:
371 logger.warn("Could not find %s Change-Id in git.properties", project)
372 break # all jars will have the same git.properties
373 if pfile is not None:
374 break # all jars will have the same git.properties
376 logger.warn("Could not find a git.properties file for %s", project)
377 return ChangeId(None, False)
380 self.gerritquery = gerritquery.GerritQuery(self.remote_url, self.branch, self.qlimit, self.verbose)
381 self.set_projects(self.project_names)
383 def print_options(self):
384 print("Using these options: branch: %s, limit: %d, qlimit: %d"
385 % (self.branch, self.limit, self.qlimit))
386 print("remote_url: %s" % self.remote_url)
387 print("distro_path: %s" % self.distro_path)
388 print("projects: %s" % (", ".join(map(str, self.projects))))
389 print("gerrit 00 is the most recent patch from which the project was built followed by the next most"
390 " recently merged patches up to %s." % self.limit)
394 Internal wrapper between main, options parser and internal code.
396 Get the gerrit for the given Change-Id and parse it.
397 Loop over all projects:
398 get qlimit gerrits and parse them
399 copy up to limit gerrits with a SUBM time (grantedOn) <= to the given change-id
401 # TODO: need method to validate the branch matches the distribution
406 if self.distro_url is not None:
407 self.download_distro()
409 for project in sorted(self.projects):
410 logger.info("Processing %s", project)
411 changeid = self.find_distro_changeid(project)
412 if changeid.changeid:
413 self.projects[project]['commit'] = changeid.changeid
414 self.projects[project]["includes"] =\
415 self.get_includes(project, changeid.changeid, msg=None, merged=changeid.merged)
419 parser = argparse.ArgumentParser(description=COPYRIGHT)
421 parser.add_argument("-b", "--branch", default=self.BRANCH,
422 help="git branch for patch under test")
423 parser.add_argument("-d", "--distro-path", dest="distro_path", default=self.DISTRO_PATH,
424 help="path to the expanded distribution, i.e. " + self.DISTRO_PATH)
425 parser.add_argument("-u", "--distro-url", dest="distro_url", default=self.DISTRO_URL,
426 help="optional url to download a distribution " + str(self.DISTRO_URL))
427 parser.add_argument("-l", "--limit", dest="limit", type=int, default=self.LIMIT,
428 help="number of gerrits to return")
429 parser.add_argument("-p", "--projects", dest="projects", default=self.PROJECT_NAMES,
430 help="list of projects to include in output")
431 parser.add_argument("-q", "--query-limit", dest="qlimit", type=int, default=self.QUERY_LIMIT,
432 help="number of gerrits to search")
433 parser.add_argument("-r", "--remote", dest="remote_url", default=self.REMOTE_URL,
434 help="git remote url to use for gerrit")
435 parser.add_argument("-v", "--verbose", dest="verbose", action="count", default=self.VERBOSE,
436 help="Output more information about what's going on")
437 parser.add_argument("--license", dest="license", action="store_true",
438 help="Print the license and exit")
439 parser.add_argument("-V", "--version", action="version",
440 version="%s version %s" %
441 (os.path.split(sys.argv[0])[-1], 0.1))
443 options = parser.parse_args()
449 self.branch = options.branch
450 self.distro_path = options.distro_path
451 self.distro_url = options.distro_url
452 self.limit = options.limit
453 self.qlimit = options.qlimit
454 self.remote_url = options.remote_url
455 self.verbose = options.verbose
456 if options.projects != self.PROJECT_NAMES:
457 self.project_names = options.projects.split(',')
459 # TODO: add check to verify that the remote can be reached,
460 # though the first gerrit query will fail anyways
462 projects = self.run_cmd()
463 self.pretty_print_projects(projects)
471 except Exception as e:
472 # If one does unguarded print(e) here, in certain locales the implicit
473 # str(e) blows up with familiar "UnicodeEncodeError ... ordinal not in
474 # range(128)". See rhbz#1058167.
478 # Python 3, we"re home free.
481 logger.warn(u.encode("utf-8"))
483 sys.exit(getattr(e, "EXIT_CODE", -1))
486 if __name__ == "__main__":