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