13 1. What about the time between when a merge is submitted and
14 the patch is in the distribution? Should we look at the other
15 events and see when the merge job finished?
16 2. Use the git query option to request records in multiple queries
17 rather than grabbing all 50 in one shot. Keep going until the requested
18 number is found. Verify if this is better than just doing all 50 in one
19 shot since multiple requests are ssh round trips per request.
22 # This file started as an exact copy of git-review so including it"s copyright
25 Copyright (C) 2011-2017 OpenStack LLC.
27 Licensed under the Apache License, Version 2.0 (the "License");
28 you may not use this file except in compliance with the License.
29 You may obtain a copy of the License at
31 http://www.apache.org/licenses/LICENSE-2.0
33 Unless required by applicable law or agreed to in writing, software
34 distributed under the License is distributed on an "AS IS" BASIS,
35 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
38 See the License for the specific language governing permissions and
39 limitations under the License."""
42 class ChangeId(object):
43 def __init__(self, changeid, merged):
44 self.changeid = changeid
48 class Changes(object):
49 # NETVIRT_PROJECTS, as taken from autorelease dependency info [0]
50 # TODO: it would be nice to fetch the dependency info on the fly in case it changes down the road
51 # [0] https://logs.opendaylight.org/releng/jenkins092/autorelease-release-carbon/127/archives/dependencies.log.gz
52 NETVIRT_PROJECTS = ["netvirt", "controller", "dlux", "dluxapps", "genius", "infrautils", "mdsal", "netconf",
53 "neutron", "odlparent", "openflowplugin", "ovsdb", "sfc", "yangtools"]
54 PROJECT_NAMES = NETVIRT_PROJECTS
56 DISTRO_PATH = "/tmp/distribution-karaf"
58 REMOTE_URL = gerritquery.GerritQuery.REMOTE_URL
64 distro_path = DISTRO_PATH
65 distro_url = DISTRO_URL
66 project_names = PROJECT_NAMES
70 remote_url = REMOTE_URL
74 def __init__(self, branch=BRANCH, distro_path=DISTRO_PATH,
75 limit=LIMIT, qlimit=QUERY_LIMIT,
76 project_names=PROJECT_NAMES, remote_url=REMOTE_URL,
79 self.distro_path = distro_path
82 self.project_names = project_names
83 self.remote_url = remote_url
84 self.verbose = verbose
87 def epoch_to_utc(self, epoch):
88 utc = time.gmtime(epoch)
90 return time.strftime("%Y-%m-%d %H:%M:%S", utc)
92 def pretty_print_gerrits(self, project, gerrits):
95 print("i grantedOn lastUpdatd chang subject")
96 print("-- ------------------- ------------------- ----- -----------------------------------------")
98 print("gerrit is under review")
100 for i, gerrit in enumerate(gerrits):
101 if isinstance(gerrit, dict):
102 print("%02d %19s %19s %5s %s"
104 self.epoch_to_utc(gerrit["grantedOn"]) if "grantedOn" in gerrit else 0,
105 self.epoch_to_utc(gerrit["lastUpdated"]) if "lastUpdated" in gerrit else 0,
106 gerrit["number"] if "number" in gerrit else "00000",
107 gerrit["subject"].encode('ascii', 'replace') if "subject" in gerrit else "none"))
109 def pretty_print_projects(self, projects):
110 if isinstance(projects, dict):
111 for project_name, values in sorted(projects.items()):
112 if "includes" in values:
113 self.pretty_print_gerrits(project_name, values["includes"])
115 def set_projects(self, project_names=PROJECT_NAMES):
116 for project in project_names:
117 self.projects[project] = {"commit": [], "includes": []}
119 def download_distro(self):
121 Download the distribution from self.distro_url and extract it to self.distro_path
123 if self.verbose >= 2:
124 print("attempting to download distribution from %s and extract to %s " %
125 (self.distro_url, self.distro_path))
127 tmp_distro_zip = '/tmp/distro.zip'
128 tmp_unzipped_location = '/tmp/distro_unzipped'
129 downloader = urllib3.PoolManager(cert_reqs='CERT_NONE')
131 # disabling warnings to prevent scaring the user with InsecureRequestWarning
132 urllib3.disable_warnings()
134 downloaded_distro = downloader.request('GET', self.distro_url)
135 with open(tmp_distro_zip, 'wb') as f:
136 f.write(downloaded_distro.data)
138 downloaded_distro.release_conn()
140 # after the .zip is extracted we want to rename it to be the distro_path which may have
141 # been given by the user
142 distro_zip = zipfile.ZipFile(tmp_distro_zip, 'r')
143 distro_zip.extractall(tmp_unzipped_location)
144 unzipped_distro_folder = os.listdir(tmp_unzipped_location)
146 # if the distro_path already exists, we wont overwrite it and just continue hoping what's
147 # there is relevant (and maybe already put there by this tool earlier)
149 os.rename(tmp_unzipped_location + "/" + unzipped_distro_folder[0], self.distro_path)
152 print("Unable to move extracted files from %s to %s. Using whatever bits are already there" %
153 (tmp_unzipped_location, self.distro_path))
155 def get_includes(self, project, changeid=None, msg=None, merged=True):
157 Get the gerrits that would be included before the change merge time.
159 :param str project: The project to search
160 :param str or None changeid: The Change-Id of the gerrit to use for the merge time
161 :param str or None msg: The commit message of the gerrit to use for the merge time
162 :param bool merged: The requested gerrit was merged
163 :return list: includes[0] is the gerrit requested, [1 to limit] are the gerrits found.
166 includes = self.gerritquery.get_gerrits(project, changeid, 1, msg, status="merged")
168 includes = self.gerritquery.get_gerrits(project, changeid, 1, None, None, True)
170 print("Review %s in %s:%s was not found" % (changeid, project, self.gerritquery.branch))
173 gerrits = self.gerritquery.get_gerrits(project, changeid=None, limit=self.qlimit, msg=msg, status="merged")
174 for gerrit in gerrits:
175 # don"t include the same change in the list
176 if gerrit["id"] == changeid:
179 # TODO: should the check be < or <=?
180 if gerrit["grantedOn"] <= includes[0]["grantedOn"]:
181 includes.append(gerrit)
183 # break out if we have the number requested
184 if len(includes) == self.limit + 1:
187 if len(includes) != self.limit + 1:
188 print("%s query limit was not large enough to capture %d gerrits" % (project, self.limit))
193 def extract_gitproperties_file(fullpath):
195 Extract a git.properties from a jar archive.
197 :param str fullpath: Path to the jar
198 :return str: Containing git.properties or None if not found
200 if zipfile.is_zipfile(fullpath):
201 zf = zipfile.ZipFile(fullpath, "r")
203 pfile = zf.open("META-INF/git.properties")
204 return str(pfile.read())
209 def get_changeid_from_properties(self, project, pfile):
211 Parse the git.properties file to find a Change-Id.
213 There are a few different forms that we know of so far:
214 - I0123456789012345678901234567890123456789
216 - no Change-Id at all. There is a commit message and commit hash.
217 In this example the commit hash cannot be found because it was a merge
218 so you must use the message. Note spaces need to be replaced with 's.
219 - a patch that has not been merged. For these we look at the gerrit comment
220 for when the patch-test job starts.
222 :param str project: The project to search
223 :param str pfile: String containing the content of the git.properties file
224 :return ChangeId: The Change-Id with a valid Change-Id or None if not found
226 # match a 40 or 8 char Change-Id hash. both start with I
227 regex = re.compile(r'\bI([a-f0-9]{40})\b|\bI([a-f0-9]{8})\b')
228 changeid = regex.search(pfile)
230 if self.verbose >= 1:
231 print("trying Change-Id as merged in %s" % (project))
233 gerrits = self.gerritquery.get_gerrits(project, changeid.group(), 1, None, status="merged")
235 return ChangeId(changeid.group(), True)
237 # Maybe this is a patch that has not merged yet
238 if self.verbose >= 1:
239 print("did not find Change-Id as merged in %s, trying as unmerged" % project)
241 gerrits = self.gerritquery.get_gerrits(project, changeid.group(), 1, None, status=None, comments=True)
243 return ChangeId(gerrits[0]["id"], False)
245 # Didn't find a Change-Id so try to get a commit message
246 # match on "blah" but only keep the blah
247 regex_msg = re.compile(r'"([^"]*)"|^git.commit.message.short=(.*)$')
248 msg = regex_msg.search(pfile)
250 if self.verbose >= 1:
251 print("did not find Change-Id in %s, trying with commit-msg: %s" % (project, msg.group()))
253 gerrits = self.gerritquery.get_gerrits(project, None, 1, msg.group())
255 return ChangeId(gerrits[0]["id"], True)
257 msg_no_spaces = msg.group().replace(" ", "+")
258 if self.verbose >= 1:
259 print("did not find Change-Id in %s, trying with commit-msg (no spaces): %s" % (project, msg_no_spaces))
261 gerrits = self.gerritquery.get_gerrits(project, None, 1, msg_no_spaces)
263 return ChangeId(gerrits[0]["id"], True)
265 # Maybe one of the monster 'merge the world' gerrits
266 regex_msg = re.compile(r'git.commit.message.full=(.*)')
267 msg = regex_msg.search(pfile)
270 lines = str(msg.group()).split("\\n")
271 cli = next((i for i, line in enumerate(lines[:-1]) if '* changes\\:' in line), None)
272 first_msg = lines[cli+1] if cli else None
274 if self.verbose >= 1:
275 print("did not find Change-Id or commit-msg in %s, trying with merge commit-msg: %s"
276 % (project, first_msg))
277 gerrits = self.gerritquery.get_gerrits(project, None, 1, first_msg)
279 return ChangeId(gerrits[0]["id"], True)
281 print("did not find Change-Id for %s" % project)
283 return ChangeId(None, False)
285 def find_distro_changeid(self, project):
287 Find a distribution Change-Id by finding a project jar in
288 the distribution and parsing it's git.properties.
290 :param str project: The project to search
291 :return ChangeId: The Change-Id with a valid Change-Id or None if not found
293 project_dir = os.path.join(self.distro_path, "system", "org", "opendaylight", project)
295 for root, dirs, files in os.walk(project_dir):
297 if file_.endswith(".jar"):
298 fullpath = os.path.join(root, file_)
299 pfile = self.extract_gitproperties_file(fullpath)
301 changeid = self.get_changeid_from_properties(project, pfile)
302 if changeid.changeid:
305 print("Could not find %s Change-Id in git.properties" % project)
306 break # all jars will have the same git.properties
307 if pfile is not None:
308 break # all jars will have the same git.properties
310 print("Could not find a git.properties file for %s" % project)
311 return ChangeId(None, False)
314 self.gerritquery = gerritquery.GerritQuery(self.remote_url, self.branch, self.qlimit, self.verbose)
315 self.set_projects(self.project_names)
317 def print_options(self):
318 print("Using these options: branch: %s, limit: %d, qlimit: %d"
319 % (self.branch, self.limit, self.qlimit))
320 print("remote_url: %s" % self.remote_url)
321 print("distro_path: %s" % self.distro_path)
322 print("projects: %s" % (", ".join(map(str, self.projects))))
323 print("gerrit 00 is the most recent patch from which the project was built followed by the next most"
324 " recently merged patches up to %s." % self.limit)
328 Internal wrapper between main, options parser and internal code.
330 Get the gerrit for the given Change-Id and parse it.
331 Loop over all projects:
332 get qlimit gerrits and parse them
333 copy up to limit gerrits with a SUBM time (grantedOn) <= to the given change-id
335 # TODO: need method to validate the branch matches the distribution
340 if self.distro_url is not None:
341 self.download_distro()
343 for project in sorted(self.projects):
344 if self.verbose >= 1:
345 print("Processing %s" % project)
346 changeid = self.find_distro_changeid(project)
347 if changeid.changeid:
348 self.projects[project]['commit'] = changeid.changeid
349 self.projects[project]["includes"] =\
350 self.get_includes(project, changeid.changeid, msg=None, merged=changeid.merged)
354 parser = argparse.ArgumentParser(description=COPYRIGHT)
356 parser.add_argument("-b", "--branch", default=self.BRANCH,
357 help="git branch for patch under test")
358 parser.add_argument("-d", "--distro-path", dest="distro_path", default=self.DISTRO_PATH,
359 help="path to the expanded distribution, i.e. " + self.DISTRO_PATH)
360 parser.add_argument("-u", "--distro-url", dest="distro_url", default=self.DISTRO_URL,
361 help="optional url to download a distribution " + str(self.DISTRO_URL))
362 parser.add_argument("-l", "--limit", dest="limit", type=int, default=self.LIMIT,
363 help="number of gerrits to return")
364 parser.add_argument("-p", "--projects", dest="projects", default=self.PROJECT_NAMES,
365 help="list of projects to include in output")
366 parser.add_argument("-q", "--query-limit", dest="qlimit", type=int, default=self.QUERY_LIMIT,
367 help="number of gerrits to search")
368 parser.add_argument("-r", "--remote", dest="remote_url", default=self.REMOTE_URL,
369 help="git remote url to use for gerrit")
370 parser.add_argument("-v", "--verbose", dest="verbose", action="count", default=self.VERBOSE,
371 help="Output more information about what's going on")
372 parser.add_argument("--license", dest="license", action="store_true",
373 help="Print the license and exit")
374 parser.add_argument("-V", "--version", action="version",
375 version="%s version %s" %
376 (os.path.split(sys.argv[0])[-1], 0.1))
378 options = parser.parse_args()
384 self.branch = options.branch
385 self.distro_path = options.distro_path
386 self.distro_url = options.distro_url
387 self.limit = options.limit
388 self.qlimit = options.qlimit
389 self.remote_url = options.remote_url
390 self.verbose = options.verbose
391 if options.projects != self.PROJECT_NAMES:
392 self.project_names = options.projects.split(',')
394 # TODO: add check to verify that the remote can be reached,
395 # though the first gerrit query will fail anyways
397 projects = self.run_cmd()
398 self.pretty_print_projects(projects)
406 except Exception as e:
407 # If one does unguarded print(e) here, in certain locales the implicit
408 # str(e) blows up with familiar "UnicodeEncodeError ... ordinal not in
409 # range(128)". See rhbz#1058167.
413 # Python 3, we"re home free.
416 print(u.encode("utf-8"))
418 sys.exit(getattr(e, "EXIT_CODE", -1))
421 if __name__ == "__main__":