Skip to content

Commit f96ee74

Browse files
committed
index: added move method including test
test.helpers: temporary rw repository creators now set the working dir of the program, easing working with relative paths a lot
1 parent 76fd1d4 commit f96ee74

File tree

3 files changed

+132
-13
lines changed

3 files changed

+132
-13
lines changed

lib/git/index.py

+88-11
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,20 @@ def add(self, items, force=True, fprogress=lambda *args: None):
986986

987987
return entries_added
988988

989+
def _items_to_rela_paths(self, items):
990+
"""Returns a list of repo-relative paths from the given items which
991+
may be absolute or relative paths, entries or blobs"""
992+
paths = list()
993+
for item in items:
994+
if isinstance(item, (BaseIndexEntry,Blob)):
995+
paths.append(self._to_relative_path(item.path))
996+
elif isinstance(item, basestring):
997+
paths.append(self._to_relative_path(item))
998+
else:
999+
raise TypeError("Invalid item type: %r" % item)
1000+
# END for each item
1001+
return paths
1002+
9891003
@clear_cache
9901004
@default_index
9911005
def remove(self, items, working_tree=False, **kwargs):
@@ -1021,7 +1035,8 @@ def remove(self, items, working_tree=False, **kwargs):
10211035
as 'r' to allow recurive removal of
10221036
10231037
Returns
1024-
List(path_string, ...) list of paths that have been removed effectively.
1038+
List(path_string, ...) list of repository relative paths that have
1039+
been removed effectively.
10251040
This is interesting to know in case you have provided a directory or
10261041
globs. Paths are relative to the repository.
10271042
"""
@@ -1031,22 +1046,84 @@ def remove(self, items, working_tree=False, **kwargs):
10311046
args.append("--")
10321047

10331048
# preprocess paths
1034-
paths = list()
1035-
for item in items:
1036-
if isinstance(item, (BaseIndexEntry,Blob)):
1037-
paths.append(self._to_relative_path(item.path))
1038-
elif isinstance(item, basestring):
1039-
paths.append(self._to_relative_path(item))
1040-
else:
1041-
raise TypeError("Invalid item type: %r" % item)
1042-
# END for each item
1043-
1049+
paths = self._items_to_rela_paths(items)
10441050
removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines()
10451051

10461052
# process output to gain proper paths
10471053
# rm 'path'
10481054
return [ p[4:-1] for p in removed_paths ]
1055+
1056+
@clear_cache
1057+
@default_index
1058+
def move(self, items, skip_errors=False, **kwargs):
1059+
"""
1060+
Rename/move the items, whereas the last item is considered the destination of
1061+
the move operation. If the destination is a file, the first item ( of two )
1062+
must be a file as well. If the destination is a directory, it may be preceeded
1063+
by one or more directories or files.
1064+
1065+
The working tree will be affected in non-bare repositories.
1066+
1067+
``items``
1068+
Multiple types of items are supported, please see the 'remove' method
1069+
for reference.
1070+
``skip_errors``
1071+
If True, errors such as ones resulting from missing source files will
1072+
be skpped.
1073+
``**kwargs``
1074+
Additional arguments you would like to pass to git-mv, such as dry_run
1075+
or force.
1076+
1077+
Returns
1078+
List(tuple(source_path_string, destination_path_string), ...)
1079+
A list of pairs, containing the source file moved as well as its
1080+
actual destination. Relative to the repository root.
1081+
1082+
Raises
1083+
ValueErorr: If only one item was given
1084+
GitCommandError: If git could not handle your request
1085+
"""
1086+
args = list()
1087+
if skip_errors:
1088+
args.append('-k')
1089+
1090+
paths = self._items_to_rela_paths(items)
1091+
if len(paths) < 2:
1092+
raise ValueError("Please provide at least one source and one destination of the move operation")
1093+
1094+
was_dry_run = kwargs.pop('dry_run', kwargs.pop('n', None))
1095+
kwargs['dry_run'] = True
1096+
1097+
# first execute rename in dryrun so the command tells us what it actually does
1098+
# ( for later output )
1099+
out = list()
1100+
mvlines = self.repo.git.mv(args, paths, **kwargs).splitlines()
1101+
1102+
# parse result - first 0:n/2 lines are 'checking ', the remaining ones
1103+
# are the 'renaming' ones which we parse
1104+
for ln in xrange(len(mvlines)/2, len(mvlines)):
1105+
tokens = mvlines[ln].split(' to ')
1106+
assert len(tokens) == 2, "Too many tokens in %s" % mvlines[ln]
1107+
1108+
# [0] = Renaming x
1109+
# [1] = y
1110+
out.append((tokens[0][9:], tokens[1]))
1111+
# END for each line to parse
1112+
1113+
# either prepare for the real run, or output the dry-run result
1114+
if was_dry_run:
1115+
return out
1116+
# END handle dryrun
10491117

1118+
1119+
# now apply the actual operation
1120+
kwargs.pop('dry_run')
1121+
self.repo.git.mv(args, paths, **kwargs)
1122+
1123+
return out
1124+
1125+
1126+
10501127
@default_index
10511128
def commit(self, message, parent_commits=None, head=True):
10521129
"""

test/git/test_index.py

+31-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
import shutil
1515
from stat import *
1616

17-
class TestTree(TestBase):
17+
class TestIndex(TestBase):
1818

1919
def __init__(self, *args):
20-
super(TestTree, self).__init__(*args)
20+
super(TestIndex, self).__init__(*args)
2121
self._reset_progress()
2222

2323
def _assert_fprogress(self, entries):
@@ -498,3 +498,32 @@ def mixed_iterator():
498498
open(fake_symlink_path,'rb').read() == link_target
499499
else:
500500
assert S_ISLNK(os.lstat(fake_symlink_path)[ST_MODE])
501+
502+
# TEST RENAMING
503+
def assert_mv_rval(rval):
504+
for source, dest in rval:
505+
assert not os.path.exists(source) and os.path.exists(dest)
506+
# END for each renamed item
507+
# END move assertion utility
508+
509+
self.failUnlessRaises(ValueError, index.move, ['just_one_path'])
510+
# file onto existing file
511+
files = ['AUTHORS', 'LICENSE']
512+
self.failUnlessRaises(GitCommandError, index.move, files)
513+
514+
# again, with force
515+
assert_mv_rval(index.move(files, force=True))
516+
517+
# files into directory - dry run
518+
paths = ['LICENSE', 'VERSION', 'doc']
519+
rval = index.move(paths, dry_run=True)
520+
assert len(rval) == 2
521+
assert os.path.exists(paths[0])
522+
523+
# again, no dry run
524+
rval = index.move(paths)
525+
assert_mv_rval(rval)
526+
527+
# dir into dir
528+
rval = index.move(['doc', 'test'])
529+
assert_mv_rval(rval)

test/testlib/helper.py

+13
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def case(self, rw_repo)
9090
def bare_repo_creator(self):
9191
repo_dir = tempfile.mktemp("bare_repo")
9292
rw_repo = self.rorepo.clone(repo_dir, shared=True, bare=True)
93+
prev_cwd = os.getcwd()
9394
try:
9495
return func(self, rw_repo)
9596
finally:
@@ -106,6 +107,9 @@ def with_rw_repo(working_tree_ref):
106107
out the working tree at the given working_tree_ref.
107108
108109
This repository type is more costly due to the working copy checkout.
110+
111+
To make working with relative paths easier, the cwd will be set to the working
112+
dir of the repository.
109113
"""
110114
assert isinstance(working_tree_ref, basestring), "Decorator requires ref name for working tree checkout"
111115
def argument_passer(func):
@@ -116,9 +120,12 @@ def repo_creator(self):
116120
rw_repo.head.commit = working_tree_ref
117121
rw_repo.head.reference.checkout()
118122

123+
prev_cwd = os.getcwd()
124+
os.chdir(rw_repo.working_dir)
119125
try:
120126
return func(self, rw_repo)
121127
finally:
128+
os.chdir(prev_cwd)
122129
rw_repo.git.clear_cache()
123130
shutil.rmtree(repo_dir, onerror=_rmtree_onerror)
124131
# END cleanup
@@ -148,6 +155,8 @@ def with_rw_and_rw_remote_repo(working_tree_ref):
148155
def case(self, rw_repo, rw_remote_repo)
149156
150157
This setup allows you to test push and pull scenarios and hooks nicely.
158+
159+
See working dir info in with_rw_repo
151160
"""
152161
assert isinstance(working_tree_ref, basestring), "Decorator requires ref name for working tree checkout"
153162
def argument_passer(func):
@@ -192,9 +201,13 @@ def remote_repo_creator(self):
192201
else:
193202
raise AssertionError('Please start a git-daemon to run this test, execute: git-daemon "%s"'%tempfile.gettempdir())
194203

204+
# adjust working dir
205+
prev_cwd = os.getcwd()
206+
os.chdir(rw_repo.working_dir)
195207
try:
196208
return func(self, rw_repo, rw_remote_repo)
197209
finally:
210+
os.chdir(prev_cwd)
198211
rw_repo.git.clear_cache()
199212
rw_remote_repo.git.clear_cache()
200213
shutil.rmtree(repo_dir, onerror=_rmtree_onerror)

0 commit comments

Comments
 (0)