[PATCH v2] pwclient: Add heuristics to find a whole series of patches
Mauro Carvalho Chehab
mchehab at redhat.com
Fri Dec 21 12:20:17 EST 2012
Em Thu, 20 Dec 2012 13:06:07 -0800
Doug Anderson <dianders at chromium.org> escreveu:
> Add a new filter option '-r' that attempts to list all patches in a
> series. Since there's no built-in way in patman to do this, we use
> some heuristics to try to find the series.
Instead of adding it at the client level, IMHO, it would be a way better
to add it at the patchwork server, allowing to show the patch group
as such and letting tag the entire patch group with the same tag
(as a v2 of a patch series superseeds a v1 of the same series).
Regards,
Mauro
>
> Signed-off-by: Doug Anderson <dianders at chromium.org>
>
> ---
> Changes in v2:
> - Handle more tag formats; use tags besides just version/num parts
> (like RFC, REPOST, etc) to identify a series
>
> apps/patchwork/bin/pwclient | 151 +++++++++++++++++++++++++++++++++++++++++-
> 1 files changed, 147 insertions(+), 4 deletions(-)
>
> diff --git a/apps/patchwork/bin/pwclient b/apps/patchwork/bin/pwclient
> index 9588615..77d78c7 100755
> --- a/apps/patchwork/bin/pwclient
> +++ b/apps/patchwork/bin/pwclient
> @@ -23,11 +23,14 @@ import os
> import sys
> import xmlrpclib
> import getopt
> +import re
> import string
> +import time
> import tempfile
> import subprocess
> import base64
> import ConfigParser
> +import collections
>
> # Default Patchwork remote XML-RPC server URL
> # This script will check the PW_XMLRPC_URL environment variable
> @@ -79,6 +82,81 @@ class Filter:
> """Return human-readable description of the filter."""
> return str(self.d)
>
> +class Patch(object):
> + """Nicer representation of a patch from the server."""
> +
> + def __init__(self, patch_dict):
> + """Patch constructor.
> +
> + @patch_dict: The dictionary version of the patch.
> + """
> + # Make it easy to compare times of patches by getting an int.
> + self.time = time.mktime(time.strptime(patch_dict["date"],
> + "%Y-%m-%d %H:%M:%S"))
> +
> + self.version, self.part_num, self.num_parts, self.series_tags = \
> + self._parse_patch_name(patch_dict["name"])
> +
> + # Add a few things to make it easier...
> + self.id = patch_dict["id"]
> + self.project_id = patch_dict["project_id"]
> + self.name = patch_dict["name"]
> + self.submitter_id = patch_dict["submitter_id"]
> +
> + # Keep the dict in case we need anything else...
> + self.dict = patch_dict
> +
> + @staticmethod
> + def _parse_patch_name(name):
> + """Parse tags out of a patch name.
> +
> +
> + @name: The patch name.
> + @return: version: integer version of the patch
> + @return: part_num: integer part number of the patch
> + @return: num_parts: integer number of parts in the patch
> + @return: series_tags: A tuple of tags that should be shared by all
> + patches in this series. Should be treated as opaque other
> + than comparing equality with other patches.
> + """
> + version = 1
> + part_num = 1
> + num_parts = 1
> + series_tags = []
> +
> + # Pull out tags between []; bail if tags aren't found.
> + mo = re.match(r"\[([^\]]*)\]", name)
> + if mo:
> + tags = mo.group(1).split(',')
> +
> + # Work on one tag at a time
> + for tag in tags:
> + 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)
> +
> + # Turn series_tags into a tuple so it's hashable
> + series_tags = tuple(series_tags)
> +
> + return (version, part_num, num_parts, series_tags)
> +
> + def __str__(self):
> + return str(self.dict)
> +
> + def __repr__(self):
> + return repr(self.dict)
> +
> class BasicHTTPAuthTransport(xmlrpclib.SafeTransport):
>
> def __init__(self, username = None, password = None, use_https = False):
> @@ -128,7 +206,8 @@ def usage():
> -w <who> : Filter by submitter (name, e-mail substring search)
> -d <who> : Filter by delegate (name, e-mail substring search)
> -n <max #> : Restrict number of results
> - -m <messageid>: Filter by Message-Id\n""")
> + -m <messageid>: Filter by Message-Id
> + -r <ID> : Filter by patches in the same series as <ID>\n""")
> sys.stderr.write("""\nActions that take an ID argument can also be \
> invoked with:
> -h <hash> : Lookup by patch hash\n""")
> @@ -162,6 +241,56 @@ def person_ids_by_name(rpc, name):
> people = rpc.person_list(name, 0)
> return map(lambda x: x['id'], people)
>
> +def patch_id_to_series(rpc, patch_id):
> + """Take a patch ID and return a list of patches in the same series.
> +
> + This function uses the following heuristics to find patches in a series:
> + - It searches for all patches with the same submitter that 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), though 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").
> + - For each part number it finds the matching patch that has a date value
> + closest to the original patch.
> +
> + It would be nice to use "Message-ID" and "In-Reply-To", but that's not
> + exported to the xmlrpc interface as far as I can tell. :(
> +
> + @patch_id: The patch ID that's part of the series.
> + @return: A list of patches in the series.
> + """
> + # Find this patch
> + patch = Patch(rpc.patch_get(patch_id))
> +
> + # Get the all patches by the submitter, ignoring project.
> + filter = Filter()
> + filter.add("submitter_id", patch.submitter_id)
> + all_patches = [Patch(p) for p in rpc.patch_list(filter.d)]
> +
> + # Whittle down--only those with matching series_tags.
> + all_patches = [p for p in all_patches if p.series_tags == patch.series_tags]
> +
> + # 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 - patch.time),
> + abs(p.project_id - patch.project_id),
> + p) for p in patch_list]
> +
> + best = sorted(patch_list)[0][-1]
> + final_list.append(best)
> +
> + return final_list
> +
> def list_patches(patches):
> """Dump a list of patches to stdout."""
> print("%-5s %-12s %s" % ("ID", "State", "Name"))
> @@ -169,9 +298,20 @@ def list_patches(patches):
> for patch in patches:
> print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name']))
>
> -def action_list(rpc, filter, submitter_str, delegate_str):
> +def action_list(rpc, filter, submitter_str, delegate_str, series_str):
> filter.resolve_ids(rpc)
>
> + if series_str != "":
> + try:
> + patch_id = int(series_str)
> + except:
> + sys.stderr.write("Invalid patch ID given\n")
> + sys.exit(1)
> +
> + patches = patch_id_to_series(rpc, patch_id)
> + list_patches([patch.dict for patch in patches])
> + return
> +
> if submitter_str != "":
> ids = person_ids_by_name(rpc, submitter_str)
> if len(ids) == 0:
> @@ -320,7 +460,7 @@ auth_actions = ['update']
>
> def main():
> try:
> - opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:m:')
> + opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:m:r:')
> except getopt.GetoptError, err:
> print str(err)
> usage()
> @@ -337,6 +477,7 @@ def main():
> project_str = ""
> commit_str = ""
> state_str = ""
> + series_str = ""
> hash_str = ""
> msgid_str = ""
> url = DEFAULT_URL
> @@ -354,6 +495,8 @@ def main():
> for name, value in opts:
> if name == '-s':
> state_str = value
> + elif name == '-r':
> + series_str = value
> elif name == '-p':
> project_str = value
> elif name == '-w':
> @@ -424,7 +567,7 @@ def main():
> if action == 'list' or action == 'search':
> if len(args) > 0:
> filt.add("name__icontains", args[0])
> - action_list(rpc, filt, submitter_str, delegate_str)
> + action_list(rpc, filt, submitter_str, delegate_str, series_str)
>
> elif action.startswith('project'):
> action_projects(rpc)
--
Cheers,
Mauro
More information about the Patchwork
mailing list