git-cl: add --fixed / -x option to upload

This option take the same format as -b / --bug but
spits out "Fixed: #,#" instead of "Bug: #,#".

This CL also looks for fix-, fixed-, and fixes-
branch prefixes and uses those as cue to add "Fixed:"
to the CL description when editing. This is a slight
behavioral change for fix- specifically, as it used
to produce "Bug: " instead.

Bug: monorail:4470
Change-Id: Ib24a1ff33ca3674e53fc5437f459ea5708988290
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/1846785
Commit-Queue: Dan Beam <dbeam@chromium.org>
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
Auto-Submit: Dan Beam <dbeam@chromium.org>
diff --git a/git_cl.py b/git_cl.py
index ff6d5b3..e79490b 100755
--- a/git_cl.py
+++ b/git_cl.py
@@ -2305,9 +2305,14 @@
 
     # Extract bug number from branch name.
     bug = options.bug
-    match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
-    if not bug and match:
-      bug = match.group(1)
+    fixed = options.fixed
+    match = re.match(r'(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)',
+                     self.GetBranch())
+    if not bug and not fixed and match:
+      if match.group('type') == 'bug':
+        bug = match.group('bugnum')
+      else:
+        fixed = match.group('bugnum')
 
     if options.squash:
       self._GerritCommitMsgHookCheck(offer_removal=not options.force)
@@ -2346,7 +2351,7 @@
           # Change-Id. Thus, just create a new footer, but let user verify the
           # new description.
           message = '%s\n\nChange-Id: %s' % (message, change_id)
-          change_desc = ChangeDescription(message, bug=bug)
+          change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
           if not options.force:
             print(
                 'WARNING: change %s has Change-Id footer(s):\n'
@@ -2367,7 +2372,7 @@
         # Sanity check of this code - we should end up with proper message
         # footer.
         assert [change_id] == git_footers.get_footer_change_id(message)
-        change_desc = ChangeDescription(message, bug=bug)
+        change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
       else:  # if not self.GetIssue()
         if options.message:
           message = options.message
@@ -2375,7 +2380,7 @@
           message = _create_description_from_log(git_diff_args)
           if options.title:
             message = options.title + '\n\n' + message
-        change_desc = ChangeDescription(message, bug=bug)
+        change_desc = ChangeDescription(message, bug=bug, fixed=fixed)
         if not options.force:
           change_desc.prompt()
 
@@ -2752,13 +2757,14 @@
   R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
   CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
   BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
+  FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
   CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
   STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
   BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
   COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
   BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
 
-  def __init__(self, description, bug=None):
+  def __init__(self, description, bug=None, fixed=None):
     self._description_lines = (description or '').strip().splitlines()
     if bug:
       regexp = re.compile(self.BUG_LINE)
@@ -2766,6 +2772,12 @@
       if not any((regexp.match(line) for line in self._description_lines)):
         values = list(_get_bug_line_values(prefix, bug))
         self.append_footer('Bug: %s' % ', '.join(values))
+    if fixed:
+      regexp = re.compile(self.FIXED_LINE)
+      prefix = settings.GetBugPrefix()
+      if not any((regexp.match(line) for line in self._description_lines)):
+        values = list(_get_bug_line_values(prefix, fixed))
+        self.append_footer('Fixed: %s' % ', '.join(values))
 
   @property               # www.logilab.org/ticket/89786
   def description(self):  # pylint: disable=method-hidden
@@ -2869,9 +2881,11 @@
       '#--------------------This line is 72 characters long'
       '--------------------',
     ] + self._description_lines)
-    regexp = re.compile(self.BUG_LINE)
+    bug_regexp = re.compile(self.BUG_LINE)
+    fixed_regexp = re.compile(self.FIXED_LINE)
     prefix = settings.GetBugPrefix()
-    if not any((regexp.match(line) for line in self._description_lines)):
+    has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
+    if not any((has_issue(line) for line in self._description_lines)):
       self.append_footer('Bug: %s' % prefix)
 
     content = gclient_utils.RunEditor(self.description, True,
@@ -4358,6 +4372,11 @@
                          '--use-commit-queue or --cq-dry-run.')
   parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
                     help='Host of buildbucket. The default host is %default.')
+  parser.add_option('--fixed', '-x',
+                    help='List of bugs that will be commented on and marked '
+                         'fixed (pre-populates "Fixed:" tag). Same format as '
+                         '-b option / "Bug:" tag. If fixing several issues, '
+                         'separate with commas.')
   auth.add_auth_options(parser)
 
   orig_args = args