blob: f570ae300bbb525b7fe3bda3c6e39c1e449a1d15 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed 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
8This currently processes the leading command-lines that appear
9at the top of inbound email messages to update existing issues.
10
11It could also be expanded to allow new issues to be created. Or, to
12handle commands in commit-log messages if the version control system
13invokes a webhook.
14"""
15from __future__ import print_function
16from __future__ import division
17from __future__ import absolute_import
18
19import logging
20import re
21
22from businesslogic import work_env
23from features import commands
24from features import send_notifications
25from framework import emailfmt
26from framework import exceptions
27from framework import framework_bizobj
28from framework import framework_helpers
29from framework import permissions
30from proto import tracker_pb2
31
32
33# Actions have separate 'Parse' and 'Run' implementations to allow better
34# testing coverage.
35class IssueAction(object):
36 """Base class for all issue commands."""
37
38 def __init__(self):
39 self.parser = commands.AssignmentParser(None)
40 self.description = ''
41 self.inbound_message = None
42 self.commenter_id = None
43 self.project = None
44 self.config = None
45 self.hostport = framework_helpers.GetHostPort()
46
47 def Parse(
48 self, cnxn, project_name, commenter_id, lines, services,
49 strip_quoted_lines=False, hostport=None):
50 """Populate object from raw user input.
51
52 Args:
53 cnxn: connection to SQL database.
54 project_name: Name of the project containing the issue.
55 commenter_id: int user ID of user creating comment.
56 lines: list of strings containing test to be parsed.
57 services: References to existing objects from Monorail's service layer.
58 strip_quoted_lines: boolean for whether to remove quoted lines from text.
59 hostport: Optionally override the current instance's hostport variable.
60
61 Returns:
62 A boolean for whether any command lines were found while parsing.
63
64 Side-effect:
65 Edits the values of instance variables in this class with parsing output.
66 """
67 self.project = services.project.GetProjectByName(cnxn, project_name)
68 self.config = services.config.GetProjectConfig(
69 cnxn, self.project.project_id)
70 self.commenter_id = commenter_id
71
72 has_commands = False
73
74 # Process all valid key-value lines. Once we find a non key-value line,
75 # treat the rest as the 'description'.
76 for idx, line in enumerate(lines):
77 valid_line = False
78 m = re.match(r'^\s*(\w+)\s*\:\s*(.*?)\s*$', line)
79 if m:
80 has_commands = True
81 # Process Key-Value
82 key = m.group(1).lower()
83 value = m.group(2)
84 valid_line = self.parser.ParseAssignment(
85 cnxn, key, value, self.config, services, self.commenter_id)
86
87 if not valid_line:
88 # Not Key-Value. Treat this line and remaining as 'description'.
89 # First strip off any trailing blank lines.
90 while lines and not lines[-1].strip():
91 lines.pop()
92 if lines:
93 self.description = '\n'.join(lines[idx:])
94 break
95
96 if strip_quoted_lines:
97 self.inbound_message = '\n'.join(lines)
98 self.description = emailfmt.StripQuotedText(self.description)
99
100 if hostport:
101 self.hostport = hostport
102
103 for key in ['owner_id', 'cc_add', 'cc_remove', 'summary',
104 'status', 'labels_add', 'labels_remove', 'branch']:
105 logging.info('\t%s: %s', key, self.parser.__dict__[key])
106
107 for key in ['commenter_id', 'description', 'hostport']:
108 logging.info('\t%s: %s', key, self.__dict__[key])
109
110 return has_commands
111
112 def Run(self, mc, services):
113 """Execute this action."""
114 raise NotImplementedError()
115
116
117class UpdateIssueAction(IssueAction):
118 """Implements processing email replies or the "update issue" command."""
119
120 def __init__(self, local_id):
121 super(UpdateIssueAction, self).__init__()
122 self.local_id = local_id
123
124 def Run(self, mc, services):
125 """Updates an issue based on the parsed commands."""
126 try:
127 issue = services.issue.GetIssueByLocalID(
128 mc.cnxn, self.project.project_id, self.local_id, use_cache=False)
129 except exceptions.NoSuchIssueException:
130 return # Issue does not exist, so do nothing
131
132 delta = tracker_pb2.IssueDelta()
133
134 allow_edit = permissions.CanEditIssue(
135 mc.auth.effective_ids, mc.perms, self.project, issue)
136
137 if allow_edit:
138 delta.summary = self.parser.summary or issue.summary
139 if self.parser.status is None:
140 delta.status = issue.status
141 else:
142 delta.status = self.parser.status
143
144 if self.parser.owner_id is None:
145 delta.owner_id = issue.owner_id
146 else:
147 delta.owner_id = self.parser.owner_id
148
149 delta.cc_ids_add = list(self.parser.cc_add)
150 delta.cc_ids_remove = list(self.parser.cc_remove)
151 delta.labels_add = self.parser.labels_add
152 delta.labels_remove = self.parser.labels_remove
153 # TODO(jrobbins): allow editing of custom fields
154
155 with work_env.WorkEnv(mc, services) as we:
156 we.UpdateIssue(
157 issue, delta, self.description, inbound_message=self.inbound_message)
158
159 logging.info('Updated issue %s:%s',
160 self.project.project_name, issue.local_id)
161
162 # Note: notifications are generated in work_env.