Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(38)

Side by Side Diff: appengine/monorail/features/commands.py

Issue 1868553004: Open Source Monorail (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Rebase Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 """Classes and functions that implement command-line-like issue updates."""
7
8 import logging
9 import re
10
11 from framework import framework_bizobj
12 from framework import framework_constants
13 from services import user_svc
14 from tracker import tracker_constants
15
16
17 def ParseQuickEditCommand(
18 cnxn, cmd, issue, config, logged_in_user_id, services):
19 """Parse a quick edit command into assignments and labels."""
20 parts = _BreakCommandIntoParts(cmd)
21 parser = AssignmentParser(None, easier_kv_labels=True)
22
23 for key, value in parts:
24 if key: # A key=value assignment.
25 valid_assignment = parser.ParseAssignment(
26 cnxn, key, value, config, services, logged_in_user_id)
27 if not valid_assignment:
28 logging.info('ignoring assignment: %r, %r', key, value)
29
30 elif value.startswith('-'): # Removing a label.
31 parser.labels_remove.append(_StandardizeLabel(value[1:], config))
32
33 else: # Adding a label.
34 value = value.strip('+')
35 parser.labels_add.append(_StandardizeLabel(value, config))
36
37 new_summary = parser.summary or issue.summary
38
39 if parser.status is None:
40 new_status = issue.status
41 else:
42 new_status = parser.status
43
44 if parser.owner_id is None:
45 new_owner_id = issue.owner_id
46 else:
47 new_owner_id = parser.owner_id
48
49 new_cc_ids = [cc for cc in list(issue.cc_ids) + list(parser.cc_add)
50 if cc not in parser.cc_remove]
51 (new_labels, _update_add,
52 _update_remove) = framework_bizobj.MergeLabels(
53 issue.labels, parser.labels_add, parser.labels_remove,
54 config.exclusive_label_prefixes)
55
56 return new_summary, new_status, new_owner_id, new_cc_ids, new_labels
57
58
59 ASSIGN_COMMAND_RE = re.compile(
60 r'(?P<key>\w+(?:-|\w)*)(?:=|:)'
61 r'(?:(?P<value1>(?:-|\+|\.|%|@|=|,|\w)+)|'
62 r'"(?P<value2>[^"]+)"|'
63 r"'(?P<value3>[^']+)')",
64 re.UNICODE | re.IGNORECASE)
65
66 LABEL_COMMAND_RE = re.compile(
67 r'(?P<label>(?:\+|-)?\w(?:-|\w)*)',
68 re.UNICODE | re.IGNORECASE)
69
70
71 def _BreakCommandIntoParts(cmd):
72 """Break a quick edit command into assignment and label parts.
73
74 Args:
75 cmd: string command entered by the user.
76
77 Returns:
78 A list of (key, value) pairs where key is the name of the field
79 being assigned or None for OneWord labels, and value is the value
80 to assign to it, or the whole label. Value may begin with a "+"
81 which is just ignored, or a "-" meaning that the label should be
82 removed, or neither.
83 """
84 parts = []
85 cmd = cmd.strip()
86 m = True
87
88 while m:
89 m = ASSIGN_COMMAND_RE.match(cmd)
90 if m:
91 key = m.group('key')
92 value = m.group('value1') or m.group('value2') or m.group('value3')
93 parts.append((key, value))
94 cmd = cmd[len(m.group(0)):].strip()
95 else:
96 m = LABEL_COMMAND_RE.match(cmd)
97 if m:
98 parts.append((None, m.group('label')))
99 cmd = cmd[len(m.group(0)):].strip()
100
101 return parts
102
103
104 def _ParsePlusMinusList(value):
105 """Parse a string containing a series of plus/minuse values.
106
107 Strings are seprated by whitespace, comma and/or semi-colon.
108
109 Example:
110 value = "one +two -three"
111 plus = ['one', 'two']
112 minus = ['three']
113
114 Args:
115 value: string containing unparsed plus minus values.
116
117 Returns:
118 A tuple of (plus, minus) string values.
119 """
120 plus = []
121 minus = []
122 # Treat ';' and ',' as separators (in addition to SPACE)
123 for ch in [',', ';']:
124 value = value.replace(ch, ' ')
125 terms = [i.strip() for i in value.split()]
126 for item in terms:
127 if item.startswith('-'):
128 minus.append(item.lstrip('-'))
129 else:
130 plus.append(item.lstrip('+')) # optional leading '+'
131
132 return plus, minus
133
134
135 class AssignmentParser(object):
136 """Class to parse assignment statements in quick edits or email replies."""
137
138 def __init__(self, template, easier_kv_labels=False):
139 self.cc_list = []
140 self.cc_add = []
141 self.cc_remove = []
142 self.owner_id = None
143 self.status = None
144 self.summary = None
145 self.labels_list = []
146 self.labels_add = []
147 self.labels_remove = []
148 self.branch = None
149
150 # Accept "Anything=Anything" for quick-edit, but not in commit-log-commands
151 # because it would be too error-prone when mixed with plain text comment
152 # text and without autocomplete to help users triggering it via typos.
153 self.easier_kv_labels = easier_kv_labels
154
155 if template:
156 if template.owner_id:
157 self.owner_id = template.owner_id
158 if template.summary:
159 self.summary = template.summary
160 if template.labels:
161 self.labels_list = template.labels
162 # Do not have a similar check as above for status because it could be an
163 # empty string.
164 self.status = template.status
165
166 def ParseAssignment(self, cnxn, key, value, config, services, user_id):
167 """Parse command-style text entered by the user to update an issue.
168
169 E.g., The user may want to set the issue status to "reviewed", or
170 set the owner to "me".
171
172 Args:
173 cnxn: connection to SQL database.
174 key: string name of the field to set.
175 value: string value to be interpreted.
176 config: Projects' issue tracker configuration PB.
177 services: connections to backends.
178 user_id: int user ID of the user making the change.
179
180 Returns:
181 True if the line could be parsed as an assigment, False otherwise.
182 Also, as a side-effect, the assigned values are built up in the instance
183 variables of the parser.
184 """
185 valid_line = True
186
187 if key == 'owner':
188 if framework_constants.NO_VALUE_RE.match(value):
189 self.owner_id = framework_constants.NO_USER_SPECIFIED
190 else:
191 try:
192 self.owner_id = _LookupMeOrUsername(cnxn, value, services, user_id)
193 except user_svc.NoSuchUserException:
194 logging.warning('bad owner: %r when committing to project_id %r',
195 value, config.project_id)
196 valid_line = False
197
198 elif key == 'cc':
199 try:
200 add, remove = _ParsePlusMinusList(value)
201 self.cc_add = [_LookupMeOrUsername(cnxn, cc, services, user_id)
202 for cc in add]
203 self.cc_remove = [_LookupMeOrUsername(cnxn, cc, services, user_id)
204 for cc in remove]
205 for user_id in self.cc_add:
206 if user_id not in self.cc_list:
207 self.cc_list.append(user_id)
208 self.cc_list = [user_id for user_id in self.cc_list
209 if user_id not in self.cc_remove]
210 except user_svc.NoSuchUserException:
211 logging.warning('bad cc: %r when committing to project_id %r',
212 value, config.project_id)
213 valid_line = False
214
215 elif key == 'summary':
216 self.summary = value
217
218 elif key == 'status':
219 if framework_constants.NO_VALUE_RE.match(value):
220 self.status = ''
221 else:
222 self.status = _StandardizeStatus(value, config)
223
224 elif key == 'label' or key == 'labels':
225 self.labels_add, self.labels_remove = _ParsePlusMinusList(value)
226 self.labels_add = [_StandardizeLabel(lab, config)
227 for lab in self.labels_add]
228 self.labels_remove = [_StandardizeLabel(lab, config)
229 for lab in self.labels_remove]
230 (self.labels_list, _update_add,
231 _update_remove) = framework_bizobj.MergeLabels(
232 self.labels_list, self.labels_add, self.labels_remove,
233 config.exclusive_label_prefixes)
234
235 elif (self.easier_kv_labels and
236 key not in tracker_constants.RESERVED_PREFIXES and
237 key and value):
238 if key.startswith('-'):
239 self.labels_remove.append(_StandardizeLabel(
240 '%s-%s' % (key[1:], value), config))
241 else:
242 self.labels_add.append(_StandardizeLabel(
243 '%s-%s' % (key, value), config))
244
245 else:
246 valid_line = False
247
248 return valid_line
249
250
251 def _StandardizeStatus(status, config):
252 """Attempt to match a user-supplied status with standard status values.
253
254 Args:
255 status: User-supplied status string.
256 config: Project's issue tracker configuration PB.
257
258 Returns:
259 A canonicalized status string, that matches a standard project
260 value, if found.
261 """
262 well_known_statuses = [wks.status for wks in config.well_known_statuses]
263 return _StandardizeArtifact(status, well_known_statuses)
264
265
266 def _StandardizeLabel(label, config):
267 """Attempt to match a user-supplied label with standard label values.
268
269 Args:
270 label: User-supplied label string.
271 config: Project's issue tracker configuration PB.
272
273 Returns:
274 A canonicalized label string, that matches a standard project
275 value, if found.
276 """
277 well_known_labels = [wkl.label for wkl in config.well_known_labels]
278 return _StandardizeArtifact(label, well_known_labels)
279
280
281 def _StandardizeArtifact(artifact, well_known_artifacts):
282 """Attempt to match a user-supplied artifact with standard artifact values.
283
284 Args:
285 artifact: User-supplied artifact string.
286 well_known_artifacts: List of well known values of the artifact.
287
288 Returns:
289 A canonicalized artifact string, that matches a standard project
290 value, if found.
291 """
292 artifact = framework_bizobj.CanonicalizeLabel(artifact)
293 for wka in well_known_artifacts:
294 if artifact.lower() == wka.lower():
295 return wka
296 # No match - use user-supplied artifact.
297 return artifact
298
299
300 def _LookupMeOrUsername(cnxn, username, services, user_id):
301 """Handle the 'me' syntax or lookup a user's user ID."""
302 if username.lower() == 'me':
303 return user_id
304
305 return services.user.LookupUserID(cnxn, username)
OLDNEW
« no previous file with comments | « appengine/monorail/features/autolink.py ('k') | appengine/monorail/features/commitlogcommands.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698