OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is govered by a BSD-style |
| 3 # license that can be found in the LICENSE file or at |
| 4 # https://developers.google.com/open-source/licenses/bsd |
| 5 |
| 6 """Implements processing of issue update command lines. |
| 7 |
| 8 This currently processes the leading command-lines that appear |
| 9 at the top of inbound email messages to update existing issues. |
| 10 |
| 11 It could also be expanded to allow new issues to be created. Or, to |
| 12 handle commands in commit-log messages if the version control system |
| 13 invokes a webhook. |
| 14 """ |
| 15 |
| 16 import logging |
| 17 import re |
| 18 |
| 19 from features import commands |
| 20 from features import notify |
| 21 from framework import emailfmt |
| 22 from framework import framework_bizobj |
| 23 from framework import framework_helpers |
| 24 from services import issue_svc |
| 25 |
| 26 |
| 27 # Actions have separate 'Parse' and 'Run' implementations to allow better |
| 28 # testing coverage. |
| 29 class IssueAction(object): |
| 30 """Base class for all issue commands.""" |
| 31 |
| 32 def __init__(self): |
| 33 self.parser = commands.AssignmentParser(None) |
| 34 self.description = '' |
| 35 self.inbound_message = None |
| 36 self.commenter_id = None |
| 37 self.project = None |
| 38 self.config = None |
| 39 self.hostport = framework_helpers.GetHostPort() |
| 40 |
| 41 def Parse( |
| 42 self, cnxn, project_name, commenter_id, lines, services, |
| 43 strip_quoted_lines=False, hostport=None): |
| 44 """Populate object from raw user input.""" |
| 45 self.project = services.project.GetProjectByName(cnxn, project_name) |
| 46 self.config = services.config.GetProjectConfig( |
| 47 cnxn, self.project.project_id) |
| 48 self.commenter_id = commenter_id |
| 49 |
| 50 # Process all valid key-value lines. Once we find a non key-value line, |
| 51 # treat the rest as the 'description'. |
| 52 for idx, line in enumerate(lines): |
| 53 valid_line = False |
| 54 m = re.match(r'^\s*(\w+)\s*\:\s*(.*?)\s*$', line) |
| 55 if m: |
| 56 # Process Key-Value |
| 57 key = m.group(1).lower() |
| 58 value = m.group(2) |
| 59 valid_line = self.parser.ParseAssignment( |
| 60 cnxn, key, value, self.config, services, self.commenter_id) |
| 61 |
| 62 if not valid_line: |
| 63 # Not Key-Value. Treat this line and remaining as 'description'. |
| 64 # First strip off any trailing blank lines. |
| 65 while lines and not lines[-1].strip(): |
| 66 lines.pop() |
| 67 if lines: |
| 68 self.description = '\n'.join(lines[idx:]) |
| 69 break |
| 70 |
| 71 if strip_quoted_lines: |
| 72 self.inbound_message = '\n'.join(lines) |
| 73 self.description = emailfmt.StripQuotedText(self.description) |
| 74 |
| 75 if hostport: |
| 76 self.hostport = hostport |
| 77 |
| 78 for key in ['owner_id', 'cc_add', 'cc_remove', 'summary', |
| 79 'status', 'labels_add', 'labels_remove', 'branch']: |
| 80 logging.info('\t%s: %s', key, self.parser.__dict__[key]) |
| 81 |
| 82 for key in ['commenter_id', 'description', 'hostport']: |
| 83 logging.info('\t%s: %s', key, self.__dict__[key]) |
| 84 |
| 85 def Run(self, cnxn, services, allow_edit=True): |
| 86 """Execute this action.""" |
| 87 raise NotImplementedError() |
| 88 |
| 89 |
| 90 class UpdateIssueAction(IssueAction): |
| 91 """Implements processing email replies or the "update issue" command.""" |
| 92 |
| 93 def __init__(self, local_id): |
| 94 super(UpdateIssueAction, self).__init__() |
| 95 self.local_id = local_id |
| 96 |
| 97 def Run(self, cnxn, services, allow_edit=True): |
| 98 """Updates an issue based on the parsed commands.""" |
| 99 try: |
| 100 issue = services.issue.GetIssueByLocalID( |
| 101 cnxn, self.project.project_id, self.local_id) |
| 102 except issue_svc.NoSuchIssueException: |
| 103 return # Issue does not exist, so do nothing |
| 104 |
| 105 old_owner_id = issue.owner_id |
| 106 new_summary = self.parser.summary or issue.summary |
| 107 |
| 108 if self.parser.status is None: |
| 109 new_status = issue.status |
| 110 else: |
| 111 new_status = self.parser.status |
| 112 |
| 113 if self.parser.owner_id is None: |
| 114 new_owner_id = issue.owner_id |
| 115 else: |
| 116 new_owner_id = self.parser.owner_id |
| 117 |
| 118 new_cc_ids = [cc for cc in list(issue.cc_ids) + list(self.parser.cc_add) |
| 119 if cc not in self.parser.cc_remove] |
| 120 (new_labels, _update_add, |
| 121 _update_remove) = framework_bizobj.MergeLabels( |
| 122 issue.labels, self.parser.labels_add, |
| 123 self.parser.labels_remove, |
| 124 self.config.exclusive_label_prefixes) |
| 125 |
| 126 new_field_values = issue.field_values # TODO(jrobbins): edit custom ones |
| 127 |
| 128 if not allow_edit: |
| 129 # If user can't edit, then only consider the plain-text comment, |
| 130 # and set all other fields back to their original values. |
| 131 logging.info('Processed reply from user who can not edit issue') |
| 132 new_summary = issue.summary |
| 133 new_status = issue.status |
| 134 new_owner_id = issue.owner_id |
| 135 new_cc_ids = issue.cc_ids |
| 136 new_labels = issue.labels |
| 137 new_field_values = issue.field_values |
| 138 |
| 139 amendments, _comment_pb = services.issue.ApplyIssueComment( |
| 140 cnxn, services, self.commenter_id, |
| 141 self.project.project_id, issue.local_id, new_summary, new_status, |
| 142 new_owner_id, new_cc_ids, new_labels, new_field_values, |
| 143 issue.component_ids, issue.blocked_on_iids, issue.blocking_iids, |
| 144 issue.dangling_blocked_on_refs, issue.dangling_blocking_refs, |
| 145 issue.merged_into, comment=self.description, |
| 146 inbound_message=self.inbound_message) |
| 147 |
| 148 logging.info('Updated issue %s:%s w/ amendments %r', |
| 149 self.project.project_name, issue.local_id, amendments) |
| 150 |
| 151 if amendments or self.description: # Avoid completely empty comments. |
| 152 cmnts = services.issue.GetCommentsForIssue(cnxn, issue.issue_id) |
| 153 notify.PrepareAndSendIssueChangeNotification( |
| 154 self.project.project_id, self.local_id, self.hostport, |
| 155 self.commenter_id, len(cmnts) - 1, old_owner_id=old_owner_id) |
OLD | NEW |