forklift: Support FROMGIT patches

Add a couple arguments to generate-report which allow the caller to
specify a subject prefix other than UPSTREAM and to specify a remote
tree for the "cherry from commit" line. These can be used to facilitate
FROMGIT forklifts.

Change-Id: I83c9a99c7b7bb5855fc2884cf248ff0168108f17
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2989688
Reviewed-by: Drew Davenport <ddavenport@chromium.org>
Tested-by: Sean Paul <seanpaul@chromium.org>
Commit-Queue: Sean Paul <seanpaul@chromium.org>
diff --git a/contrib/forklift/forklift.py b/contrib/forklift/forklift.py
index bbcbbde..47aeac7 100755
--- a/contrib/forklift/forklift.py
+++ b/contrib/forklift/forklift.py
@@ -9,6 +9,7 @@
 import argparse
 import json
 import pathlib
+import re
 import sys
 
 from git import Git
@@ -34,7 +35,7 @@
             self.sha = sha
             self.backported = backported
 
-    def __init__(self, report_path, bug=None, test=None):
+    def __init__(self, report_path, prefix=None, bug=None, test=None, tree=None):
         """Initializes the forklift report.
 
         Args:
@@ -43,13 +44,16 @@
             test: The value of TEST= field in the commit messages.
         """
         self._path = report_path
+        self.prefix = prefix
         self.bug = bug
         self.test = test
+        self.tree = tree
         self.commits = []
 
     def save(self):
         """Saves the report to the path given at init."""
-        output = {'bug': self.bug, 'test': self.test, 'commits': []}
+        output = {'prefix': self.prefix, 'bug': self.bug, 'test': self.test,
+                  'tree': self.tree, 'commits': []}
         for c in self.commits:
             output['commits'].append(c.__dict__)
 
@@ -66,8 +70,10 @@
             with open(self._path, mode='rb') as f:
                 j = json.load(f)
 
+            self.prefix = j['prefix']
             self.bug = j['bug']
             self.test = j['test']
+            self.tree = j['tree']
             for c in j['commits']:
                 self.commits.append(ForkliftReport.Commit(c['sha'],
                                                           c['backported']))
@@ -153,7 +159,8 @@
     if (args.list and not args.msg_id) or (not args.list and args.msg_id):
         raise ValueError('List and Message-Id must both be specified.')
 
-    report = ForkliftReport(args.report_path, args.bug, args.test)
+    report = ForkliftReport(args.report_path, args.prefix, args.bug, args.test,
+                            args.tree)
     pull_request = PullRequest(args.list, args.msg_id, args.local_pull)
     git = Git(args.git_path)
     if not git.fetch_refspec_from_remote(pull_request.source_tree,
@@ -177,15 +184,17 @@
 
     return 0
 
-def _format_commit_message(git, message, conflicted, bug, test):
+def _format_commit_message(git, report, message, conflicted):
     msg = message.rstrip().splitlines()
     if conflicted:
         if not msg[0].startswith('BACKPORT'):
-            msg[0] = 'BACKPORT: ' + msg[0]
+            msg[0] = f'BACKPORT/{report.prefix}: ' + msg[0]
     else:
-        msg[0] = 'UPSTREAM: ' + msg[0]
+        msg[0] = f'{report.prefix}: ' + msg[0]
 
-    found = {'bug': None, 'test': None, 'change-id': None}
+    re_cp = re.compile('\(cherry picked from commit ([0-9a-fA-F]+)\)')
+
+    found = {'bug': None, 'test': None, 'cherry-pick': None, 'change-id': None}
     for i, l in enumerate(msg[1:]):
         if l.startswith('BUG='):
             found['bug'] = i + 1
@@ -193,6 +202,8 @@
             found['test'] = i + 1
         elif l.startswith('Change-Id'):
             found['change-id'] = i + 1
+        elif re_cp.fullmatch(l):
+            found['cherry-pick'] = i + 1
 
     change_id = ''
     if found['change-id']:
@@ -202,13 +213,25 @@
         if ret:
             change_id = f'Change-Id: {cid}'
 
+    cherry_pick = ''
+    if found['cherry-pick']:
+        m = re_cp.fullmatch(msg.pop(found['cherry-pick']))
+        cherry_pick = f'(cherry picked from commit {m.group(1)}'
+        if report.tree:
+            msg.insert(found['cherry-pick'], cherry_pick)
+            msg.insert(found['cherry-pick'] + 1, f' {report.tree})')
+        else:
+            msg.insert(found['cherry-pick'], cherry_pick + ')')
+    else:
+        raise ValueError('Could not find cherry pick string!')
+
     if not found['bug'] or not found['test']:
         msg.append('')
 
     if not found['bug']:
-        msg.append(f'BUG={bug}')
+        msg.append(f'BUG={report.bug}')
     if not found['test']:
-        msg.append(f'TEST={test}')
+        msg.append(f'TEST={report.test}')
 
     if change_id:
         msg.append('')
@@ -273,8 +296,7 @@
         if not skipped:
             ret, msg = git.get_commit_message()
             if ret:
-                msg = _format_commit_message(git, msg, conflicted, report.bug,
-                                             report.test)
+                msg = _format_commit_message(git, report, msg, conflicted)
                 git.set_commit_message(msg)
 
         c.backported = True
@@ -446,6 +468,12 @@
                         help=('Optional common ancestor between the local and '
                               'remote trees. Improves execution time if '
                               'provided.'))
+    subparser_gen.add_argument('--prefix', type=str, default='UPSTREAM',
+                        help=('Optional subject prefix to use for forklifted '
+                              'patches. Defaults to "UPSTREAM"'))
+    subparser_gen.add_argument('--tree', type=str, default=None,
+                        help=('Optional tree to use in the "cherry picked" '
+                              'line in the commit message. Defaults to empty.'))
     subparser_gen.set_defaults(func=command_gen_report)
 
     subparser_pick = subparsers.add_parser('cherry-pick',