Patchwork [1/3] models: Add to_series() method to models

login
register
mail settings
Submitter Doug Anderson
Date Dec. 30, 2012, 6 a.m.
Message ID <1356847227-8917-2-git-send-email-dianders@chromium.org>
Download mbox | patch
Permalink /patch/208712/
State New
Headers show

Comments

Doug Anderson - Dec. 30, 2012, 6 a.m.
This method attempts to find other patches the are in the same series
as the specified patch.  This will be used in a follow-on patch which
adds an XMLRPC call to export this information.

Signed-off-by: Doug Anderson <dianders@chromium.org>

---
 apps/patchwork/models.py |  184 ++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 184 insertions(+), 0 deletions(-)

Patch

diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py
index 86a5266..e4ef62a 100644
--- a/apps/patchwork/models.py
+++ b/apps/patchwork/models.py
@@ -25,6 +25,7 @@  from django.conf import settings
 from patchwork.parser import hash_patch
 
 import re
+import collections
 import datetime, time
 import random
 
@@ -243,6 +244,189 @@  class Patch(models.Model):
         str = fname_re.sub('-', self.name)
         return str.strip('-') + '.patch'
 
+    @staticmethod
+    def _raw_patch_tags(name):
+        """Return a list of tags for the patch.
+
+        Tags are all in the [] section at the start of the patch name.
+
+        >>> Patch._raw_patch_tags('[1/2] Grok the frobber')
+        ['1/2']
+        >>> Patch._raw_patch_tags('[1/2,v5,RFC] Frob the grokker')
+        ['1/2', 'v5', 'RFC']
+        >>> Patch._raw_patch_tags('[RFC,V5,1/2] Krof the grubber')
+        ['RFC', 'V5', '1/2']
+        >>> Patch._raw_patch_tags('[1/2,v5] Brof the krogger [RESEND]')
+        ['1/2', 'v5']
+        >>> Patch._raw_patch_tags('Grok everything')
+        []
+
+        @name: The patch name
+        @return: A list of tags with no processing done on them.
+        """
+        mo = re.match(r"\[([^\]]*)\]", name)
+        if mo:
+            return mo.group(1).split(',')
+        return []
+
+    @staticmethod
+    def _parse_patch_name(name):
+        """Parse tags out of a patch name.
+
+        >>> sorted(Patch._parse_patch_name('[1/2] Grok the frobber').items())
+        [('num_parts', 2), ('part_num', 1), ('series_hash', 4004081833329042552), ('version', 1)]
+        >>> sorted(Patch._parse_patch_name('[2/2] Grok the frobber').items())
+        [('num_parts', 2), ('part_num', 2), ('series_hash', 4004081833329042552), ('version', 1)]
+
+        >>> sorted(Patch._parse_patch_name('[1/3,v5,RFC] Frob the grokker').items())
+        [('num_parts', 3), ('part_num', 1), ('series_hash', 8297604936906614254), ('version', 5)]
+        >>> sorted(Patch._parse_patch_name('[3/3,v5,RFC] Frob the grokker').items())
+        [('num_parts', 3), ('part_num', 3), ('series_hash', 8297604936906614254), ('version', 5)]
+
+        >>> sorted(Patch._parse_patch_name('[RFC,V5,1/2] Krof the grubber').items())
+        [('num_parts', 2), ('part_num', 1), ('series_hash', -776693167832596241), ('version', 5)]
+
+        >>> sorted(Patch._parse_patch_name('[1/2,v5] Brof the krogger [RESEND]').items())
+        [('num_parts', 2), ('part_num', 1), ('series_hash', -336123167251532293), ('version', 5)]
+
+        >>> sorted(Patch._parse_patch_name('Grok everything').items())
+        [('num_parts', 1), ('part_num', 1), ('series_hash', -3996966040418261153), ('version', 1)]
+
+        @name: The patch name.
+        @return: A dictionary with the following keys
+            version: integer version of the patch
+            part_num: integer part number of the patch
+            num_parts: integer number of parts in the patch
+            series_hash: A hash that all patches in the series will
+                share.  See the series_hash() method for details.
+        """
+        version = 1
+        part_num = 1
+        num_parts = 1
+        series_tags = []
+
+        # Work on one tag at a time
+        for tag in Patch._raw_patch_tags(name):
+            mo = re.match(r"(\d*)/(\d*)", tag)
+            if mo:
+                part_num = int(mo.group(1))
+                num_parts = int(mo.group(2))
+                continue
+
+            mo = re.match(r"[vV](\d*)", tag)
+            if mo:
+                version = int(mo.group(1))
+
+            series_tags.append(tag)
+
+        # Add num_parts to the series tags
+        series_tags.append("%d parts" % num_parts)
+
+        # Hash the tags so they're easy to compare
+        series_hash = hash(tuple(series_tags))
+
+        return {'version': version, 'part_num': part_num,
+                'num_parts': num_parts, 'series_hash': series_hash}
+
+    @property
+    def version(self):
+        """Get the version of this patch
+
+        @return: An integral version number.
+        """
+        return self._parse_patch_name(self.name)['version']
+
+    @property
+    def num_parts(self):
+        """Get the number of parts in the series this patch belongs to.
+
+        @return: The number of parts in the series.
+        """
+        return self._parse_patch_name(self.name)['num_parts']
+
+    @property
+    def part_num(self):
+        """Get the part number of this patch in its series.
+
+        @return: The part number of this patch in its series.
+        """
+        return self._parse_patch_name(self.name)['part_num']
+
+    @property
+    def series_hash(self):
+        """Get a hash that all patches in a series will share.
+
+        It's possible that patches that are not in the same series
+        will also have the same_hash.  However if the series_hash of
+        two patches is different then they're definitely not in the
+        same series.
+
+        The series hash includes:
+        - num parts
+        - tags (other than part number), including version number
+
+        @return: The series hash of this patch.
+        """
+        return self._parse_patch_name(self.name)['series_hash']
+
+    @property
+    def time(self):
+        """Get a numeric version of the patches date/time.
+
+        @return: A value from time.mktime
+        """
+        return time.mktime(self.date.timetuple())
+
+    def to_series(self):
+        """Return a list of patches in the same series as this one.
+
+        This function uses the following heuristics to find patches in
+        a series:
+
+        - It searches for all patches with the same submitter, the
+          same version number and same number of parts.
+        - It allows patches to span multiple projects (though they
+          must all be on the same patchwork server).  It prefers
+          patches that are part of the same project.  This handles
+          cases where some parts in a series might have only been sent
+          to a topic project (like "linux-mmc") but still tries to get
+          all patches from the same project if possible.
+        - For each part number it finds the matching patch that has a
+          date value closest to the original patch.
+
+        This does not currently try to take advantage of "Message-ID"
+        and "In_Reply-To".
+
+        @return: A list of patches in the series.
+        """
+        # Get the all patches by the submitter, ignoring project.
+        all_patches = Patch.objects.filter(submitter=self.submitter_id)
+
+        # Whittle down--only those with matching series_hash.
+        all_patches = [p for p in all_patches
+                       if p.series_hash == self.series_hash]
+
+        # Organize by part_num.
+        by_part_num = collections.defaultdict(list)
+        for p in all_patches:
+            by_part_num[p.part_num].append(p)
+
+        # Find the part that's closest in time to ours for each part num.
+        final_list = []
+        for part_num, patch_list in sorted(by_part_num.iteritems()):
+            # Create a list of tuples to make sorting easier.  We want
+            # to find the patch that has the closet time.  If there's
+            # a tie then we want the patch that has the same project
+            # ID...
+            patch_list = [(abs(p.time - self.time),
+                           abs(p.project_id - self.project_id),
+                           p) for p in patch_list]
+
+            best = sorted(patch_list)[0][-1]
+            final_list.append(best)
+
+        return final_list
+
     def mbox(self):
         postscript_re = re.compile('\n-{2,3} ?\n')