X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~mdw/git/stgit/blobdiff_plain/5e888f30cf851fc0989ba73f935f6da7681d4903..bea1855455ee0df02edff19b26c49f8eb492fcf3:/stgit/utils.py?ds=sidebyside diff --git a/stgit/utils.py b/stgit/utils.py index 38dd474..2983ea8 100644 --- a/stgit/utils.py +++ b/stgit/utils.py @@ -1,7 +1,8 @@ """Common utility functions """ -import errno, os, os.path, re, sys +import errno, optparse, os, os.path, re, sys +from stgit.exception import * from stgit.config import config from stgit.out import * @@ -166,7 +167,7 @@ def rename(basedir, file1, file2): # file1's parent dir may not be empty after move pass -class EditorException(Exception): +class EditorException(StgException): pass def call_editor(filename): @@ -174,12 +175,8 @@ def call_editor(filename): # the editor editor = config.get('stgit.editor') - if editor: - pass - elif 'EDITOR' in os.environ: - editor = os.environ['EDITOR'] - else: - editor = 'vi' + if not editor: + editor = os.environ.get('EDITOR', 'vi') editor += ' %s' % filename out.start('Invoking the editor: "%s"' % editor) @@ -188,15 +185,29 @@ def call_editor(filename): raise EditorException, 'editor failed, exit code: %d' % err out.done() +def edit_string(s, filename): + f = file(filename, 'w') + f.write(s) + f.close() + call_editor(filename) + f = file(filename) + s = f.read() + f.close() + os.remove(filename) + return s + def patch_name_from_msg(msg): """Return a string to be used as a patch name. This is generated - from the top line of the string passed as argument, and is at most - 30 characters long.""" + from the top line of the string passed as argument.""" if not msg: return None + name_len = config.get('stgit.namelength') + if not name_len: + name_len = 30 + subject_line = msg.split('\n', 1)[0].lstrip().lower() - return re.sub('[\W]+', '-', subject_line).strip('-')[:30] + return re.sub('[\W]+', '-', subject_line).strip('-')[:name_len] def make_patch_name(msg, unacceptable, default_name = 'patch'): """Return a patch name generated from the given commit message, @@ -211,3 +222,172 @@ def make_patch_name(msg, unacceptable, default_name = 'patch'): suffix += 1 patchname = '%s-%d' % (patchname, suffix) return patchname + +# any and all functions are builtin in Python 2.5 and higher, but not +# in 2.4. +if not 'any' in dir(__builtins__): + def any(bools): + for b in bools: + if b: + return True + return False +if not 'all' in dir(__builtins__): + def all(bools): + for b in bools: + if not b: + return False + return True + +def make_sign_options(): + def callback(option, opt_str, value, parser, sign_str): + if parser.values.sign_str not in [None, sign_str]: + raise optparse.OptionValueError( + '--ack and --sign were both specified') + parser.values.sign_str = sign_str + return [optparse.make_option('--sign', action = 'callback', + callback = callback, dest = 'sign_str', + callback_args = ('Signed-off-by',), + help = 'add Signed-off-by line'), + optparse.make_option('--ack', action = 'callback', + callback = callback, dest = 'sign_str', + callback_args = ('Acked-by',), + help = 'add Acked-by line')] + +def add_sign_line(desc, sign_str, name, email): + if not sign_str: + return desc + sign_str = '%s: %s <%s>' % (sign_str, name, email) + if sign_str in desc: + return desc + desc = desc.rstrip() + if not any(s in desc for s in ['\nSigned-off-by:', '\nAcked-by:']): + desc = desc + '\n' + return '%s\n%s\n' % (desc, sign_str) + +def make_message_options(): + def no_dup(parser): + if parser.values.message != None: + raise optparse.OptionValueError( + 'Cannot give more than one --message or --file') + def no_combine(parser): + if (parser.values.message != None + and parser.values.save_template != None): + raise optparse.OptionValueError( + 'Cannot give both --message/--file and --save-template') + def msg_callback(option, opt_str, value, parser): + no_dup(parser) + parser.values.message = value + no_combine(parser) + def file_callback(option, opt_str, value, parser): + no_dup(parser) + if value == '-': + parser.values.message = sys.stdin.read() + else: + f = file(value) + parser.values.message = f.read() + f.close() + no_combine(parser) + def templ_callback(option, opt_str, value, parser): + if value == '-': + def w(s): + sys.stdout.write(s) + else: + def w(s): + f = file(value, 'w+') + f.write(s) + f.close() + parser.values.save_template = w + no_combine(parser) + m = optparse.make_option + return [m('-m', '--message', action = 'callback', callback = msg_callback, + dest = 'message', type = 'string', + help = 'use MESSAGE instead of invoking the editor'), + m('-f', '--file', action = 'callback', callback = file_callback, + dest = 'message', type = 'string', metavar = 'FILE', + help = 'use FILE instead of invoking the editor'), + m('--save-template', action = 'callback', callback = templ_callback, + metavar = 'FILE', dest = 'save_template', type = 'string', + help = 'save the message template to FILE and exit')] + +def make_diff_opts_option(): + def diff_opts_callback(option, opt_str, value, parser): + if value: + parser.values.diff_flags.extend(value.split()) + else: + parser.values.diff_flags = [] + return [optparse.make_option( + '-O', '--diff-opts', dest = 'diff_flags', + default = (config.get('stgit.diff-opts') or '').split(), + action = 'callback', callback = diff_opts_callback, + type = 'string', metavar = 'OPTIONS', + help = 'extra options to pass to "git diff"')] + +def parse_name_email(address): + """Return a tuple consisting of the name and email parsed from a + standard 'name ' or 'email (name)' string.""" + address = re.sub(r'[\\"]', r'\\\g<0>', address) + str_list = re.findall(r'^(.*)\s*<(.*)>\s*$', address) + if not str_list: + str_list = re.findall(r'^(.*)\s*\((.*)\)\s*$', address) + if not str_list: + return None + return (str_list[0][1], str_list[0][0]) + return str_list[0] + +def parse_name_email_date(address): + """Return a tuple consisting of the name, email and date parsed + from a 'name date' string.""" + address = re.sub(r'[\\"]', r'\\\g<0>', address) + str_list = re.findall('^(.*)\s*<(.*)>\s*(.*)\s*$', address) + if not str_list: + return None + return str_list[0] + +def make_person_options(person, short): + """Sets options. to a function that modifies a Person + according to the commandline options.""" + def short_callback(option, opt_str, value, parser, field): + f = getattr(parser.values, person) + setattr(parser.values, person, + lambda p: getattr(f(p), 'set_' + field)(value)) + def full_callback(option, opt_str, value, parser): + ne = parse_name_email(value) + if not ne: + raise optparse.OptionValueError( + 'Bad %s specification: %r' % (opt_str, value)) + name, email = ne + short_callback(option, opt_str, name, parser, 'name') + short_callback(option, opt_str, email, parser, 'email') + return ([optparse.make_option( + '--%s' % person, metavar = '"NAME "', type = 'string', + action = 'callback', callback = full_callback, dest = person, + default = lambda p: p, help = 'set the %s details' % person)] + + [optparse.make_option( + '--%s%s' % (short, f), metavar = f.upper(), type = 'string', + action = 'callback', callback = short_callback, dest = person, + callback_args = (f,), help = 'set the %s %s' % (person, f)) + for f in ['name', 'email', 'date']]) + +def make_author_committer_options(): + return (make_person_options('author', 'auth') + + make_person_options('committer', 'comm')) + +# Exit codes. +STGIT_SUCCESS = 0 # everything's OK +STGIT_GENERAL_ERROR = 1 # seems to be non-command-specific error +STGIT_COMMAND_ERROR = 2 # seems to be a command that failed +STGIT_CONFLICT = 3 # merge conflict, otherwise OK +STGIT_BUG_ERROR = 4 # a bug in StGit + +def strip_leading(prefix, s): + """Strip leading prefix from a string. Blow up if the prefix isn't + there.""" + assert s.startswith(prefix) + return s[len(prefix):] + +def add_dict(d1, d2): + """Return a new dict with the contents of both d1 and d2. In case of + conflicting mappings, d2 takes precedence.""" + d = dict(d1) + d.update(d2) + return d