blob: 16c6e3de48034a2b75ed3fde59ec64221a8531cb [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is govered by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""Implements processing of issue update command lines.
This currently processes the leading command-lines that appear
at the top of inbound email messages to update existing issues.
It could also be expanded to allow new issues to be created. Or, to
handle commands in commit-log messages if the version control system
invokes a webhook.
"""
import logging
import re
from features import commands
from features import notify
from framework import emailfmt
from framework import framework_bizobj
from framework import framework_helpers
from services import issue_svc
# Actions have separate 'Parse' and 'Run' implementations to allow better
# testing coverage.
class IssueAction(object):
"""Base class for all issue commands."""
def __init__(self):
self.parser = commands.AssignmentParser(None)
self.description = ''
self.inbound_message = None
self.commenter_id = None
self.project = None
self.config = None
self.hostport = framework_helpers.GetHostPort()
def Parse(
self, cnxn, project_name, commenter_id, lines, services,
strip_quoted_lines=False, hostport=None):
"""Populate object from raw user input."""
self.project = services.project.GetProjectByName(cnxn, project_name)
self.config = services.config.GetProjectConfig(
cnxn, self.project.project_id)
self.commenter_id = commenter_id
# Process all valid key-value lines. Once we find a non key-value line,
# treat the rest as the 'description'.
for idx, line in enumerate(lines):
valid_line = False
m = re.match(r'^\s*(\w+)\s*\:\s*(.*?)\s*$', line)
if m:
# Process Key-Value
key = m.group(1).lower()
value = m.group(2)
valid_line = self.parser.ParseAssignment(
cnxn, key, value, self.config, services, self.commenter_id)
if not valid_line:
# Not Key-Value. Treat this line and remaining as 'description'.
# First strip off any trailing blank lines.
while lines and not lines[-1].strip():
lines.pop()
if lines:
self.description = '\n'.join(lines[idx:])
break
if strip_quoted_lines:
self.inbound_message = '\n'.join(lines)
self.description = emailfmt.StripQuotedText(self.description)
if hostport:
self.hostport = hostport
for key in ['owner_id', 'cc_add', 'cc_remove', 'summary',
'status', 'labels_add', 'labels_remove', 'branch']:
logging.info('\t%s: %s', key, self.parser.__dict__[key])
for key in ['commenter_id', 'description', 'hostport']:
logging.info('\t%s: %s', key, self.__dict__[key])
def Run(self, cnxn, services, allow_edit=True):
"""Execute this action."""
raise NotImplementedError()
class UpdateIssueAction(IssueAction):
"""Implements processing email replies or the "update issue" command."""
def __init__(self, local_id):
super(UpdateIssueAction, self).__init__()
self.local_id = local_id
def Run(self, cnxn, services, allow_edit=True):
"""Updates an issue based on the parsed commands."""
try:
issue = services.issue.GetIssueByLocalID(
cnxn, self.project.project_id, self.local_id)
except issue_svc.NoSuchIssueException:
return # Issue does not exist, so do nothing
old_owner_id = issue.owner_id
new_summary = self.parser.summary or issue.summary
if self.parser.status is None:
new_status = issue.status
else:
new_status = self.parser.status
if self.parser.owner_id is None:
new_owner_id = issue.owner_id
else:
new_owner_id = self.parser.owner_id
new_cc_ids = [cc for cc in list(issue.cc_ids) + list(self.parser.cc_add)
if cc not in self.parser.cc_remove]
(new_labels, _update_add,
_update_remove) = framework_bizobj.MergeLabels(
issue.labels, self.parser.labels_add,
self.parser.labels_remove,
self.config.exclusive_label_prefixes)
new_field_values = issue.field_values # TODO(jrobbins): edit custom ones
if not allow_edit:
# If user can't edit, then only consider the plain-text comment,
# and set all other fields back to their original values.
logging.info('Processed reply from user who can not edit issue')
new_summary = issue.summary
new_status = issue.status
new_owner_id = issue.owner_id
new_cc_ids = issue.cc_ids
new_labels = issue.labels
new_field_values = issue.field_values
amendments, _comment_pb = services.issue.ApplyIssueComment(
cnxn, services, self.commenter_id,
self.project.project_id, issue.local_id, new_summary, new_status,
new_owner_id, new_cc_ids, new_labels, new_field_values,
issue.component_ids, issue.blocked_on_iids, issue.blocking_iids,
issue.dangling_blocked_on_refs, issue.dangling_blocking_refs,
issue.merged_into, comment=self.description,
inbound_message=self.inbound_message)
logging.info('Updated issue %s:%s w/ amendments %r',
self.project.project_name, issue.local_id, amendments)
if amendments or self.description: # Avoid completely empty comments.
cmnts = services.issue.GetCommentsForIssue(cnxn, issue.issue_id)
notify.PrepareAndSendIssueChangeNotification(
self.project.project_id, self.local_id, self.hostport,
self.commenter_id, len(cmnts) - 1, old_owner_id=old_owner_id)