summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/git/index.py99
-rw-r--r--test/git/test_index.py33
-rw-r--r--test/testlib/helper.py13
3 files changed, 132 insertions, 13 deletions
diff --git a/lib/git/index.py b/lib/git/index.py
index 3bf1fac9..b19e0fff 100644
--- a/lib/git/index.py
+++ b/lib/git/index.py
@@ -986,6 +986,20 @@ class IndexFile(LazyMixin, diff.Diffable):
return entries_added
+ def _items_to_rela_paths(self, items):
+ """Returns a list of repo-relative paths from the given items which
+ may be absolute or relative paths, entries or blobs"""
+ paths = list()
+ for item in items:
+ if isinstance(item, (BaseIndexEntry,Blob)):
+ paths.append(self._to_relative_path(item.path))
+ elif isinstance(item, basestring):
+ paths.append(self._to_relative_path(item))
+ else:
+ raise TypeError("Invalid item type: %r" % item)
+ # END for each item
+ return paths
+
@clear_cache
@default_index
def remove(self, items, working_tree=False, **kwargs):
@@ -1021,7 +1035,8 @@ class IndexFile(LazyMixin, diff.Diffable):
as 'r' to allow recurive removal of
Returns
- List(path_string, ...) list of paths that have been removed effectively.
+ List(path_string, ...) list of repository relative paths that have
+ been removed effectively.
This is interesting to know in case you have provided a directory or
globs. Paths are relative to the repository.
"""
@@ -1031,22 +1046,84 @@ class IndexFile(LazyMixin, diff.Diffable):
args.append("--")
# preprocess paths
- paths = list()
- for item in items:
- if isinstance(item, (BaseIndexEntry,Blob)):
- paths.append(self._to_relative_path(item.path))
- elif isinstance(item, basestring):
- paths.append(self._to_relative_path(item))
- else:
- raise TypeError("Invalid item type: %r" % item)
- # END for each item
-
+ paths = self._items_to_rela_paths(items)
removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
# process output to gain proper paths
# rm 'path'
return [ p[4:-1] for p in removed_paths ]
+
+ @clear_cache
+ @default_index
+ def move(self, items, skip_errors=False, **kwargs):
+ """
+ Rename/move the items, whereas the last item is considered the destination of
+ the move operation. If the destination is a file, the first item ( of two )
+ must be a file as well. If the destination is a directory, it may be preceeded
+ by one or more directories or files.
+
+ The working tree will be affected in non-bare repositories.
+
+ ``items``
+ Multiple types of items are supported, please see the 'remove' method
+ for reference.
+ ``skip_errors``
+ If True, errors such as ones resulting from missing source files will
+ be skpped.
+ ``**kwargs``
+ Additional arguments you would like to pass to git-mv, such as dry_run
+ or force.
+
+ Returns
+ List(tuple(source_path_string, destination_path_string), ...)
+ A list of pairs, containing the source file moved as well as its
+ actual destination. Relative to the repository root.
+
+ Raises
+ ValueErorr: If only one item was given
+ GitCommandError: If git could not handle your request
+ """
+ args = list()
+ if skip_errors:
+ args.append('-k')
+
+ paths = self._items_to_rela_paths(items)
+ if len(paths) < 2:
+ raise ValueError("Please provide at least one source and one destination of the move operation")
+
+ was_dry_run = kwargs.pop('dry_run', kwargs.pop('n', None))
+ kwargs['dry_run'] = True
+
+ # first execute rename in dryrun so the command tells us what it actually does
+ # ( for later output )
+ out = list()
+ mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
+
+ # parse result - first 0:n/2 lines are 'checking ', the remaining ones
+ # are the 'renaming' ones which we parse
+ for ln in xrange(len(mvlines)/2, len(mvlines)):
+ tokens = mvlines[ln].split(' to ')
+ assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
+
+ # [0] = Renaming x
+ # [1] = y
+ out.append((tokens[0][9:], tokens[1]))
+ # END for each line to parse
+
+ # either prepare for the real run, or output the dry-run result
+ if was_dry_run:
+ return out
+ # END handle dryrun
+
+ # now apply the actual operation
+ kwargs.pop('dry_run')
+ self.repo.git.mv(args, paths, **kwargs)
+
+ return out
+
+
+
@default_index
def commit(self, message, parent_commits=None, head=True):
"""
diff --git a/test/git/test_index.py b/test/git/test_index.py
index a2689f99..7d3f13cd 100644
--- a/test/git/test_index.py
+++ b/test/git/test_index.py
@@ -14,10 +14,10 @@ import glob
import shutil
from stat import *
-class TestTree(TestBase):
+class TestIndex(TestBase):
def __init__(self, *args):
- super(TestTree, self).__init__(*args)
+ super(TestIndex, self).__init__(*args)
self._reset_progress()
def _assert_fprogress(self, entries):
@@ -498,3 +498,32 @@ class TestTree(TestBase):
open(fake_symlink_path,'rb').read() == link_target
else:
assert S_ISLNK(os.lstat(fake_symlink_path)[ST_MODE])
+
+ # TEST RENAMING
+ def assert_mv_rval(rval):
+ for source, dest in rval:
+ assert not os.path.exists(source) and os.path.exists(dest)
+ # END for each renamed item
+ # END move assertion utility
+
+ self.failUnlessRaises(ValueError, index.move, ['just_one_path'])
+ # file onto existing file
+ files = ['AUTHORS', 'LICENSE']
+ self.failUnlessRaises(GitCommandError, index.move, files)
+
+ # again, with force
+ assert_mv_rval(index.move(files, force=True))
+
+ # files into directory - dry run
+ paths = ['LICENSE', 'VERSION', 'doc']
+ rval = index.move(paths, dry_run=True)
+ assert len(rval) == 2
+ assert os.path.exists(paths[0])
+
+ # again, no dry run
+ rval = index.move(paths)
+ assert_mv_rval(rval)
+
+ # dir into dir
+ rval = index.move(['doc', 'test'])
+ assert_mv_rval(rval)
diff --git a/test/testlib/helper.py b/test/testlib/helper.py
index da4a4207..ba748a15 100644
--- a/test/testlib/helper.py
+++ b/test/testlib/helper.py
@@ -90,6 +90,7 @@ def with_bare_rw_repo(func):
def bare_repo_creator(self):
repo_dir = tempfile.mktemp("bare_repo")
rw_repo = self.rorepo.clone(repo_dir, shared=True, bare=True)
+ prev_cwd = os.getcwd()
try:
return func(self, rw_repo)
finally:
@@ -106,6 +107,9 @@ def with_rw_repo(working_tree_ref):
out the working tree at the given working_tree_ref.
This repository type is more costly due to the working copy checkout.
+
+ To make working with relative paths easier, the cwd will be set to the working
+ dir of the repository.
"""
assert isinstance(working_tree_ref, basestring), "Decorator requires ref name for working tree checkout"
def argument_passer(func):
@@ -116,9 +120,12 @@ def with_rw_repo(working_tree_ref):
rw_repo.head.commit = working_tree_ref
rw_repo.head.reference.checkout()
+ prev_cwd = os.getcwd()
+ os.chdir(rw_repo.working_dir)
try:
return func(self, rw_repo)
finally:
+ os.chdir(prev_cwd)
rw_repo.git.clear_cache()
shutil.rmtree(repo_dir, onerror=_rmtree_onerror)
# END cleanup
@@ -148,6 +155,8 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
def case(self, rw_repo, rw_remote_repo)
This setup allows you to test push and pull scenarios and hooks nicely.
+
+ See working dir info in with_rw_repo
"""
assert isinstance(working_tree_ref, basestring), "Decorator requires ref name for working tree checkout"
def argument_passer(func):
@@ -192,9 +201,13 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
else:
raise AssertionError('Please start a git-daemon to run this test, execute: git-daemon "%s"'%tempfile.gettempdir())
+ # adjust working dir
+ prev_cwd = os.getcwd()
+ os.chdir(rw_repo.working_dir)
try:
return func(self, rw_repo, rw_remote_repo)
finally:
+ os.chdir(prev_cwd)
rw_repo.git.clear_cache()
rw_remote_repo.git.clear_cache()
shutil.rmtree(repo_dir, onerror=_rmtree_onerror)