+# -*- coding: utf-8 -*-
__copyright__ = """
Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
+Copyright (C) 2008, Karl Hasselström <kha@treskal.com>
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
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
"""
-import sys, os
from stgit.argparse import opt
-from stgit.commands.common import *
-from stgit.utils import *
-from stgit.out import *
-from stgit import stack, git
-from stgit.config import config
+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] [<files or dirs>]']
description = """
-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 '--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."""
+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('-f', '--force', action = 'store_true',
- short = 'Force the refresh even if HEAD and top differ'),
- opt('--update', action = 'store_true',
+ opt('-u', '--update', action = 'store_true',
short = 'Only update the current patch files'),
- opt('--index', action = 'store_true',
+ 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('--undo', action = 'store_true',
- short = 'Revert the commit generated by the last refresh'),
- opt('-a', '--annotate', metavar = 'NOTE',
- short = 'Annotate the patch log entry'),
opt('-p', '--patch',
short = 'Refresh (applied) PATCH instead of the top patch')]
-directory = DirectoryHasRepository(log = True)
+directory = common.DirectoryHasRepositoryLib()
-def func(parser, options, args):
- """Generate a new commit for the current or given patch.
- """
- args = git.ls_files(args)
- directory.cd_to_topdir()
-
- autoresolved = config.get('stgit.autoresolved')
- if autoresolved != 'yes':
- check_conflicts()
-
- if options.patch:
- if args or options.update:
- raise CmdException, \
- 'Only full refresh is available with the --patch option'
- patch = options.patch
- if not crt_series.patch_applied(patch):
- raise CmdException, 'Patches "%s" not applied' % patch
+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:
+ 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<stgit.git.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:
+ 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:
- patch = crt_series.get_current()
- if not patch:
- raise CmdException, 'No patches applied'
-
- if options.index:
- if args or options.update:
- raise CmdException, \
- 'Only full refresh is available with the --index option'
- if options.patch:
- raise CmdException, \
- '--patch is not compatible with the --index option'
-
- if not options.force:
- check_head_top_equal(crt_series)
-
- if options.undo:
- out.start('Undoing the refresh of "%s"' % patch)
- crt_series.undo_refresh()
- out.done()
- return
-
- if not options.index:
- files = [path for (stat, path) in git.tree_status(files = args, verbose = True)]
-
- if options.index or files or not crt_series.head_top_equal():
- if options.patch:
- applied = crt_series.get_applied()
- between = applied[:applied.index(patch):-1]
- pop_patches(crt_series, between, keep = True)
- elif options.update:
- rev1 = git_id(crt_series, 'HEAD^')
- rev2 = git_id(crt_series, 'HEAD')
- patch_files = git.barefiles(rev1, rev2).split('\n')
- files = [f for f in files if f in patch_files]
- if not files:
- out.info('No modified files for updating patch "%s"' % patch)
- return
-
- out.start('Refreshing patch "%s"' % patch)
-
- if autoresolved == 'yes':
- resolved_all()
-
- if options.index:
- crt_series.refresh_patch(cache_update = False,
- backup = True, notes = options.annotate)
- else:
- crt_series.refresh_patch(files = files,
- backup = True, notes = options.annotate)
-
- if crt_series.empty_patch(patch):
- out.done('empty patch')
- else:
- out.done()
-
- if options.patch:
- between.reverse()
- push_patches(crt_series, between)
- elif options.annotate:
- # only annotate the top log entry as there is no need to
- # refresh the patch and generate a full commit
- crt_series.log_patch(crt_series.get_patch(patch), None,
- notes = options.annotate)
+ # 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:
- out.info('Patch "%s" is already up to date' % patch)
+ 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)
@type env: dict
@param env: Extra environment"""
return RunWithEnv.run(self, args, env).cwd(self.cwd)
+ def run_in_cwd(self, args):
+ """Run the given command with an environment given by self.env and
+ self.env_in_cwd, without changing the current working
+ directory.
+
+ @type args: list of strings
+ @param args: Command and argument vector"""
+ return RunWithEnv.run(self, args, self.env_in_cwd)
class Repository(RunWithEnv):
"""Represents a git repository."""
assert isinstance(t2, Tree)
return self.run(['git', 'diff-tree', '-p'] + list(diff_opts)
+ [t1.sha1, t2.sha1]).raw_output()
+ def diff_tree_files(self, t1, t2):
+ """Given two L{Tree}s C{t1} and C{t2}, iterate over all files for
+ which they differ. For each file, yield a tuple with the old
+ file mode, the new file mode, the old blob, the new blob, the
+ status, the old filename, and the new filename. Except in case
+ of a copy or a rename, the old and new filenames are
+ identical."""
+ assert isinstance(t1, Tree)
+ assert isinstance(t2, Tree)
+ i = iter(self.run(['git', 'diff-tree', '-r', '-z'] + [t1.sha1, t2.sha1]
+ ).raw_output().split('\0'))
+ try:
+ while True:
+ x = i.next()
+ if not x:
+ continue
+ omode, nmode, osha1, nsha1, status = x[1:].split(' ')
+ fn1 = i.next()
+ if status[0] in ['C', 'R']:
+ fn2 = i.next()
+ else:
+ fn2 = fn1
+ yield (omode, nmode, self.get_blob(osha1),
+ self.get_blob(nsha1), status, fn1, fn2)
+ except StopIteration:
+ pass
class MergeException(exception.StgException):
"""Exception raised when a merge fails for some reason."""
def read_tree(self, tree):
self.run(['git', 'read-tree', tree.sha1]).no_output()
def write_tree(self):
+ """Write the index contents to the repository.
+ @return: The resulting L{Tree}
+ @rtype: L{Tree}"""
try:
return self.__repository.get_tree(
self.run(['git', 'write-tree']).discard_stderr(
def __init__(self, directory):
self.__directory = directory
env = property(lambda self: { 'GIT_WORK_TREE': '.' })
+ env_in_cwd = property(lambda self: { 'GIT_WORK_TREE': self.directory })
directory = property(lambda self: self.__directory)
class CheckoutException(exception.StgException):
index = property(lambda self: self.__index)
env = property(lambda self: utils.add_dict(self.__index.env,
self.__worktree.env))
+ env_in_cwd = property(lambda self: self.__worktree.env_in_cwd)
cwd = property(lambda self: self.__worktree.directory)
def checkout_hard(self, tree):
assert isinstance(tree, Tree)
raise MergeConflictException()
else:
raise MergeException('Index/worktree dirty')
- def changed_files(self):
- return self.run(['git', 'diff-files', '--name-only']).output_lines()
- def update_index(self, files):
- self.run(['git', 'update-index', '--remove', '-z', '--stdin']
- ).input_nulterm(files).discard_output()
+ def changed_files(self, tree, pathlimits = []):
+ """Return the set of files in the worktree that have changed with
+ respect to C{tree}. The listing is optionally restricted to
+ those files that match any of the path limiters given.
+
+ The path limiters are relative to the current working
+ directory; the returned file names are relative to the
+ repository root."""
+ assert isinstance(tree, Tree)
+ return set(self.run_in_cwd(
+ ['git', 'diff-index', tree.sha1, '--name-only', '-z', '--']
+ + list(pathlimits)).raw_output().split('\0')[:-1])
+ def update_index(self, paths):
+ """Update the index with files from the worktree. C{paths} is an
+ iterable of paths relative to the root of the repository."""
+ cmd = ['git', 'update-index', '--remove']
+ self.run(cmd + ['-z', '--stdin']
+ ).input_nulterm(paths).discard_output()
class Branch(object):
"""Represents a Git branch."""