X-Git-Url: https://git.distorted.org.uk/~mdw/stgit/blobdiff_plain/6ad48e4898f40d58a236de38f0f49a9f8bfb7ab2..85aaed816bde469b3de72e80d8f8ed7830302fbf:/stgit/commands/refresh.py diff --git a/stgit/commands/refresh.py b/stgit/commands/refresh.py index 48b406a..384cfb9 100644 --- a/stgit/commands/refresh.py +++ b/stgit/commands/refresh.py @@ -1,6 +1,8 @@ +# -*- coding: utf-8 -*- __copyright__ = """ Copyright (C) 2005, Catalin Marinas +Copyright (C) 2008, Karl Hasselström This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as @@ -16,96 +18,214 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -import sys, os -from optparse import OptionParser, make_option - -from stgit.commands.common import * -from stgit.utils import * -from stgit import stack, git -from stgit.config import config - - -help = 'generate a new commit for the current patch' -usage = """%prog [options] - -Include the latest tree changes in the current patch. This command -generates a new GIT commit object with the patch details, the previous -one no longer being visible. The patch attributes like author, -committer and description can be changed with the command line -options. The '--force' option is useful when a commit object was -created with a different tool but the changes need to be included in -the current patch.""" - -options = [make_option('-f', '--force', - help = 'force the refresh even if HEAD and '\ - 'top differ', - action = 'store_true'), - make_option('-e', '--edit', - help = 'invoke an editor for the patch '\ - 'description', - action = 'store_true'), - make_option('-s', '--showpatch', - help = 'show the patch content in the editor buffer', - action = 'store_true'), - make_option('-m', '--message', - help = 'use MESSAGE as the patch ' \ - 'description'), - make_option('-a', '--author', metavar = '"NAME "', - help = 'use "NAME " as the author details'), - make_option('--authname', - help = 'use AUTHNAME as the author name'), - make_option('--authemail', - help = 'use AUTHEMAIL as the author e-mail'), - make_option('--authdate', - help = 'use AUTHDATE as the author date'), - make_option('--commname', - help = 'use COMMNAME as the committer name'), - make_option('--commemail', - help = 'use COMMEMAIL as the committer ' \ - 'e-mail')] - - -def func(parser, options, args): - if len(args) != 0: - parser.error('incorrect number of arguments') - - if config.has_option('stgit', 'autoresolved'): - autoresolved = config.get('stgit', 'autoresolved') +from stgit.argparse import opt +from stgit.commands import common +from stgit.lib import git, transaction +from stgit.out import out +from stgit import utils + +help = 'Generate a new commit for the current patch' +kind = 'patch' +usage = ['[options] []'] +description = """ +Include the latest work tree and index changes in the current patch. +This command generates a new git commit object for the patch; the old +commit is no longer visible. + +You may optionally list one or more files or directories relative to +the current working directory; if you do, only matching files will be +updated. + +Behind the scenes, stg refresh first creates a new temporary patch +with your updates, and then merges that patch into the patch you asked +to have refreshed. If you asked to refresh a patch other than the +topmost patch, there can be conflicts; in that case, the temporary +patch will be left for you to take care of, for example with stg +coalesce. + +The creation of the temporary patch is recorded in a separate entry in +the patch stack log; this means that one undo step will undo the merge +between the other patch and the temp patch, and two undo steps will +additionally get rid of the temp patch.""" + +options = [ + opt('-u', '--update', action = 'store_true', + short = 'Only update the current patch files'), + opt('-i', '--index', action = 'store_true', + short = 'Refresh from index instead of worktree', long = """ + Instead of setting the patch top to the current contents of + the worktree, set it to the current contents of the index."""), + opt('-p', '--patch', + short = 'Refresh (applied) PATCH instead of the top patch')] + +directory = common.DirectoryHasRepositoryLib() + +def get_patch(stack, given_patch): + """Get the name of the patch we are to refresh.""" + if given_patch: + patch_name = given_patch + if not stack.patches.exists(patch_name): + raise common.CmdException('%s: no such patch' % patch_name) + return patch_name else: - autoresolved = 'no' - - if autoresolved != 'yes': - check_conflicts() - - patch = crt_series.get_current() - if not patch: - raise CmdException, 'No patches applied' - - if not options.force: - check_head_top_equal() - - if options.author: - options.authname, options.authemail = name_email(options.author) - - if git.local_changes() \ - or not crt_series.head_top_equal() \ - or options.edit or options.message \ - or options.authname or options.authemail or options.authdate \ - or options.commname or options.commemail: - print 'Refreshing patch "%s"...' % patch, - sys.stdout.flush() - - if autoresolved == 'yes': - resolved_all() - crt_series.refresh_patch(message = options.message, - edit = options.edit, - show_patch = options.showpatch, - author_name = options.authname, - author_email = options.authemail, - author_date = options.authdate, - committer_name = options.commname, - committer_email = options.commemail) - - print 'done' + if not stack.patchorder.applied: + raise common.CmdException( + 'Cannot refresh top patch, because no patches are applied') + return stack.patchorder.applied[-1] + +def list_files(stack, patch_name, args, index, update): + """Figure out which files to update.""" + if index: + # --index: Don't update the index. + return set() + paths = stack.repository.default_iw.changed_files( + stack.head.data.tree, args or []) + if update: + # --update: Restrict update to the paths that were already + # part of the patch. + paths &= stack.patches.get(patch_name).files() + return paths + +def write_tree(stack, paths, temp_index): + """Possibly update the index, and then write its tree. + @return: The written tree. + @rtype: L{Tree}""" + def go(index): + if paths: + iw = git.IndexAndWorktree(index, stack.repository.default_worktree) + iw.update_index(paths) + return index.write_tree() + if temp_index: + index = stack.repository.temp_index() + try: + index.read_tree(stack.head) + return go(index) + finally: + index.delete() + stack.repository.default_iw.update_index(paths) else: - print 'Patch "%s" is already up to date' % patch + return go(stack.repository.default_index) + +def make_temp_patch(stack, patch_name, paths, temp_index): + """Commit index to temp patch, in a complete transaction. If any path + limiting is in effect, use a temp index.""" + tree = write_tree(stack, paths, temp_index) + commit = stack.repository.commit(git.CommitData( + tree = tree, parents = [stack.head], + message = 'Refresh of %s' % patch_name)) + temp_name = utils.make_patch_name('refresh-temp', stack.patches.exists) + trans = transaction.StackTransaction(stack, + 'refresh (create temporary patch)') + trans.patches[temp_name] = commit + trans.applied.append(temp_name) + return trans.run(stack.repository.default_iw, + print_current_patch = False), temp_name + +def absorb_applied(trans, iw, patch_name, temp_name): + """Absorb the temp patch (C{temp_name}) into the given patch + (C{patch_name}), which must be applied. + + @return: C{True} if we managed to absorb the temp patch, C{False} + if we had to leave it for the user to deal with.""" + temp_absorbed = False + try: + # Pop any patch on top of the patch we're refreshing. + to_pop = trans.applied[trans.applied.index(patch_name)+1:] + if len(to_pop) > 1: + popped = trans.pop_patches(lambda pn: pn in to_pop) + assert not popped # no other patches were popped + trans.push_patch(temp_name, iw) + assert to_pop.pop() == temp_name + + # Absorb the temp patch. + temp_cd = trans.patches[temp_name].data + assert trans.patches[patch_name] == temp_cd.parent + trans.patches[patch_name] = trans.stack.repository.commit( + trans.patches[patch_name].data.set_tree(temp_cd.tree)) + popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True) + assert not popped # the temp patch was topmost + temp_absorbed = True + + # Push back any patch we were forced to pop earlier. + for pn in to_pop: + trans.push_patch(pn, iw) + except transaction.TransactionHalted: + pass + return temp_absorbed + +def absorb_unapplied(trans, iw, patch_name, temp_name): + """Absorb the temp patch (C{temp_name}) into the given patch + (C{patch_name}), which must be unapplied. + + @param iw: Not used. + @return: C{True} if we managed to absorb the temp patch, C{False} + if we had to leave it for the user to deal with.""" + + # Pop the temp patch. + popped = trans.pop_patches(lambda pn: pn == temp_name) + assert not popped # the temp patch was topmost + + # Try to create the new tree of the refreshed patch. (This is the + # same operation as pushing the temp patch onto the patch we're + # trying to refresh -- but we don't have a worktree to spill + # conflicts to, so if the simple merge doesn't succeed, we have to + # give up.) + patch_cd = trans.patches[patch_name].data + temp_cd = trans.patches[temp_name].data + new_tree = trans.stack.repository.simple_merge( + base = temp_cd.parent.data.tree, + ours = patch_cd.tree, theirs = temp_cd.tree) + if new_tree: + # It worked. Refresh the patch with the new tree, and delete + # the temp patch. + trans.patches[patch_name] = trans.stack.repository.commit( + patch_cd.set_tree(new_tree)) + popped = trans.delete_patches(lambda pn: pn == temp_name, quiet = True) + assert not popped # the temp patch was not applied + return True + else: + # Nope, we couldn't create the new tree, so we'll just have to + # leave the temp patch for the user. + return False + +def absorb(stack, patch_name, temp_name): + """Absorb the temp patch into the target patch.""" + trans = transaction.StackTransaction(stack, 'refresh') + iw = stack.repository.default_iw + f = { True: absorb_applied, False: absorb_unapplied + }[patch_name in trans.applied] + if f(trans, iw, patch_name, temp_name): + def info_msg(): pass + else: + def info_msg(): + out.warn('The new changes did not apply cleanly to %s.' + % patch_name, 'They were saved in %s.' % temp_name) + r = trans.run(iw) + info_msg() + return r + +def func(parser, options, args): + """Generate a new commit for the current or given patch.""" + + # Catch illegal argument combinations. + path_limiting = bool(args or options.update) + if options.index and path_limiting: + raise common.CmdException( + 'Only full refresh is available with the --index option') + + stack = directory.repository.current_stack + patch_name = get_patch(stack, options.patch) + paths = list_files(stack, patch_name, args, options.index, options.update) + + # Make sure there are no conflicts in the files we want to + # refresh. + if stack.repository.default_index.conflicts() & paths: + raise common.CmdException( + 'Cannot refresh -- resolve conflicts first') + + # Commit index to temp patch, and absorb it into the target patch. + retval, temp_name = make_temp_patch( + stack, patch_name, paths, temp_index = path_limiting) + if retval != utils.STGIT_SUCCESS: + return retval + return absorb(stack, patch_name, temp_name)