diff options
-rw-r--r-- | lib/git/index.py | 99 | ||||
-rw-r--r-- | test/git/test_index.py | 33 | ||||
-rw-r--r-- | test/testlib/helper.py | 13 |
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) |