forklift: Add ability to use a local pull request from a file

In case a pull request hasn't hit the list yet, this patch adds the
ability to generate a pull request locally and then use the local file
as the forklift source.

Change-Id: I95acb8caddb33c876b976896aca3ba67b01aaa21
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2989673
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 aec1a65..bbcbbde 100755
--- a/contrib/forklift/forklift.py
+++ b/contrib/forklift/forklift.py
@@ -142,8 +142,19 @@
     Returns:
         0 if successful, non-zero otherwise.
     """
+
+    """Python doesn't support nesting of mutually exclusive arg groups yet
+    (https://bugs.python.org/issue10984), so we have to do this by hand
+    """
+    if not args.list and not args.msg_id and not args.local_pull:
+        raise ValueError('Must specify a pull request from the list or local.')
+    if (args.list or args.msg_id) and args.local_pull:
+        raise ValueError('Cannot specify a pull request from list and local.')
+    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)
-    pull_request = PullRequest(args.list, args.msg_id)
+    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,
                                          pull_request.source_ref):
@@ -421,10 +432,12 @@
     subparser_gen = subparsers.add_parser('generate-report',
                         parents=[parser_git, parser_report],
                         help='Generate a forklift report from pull request.')
-    subparser_gen.add_argument('--list', type=str, required=True,
+    subparser_gen.add_argument('--list', type=str,
                         help='Mailing list from lore.kernel.org/lists.html.')
-    subparser_gen.add_argument('--msg-id', type=str, required=True,
+    subparser_gen.add_argument('--msg-id', type=str,
                         help='Message-Id for the pull request to process.')
+    subparser_gen.add_argument('--local-pull', type=str,
+                               help='Path to a local pull request.')
     subparser_gen.add_argument('--bug', type=str, default='None',
                         help='Value to use for BUG= in commit descriptions.')
     subparser_gen.add_argument('--test', type=str, default='None',
diff --git a/contrib/forklift/pull_request.py b/contrib/forklift/pull_request.py
index 5d50e3f..e3b25d5 100644
--- a/contrib/forklift/pull_request.py
+++ b/contrib/forklift/pull_request.py
@@ -19,22 +19,30 @@
         source_tree: The location of the remote tree to fetch this pull from
         source_ref: The refspec from the remote to find the pull
     """
-    def __init__(self, mailing_list, msg_id):
+    def __init__(self, mailing_list, msg_id, local_pull):
         """Inits PullRequest class with given list/msgid.
 
         Args:
             mailing_list: The name of the mailing list to use on lore.
             msg_id: The Message-Id of the pull request e-mail.
+            local_pull: The path to a local pull request.
         """
-        url = f'https://lore.kernel.org/{mailing_list}/{msg_id}/raw'
-        req = requests.get(url)
-        req.raise_for_status()
+        if mailing_list and msg_id:
+            url = f'https://lore.kernel.org/{mailing_list}/{msg_id}/raw'
+            req = requests.get(url)
+            req.raise_for_status()
+            msg = mailbox.mboxMessage(req.text)
+            charset = msg.get_param('charset')
+            if not charset:
+                charset = 'us-ascii'
+            self._pull_request = msg.get_payload(decode=True).decode(charset)
 
-        msg = mailbox.mboxMessage(req.text)
-        charset = msg.get_param('charset')
-        if not charset:
-            charset = 'us-ascii'
-        self._pull_request = msg.get_payload(decode=True).decode(charset)
+        elif local_pull:
+            with open(local_pull, 'r') as f:
+                self._pull_request = f.read()
+        else:
+            raise ValueError('Invalid pull request source!')
+
         self._parse_pull_request()