Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/features/commitlogcommands.py b/features/commitlogcommands.py
new file mode 100644
index 0000000..f570ae3
--- /dev/null
+++ b/features/commitlogcommands.py
@@ -0,0 +1,162 @@
+# Copyright 2016 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 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.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import logging
+import re
+
+from businesslogic import work_env
+from features import commands
+from features import send_notifications
+from framework import emailfmt
+from framework import exceptions
+from framework import framework_bizobj
+from framework import framework_helpers
+from framework import permissions
+from proto import tracker_pb2
+
+
+# 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.
+
+ Args:
+ cnxn: connection to SQL database.
+ project_name: Name of the project containing the issue.
+ commenter_id: int user ID of user creating comment.
+ lines: list of strings containing test to be parsed.
+ services: References to existing objects from Monorail's service layer.
+ strip_quoted_lines: boolean for whether to remove quoted lines from text.
+ hostport: Optionally override the current instance's hostport variable.
+
+ Returns:
+ A boolean for whether any command lines were found while parsing.
+
+ Side-effect:
+ Edits the values of instance variables in this class with parsing output.
+ """
+ self.project = services.project.GetProjectByName(cnxn, project_name)
+ self.config = services.config.GetProjectConfig(
+ cnxn, self.project.project_id)
+ self.commenter_id = commenter_id
+
+ has_commands = False
+
+ # 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:
+ has_commands = True
+ # 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])
+
+ return has_commands
+
+ def Run(self, mc, services):
+ """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, mc, services):
+ """Updates an issue based on the parsed commands."""
+ try:
+ issue = services.issue.GetIssueByLocalID(
+ mc.cnxn, self.project.project_id, self.local_id, use_cache=False)
+ except exceptions.NoSuchIssueException:
+ return # Issue does not exist, so do nothing
+
+ delta = tracker_pb2.IssueDelta()
+
+ allow_edit = permissions.CanEditIssue(
+ mc.auth.effective_ids, mc.perms, self.project, issue)
+
+ if allow_edit:
+ delta.summary = self.parser.summary or issue.summary
+ if self.parser.status is None:
+ delta.status = issue.status
+ else:
+ delta.status = self.parser.status
+
+ if self.parser.owner_id is None:
+ delta.owner_id = issue.owner_id
+ else:
+ delta.owner_id = self.parser.owner_id
+
+ delta.cc_ids_add = list(self.parser.cc_add)
+ delta.cc_ids_remove = list(self.parser.cc_remove)
+ delta.labels_add = self.parser.labels_add
+ delta.labels_remove = self.parser.labels_remove
+ # TODO(jrobbins): allow editing of custom fields
+
+ with work_env.WorkEnv(mc, services) as we:
+ we.UpdateIssue(
+ issue, delta, self.description, inbound_message=self.inbound_message)
+
+ logging.info('Updated issue %s:%s',
+ self.project.project_name, issue.local_id)
+
+ # Note: notifications are generated in work_env.