blob: d0415e7c459cd553a3e4eb9b17a7aa2b001a9178 [file] [log] [blame]
# Copyright 2017 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Functions for forgiving false CQ rejections."""
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
import itertools
from chromite.lib import cros_logging as logging
from chromite.lib import gerrit
from exonerator import constants
COMMIT_QUEUE_LABEL = 'Commit-Queue'
CODE_REVIEW_LABEL = 'Code-Review'
# TODO(phobbs) is there a way to make this more robust than just checking a
# substring? Maybe a shared constant with chromite?
_FAILED_TO_APPLY_SNIPPET = 'failed to apply your change'
_VERIFIED_LABEL = 'Verified'
_EXONERATOR_CQ_MESSAGE = """\
This CL has been marked innocent in the CL annotator; re-marking as CQ+1.\
See the annotator entry here:
https://chromiumos-build-annotator.googleplex.com/build_annotations/\
edit_annotations/master-paladin/%s"""
# TODO(phobbs): Make the reason dependent on how the CL was exonerated - maybe
# use the message_subtype of the annotations_finalized buildMessage?
def MaybeExonerate(change, build_id):
"""Mark a CL as CQ+1 only if it hasn't already been.
Args:
change: A GerritPatchTuple.
build_id: The CIDB id for the build which the change was kicked out of.
"""
helper = GetHelper(internal=change.internal)
details = helper.GetChangeDetail(change.gerrit_number)
if not details:
logging.warning('Change details was None for %s', str(change))
return False
if not _ShouldBeMarkedCQReady(details, change.patch_number):
logging.info("Change %s was not ready", str(change))
return False
try:
helper.SetReview(
change.gerrit_number,
msg=_EXONERATOR_CQ_MESSAGE % build_id,
labels={COMMIT_QUEUE_LABEL: 1})
logging.info('Exonerated %s for a CQ failure.', str(change))
return True
# Network errors or permission errors won't get retried. That's ok for now.
except Exception:
logging.exception('Encountered an exception while marking %s with CQ+1',
str(change))
return False
def CanBeMarkedReady(change):
"""Whether a change is ready to be exonerated.
Args:
change: A GerritPatchTuple to test.
"""
helper = GetHelper(internal=change.internal)
details = helper.GetChangeDetail(change.gerrit_number)
if details:
return _ShouldBeMarkedCQReady(details, change.patch_number)
else:
logging.warning('Change details was None for %s', str(change))
return False
def SanityCheckChange(change_details, known_patch_number):
"""Whether the change's latest revision is the known patch number.
Check that the change's details contains 'revisions', and that the latest
revision matches the patch number we want to forgive.
Args:
change_details: The change details response from Gerrit.
known_patch_number: The revision number which we want to forgive.
"""
if 'revisions' not in change_details:
logging.warning('Change details has no revisions')
return False
# If a newer patchset was uploaded, we don't want to forgive it.
missing = -1
max_rev = max(rev.get('_number', missing)
for rev in change_details['revisions'].values())
return not (max_rev == missing or max_rev > known_patch_number)
def _ShouldBeMarkedCQReady(change_details, known_patch_number):
"""Whether a CL should be re-marked as CQ ready.
We only want to re-label CLs as CQ+1 if they haven't been marked as CQ+1 or
CQ-1 by someone else, and it's still on the same patchset that was kicked
out.
Args:
change_details: The change details response from Gerrit.
known_patch_number: The revision number which we want to forgive.
"""
return (
SanityCheckChange(change_details, known_patch_number)
# It shouldn't already have CQ+1 or CQ-1
and not (LabelValues(change_details, COMMIT_QUEUE_LABEL) - {0})
# The CL must have Verified+1 still.
and any(label > 0
for label in LabelValues(change_details, _VERIFIED_LABEL))
# Must not have Verified-1
and not any(label < 0
for label in LabelValues(change_details, _VERIFIED_LABEL))
# Don't exonerate CLs with Code-Review-1 or Code-Review-2
and not any(label < 0
for label in LabelValues(change_details, CODE_REVIEW_LABEL))
and CQApprovalWasRemovedByBot(change_details)
and not _FailedToApply(change_details)
)
def CQApprovalWasRemovedByBot(change_details):
"""Returns whether the CQ+1 label was removed by a bot.
Args:
change_details: The response from gerrit with the change's details.
"""
for message in reversed(change_details['messages']):
if '-Commit-Queue' in message['message']:
return (message['real_author']['email']
== constants.CHROMEOS_COMMIT_BOT_EMAIL)
logging.error('No Commit-Queue removal message was found for change %s.'
' This should never happen' % (
change_details.get('_number', change_details['id'])))
return False
def _FailedToApply(change_details):
"""Tests for whether the change failed to apply recently.
Checks to see whether the second to last message by chromeos-commit-bot
indicates that the Commit Queue failed to apply the change.
Args:
change_details: The change details response from Gerrit.
"""
commit_bot_messages = [
m for m in change_details['messages']
if m['real_author']['email'] == constants.CHROMEOS_COMMIT_BOT_EMAIL
]
last_two_messages = itertools.islice(reversed(commit_bot_messages), 2)
return any(
_FAILED_TO_APPLY_SNIPPET in m['message']
for m in last_two_messages)
def LabelValues(change_details, label):
"""Returns a set of values that reviewers have given for |label|.
Args:
change_details: The change details object from Gerrit.
label: The label, e.g. 'Commit-Queue'
Returns:
A set of values that the reviewers have given the change.
"""
return set(entry['value']
for entry in change_details['labels'][label].get('all', ())
if 'value' in entry)
def GetHelper(internal):
"""Returns the GerritHelper for internal or external changes.
Args:
internal: Boolean indicating whether to get the internal or external helper.
"""
if internal:
return gerrit.GetCrosInternal()
else:
return gerrit.GetCrosExternal()