[PATCH v2] pwclient: Add heuristics to find a whole series of patches
Doug Anderson
dianders at chromium.org
Fri Dec 21 08:06:07 EST 2012
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 <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)
--
1.7.7.3
More information about the Patchwork
mailing list