| # 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() |
| ] |
| allow_existent_branch = kwargs.pop('allow_existent_branch', False) |
| if allow_existent_branch: |
| args.append('--allow-existent-branch') |
| 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 |