| Index: appengine/monorail/features/commitlogcommands.py
|
| diff --git a/appengine/monorail/features/commitlogcommands.py b/appengine/monorail/features/commitlogcommands.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..16c6e3de48034a2b75ed3fde59ec64221a8531cb
|
| --- /dev/null
|
| +++ b/appengine/monorail/features/commitlogcommands.py
|
| @@ -0,0 +1,155 @@
|
| +# 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)
|
|
|