patman: Move Patch and Review to patchwork module

These relate to information obtained from the patchwork server, so move
their definition into the new patchwork module.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/tools/patman/status.py b/tools/patman/status.py
index 5fb436e..8edb4ce 100644
--- a/tools/patman/status.py
+++ b/tools/patman/status.py
@@ -11,25 +11,16 @@
 import collections
 import concurrent.futures
 from itertools import repeat
-import re
 
 import pygit2
 import requests
 
-from patman import patchstream
-from patman.patchstream import PatchStream
 from u_boot_pylib import terminal
 from u_boot_pylib import tout
-
-# Patches which are part of a multi-patch series are shown with a prefix like
-# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last
-# part is optional. This decodes the string into groups. For single patches
-# the [] part is not present:
-# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject)
-RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$')
+from patman import patchstream
+from patman import patchwork
+from patman.patchstream import PatchStream
 
-# This decodes the sequence string into a patch number and patch count
-RE_SEQ = re.compile(r'(\d+)/(\d+)')
 
 def to_int(vals):
     """Convert a list of strings into integers, using 0 if not an integer
@@ -43,106 +34,6 @@
     out = [int(val) if val.isdigit() else 0 for val in vals]
     return out
 
-
-class Patch(dict):
-    """Models a patch in patchwork
-
-    This class records information obtained from patchwork
-
-    Some of this information comes from the 'Patch' column:
-
-        [RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm
-
-    This shows the prefix, version, seq, count and subject.
-
-    The other properties come from other columns in the display.
-
-    Properties:
-        pid (str): ID of the patch (typically an integer)
-        seq (int): Sequence number within series (1=first) parsed from sequence
-            string
-        count (int): Number of patches in series, parsed from sequence string
-        raw_subject (str): Entire subject line, e.g.
-            "[1/2,v2] efi_loader: Sort header file ordering"
-        prefix (str): Prefix string or None (e.g. 'RFC')
-        version (str): Version string or None (e.g. 'v2')
-        raw_subject (str): Raw patch subject
-        subject (str): Patch subject with [..] part removed (same as commit
-            subject)
-    """
-    def __init__(self, pid):
-        super().__init__()
-        self.id = pid  # Use 'id' to match what the Rest API provides
-        self.seq = None
-        self.count = None
-        self.prefix = None
-        self.version = None
-        self.raw_subject = None
-        self.subject = None
-
-    # These make us more like a dictionary
-    def __setattr__(self, name, value):
-        self[name] = value
-
-    def __getattr__(self, name):
-        return self[name]
-
-    def __hash__(self):
-        return hash(frozenset(self.items()))
-
-    def __str__(self):
-        return self.raw_subject
-
-    def parse_subject(self, raw_subject):
-        """Parse the subject of a patch into its component parts
-
-        See RE_PATCH for details. The parsed info is placed into seq, count,
-        prefix, version, subject
-
-        Args:
-            raw_subject (str): Subject string to parse
-
-        Raises:
-            ValueError: the subject cannot be parsed
-        """
-        self.raw_subject = raw_subject.strip()
-        mat = RE_PATCH.search(raw_subject.strip())
-        if not mat:
-            raise ValueError("Cannot parse subject '%s'" % raw_subject)
-        self.prefix, self.version, seq_info, self.subject = mat.groups()[3:]
-        mat_seq = RE_SEQ.match(seq_info) if seq_info else False
-        if mat_seq is None:
-            self.version = seq_info
-            seq_info = None
-        if self.version and not self.version.startswith('v'):
-            self.prefix = self.version
-            self.version = None
-        if seq_info:
-            if mat_seq:
-                self.seq = int(mat_seq.group(1))
-                self.count = int(mat_seq.group(2))
-        else:
-            self.seq = 1
-            self.count = 1
-
-
-class Review:
-    """Represents a single review email collected in Patchwork
-
-    Patches can attract multiple reviews. Each consists of an author/date and
-    a variable number of 'snippets', which are groups of quoted and unquoted
-    text.
-    """
-    def __init__(self, meta, snippets):
-        """Create new Review object
-
-        Args:
-            meta (str): Text containing review author and date
-            snippets (list): List of snippets in th review, each a list of text
-                lines
-        """
-        self.meta = ' : '.join([line for line in meta.splitlines() if line])
-        self.snippets = snippets
 
 def compare_with_series(series, patches):
     """Compare a list of patches with a series it came from
@@ -253,7 +144,7 @@
     # Work through each row (patch) one at a time, collecting the information
     warn_count = 0
     for pw_patch in patch_dict:
-        patch = Patch(pw_patch['id'])
+        patch = patchwork.Patch(pw_patch['id'])
         patch.parse_subject(pw_patch['name'])
         patches.append(patch)
     if warn_count > 1:
@@ -304,7 +195,7 @@
         if pstrm.snippets:
             submitter = comment['submitter']
             person = '%s <%s>' % (submitter['name'], submitter['email'])
-            reviews.append(Review(person, pstrm.snippets))
+            reviews.append(patchwork.Review(person, pstrm.snippets))
         for response, people in pstrm.commit.rtags.items():
             rtags[response].update(people)