[PATCH 1/3] models: Add to_series() method to models
Doug Anderson
dianders at chromium.org
Sun Dec 30 17:00:25 EST 2012
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 at chromium.org>
---
apps/patchwork/models.py | 184 ++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 184 insertions(+), 0 deletions(-)
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')
--
1.7.7.3
More information about the Patchwork
mailing list