X-Git-Url: https://git.ozlabs.org/?a=blobdiff_plain;f=apps%2Fpatchwork%2Fbin%2Fpwclient;h=bd79b3a601de9d5834fea1bf424a0228bb428333;hb=af90fe7c550ada3c035f1a5a4f9b18a0b816232c;hp=dc836e918121fdac782e2111987817ecf33d2d93;hpb=d9552d03e981f1164856fc57e43dde9bdb1bf3d9;p=patchwork diff --git a/apps/patchwork/bin/pwclient b/apps/patchwork/bin/pwclient index dc836e9..bd79b3a 100755 --- a/apps/patchwork/bin/pwclient +++ b/apps/patchwork/bin/pwclient @@ -22,19 +22,20 @@ import os import sys import xmlrpclib -import getopt +import argparse import string import tempfile import subprocess import base64 import ConfigParser +import shutil # Default Patchwork remote XML-RPC server URL # This script will check the PW_XMLRPC_URL environment variable # for the URL to access. If that is unspecified, it will fallback to # the hardcoded default value specified here. DEFAULT_URL = "http://patchwork/xmlrpc/" -CONFIG_FILES = [os.path.expanduser('~/.pwclientrc')] +CONFIG_FILE = os.path.expanduser('~/.pwclientrc') class Filter: """Filter for selecting patches.""" @@ -105,33 +106,6 @@ class BasicHTTPAuthTransport(xmlrpclib.SafeTransport): fn = xmlrpclib.Transport.make_connection return fn(self, host) -def usage(): - sys.stderr.write("Usage: %s [options]\n\n" % \ - (os.path.basename(sys.argv[0]))) - sys.stderr.write("Where is one of:\n") - sys.stderr.write( -""" apply : Apply a patch (in the current dir, using -p1) - get : Download a patch and save it locally - projects : List all projects - states : Show list of potential patch states - list [str] : List patches, using the optional filters specified - below and an optional substring to search for patches - by name - search [str] : Same as 'list' - view : View a patch - update [-s state] [-c commit-ref] - : Update patch\n""") - sys.stderr.write("""\nFilter options for 'list' and 'search': - -s : Filter by patch state (e.g., 'New', 'Accepted', etc.) - -p : Filter by project name (see 'projects' for list) - -w : Filter by submitter (name, e-mail substring search) - -d : Filter by delegate (name, e-mail substring search) - -n : Restrict number of results\n""") - sys.stderr.write("""\nActions that take an ID argument can also be \ -invoked with: - -h : Lookup by patch hash\n""") - sys.exit(1) - def project_id_by_name(rpc, linkname): """Given a project short name, look up the Project ID.""" if len(linkname) == 0: @@ -162,10 +136,10 @@ def person_ids_by_name(rpc, name): def list_patches(patches): """Dump a list of patches to stdout.""" - print("%-5s %-12s %s" % ("ID", "State", "Name")) - print("%-5s %-12s %s" % ("--", "-----", "----")) + print("%-7s %-12s %s" % ("ID", "State", "Name")) + print("%-7s %-12s %s" % ("--", "-----", "----")) for patch in patches: - print("%-5d %-12s %s" % (patch['id'], patch['state'], patch['name'])) + print("%-7d %-12s %s" % (patch['id'], patch['state'], patch['name'])) def action_list(rpc, filter, submitter_str, delegate_str): filter.resolve_ids(rpc) @@ -173,13 +147,14 @@ def action_list(rpc, filter, submitter_str, delegate_str): if submitter_str != "": ids = person_ids_by_name(rpc, submitter_str) if len(ids) == 0: - sys.stderr.write("Note: Nobody found matching *%s*\n", \ + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ submitter_str) else: for id in ids: person = rpc.person_get(id) print "Patches submitted by %s <%s>:" % \ - (person['name'], person['email']) + (unicode(person['name']).encode("utf-8"), \ + unicode(person['email']).encode("utf-8")) f = filter f.add("submitter_id", id) patches = rpc.patch_list(f.d) @@ -189,7 +164,7 @@ def action_list(rpc, filter, submitter_str, delegate_str): if delegate_str != "": ids = person_ids_by_name(rpc, delegate_str) if len(ids) == 0: - sys.stderr.write("Note: Nobody found matching *%s*\n", \ + sys.stderr.write("Note: Nobody found matching *%s*\n" % \ delegate_str) else: for id in ids: @@ -221,6 +196,14 @@ def action_states(rpc): for state in states: print("%-5d %s" % (state['id'], state['name'])) +def action_info(rpc, patch_id): + patch = rpc.patch_get(patch_id) + s = "Information for patch id %d" % (patch_id) + print(s) + print('-' * len(s)) + for key, value in sorted(patch.iteritems()): + print("- %- 14s: %s" % (key, unicode(value).encode("utf-8"))) + def action_get(rpc, patch_id): patch = rpc.patch_get(patch_id) s = rpc.patch_get_mbox(patch_id) @@ -249,18 +232,25 @@ def action_get(rpc, patch_id): sys.stderr.write("Failed to write to %s\n" % fname) sys.exit(1) -def action_apply(rpc, patch_id): +def action_apply(rpc, patch_id, apply_cmd=None): patch = rpc.patch_get(patch_id) if patch == {}: sys.stderr.write("Error getting information on patch ID %d\n" % \ patch_id) sys.exit(1) - print "Applying patch #%d to current directory" % patch_id + + if apply_cmd is None: + print "Applying patch #%d to current directory" % patch_id + apply_cmd = ['patch', '-p1'] + else: + print "Applying patch #%d using %s" % ( + patch_id, repr(' '.join(apply_cmd))) + print "Description: %s" % patch['name'] s = rpc.patch_get_mbox(patch_id) if len(s) > 0: - proc = subprocess.Popen(['patch', '-p1'], stdin = subprocess.PIPE) - proc.communicate(s) + proc = subprocess.Popen(apply_cmd, stdin = subprocess.PIPE) + proc.communicate(unicode(s).encode('utf-8')) else: sys.stderr.write("Error: No patch content found\n") sys.exit(1) @@ -302,23 +292,194 @@ def patch_id_from_hash(rpc, project, hash): patch = rpc.patch_get_by_hash(hash) if patch == {}: - return None + sys.stderr.write("No patch has the hash provided\n") + sys.exit(1) - return patch['id'] + patch_id = patch['id'] + # be super paranoid + try: + patch_id = int(patch_id) + except: + sys.stderr.write("Invalid patch ID obtained from server\n") + sys.exit(1) + return patch_id auth_actions = ['update'] -def main(): - try: - opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:h:') - except getopt.GetoptError, err: - print str(err) - usage() +# unfortunately we currently have to revert to this ugly hack.. +class _RecursiveHelpAction(argparse._HelpAction): + + def __call__(self, parser, namespace, values, option_string=None): + parser.print_help() + print + + subparsers_actions = [ + action for action in parser._actions + if isinstance(action, argparse._SubParsersAction) + ] + for subparsers_action in subparsers_actions: + for choice, subparser in subparsers_action.choices.items(): + # gross but the whole thing is.. + if (len(subparser._actions) == 2 \ + and ['hash', 'id'] == [a.dest for a in subparser._actions])\ + or len(subparser._actions) == 0: + continue + print("command '{}'".format(choice)) + print(subparser.format_help()) + + parser.exit() +def main(): + hash_parser = argparse.ArgumentParser(add_help=False, version=False) + hash_parser.add_argument( + '-h', metavar='HASH', dest='hash', action='store', required=False, + help='''Lookup by patch hash''' + ) + hash_parser.add_argument( + 'id', metavar='ID', nargs='*', action='store', type=int, + help='Patch ID', + ) + + filter_parser = argparse.ArgumentParser(add_help=False, version=False) + filter_parser.add_argument( + '-s', metavar='STATE', + help='''Filter by patch state (e.g., 'New', 'Accepted', etc.)''' + ) + filter_parser.add_argument( + '-p', metavar='PROJECT', + help='''Filter by project name (see 'projects' for list)''' + ) + filter_parser.add_argument( + '-w', metavar='WHO', + help='''Filter by submitter (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-d', metavar='WHO', + help='''Filter by delegate (name, e-mail substring search)''' + ) + filter_parser.add_argument( + '-n', metavar='MAX#', + type=int, + help='''Restrict number of results''' + ) + filter_parser.add_argument( + '-m', metavar='MESSAGEID', + help='''Filter by Message-Id''' + ) + filter_parser.add_argument( + 'patch_name', metavar='STR', nargs='?', + help='substring to search for patches by name', + ) + + action_parser = argparse.ArgumentParser( + prog='pwclient', + add_help=False, + version=False, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='''(apply | get | info | view | update) (-h HASH | ID [ID ...])''', + ) + action_parser.add_argument( + '--help', + #action='help', + action=_RecursiveHelpAction, + help='''Print this help text''' + ) + + subparsers = action_parser.add_subparsers( + title='Commands', + metavar='' + ) + apply_parser = subparsers.add_parser( + 'apply', parents=[hash_parser], + add_help=False, + help='''Apply a patch (in the current dir, using -p1)''' + ) + apply_parser.set_defaults(subcmd='apply') + git_am_parser = subparsers.add_parser( + 'git-am', parents=[hash_parser], + add_help=False, + help='''Apply a patch to current git branch using "git am".''' + ) + git_am_parser.set_defaults(subcmd='git-am') + git_am_parser.add_argument( + '-s', '--signoff', + action='store_true', + help='''pass --signoff to git-am''' + ) + get_parser = subparsers.add_parser( + 'get', parents=[hash_parser], + add_help=False, + help='''Download a patch and save it locally''' + ) + get_parser.set_defaults(subcmd='get') + info_parser = subparsers.add_parser( + 'info', parents=[hash_parser], + add_help=False, + help='''Display patchwork info about a given patch ID''' + ) + info_parser.set_defaults(subcmd='info') + projects_parser = subparsers.add_parser( + 'projects', + add_help=False, + help='''List all projects''' + ) + projects_parser.set_defaults(subcmd='projects') + states_parser = subparsers.add_parser( + 'states', + add_help=False, + help='''Show list of potential patch states''' + ) + states_parser.set_defaults(subcmd='states') + view_parser = subparsers.add_parser( + 'view', parents=[hash_parser], + add_help=False, + help='''View a patch''' + ) + view_parser.set_defaults(subcmd='view') + update_parser = subparsers.add_parser( + 'update', parents=[hash_parser], + add_help=False, + help='''Update patch''', + epilog='''Using a COMMIT-REF allows for only one ID to be specified''', + ) + update_parser.set_defaults(subcmd='update') + update_parser.add_argument( + '-c', metavar='COMMIT-REF', + help='''commit reference hash''' + ) + update_parser.add_argument( + '-s', metavar='STATE', + required=True, + help='''Set patch state (e.g., 'Accepted', 'Superseded' etc.)''' + ) + + list_parser = subparsers.add_parser("list", + add_help=False, + #aliases=['search'], + parents=[filter_parser], + help='''List patches, using the optional filters specified + below and an optional substring to search for patches + by name''' + ) + list_parser.set_defaults(subcmd='list') + search_parser = subparsers.add_parser("search", + add_help=False, + parents=[filter_parser], + help='''Alias for "list"''' + ) + search_parser.set_defaults(subcmd='list') if len(sys.argv) < 2: - usage() + action_parser.print_help() + sys.exit(0) - action = sys.argv[1].lower() + args = action_parser.parse_args() + args=dict(vars(args)) + + if args.get('hash') and len(args.get('id')): + # mimic mutual exclusive group + sys.stderr.write("[-h HASH] and [ID [ID ...]] are mutually exlusive!\n") + action_parser.print_help() + sys.exit(1) # set defaults filt = Filter() @@ -327,57 +488,110 @@ def main(): project_str = "" commit_str = "" state_str = "" - hash_str = "" + hash_str = None + msgid_str = "" + patch_ids = None url = DEFAULT_URL - config = ConfigParser.ConfigParser() - config.read(CONFIG_FILES) + action = args.get('subcmd') + + if args.get('s'): + state_str = args.get('s') + if args.get('p'): + project_str = args.get('p') + if args.get('w'): + submitter_str = args.get('w') + if args.get('d'): + delegate_str = args.get('d') + if args.get('hash'): + hash_str = args.get('hash') + if args.get('id'): + patch_ids = frozenset(args.get('id')) + if args.get('c'): + # update multiple IDs with a single commit-hash does not make sense + if action == 'update' and patch_ids and len(patch_ids) > 1: + sys.stderr.write("Declining update with COMMIT-REF on multiple IDs\n") + update_parser.print_help() + sys.exit(1) + commit_str = args.get('c') + if args.get('m'): + msgid_str = args.get('m') + if args.get('n') != None: + try: + filt.add("max_count", args.get('n')) + except: + sys.stderr.write("Invalid maximum count '%s'\n" % args.get('n')) + action_parser.print_help() + sys.exit(1) + + do_signoff = args.get('signoff') # grab settings from config files - if config.has_option('base', 'url'): - url = config.get('base', 'url') - - if config.has_option('base', 'project'): - project_str = config.get('base', 'project') - - for name, value in opts: - if name == '-s': - state_str = value - elif name == '-p': - project_str = value - elif name == '-w': - submitter_str = value - elif name == '-d': - delegate_str = value - elif name == '-c': - commit_str = value - elif name == '-h': - hash_str = value - elif name == '-n': - try: - filt.add("max_count", int(value)) - except: - sys.stderr.write("Invalid maximum count '%s'\n" % value) - usage() - else: - sys.stderr.write("Unknown option '%s'\n" % name) - usage() + config = ConfigParser.ConfigParser() + config.read([CONFIG_FILE]) + + if not config.has_section('options'): + sys.stderr.write('~/.pwclientrc is in the old format. Migrating it...') + + old_project = config.get('base','project') + + new_config = ConfigParser.ConfigParser() + new_config.add_section('options') + + new_config.set('options','default',old_project) + new_config.add_section(old_project) + + new_config.set(old_project,'url',config.get('base','url')) + if config.has_option('auth', 'username'): + new_config.set(old_project,'username',config.get('auth','username')) + if config.has_option('auth', 'password'): + new_config.set(old_project,'password',config.get('auth','password')) + + old_config_file = CONFIG_FILE + '.orig' + shutil.copy2(CONFIG_FILE,old_config_file) + + with open(CONFIG_FILE, 'wb') as fd: + new_config.write(fd) - if len(args) > 1: - sys.stderr.write("Too many arguments specified\n") - usage() + sys.stderr.write(' Done.\n') + sys.stderr.write('Your old ~/.pwclientrc was saved to %s\n' % old_config_file) + sys.stderr.write('and was converted to the new format. You may want to\n') + sys.stderr.write('inspect it before continuing.\n') + sys.exit(1) + + if not project_str: + try: + project_str = config.get('options', 'default') + except: + sys.stderr.write("No default project configured in ~/.pwclientrc\n") + action_parser.print_help() + sys.exit(1) + + if not config.has_section(project_str): + sys.stderr.write("No section for project %s\n" % project_str) + sys.exit(1) + + if not config.has_option(project_str, 'url'): + sys.stderr.write("No URL for project %s\n" % project_str) + sys.exit(1) + if not do_signoff and config.has_option('options', 'signoff'): + do_signoff = config.getboolean('options', 'signoff') + if not do_signoff and config.has_option(project_str, 'signoff'): + do_signoff = config.getboolean(project_str, 'signoff') + + url = config.get(project_str, 'url') (username, password) = (None, None) transport = None if action in auth_actions: - if config.has_option('auth', 'username') and \ - config.has_option('auth', 'password'): + if config.has_option(project_str, 'username') and \ + config.has_option(project_str, 'password'): use_https = url.startswith('https') transport = BasicHTTPAuthTransport( \ - config.get('auth', 'username'), - config.get('auth', 'password'), + config.get(project_str, 'username'), + config.get(project_str, 'password'), use_https) else: @@ -391,23 +605,22 @@ def main(): if state_str: filt.add("state", state_str) + if msgid_str: + filt.add("msgid", msgid_str) + try: rpc = xmlrpclib.Server(url, transport = transport) except: sys.stderr.write("Unable to connect to %s\n" % url) sys.exit(1) - patch_id = None - if hash_str: - patch_id = patch_id_from_hash(rpc, project_str, hash_str) - if patch_id is None: - sys.stderr.write("No patch has the hash provided\n") - sys.exit(1) - + # It should be safe to assume hash_str is not zero, but who knows.. + if hash_str != None: + patch_ids = [patch_id_from_hash(rpc, project_str, hash_str)] if action == 'list' or action == 'search': - if len(args) > 0: - filt.add("name__icontains", args[0]) + if args.get('patch_name') != None: + filt.add("name__icontains", args.get('patch_name')) action_list(rpc, filt, submitter_str, delegate_str) elif action.startswith('project'): @@ -417,47 +630,34 @@ def main(): action_states(rpc) elif action == 'view': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - s = rpc.patch_get_mbox(patch_id) - if len(s) > 0: - print unicode(s).encode("utf-8") - - elif action == 'get' or action == 'save': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - action_get(rpc, patch_id) + for patch_id in patch_ids: + s = rpc.patch_get_mbox(patch_id) + if len(s) > 0: + print unicode(s).encode("utf-8") + + elif action in ('get', 'save', 'info'): + if action == 'info': + [action_info(rpc, patch_id) for patch_id in patch_ids] + else: + [action_get(rpc, patch_id) for patch_id in patch_ids] elif action == 'apply': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) + [action_apply(rpc, patch_id) for patch_id in patch_ids] - action_apply(rpc, patch_id) + elif action == 'git-am': + cmd = ['git', 'am'] + if do_signoff: + cmd.append('-s') + [action_apply(rpc, patch_id, cmd) for patch_id in patch_ids] elif action == 'update': - try: - patch_id = patch_id or int(args[0]) - except: - sys.stderr.write("Invalid patch ID given\n") - sys.exit(1) - - action_update_patch(rpc, patch_id, state = state_str, - commit = commit_str) + [action_update_patch(rpc, patch_id, state = state_str, + commit = commit_str) for patch_id in patch_ids] else: sys.stderr.write("Unknown action '%s'\n" % action) - usage() + action_parser.print_help() + sys.exit(1) if __name__ == "__main__": main()