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.
Signed-off-by: Doug Anderson <[email protected]> --- apps/patchwork/bin/pwclient | 128 +++++++++++++++++++++++++++++++++++++++++- 1 files changed, 124 insertions(+), 4 deletions(-) diff --git a/apps/patchwork/bin/pwclient b/apps/patchwork/bin/pwclient index 9588615..0c03ff5 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,57 @@ 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._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 a patch name into version, part_num, num_parts. + + @name: The patch name. + @return: (version, part_num, num_parts) + """ + mo = re.match(r"\[v(\d*),(\d*)/(\d*)\]", name) + if mo: + return mo.groups() + + mo = re.match(r"\[(\d*)/(\d*)\]", name) + if mo: + return (1, mo.groups()[0], mo.groups()[1]) + + mo = re.match(r"\[v(\d*)]", name) + if mo: + return (mo.groups()[0], 1, 1) + + return (1, 1, 1) + + 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 +182,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 +217,57 @@ 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 version / num_parts. + key = (patch.version, patch.num_parts) + all_patches = [p for p in all_patches if (p.version, p.num_parts) == key] + + # 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 +275,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 +437,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 +454,7 @@ def main(): project_str = "" commit_str = "" state_str = "" + series_str = "" hash_str = "" msgid_str = "" url = DEFAULT_URL @@ -354,6 +472,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 +544,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) -- 1.7.7.3 _______________________________________________ Patchwork mailing list [email protected] https://lists.ozlabs.org/listinfo/patchwork
