# Copyright 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from recipe_engine import recipe_api class GerritApi(recipe_api.RecipeApi): """Module for interact with Gerrit endpoints""" def __init__(self, *args, **kwargs): super(GerritApi, self).__init__(*args, **kwargs) self._changes_target_branch_cache = {} def __call__(self, name, cmd, infra_step=True, **kwargs): """Wrapper for easy calling of gerrit_utils steps.""" assert isinstance(cmd, (list, tuple)) prefix = 'gerrit ' env = self.m.context.env env.setdefault('PATH', '%(PATH)s') env['PATH'] = self.m.path.pathsep.join([ env['PATH'], str(self.repo_resource())]) with self.m.context(env=env): return self.m.step( prefix + name, ['vpython3', self.repo_resource('gerrit_client.py')] + cmd, infra_step=infra_step, **kwargs) def call_raw_api(self, host, path, method=None, body=None, accept_statuses=None, name=None, **kwargs): """Call an arbitrary Gerrit API that returns a JSON response. Returns: The JSON response data. """ args = [ 'rawapi', '--host', host, '--path', path, '--json_file', self.m.json.output() ] if method: args.extend(['--method', method]) if body: args.extend(['--body', self.m.json.dumps(body)]) if accept_statuses: args.extend( ['--accept_status', ','.join(str(i) for i in accept_statuses)]) step_name = name or 'call_raw_api (%s)' % path step_result = self(step_name, args, **kwargs) return step_result.json.output def create_gerrit_branch(self, host, project, branch, commit, **kwargs): """Creates a new branch from given project and commit Returns: The ref of the branch created """ args = [ 'branch', '--host', host, '--project', project, '--branch', branch, '--commit', commit, '--json_file', self.m.json.output() ] step_name = 'create_gerrit_branch (%s %s)' % (project, branch) step_result = self(step_name, args, **kwargs) ref = step_result.json.output.get('ref') return ref def create_gerrit_tag(self, host, project, tag, commit, **kwargs): """Creates a new tag at the given commit. Returns: The ref of the tag created. """ args = [ 'tag', '--host', host, '--project', project, '--tag', tag, '--commit', commit, '--json_file', self.m.json.output() ] step_name = 'create_gerrit_tag (%s %s)' % (project, tag) step_result = self(step_name, args, **kwargs) ref = step_result.json.output.get('ref') return ref # TODO(machenbach): Rename to get_revision? And maybe above to # create_ref? def get_gerrit_branch(self, host, project, branch, **kwargs): """Gets a branch from given project and commit Returns: The revision of the branch """ args = [ 'branchinfo', '--host', host, '--project', project, '--branch', branch, '--json_file', self.m.json.output() ] step_name = 'get_gerrit_branch (%s %s)' % (project, branch) step_result = self(step_name, args, **kwargs) revision = step_result.json.output.get('revision') return revision def get_change_description(self, host, change, patchset, timeout=None, step_test_data=None): """Gets the description for a given CL and patchset. Args: host: URL of Gerrit host to query. change: The change number. patchset: The patchset number. Returns: The description corresponding to given CL and patchset. """ ri = self.get_revision_info(host, change, patchset, timeout, step_test_data) return ri['commit']['message'] def get_revision_info(self, host, change, patchset, timeout=None, step_test_data=None): """ Returns the info for a given patchset of a given change. Args: host: Gerrit host to query. change: The change number. patchset: The patchset number. Returns: A dict for the target revision as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes """ assert int(change), change assert int(patchset), patchset step_test_data = step_test_data or ( lambda: self.test_api.get_one_change_response_data(change_number=change, patchset=patchset)) cls = self.get_changes(host, query_params=[('change', str(change))], o_params=['ALL_REVISIONS', 'ALL_COMMITS'], limit=1, timeout=timeout, step_test_data=step_test_data) cl = cls[0] if len(cls) == 1 else {'revisions': {}} for ri in cl['revisions'].values(): # TODO(tandrii): add support for patchset=='current'. if str(ri['_number']) == str(patchset): return ri raise self.m.step.InfraFailure( 'Error querying for CL description: host:%r change:%r; patchset:%r' % ( host, change, patchset)) def get_changes(self, host, query_params, start=None, limit=None, o_params=None, step_test_data=None, **kwargs): """Queries changes for the given host. Args: * host: URL of Gerrit host to query. * query_params: Query parameters as list of (key, value) tuples to form a query as documented here: https://gerrit-review.googlesource.com/Documentation/user-search.html#search-operators * start: How many changes to skip (starting with the most recent). * limit: Maximum number of results to return. * o_params: A list of additional output specifiers, as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes * step_test_data: Optional mock test data for the underlying gerrit client. Returns: A list of change dicts as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#list-changes """ args = [ 'changes', '--verbose', '--host', host, '--json_file', self.m.json.output() ] if start: args += ['--start', str(start)] if limit: args += ['--limit', str(limit)] for k, v in query_params: args += ['-p', '%s=%s' % (k, v)] for v in (o_params or []): args += ['-o', v] if not step_test_data: step_test_data = lambda: self.test_api.get_one_change_response_data() return self( kwargs.pop('name', 'changes'), args, step_test_data=step_test_data, **kwargs ).json.output def get_related_changes(self, host, change, revision='current', step_test_data=None): """Queries related changes for a given host, change, and revision. Args: * host: URL of Gerrit host to query. * change: The change-id of the change to get related changes for as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-id * revision: The revision-id of the revision to get related changes for as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#revision-id This defaults to current, which names the most recent patch set. * step_test_data: Optional mock test data for the underlying gerrit client. Returns: A related changes dictionary as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#related-changes-info """ args = [ 'relatedchanges', '--host', host, '--change', change, '--revision', revision, '--json_file', self.m.json.output(), ] if not step_test_data: step_test_data = lambda: self.test_api.get_related_changes_response_data() return self('relatedchanges', args, step_test_data=step_test_data).json.output def abandon_change(self, host, change, message=None, name=None, step_test_data=None): args = [ 'abandon', '--host', host, '--change', int(change), '--json_file', self.m.json.output(), ] if message: args.extend(['--message', message]) if not step_test_data: step_test_data = lambda: self.test_api.get_one_change_response_data( status='ABANDONED', _number=str(change)) return self( name or 'abandon', args, step_test_data=step_test_data, ).json.output def set_change_label(self, host, change, label_name, label_value, name=None, step_test_data=None): args = [ 'setlabel', '--host', host, '--change', int(change), '--json_file', self.m.json.output(), '-l', label_name, label_value ] return self( name or 'setlabel', args, step_test_data=step_test_data, ).json.output def move_changes(self, host, project, from_branch, to_branch, step_test_data=None): args = [ 'movechanges', '--host', host, '-p', 'project=%s' % project, '-p', 'branch=%s' % from_branch, '-p', 'status=open', '--destination_branch', to_branch, '--json_file', self.m.json.output() ] if not step_test_data: step_test_data = lambda: self.test_api.get_one_change_response_data( branch=to_branch) return self( 'move changes', args, step_test_data=step_test_data, ).json.output def update_files(self, host, project, branch, new_contents_by_file_path, commit_msg, params=frozenset(['status=NEW']), cc_list=frozenset([]), submit=False, submit_later=False, step_test_data_create_change=None, step_test_data_submit_change=None): """Update a set of files by creating and submitting a Gerrit CL. Args: * host: URL of Gerrit host to name. * project: Gerrit project name, e.g. chromium/src. * branch: The branch to land the change, e.g. main * new_contents_by_file_path: Dict of the new contents with file path as the key. * commit_msg: Description to add to the CL. * params: A list of additional ChangeInput specifiers, with format 'key=value'. * cc_list: A list of addresses to notify. * submit: Should land this CL instantly. * submit_later: If this change has related CLs, we may want to commit them in a chain. So only set Bot-Commit+1, making it ready for submit together. Ignored if submit is True. * step_test_data_create_change: Optional mock test data for the step create gerrit change. * step_test_data_submit_change: Optional mock test data for the step submit gerrit change. Returns: A ChangeInfo dictionary as documented here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#create-change Or if the change is submitted, here: https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#submit-change """ assert len(new_contents_by_file_path ) > 0, 'The dict of file paths should not be empty.' command = [ 'createchange', '--host', host, '--project', project, '--branch', branch, '--subject', commit_msg, '--json_file', self.m.json.output(), ] for p in params: command.extend(['-p', p]) for cc in cc_list: command.extend(['--cc', cc]) step_test_data = step_test_data_create_change or ( lambda: self.test_api.update_files_response_data()) step_result = self('create change at (%s %s)' % (project, branch), command, step_test_data=step_test_data) change = int(step_result.json.output.get('_number')) step_result.presentation.links['change %d' % change] = '%s/#/q/%d' % (host, change) with self.m.step.nest('update contents in CL %d' % change): for path, content in new_contents_by_file_path.items(): _file = self.m.path.mkstemp() self.m.file.write_raw('store the new content for %s' % path, _file, content) self('edit file %s' % path, [ 'changeedit', '--host', host, '--change', change, '--path', path, '--file', _file, ]) self('publish edit', [ 'publishchangeedit', '--host', host, '--change', change, ]) # Make sure the new patchset is propagated to Gerrit backend. with self.m.step.nest('verify the patchset exists on CL %d' % change): retries = 0 max_retries = 2 while retries <= max_retries: try: if self.get_revision_info(host, change, 2): break except self.m.step.InfraFailure: if retries == max_retries: # pragma: no cover raise retries += 1 with self.m.step.nest('waiting before retry'): self.m.time.sleep((2**retries) * 10) if submit or submit_later: self('set Bot-Commit+1 for change %d' % change, [ 'setbotcommit', '--host', host, '--change', change, ]) if submit: submit_cmd = [ 'submitchange', '--host', host, '--change', change, '--json_file', self.m.json.output(), ] step_test_data = step_test_data_submit_change or ( lambda: self.test_api.update_files_response_data(status='MERGED')) step_result = self('submit change %d' % change, submit_cmd, step_test_data=step_test_data) return step_result.json.output