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

Unified Diff: appengine/monorail/features/commitlogcommands.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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « appengine/monorail/features/commands.py ('k') | appengine/monorail/features/cues.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: appengine/monorail/features/commitlogcommands.py
diff --git a/appengine/monorail/features/commitlogcommands.py b/appengine/monorail/features/commitlogcommands.py
new file mode 100644
index 0000000000000000000000000000000000000000..16c6e3de48034a2b75ed3fde59ec64221a8531cb
--- /dev/null
+++ b/appengine/monorail/features/commitlogcommands.py
@@ -0,0 +1,155 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is govered 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.
+"""
+
+import logging
+import re
+
+from features import commands
+from features import notify
+from framework import emailfmt
+from framework import framework_bizobj
+from framework import framework_helpers
+from services import issue_svc
+
+
+# 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."""
+ self.project = services.project.GetProjectByName(cnxn, project_name)
+ self.config = services.config.GetProjectConfig(
+ cnxn, self.project.project_id)
+ self.commenter_id = commenter_id
+
+ # 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:
+ # 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])
+
+ def Run(self, cnxn, services, allow_edit=True):
+ """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, cnxn, services, allow_edit=True):
+ """Updates an issue based on the parsed commands."""
+ try:
+ issue = services.issue.GetIssueByLocalID(
+ cnxn, self.project.project_id, self.local_id)
+ except issue_svc.NoSuchIssueException:
+ return # Issue does not exist, so do nothing
+
+ old_owner_id = issue.owner_id
+ new_summary = self.parser.summary or issue.summary
+
+ if self.parser.status is None:
+ new_status = issue.status
+ else:
+ new_status = self.parser.status
+
+ if self.parser.owner_id is None:
+ new_owner_id = issue.owner_id
+ else:
+ new_owner_id = self.parser.owner_id
+
+ new_cc_ids = [cc for cc in list(issue.cc_ids) + list(self.parser.cc_add)
+ if cc not in self.parser.cc_remove]
+ (new_labels, _update_add,
+ _update_remove) = framework_bizobj.MergeLabels(
+ issue.labels, self.parser.labels_add,
+ self.parser.labels_remove,
+ self.config.exclusive_label_prefixes)
+
+ new_field_values = issue.field_values # TODO(jrobbins): edit custom ones
+
+ if not allow_edit:
+ # If user can't edit, then only consider the plain-text comment,
+ # and set all other fields back to their original values.
+ logging.info('Processed reply from user who can not edit issue')
+ new_summary = issue.summary
+ new_status = issue.status
+ new_owner_id = issue.owner_id
+ new_cc_ids = issue.cc_ids
+ new_labels = issue.labels
+ new_field_values = issue.field_values
+
+ amendments, _comment_pb = services.issue.ApplyIssueComment(
+ cnxn, services, self.commenter_id,
+ self.project.project_id, issue.local_id, new_summary, new_status,
+ new_owner_id, new_cc_ids, new_labels, new_field_values,
+ issue.component_ids, issue.blocked_on_iids, issue.blocking_iids,
+ issue.dangling_blocked_on_refs, issue.dangling_blocking_refs,
+ issue.merged_into, comment=self.description,
+ inbound_message=self.inbound_message)
+
+ logging.info('Updated issue %s:%s w/ amendments %r',
+ self.project.project_name, issue.local_id, amendments)
+
+ if amendments or self.description: # Avoid completely empty comments.
+ cmnts = services.issue.GetCommentsForIssue(cnxn, issue.issue_id)
+ notify.PrepareAndSendIssueChangeNotification(
+ self.project.project_id, self.local_id, self.hostport,
+ self.commenter_id, len(cmnts) - 1, old_owner_id=old_owner_id)
« no previous file with comments | « appengine/monorail/features/commands.py ('k') | appengine/monorail/features/cues.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698