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 Changes(object):
43 # NETVIRT_PROJECTS, as taken from autorelease dependency info [0]
44 # TODO: it would be nice to fetch the dependency info on the fly in case it changes down the road
45 # [0] https://logs.opendaylight.org/releng/jenkins092/autorelease-release-carbon/127/archives/dependencies.log.gz
46 NETVIRT_PROJECTS = ["netvirt", "controller", "dlux", "dluxapps", "genius", "infrautils", "mdsal", "netconf",
47 "neutron", "odlparent", "openflowplugin", "ovsdb", "sfc", "yangtools"]
48 PROJECT_NAMES = NETVIRT_PROJECTS
50 DISTRO_PATH = "/tmp/distribution-karaf"
52 REMOTE_URL = gerritquery.GerritQuery.REMOTE_URL
58 distro_path = DISTRO_PATH
59 distro_url = DISTRO_URL
60 project_names = PROJECT_NAMES
64 remote_url = REMOTE_URL
68 def __init__(self, branch=BRANCH, distro_path=DISTRO_PATH,
69 limit=LIMIT, qlimit=QUERY_LIMIT,
70 project_names=PROJECT_NAMES, remote_url=REMOTE_URL,
73 self.distro_path = distro_path
76 self.project_names = project_names
77 self.remote_url = remote_url
78 self.verbose = verbose
81 def epoch_to_utc(self, epoch):
82 utc = time.gmtime(epoch)
84 return time.strftime("%Y-%m-%d %H:%M:%S", utc)
86 def pretty_print_gerrits(self, project, gerrits):
90 print("i grantedOn lastUpdatd chang subject")
91 print("-- ------------------- ------------------- ----- -----------------------------------------")
92 for i, gerrit in enumerate(gerrits):
93 if isinstance(gerrit, dict):
94 print("%02d %19s %19s %5s %s"
96 self.epoch_to_utc(gerrit["grantedOn"]) if "grantedOn" in gerrit else 0,
97 self.epoch_to_utc(gerrit["lastUpdated"]) if "lastUpdated" in gerrit else 0,
98 gerrit["number"] if "number" in gerrit else "00000",
99 gerrit["subject"] if "subject" in gerrit else "none"))
101 def pretty_print_projects(self, projects):
102 if isinstance(projects, dict):
103 for project_name, values in projects.items():
104 if "includes" in values:
105 self.pretty_print_gerrits(project_name, values["includes"])
107 def set_projects(self, project_names=PROJECT_NAMES):
108 for project in project_names:
109 self.projects[project] = {"commit": [], "includes": []}
111 def download_distro(self):
113 Download the distribution from self.distro_url and extract it to self.distro_path
115 if self.verbose >= 2:
116 print("attempting to download distribution from %s and extract to %s " %
117 (self.distro_url, self.distro_path))
119 tmp_distro_zip = '/tmp/distro.zip'
120 tmp_unzipped_location = '/tmp/distro_unzipped'
121 downloader = urllib3.PoolManager(cert_reqs='CERT_NONE')
123 # disabling warnings to prevent scaring the user with InsecureRequestWarning
124 urllib3.disable_warnings()
126 downloaded_distro = downloader.request('GET', self.distro_url)
127 with open(tmp_distro_zip, 'wb') as f:
128 f.write(downloaded_distro.data)
130 downloaded_distro.release_conn()
132 # after the .zip is extracted we want to rename it to be the distro_path which may have
133 # been given by the user
134 distro_zip = zipfile.ZipFile(tmp_distro_zip, 'r')
135 distro_zip.extractall(tmp_unzipped_location)
136 unzipped_distro_folder = os.listdir(tmp_unzipped_location)
138 # if the distro_path already exists, we wont overwrite it and just continue hoping what's
139 # there is relevant (and maybe already put there by this tool earlier)
141 os.rename(tmp_unzipped_location + "/" + unzipped_distro_folder[0], self.distro_path)
144 print("Unable to move extracted files from %s to %s. Using whatever bits are already there" %
145 (tmp_unzipped_location, self.distro_path))
147 def get_includes(self, project, changeid=None, msg=None):
149 Get the gerrits that would be included before the change merge time.
151 :param str project: The project to search
152 :param str changeid: The Change-Id of the gerrit to use for the merge time
153 :param str msg: The commit message of the gerrit to use for the merge time
154 :return list: includes[0] is the gerrit requested, [1 to limit] are the gerrits found.
156 includes = self.gerritquery.get_gerrits(project, changeid, 1, msg)
158 print("Review %s in %s:%s was not found" % (changeid, project, self.gerritquery.branch))
161 gerrits = self.gerritquery.get_gerrits(project, changeid=None, limit=self.qlimit, msg=msg)
162 for gerrit in gerrits:
163 # don"t include the same change in the list
164 if gerrit["id"] == changeid:
167 # TODO: should the check be < or <=?
168 if gerrit["grantedOn"] <= includes[0]["grantedOn"]:
169 includes.append(gerrit)
171 # break out if we have the number requested
172 if len(includes) == self.limit + 1:
175 if len(includes) != self.limit + 1:
176 print("%s query limit was not large enough to capture %d gerrits" % (project, self.limit))
181 def extract_gitproperties_file(fullpath):
183 Extract a git.properties from a jar archive.
185 :param str fullpath: Path to the jar
186 :return str: Containing git.properties or None if not found
188 if zipfile.is_zipfile(fullpath):
189 zf = zipfile.ZipFile(fullpath, "r")
191 pfile = zf.open("META-INF/git.properties")
192 return str(pfile.read())
197 def get_changeid_from_properties(self, project, pfile):
199 Parse the git.properties file to find a Change-Id.
201 There are a few different forms that we know of so far:
202 - I0123456789012345678901234567890123456789
204 - no Change-Id at all. There is a commit message and commit hash.
205 In this example the commit hash cannot be found because it was a merge
206 so you must use the message. Note spaces need to be replaced with +"s
208 :param str project: The project to search
209 :param str pfile: String containing the content of the git.properties file
210 :return str: The Change-Id or None if not found
212 # match a 40 or 8 char Change-Id hash. both start with I
213 regex = re.compile(r'\bI([a-f0-9]{40})\b|\bI([a-f0-9]{8})\b')
214 changeid = regex.search(pfile)
216 return changeid.group()
218 # Didn"t find a Change-Id so try to get a commit message
219 # match on "blah" but only keep the blah
220 regex_msg = re.compile(r'"([^"]*)"|^git.commit.message.short=(.*)$')
221 msg = regex_msg.search(pfile)
222 if self.verbose >= 2:
223 print("did not find Change-Id in %s, trying with commit-msg: %s" % (project, msg.group()))
226 # TODO: add new query using this msg
227 gerrits = self.gerritquery.get_gerrits(project, None, 1, msg.group())
229 return gerrits[0]["id"]
232 def find_distro_changeid(self, project):
234 Find a distribution Change-Id by finding a project jar in
235 the distribution and parsing it's git.properties.
237 :param str project: The project to search
238 :return str: The Change-Id or None if not found
240 project_dir = os.path.join(self.distro_path, "system", "org", "opendaylight", project)
242 for root, dirs, files in os.walk(project_dir):
244 if file_.endswith(".jar"):
245 fullpath = os.path.join(root, file_)
246 pfile = self.extract_gitproperties_file(fullpath)
248 changeid = self.get_changeid_from_properties(project, pfile)
252 print("Could not find %s Change-Id in git.properties" % project)
253 break # all jars will have the same git.properties
254 if pfile is not None:
255 break # all jars will have the same git.properties
259 self.gerritquery = gerritquery.GerritQuery(self.remote_url, self.branch, self.qlimit, self.verbose)
260 self.set_projects(self.project_names)
262 def print_options(self):
263 print("Using these options: branch: %s, limit: %d, qlimit: %d"
264 % (self.branch, self.limit, self.qlimit))
265 print("remote_url: %s" % self.remote_url)
266 print("distro_path: %s" % self.distro_path)
267 print("projects: %s" % (", ".join(map(str, self.projects))))
268 print("gerrit 00 is the most recent patch from which the project was built followed by the next most"
269 " recently merged patches up to %s." % self.limit)
273 Internal wrapper between main, options parser and internal code.
275 Get the gerrit for the given Change-Id and parse it.
276 Loop over all projects:
277 get qlimit gerrits and parse them
278 copy up to limit gerrits with a SUBM time (grantedOn) <= to the given change-id
280 # TODO: need method to validate the branch matches the distribution
285 if self.distro_url is not None:
286 self.download_distro()
288 for project in self.projects:
289 changeid = self.find_distro_changeid(project)
291 self.projects[project]['commit'] = changeid
292 self.projects[project]["includes"] = self.get_includes(project, changeid)
296 parser = argparse.ArgumentParser(description=COPYRIGHT)
298 parser.add_argument("-b", "--branch", default=self.BRANCH,
299 help="git branch for patch under test")
300 parser.add_argument("-d", "--distro-path", dest="distro_path", default=self.DISTRO_PATH,
301 help="path to the expanded distribution, i.e. " + self.DISTRO_PATH)
302 parser.add_argument("-u", "--distro-url", dest="distro_url", default=self.DISTRO_URL,
303 help="optional url to download a distribution " + str(self.DISTRO_URL))
304 parser.add_argument("-l", "--limit", dest="limit", type=int, default=self.LIMIT,
305 help="number of gerrits to return")
306 parser.add_argument("-p", "--projects", dest="projects", default=self.PROJECT_NAMES,
307 help="list of projects to include in output")
308 parser.add_argument("-q", "--query-limit", dest="qlimit", type=int, default=self.QUERY_LIMIT,
309 help="number of gerrits to search")
310 parser.add_argument("-r", "--remote", dest="remote_url", default=self.REMOTE_URL,
311 help="git remote url to use for gerrit")
312 parser.add_argument("-v", "--verbose", dest="verbose", action="count", default=self.VERBOSE,
313 help="Output more information about what's going on")
314 parser.add_argument("--license", dest="license", action="store_true",
315 help="Print the license and exit")
316 parser.add_argument("-V", "--version", action="version",
317 version="%s version %s" %
318 (os.path.split(sys.argv[0])[-1], 0.1))
320 options = parser.parse_args()
326 self.branch = options.branch
327 self.distro_path = options.distro_path
328 self.distro_url = options.distro_url
329 self.limit = options.limit
330 self.qlimit = options.qlimit
331 self.remote_url = options.remote_url
332 self.verbose = options.verbose
333 if options.projects != self.PROJECT_NAMES:
334 self.project_names = options.projects.split(',')
336 # TODO: add check to verify that the remote can be reached,
337 # though the first gerrit query will fail anyways
339 projects = self.run_cmd()
340 self.pretty_print_projects(projects)
348 except Exception as e:
349 # If one does unguarded print(e) here, in certain locales the implicit
350 # str(e) blows up with familiar "UnicodeEncodeError ... ordinal not in
351 # range(128)". See rhbz#1058167.
355 # Python 3, we"re home free.
358 print(u.encode("utf-8"))
360 sys.exit(getattr(e, "EXIT_CODE", -1))
363 if __name__ == "__main__":