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 <[email protected]> --- 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 _______________________________________________ Patchwork mailing list [email protected] https://lists.ozlabs.org/listinfo/patchwork
