blob: b8ecba5508c5d0bb52a055ba903fa0fb9c1842c9 [file] [log] [blame]
# Copyright 2017 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import urlparse
from recipe_engine import recipe_api
from RECIPE_MODULES.fuchsia.utils import cached_property
CIPD_PACKAGE = "infra/tools/luci/gerrit/${platform}"
CIPD_VERSION = "git_revision:f8d361aa92ab20d2175a34db28cff6e832bfb1fc"
class GerritApi(recipe_api.RecipeApi):
"""Module for querying a Gerrit host through the Gerrit API."""
def normalize_host(self, host):
"""Remove most elements if present, then add them back."""
host = host.rstrip("/")
if host.startswith("https://"):
host = host[len("https://") :]
if host.endswith(".googlesource.com"):
host = host[0 : -len(".googlesource.com")]
if host.endswith("-review"):
host = host[0 : -len("-review")]
return "{}-review.googlesource.com".format(host)
def __call__(
self,
name,
subcmd,
input_json,
host=None,
test_data=None,
max_attempts=1,
**kwargs
):
"""Invoke the gerrit command-line interface.
Args:
name (str): The name of the step.
subcmd (str): The subcommand to invoke.
input_json (json): Input to the subcommand.
host (str): The Gerrit host to make the query against.
If not set, inferred from Buildbucket input.
test_data (recipe_test_api.StepTestData): Test JSON output data for
this step.
max_attempts (int): The maximum number of times to attempt this
command.
"""
if not host:
assert self.host
host = self.host
host = self.normalize_host(host)
cmd = [
self._gerrit_path,
subcmd,
"-host",
"https://" + host,
"-input",
self.m.json.input(input_json),
"-output",
self.m.json.output(),
]
# If there's test data, create a factory for it.
step_test_data = None
if test_data is not None:
step_test_data = lambda: test_data
kwargs.setdefault("timeout", 10 * 60)
sleep = 5
for i in range(max_attempts):
try:
# Run the gerrit client command.
return self.m.step(name, cmd, step_test_data=step_test_data, **kwargs)
except self.m.step.StepFailure:
# Raise exception if not a transient failure or on the last attempt
step = self.m.step.active_result
# Most gerrit command failures result in an exit code of 1, but
# timeouts have an exit code of -15 because the recipe engine
# kills them with SIGTERM which is 15.
if step.retcode not in (1, -15) or i == max_attempts - 1:
raise
self.m.time.sleep(sleep)
sleep *= 2
finally:
self.m.step.active_result.presentation.logs[
"json.input"
] = self.m.json.dumps(input_json, indent=2).splitlines()
# If this call operates on a single change id, link to it.
if "change_id" in input_json:
self.m.step.active_result.presentation.links[
"gerrit link"
] = "https://{}/q/{}".format(host, input_json["change_id"])
def host_from_remote_url(self, remote):
"""Given a git remote URL, returns the Gerrit host."""
parsed_remote = urlparse.urlparse(remote)
host = parsed_remote.netloc
host_parts = host.split(".")
if parsed_remote.scheme == "sso" and len(host_parts) == 1:
host_parts.extend(["googlesource", "com"])
host_parts[0] += "-review"
return ".".join(host_parts)
@property
def _gerrit_path(self):
version = CIPD_VERSION
if self._test_data.enabled:
# Mock the version used by tests so that bumping the pinned version
# doesn't produce a massive expectation file diff.
version = "pinned-gerrit-version"
return self.m.cipd.ensure_tool(CIPD_PACKAGE, version)
@cached_property
def host(self):
bb_input = self.m.buildbucket.build.input
if bb_input.gerrit_changes:
return bb_input.gerrit_changes[0].host
elif bb_input.gitiles_commit.host:
gitiles_host = bb_input.gitiles_commit.host
return gitiles_host.replace(".", "-review.", 1)
return None # pragma: no cover
def abandon(self, name, change_id, message=None, notify=None, **kwargs):
"""Abandons a change.
Returns the details of the change, after attempting to abandon.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
message (str): A message explaining the reason for abandoning the
change.
notify (str): One of "NONE", "OWNER", "OWNER_REVIEWERS", "ALL"
specifying whom to send notifications to.
**kwargs: Passed through to __call__().
"""
input_json = {"change_id": str(change_id)}
if message:
input_json["input"] = {"message": message}
if notify:
assert notify in ("NONE", "OWNER", "OWNER_REVIEWERS", "ALL")
input_json["input"]["notify"] = notify
return self(name=name, subcmd="change-abandon", input_json=input_json, **kwargs)
def create_change(self, name, project, subject, branch, topic=None, **kwargs):
"""Creates a new change for a given project on the gerrit host.
Returns the details of the newly-created change.
Args:
name (str): The name of the step.
project (str): The name of the project on the host to create a change
for.
subject (str): The subject of the new change.
branch (str): The branch onto which the change will be made.
topic (str): A gerrit topic that can be used to atomically land the
change with other changes in the same topic.
**kwargs: Passed through to __call__().
"""
input_json = {
"input": {"project": project, "subject": subject, "branch": branch}
}
if topic:
input_json["input"]["topic"] = topic
return self(name=name, subcmd="change-create", input_json=input_json, **kwargs)
def set_review(
self,
name,
change_id,
labels=None,
message=None,
reviewers=None,
ccs=None,
revision="current",
notify=None,
**kwargs
):
"""Sets a change at a revision for review. Can optionally set labels, reviewers,
and CCs.
Returns updated labels, reviewers, and whether the change is ready for
review as a JSON dict.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
labels (dict): A map of labels (with names as strings, e.g.
'Code-Review') to the integral values you wish to set them to.
message (str): A message to be added as a review comment.
reviewers (list): A list of strings containing reviewer IDs (e.g.
email addresses).
ccs (list): A list of strings containing reviewer IDs (e.g. email
addresses).
revision (str): A revision ID that identifies a revision for the
change (default is 'current').
notify (str): One of "NONE", "OWNER", "OWNER_REVIEWERS", "ALL"
specifying whom to send notifications to.
**kwargs: Passed through to __call__().
"""
input_json = {
"change_id": str(change_id),
"revision_id": revision,
"input": {},
}
if labels:
input_json["input"]["labels"] = labels
if message:
input_json["input"]["message"] = message
if reviewers or ccs:
input_json["input"]["reviewers"] = []
if reviewers:
input_json["input"]["reviewers"] += [{"reviewer": i} for i in reviewers]
if ccs:
input_json["input"]["reviewers"] += [
{"reviewer": i, "state": "CC"} for i in ccs
]
if notify:
assert notify in ("NONE", "OWNER", "OWNER_REVIEWERS", "ALL")
input_json["input"]["notify"] = notify
return self(name=name, subcmd="set-review", input_json=input_json, **kwargs)
def submit(self, name, change_id, **kwargs):
"""Submit a change a repository.
Returns a JSON dict of details regarding the change.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
**kwargs: Passed through to __call__().
"""
return self(
name=name,
subcmd="submit",
input_json={"change_id": str(change_id)},
**kwargs
)
def rebase(self, name, change_id, **kwargs):
"""Rebase a change.
Returns a JSON dict of details regarding the change.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
**kwargs: Passed through to __call__().
"""
return self(
name=name,
subcmd="rebase",
input_json={"change_id": str(change_id)},
**kwargs
)
def change_details(self, name, change_id, query_params=None, **kwargs):
"""Returns a JSON dict of details regarding a specific change.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
query_params (list): A list of Gerrit REST query parameters (strings).
Documented at
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#query-options
**kwargs: Passed through to __call__().
"""
query_params = query_params or []
input_json = {"change_id": str(change_id)}
if query_params:
input_json["params"] = {"o": query_params}
return self(name=name, subcmd="change-detail", input_json=input_json, **kwargs)
def restore_change(self, name, change_id, message=None, **kwargs):
"""Restores a change.
Returns the details of the change, after attempting to restore.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
message (str): An optional message explaining the reason for restoring
the change.
**kwargs: Passed through to __call__().
"""
input_json = {"change_id": str(change_id)}
if message:
input_json["input"] = {"message": message}
return self(name=name, subcmd="restore", input_json=input_json, **kwargs)
def change_query(self, name, query_string, query_params=None, **kwargs):
"""Searches for changes.
Returns a list of matched changes (or None if no changes were matched).
See the Gerrit documentation for supported query params:
Args:
name (str): The name of the step.
query_string (str): The parameters to use for the search, e.g.
'status:open+-status:wip. See the Gerrit docs for supported parameters:
https://gerrit-review.googlesource.com/Documentation/user-search.html#_search_operators
query_params (list): A list of Gerrit REST query parameters (strings).
Documented at
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#query-options
**kwargs: Passed through to __call__().
Returns:
A `StepData` with the search results in its `json.output`.
"""
input_json = {"params": {"q": query_string}}
if query_params:
input_json["params"]["o"] = query_params
return self(name=name, subcmd="change-query", input_json=input_json, **kwargs)
def get_mergeable(self, name, change_id, revision_id="current", **kwargs):
"""Query whether a change is submittable.
Returns a JSON dict of details regarding the change.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
revision_id (str): An ID that uniquely identifies the patchset in
the change.
**kwargs: Passed through to __call__().
"""
return self(
name=name,
subcmd="get-mergeable",
input_json={"change_id": str(change_id), "revision_id": revision_id},
**kwargs
)
def changes_submitted_together(self, name, change_id, query_params=None, **kwargs):
"""Returns a JSON dict of changes that would be submitted with the
current change.
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
query_params (list): A list of Gerrit REST query parameters (strings).
Documented at
https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#query-options
**kwargs: Passed through to __call__().
"""
input_json = {"change_id": str(change_id)}
if query_params:
input_json["params"] = {"o": query_params}
return self(
name=name,
subcmd="changes-submitted-together",
input_json=input_json,
**kwargs
)
def list_change_comments(self, name, change_id, **kwargs):
"""List comments on a change.
Returns a list of comments on the given change.
See the Gerrit documentation for supported query params:
Args:
name (str): The name of the step.
change_id (str): A change ID that uniquely defines a change on the
host.
**kwargs: Passed through to __call__().
Returns:
A `StepData` with the results in its `json.output`.
"""
input_json = {"change_id": str(change_id)}
return self(
name=name, subcmd="list-change-comments", input_json=input_json, **kwargs
)
def create_branch(self, name, project, ref, revision, **kwargs):
"""Create a branch.
Args:
name (str): The name of the step.
project (str): The name of the project.
ref (str): The name of the branch to create.
revision (str): The revision that the branch should point to.
"""
input_json = {
"project_id": project,
"input": {"ref": ref, "revision": revision},
}
return self(name=name, subcmd="create-branch", input_json=input_json, **kwargs)
def account_query(self, name, query_string, query_params=None, **kwargs):
"""Search for accounts.
Returns a list of matching accounts.
See the Gerrit documentation for supported query params:
Args:
name (str): The name of the step.
query_string (str): The parameters to use for the search, e.g.
'email:nobody@example.com'. See the Gerrit docs for supported parameters:
https://gerrit-review.googlesource.com/Documentation/user-search-accounts.html
query_params (list): A list of Gerrit REST query parameters (strings).
Documented at
https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html
**kwargs: Passed through to __call__().
Returns:
A `StepData` with the results in its `json.output`.
"""
input_json = {"params": {"q": query_string}}
if query_params:
input_json["params"]["o"] = query_params
return self(name=name, subcmd="account-query", input_json=input_json, **kwargs)