X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/7b601c9e979785f3521395a4f8b2abbe0b6408eb..4491d19b003ff492dcb614d88b32858743a9cc44:/stgit/commands/edit.py diff --git a/stgit/commands/edit.py b/stgit/commands/edit.py index a4d8f96..7daf156 100644 --- a/stgit/commands/edit.py +++ b/stgit/commands/edit.py @@ -18,14 +18,12 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -from optparse import OptionParser, make_option -from email.Utils import formatdate +from optparse import make_option -from stgit.commands.common import * -from stgit.utils import * +from stgit import git, utils +from stgit.commands import common +from stgit.lib import git as gitlib, transaction from stgit.out import * -from stgit import stack, git - help = 'edit a patch description or diff' usage = """%prog [options] [] @@ -49,29 +47,19 @@ separator: Diff text Command-line options can be used to modify specific information -without invoking the editor. +without invoking the editor. (With the --edit option, the editor is +invoked even if such command-line options are given.) -If the patch diff is edited but the patch application fails, the -rejected patch is stored in the .stgit-failed.patch file (and also in -.stgit-edit.{diff,txt}). The edited patch can be replaced with one of -these files using the '--file' and '--diff' options. -""" +If the patch diff is edited but does not apply, no changes are made to +the patch at all. The edited patch is saved to a file which you can +feed to "stg edit --file", once you have made sure it does apply.""" -directory = DirectoryGotoToplevel() +directory = common.DirectoryHasRepositoryLib() options = [make_option('-d', '--diff', help = 'edit the patch diff', action = 'store_true'), - make_option('-f', '--file', - help = 'use FILE instead of invoking the editor'), - make_option('-O', '--diff-opts', - help = 'options to pass to git-diff'), - make_option('--undo', - help = 'revert the commit generated by the last edit', - action = 'store_true'), - make_option('-a', '--annotate', metavar = 'NOTE', - help = 'annotate the patch log entry'), - make_option('-m', '--message', - help = 'replace the patch description with MESSAGE'), + make_option('-e', '--edit', action = 'store_true', + help = 'invoke interactive editor'), make_option('--author', metavar = '"NAME "', help = 'replae the author details with "NAME "'), make_option('--authname', @@ -84,161 +72,143 @@ options = [make_option('-d', '--diff', help = 'replace the committer name with COMMNAME'), make_option('--commemail', help = 'replace the committer e-mail with COMMEMAIL') - ] + make_sign_options() - -def __update_patch(pname, fname, options): - """Update the current patch from the given file. - """ - patch = crt_series.get_patch(pname) - - bottom = patch.get_bottom() - top = patch.get_top() - - f = open(fname) - message, author_name, author_email, author_date, diff = parse_patch(f) - f.close() - - out.start('Updating patch "%s"' % pname) - - if options.diff: - git.switch(bottom) - try: - git.apply_patch(fname) - except: - # avoid inconsistent repository state - git.switch(top) - raise - - crt_series.refresh_patch(message = message, - author_name = author_name, - author_email = author_email, - author_date = author_date, - backup = True, log = 'edit') - - if crt_series.empty_patch(pname): - out.done('empty patch') - else: - out.done() - -def __edit_update_patch(pname, options): - """Edit the given patch interactively. - """ - patch = crt_series.get_patch(pname) + ] + (utils.make_sign_options() + utils.make_message_options() + + utils.make_diff_opts_option()) - if options.diff_opts: - if not options.diff: - raise CmdException, '--diff-opts only available with --diff' - diff_flags = options.diff_opts.split() +def patch_diff(repository, cd, diff, diff_flags): + if diff: + diff = repository.diff_tree(cd.parent.data.tree, cd.tree, diff_flags) + return '\n'.join([git.diffstat(diff), diff]) else: - diff_flags = [] - - # generate the file to be edited - descr = patch.get_description().strip() - authdate = patch.get_authdate() - - tmpl = 'From: %(authname)s <%(authemail)s>\n' - if authdate: - tmpl += 'Date: %(authdate)s\n' - tmpl += '\n%(descr)s\n' - - tmpl_dict = { - 'descr': descr, - 'authname': patch.get_authname(), - 'authemail': patch.get_authemail(), - 'authdate': patch.get_authdate() - } - - if options.diff: - # add the patch diff to the edited file - bottom = patch.get_bottom() - top = patch.get_top() - - tmpl += '---\n\n' \ - '%(diffstat)s\n' \ - '%(diff)s' - - tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top) - tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top, - diff_flags = diff_flags) - - for key in tmpl_dict: - # make empty strings if key is not available - if tmpl_dict[key] is None: - tmpl_dict[key] = '' - - text = tmpl % tmpl_dict - - if options.diff: - fname = '.stgit-edit.diff' - else: - fname = '.stgit-edit.txt' - - # write the file to be edited - f = open(fname, 'w+') - f.write(text) - f.close() - - # invoke the editor - call_editor(fname) - - __update_patch(pname, fname, options) + return None + +def patch_description(cd, diff): + """Generate a string containing the description to edit.""" + + desc = ['From: %s <%s>' % (cd.author.name, cd.author.email), + 'Date: %s' % cd.author.date.isoformat(), + '', + cd.message] + if diff: + desc += ['---', + '', + diff] + return '\n'.join(desc) + +def patch_desc(repository, cd, failed_diff, diff, diff_flags): + return patch_description(cd, failed_diff or patch_diff( + repository, cd, diff, diff_flags)) + +def update_patch_description(repository, cd, text): + message, authname, authemail, authdate, diff = common.parse_patch(text) + cd = (cd.set_message(message) + .set_author(cd.author.set_name(authname) + .set_email(authemail) + .set_date(gitlib.Date.maybe(authdate)))) + failed_diff = None + if diff: + tree = repository.apply(cd.parent.data.tree, diff) + if tree == None: + failed_diff = diff + else: + cd = cd.set_tree(tree) + return cd, failed_diff def func(parser, options, args): """Edit the given patch or the current one. """ - crt_pname = crt_series.get_current() + stack = directory.repository.current_stack - if not args: - pname = crt_pname - if not pname: - raise CmdException, 'No patches applied' + if len(args) == 0: + if not stack.patchorder.applied: + raise common.CmdException( + 'Cannot edit top patch, because no patches are applied') + patchname = stack.patchorder.applied[-1] elif len(args) == 1: - pname = args[0] - if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname): - raise CmdException, 'Cannot edit unapplied or hidden patches' - elif not crt_series.patch_applied(pname): - raise CmdException, 'Unknown patch "%s"' % pname + [patchname] = args + if not stack.patches.exists(patchname): + raise common.CmdException('%s: no such patch' % patchname) else: - parser.error('incorrect number of arguments') - - check_local_changes() - check_conflicts() - check_head_top_equal(crt_series) - - if pname != crt_pname: - # Go to the patch to be edited - applied = crt_series.get_applied() - between = applied[:applied.index(pname):-1] - pop_patches(crt_series, between) - - if options.author: - options.authname, options.authemail = name_email(options.author) - - if options.undo: - out.start('Undoing the editing of "%s"' % pname) - crt_series.undo_refresh() - out.done() - elif options.message or options.authname or options.authemail \ - or options.authdate or options.commname or options.commemail \ - or options.sign_str: - # just refresh the patch with the given information - out.start('Updating patch "%s"' % pname) - crt_series.refresh_patch(message = options.message, - author_name = options.authname, - author_email = options.authemail, - author_date = options.authdate, - committer_name = options.commname, - committer_email = options.commemail, - backup = True, sign_str = options.sign_str, - log = 'edit', - notes = options.annotate) - out.done() - elif options.file: - __update_patch(pname, options.file, options) - else: - __edit_update_patch(pname, options) + parser.error('Cannot edit more than one patch') + + cd = orig_cd = stack.patches.get(patchname).commit.data - if pname != crt_pname: - # Push the patches back - between.reverse() - push_patches(crt_series, between) + # Read patch from user-provided description. + if options.message == None: + failed_diff = None + else: + cd, failed_diff = update_patch_description(stack.repository, cd, + options.message) + + # Modify author and committer data. + if options.author != None: + options.authname, options.authemail = common.name_email(options.author) + for p, f, val in [('author', 'name', options.authname), + ('author', 'email', options.authemail), + ('author', 'date', gitlib.Date.maybe(options.authdate)), + ('committer', 'name', options.commname), + ('committer', 'email', options.commemail)]: + if val != None: + cd = getattr(cd, 'set_' + p)( + getattr(getattr(cd, p), 'set_' + f)(val)) + + # Add Signed-off-by: or similar. + if options.sign_str != None: + cd = cd.set_message(utils.add_sign_line( + cd.message, options.sign_str, gitlib.Person.committer().name, + gitlib.Person.committer().email)) + + if options.save_template: + options.save_template( + patch_desc(stack.repository, cd, failed_diff, + options.diff, options.diff_flags)) + return utils.STGIT_SUCCESS + + # Let user edit the patch manually. + if cd == orig_cd or options.edit: + fn = '.stgit-edit.' + ['txt', 'patch'][bool(options.diff)] + cd, failed_diff = update_patch_description( + stack.repository, cd, utils.edit_string( + patch_desc(stack.repository, cd, failed_diff, + options.diff, options.diff_flags), + fn)) + + def failed(): + fn = '.stgit-failed.patch' + f = file(fn, 'w') + f.write(patch_desc(stack.repository, cd, failed_diff, + options.diff, options.diff_flags)) + f.close() + out.error('Edited patch did not apply.', + 'It has been saved to "%s".' % fn) + return utils.STGIT_COMMAND_ERROR + + # If we couldn't apply the patch, fail without even trying to + # effect any of the changes. + if failed_diff: + return failed() + + # The patch applied, so now we have to rewrite the StGit patch + # (and any patches on top of it). + iw = stack.repository.default_iw + trans = transaction.StackTransaction(stack, 'stg edit') + if patchname in trans.applied: + popped = trans.applied[trans.applied.index(patchname)+1:] + assert not trans.pop_patches(lambda pn: pn in popped) + else: + popped = [] + trans.patches[patchname] = stack.repository.commit(cd) + try: + for pn in popped: + trans.push_patch(pn, iw) + except transaction.TransactionHalted: + pass + try: + # Either a complete success, or a conflict during push. But in + # either case, we've successfully effected the edits the user + # asked us for. + return trans.run(iw) + except transaction.TransactionException: + # Transaction aborted -- we couldn't check out files due to + # dirty index/worktree. The edits were not carried out. + return failed()