[MERGE] enhanced mv command

Marius Kruger amanic at gmail.com
Thu Jan 11 18:34:42 GMT 2007


hi,

here goes try number 4
see attached zipped bundle, and the total diff for easy reviewing.


On 1/3/07, Aaron Bentley <aaron.bentley at utoronto.ca > wrote:

>
> === modified file 'bzrlib/builtins.py' (properties changed)
>
> ^^^ I don't think it makes sense to set builtins.py executable.


I don't know how this happened, I obviously wasn't on the lookout for this
sort of thing when committing, but  I've undid all the files that became
executable (hope I didn't stuff up again!) :
bzrlib/builtins.py
bzrlib/tests/blackbox/test_mv.py
bzrlib/tests/workingtree_implementations/test_workingtree.py
bzrlib/workingtree.py


=== modified file 'bzrlib/errors.py'
> +class FilesExist(PathError):
> +    """Used when reporting that files do exist"""
> +
> +    _fmt = "File%(plural)s exist: %(paths_as_string)s%(extra)s"
> +
> +    def __init__(self, paths, extra=None):
> +        # circular import
> +        from bzrlib.osutils import quotefn
> +        BzrError.__init__(self)
> +        self.paths = paths
> +        self.paths_as_string = ' '.join(paths)
> +        if extra:
> +            self.extra = ': ' + str(extra)
> +        else:
> +            self.extra = ''
> +        if len(paths) > 1:
> +            self.plural = 's'
> +        else:
> +            self.plural = ''
>
> ^^^ I'm dubious about trying to generate correct English plurals for
> error strings -- we've always just done "File(s) exist".  You don't have
> to change this, it's just my reaction.


I was also ok with file(s), but in the end it is more user friendly to
handle plurals properly.
John sort of complained about this, and since I think this exception is
suited to one or more
files, I thought this is the best way to address this.
John's complaint:

> >          self.run_bzr_error(
> > -            ["^bzr: ERROR: can't rename: both, old name .* and new name
> .*"
> > -             " exist. Use option '--after' to force rename."],
> > +            ["^bzr: ERROR: File\(s\) exist: .+ .+: can't rename."
> > +             " Use option '--after' to force rename."],
>
> ^- You shouldn't need "File(s)" it should just be Files, since you know
> there are 2 files. Though it makes me wonder if the right exception is
> being used.
>



@@ -147,3 +152,151 @@
>          self.run_bzr('mv', 'c/b', 'b')
>          tree = workingtree.WorkingTree.open('.')
>          self.assertEqual('b-id', tree.path2id('b'))
> +
> +    def test_mv_already_moved_file(self):
> +        """a is in repository, b does not exist. User does
> +        mv a b; bzr mv a b"""
> ....
> Finally, I'm not sure what the point of committing the files is.  'bzr
> mv' doesn't and shouldn't care whether or not the files have been
> committed.

ok I've removed them, I think it was thought that they might be handled
differently.
the tests pass without them and if you assume they should be handled the
the same, the commits are not really needed.

> - --- bzrlib/tests/workingtree_implementations/test_workingtree.py
> 2006-09-10 19:04:48 +0000
> +++ bzrlib/tests/workingtree_implementations/test_workingtree.py
> 2007-01-03 02:46:14 +0000
> @@ -681,3 +681,50 @@
>                  tree.add, [u'a\u030a'])
>          finally:
>              osutils.normalized_filename = orig
> +    def test_move_deprecated_wrong_call(self):
> +        """tree.move has the deprecated parameter 'to_name'.
> +        It has been replaced by 'to_dir' for consistency.
> +        Test the new API using wrong parameter"""
> +        self.build_tree(['a1', 'sub1/'])
> +        tree = self.make_branch_and_tree ('.')
> +        tree.add(['a1', 'sub1'])
> +        tree.commit('initial commit')
> +        self.assertRaises(TypeError, tree.move, ['a1'],
> +                          to_this_parameter_does_not_exist='sub1',
> +                          after=False)
>
> ^^^ This is probably superfluous with the following test.  I must admit
> I thought you were obsoleting (not just deprecating) the parameter, at
> first.
>
> +    def test_move_deprecated_deprecated_call(self):

(This was done by Steffen Eichenberg, not me.
I'm just trying to fix it up to a commitable state remember,
I suppose thats makes me responsible.)
Is it ok if I leave this, it seems to be there for a good cause:
test that we can now and in the future use the named and
unnamed api of .move


+        This is the new mode introduced
> +        in version 0.13.
>
> More like version 0.14.  I think.  Hey, who's flying this plane, anyway?
>
More like version 0.15  :(
what plane?
btw is it good to have references this in the code?

         This returns a list of (from_path, to_path) pairs for each
> - -        entry that is moved.
> +        entry, that is moved.
>
> ^^^ The original phrasing is correct.
> As code: [pairs(entry) for entry in moved_entries()]

I could have sworn one of the previous reviewers requested this,
but i could not find it so I must be delusional.


+        # determine which move mode to use. checks also for movability
> +        rename_entries = self._determine_mv_mode(rename_entries, after)
> +
> +        # move pairs.

^^^ What does "move pairs" mean?
>
You're asking me?
 I don't know, so I just removed it.

         original_modified = self._inventory_is_modified
>          try:
> - -            if len(from_paths):
> +            if len(from_paths):
>
> ^^^ All this does is insert a trailing space.  Not an improvement :-)


very funny.
I just can't win:  If I set my editor to remove those pesky trailing
whitespace
automatically you complain that I remove them. if I don't, you complain
that I add them.
I think after this thing is merged I should start looking at doing a patch
which:
1) removes all the trailing white space once and for all and
2) introduce a test which checks that nobody violates this ever again.

And I might also write a plugin which warns you that you are
adding trailing white space when you commit (even failing the commit if you
choose).
So that this sort of thing is caught earlier.
It can maybe also add a command similar to shelve which traverses the
changes which have not been committed yet and asks you for every bunch of
trailing ws it finds, if you want to remove it and does it for you.

This plugin would be usefull for other projects too, like the projects at my
dayjob.
(where we also have differences of oppinion about trailing whitespace)

I also removed all the trailing white space this bundle would have
introduced.
I see that some of the latest commits still introduce new trailing white
space,
when is it going to stop?


+        for entry in rename_entries:
> +            try:
> +                self._move_entry(entry)
> +            except OSError, e:
> +                self._rollback_move(moved)
> +                raise errors.BzrRenameFailedError(entry.from_rel,
> entry.to_rel,
> +                    e[1])
> +            except errors.BzrError, e:
> +                self._rollback_move(moved)
> +                raise
>
> ^^^ Is it possible to catch more specific errors here?  BzrError and
> OSError are very broad.

(Again I didn't write this, so don't be too hard on me)
I think its ok, as we just want to rollback and raise it again.
It is done like that elsewere in the file too.


regards
marius.

-- 



white space is overrated
-------------- next part --------------
An HTML attachment was scrubbed...
URL: https://lists.ubuntu.com/archives/bazaar/attachments/20070111/f75591f5/attachment-0001.htm 
-------------- next part --------------
A non-text attachment was scrubbed...
Name: enhancedmv4.patch.zip
Type: application/zip
Size: 330488 bytes
Desc: not available
Url : https://lists.ubuntu.com/archives/bazaar/attachments/20070111/f75591f5/attachment-0001.zip 
-------------- next part --------------
=== modified file 'NEWS'
--- NEWS	2007-01-05 05:36:43 +0000
+++ NEWS	2007-01-11 17:20:46 +0000
@@ -2,6 +2,17 @@
 
   IMPROVEMENTS:
 
+    * ``bzr mv`` enhanced to support already moved files.
+      In the past the mv command would have failed if the source file doesn't
+      exist. In this situation ``bzr mv`` would now detect that the file has
+      already moved and update the repository accordingly, if the target file
+      does exist.
+      A new option ``--after`` has been added so that if two files already
+      exist, you could notify Bazaar that you have moved a (versioned) file and
+      replaced it with another. Thus in this case ``bzr move --after`` will
+      only update the Bazaar identifier.
+      (Steffen Eichenberg, Marius Kruger)
+
     * New connection: ``bzr+http://`` which supports tunnelling the smart
       protocol over an HTTP connection. If writing is enabled on the bzr
       server, then you can write over the http connection.

=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py	2007-01-10 03:55:13 +0000
+++ bzrlib/builtins.py	2007-01-11 17:20:46 +0000
@@ -450,16 +450,25 @@
 
     If the last argument is a versioned directory, all the other names
     are moved into it.  Otherwise, there must be exactly two arguments
-    and the file is changed to a new name, which must not already exist.
+    and the file is changed to a new name.
+
+    If OLDNAME does not exist on the filesystem but is versioned and
+    NEWNAME does exist on the filesystem but is not versioned, mv
+    assumes that the file has been manually moved and only updates
+    its internal inventory to reflect that change.
+    The same is valid when moving many SOURCE files to a DESTINATION.
 
     Files cannot be moved between branches.
     """
 
     takes_args = ['names*']
+    takes_options = [Option("after", help="move only the bzr identifier"
+        " of the file (file has already been moved). Use this flag if"
+        " bzr is not able to detect this itself.")]
     aliases = ['move', 'rename']
     encoding_type = 'replace'
 
-    def run(self, names_list):
+    def run(self, names_list, after=False):
         if names_list is None:
             names_list = []
 
@@ -469,13 +478,14 @@
         
         if os.path.isdir(names_list[-1]):
             # move into existing directory
-            for pair in tree.move(rel_names[:-1], rel_names[-1]):
+            for pair in tree.move(rel_names[:-1], rel_names[-1], after=after):
                 self.outf.write("%s => %s\n" % pair)
         else:
             if len(names_list) != 2:
-                raise errors.BzrCommandError('to mv multiple files the destination '
-                                             'must be a versioned directory')
-            tree.rename_one(rel_names[0], rel_names[1])
+                raise errors.BzrCommandError('to mv multiple files the'
+                                             ' destination must be a versioned'
+                                             ' directory')
+            tree.rename_one(rel_names[0], rel_names[1], after=after)
             self.outf.write("%s => %s\n" % (rel_names[0], rel_names[1]))
             
     

=== modified file 'bzrlib/errors.py'
--- bzrlib/errors.py	2007-01-05 05:36:42 +0000
+++ bzrlib/errors.py	2007-01-11 17:45:03 +0000
@@ -18,7 +18,8 @@
 """
 
 
-from bzrlib import symbol_versioning
+from bzrlib import (symbol_versioning,
+                    osutils,)
 from bzrlib.patches import (PatchSyntax, 
                             PatchConflict, 
                             MalformedPatchHeader,
@@ -319,6 +320,37 @@
     _fmt = "File exists: %(path)r%(extra)s"
 
 
+class FilesExist(PathError):
+    """Used when reporting that files do exist"""
+
+    _fmt = "File%(plural)s exist: %(paths_as_string)s%(extra)s"
+
+    def __init__(self, paths, extra=None):
+        # circular import
+        from bzrlib.osutils import quotefn
+        BzrError.__init__(self)
+        self.paths = paths
+        self.paths_as_string = ' '.join(paths)
+        if extra:
+            self.extra = ': ' + str(extra)
+        else:
+            self.extra = ''
+        if len(paths) > 1:
+            self.plural = 's'
+        else:
+            self.plural = ''
+
+
+class NotADirectory(PathError):
+
+    _fmt = "%(path)r is not a directory %(extra)s"
+
+
+class NotInWorkingDirectory(PathError):
+
+    _fmt = "%(path)r is not in the working directory %(extra)s"
+
+
 class DirectoryNotEmpty(PathError):
 
     _fmt = "Directory not empty: %(path)r%(extra)s"
@@ -491,17 +523,50 @@
         self.repo_format = repo_format
 
 
+class AlreadyVersionedError(BzrError):
+    """Used when a path is expected not to be versioned, but it is."""
+
+    _fmt = "%(context_info)s%(path)s is already versioned"
+
+    def __init__(self, path, context_info=None):
+        """Construct a new NotVersionedError.
+
+        :param path: This is the path which is versioned,
+        which should be in a user friendly form.
+        :param context_info: If given, this is information about the context,
+        which could explain why this is expected to not be versioned.
+        """
+        BzrError.__init__(self)
+        self.path = path
+        if context_info is None:
+            self.context_info = ''
+        else:
+            self.context_info = context_info + ". "
+
+
 class NotVersionedError(BzrError):
-
-    _fmt = "%(path)s is not versioned"
-
-    def __init__(self, path):
+    """Used when a path is expected to be versioned, but it is not."""
+
+    _fmt = "%(context_info)s%(path)s is not versioned"
+
+    def __init__(self, path, context_info=None):
+        """Construct a new NotVersionedError.
+
+        :param path: This is the path which is not versioned,
+        which should be in a user friendly form.
+        :param context_info: If given, this is information about the context,
+        which could explain why this is expected to be versioned.
+        """
         BzrError.__init__(self)
         self.path = path
+        if context_info is None:
+            self.context_info = ''
+        else:
+            self.context_info = context_info + ". "
 
 
 class PathsNotVersionedError(BzrError):
-    # used when reporting several paths are not versioned
+    """Used when reporting several paths which are not versioned"""
 
     _fmt = "Path(s) are not versioned: %(paths_as_string)s"
 
@@ -514,17 +579,21 @@
 
 class PathsDoNotExist(BzrError):
 
-    _fmt = "Path(s) do not exist: %(paths_as_string)s"
+    _fmt = "Path(s) do not exist: %(paths_as_string)s%(extra)s"
 
     # used when reporting that paths are neither versioned nor in the working
     # tree
 
-    def __init__(self, paths):
+    def __init__(self, paths, extra=None):
         # circular import
         from bzrlib.osutils import quotefn
         BzrError.__init__(self)
         self.paths = paths
         self.paths_as_string = ' '.join([quotefn(p) for p in paths])
+        if extra:
+            self.extra = ': ' + str(extra)
+        else:
+            self.extra = ''
 
 
 class BadFileKindError(BzrError):
@@ -1278,6 +1347,48 @@
     _fmt = "Moving the root directory is not supported at this time"
 
 
+class BzrMoveFailedError(BzrError):
+
+    _fmt = "Could not move %(from_path)s%(operator)s %(to_path)s%(extra)s"
+
+    def __init__(self, from_path='', to_path='', extra=None):
+        BzrError.__init__(self)
+        if extra:
+            self.extra = ': ' + str(extra)
+        else:
+            self.extra = ''
+
+        has_from = len(from_path) > 0
+        has_to = len(to_path) > 0
+        if has_from:
+            self.from_path = osutils.splitpath(from_path)[-1]
+        else:
+            self.from_path = ''
+
+        if has_to:
+            self.to_path = osutils.splitpath(to_path)[-1]
+        else:
+            self.to_path = ''
+
+        self.operator = ""
+        if has_from and has_to:
+            self.operator = " =>"
+        elif has_from:
+            self.from_path = "from " + from_path
+        elif has_to:
+            self.operator = "to"
+        else:
+            self.operator = "file"
+
+
+class BzrRenameFailedError(BzrMoveFailedError):
+
+    _fmt = "Could not rename %(from_path)s%(operator)s %(to_path)s%(extra)s"
+
+    def __init__(self, from_path, to_path, extra=None):
+        BzrMoveFailedError.__init__(self, from_path, to_path, extra)
+
+
 class BzrBadParameterNotString(BzrBadParameter):
 
     _fmt = "Parameter %(param)s is not a string or unicode string."

=== modified file 'bzrlib/tests/__init__.py'
--- bzrlib/tests/__init__.py	2007-01-11 17:00:25 +0000
+++ bzrlib/tests/__init__.py	2007-01-11 17:31:34 +0000
@@ -665,6 +665,16 @@
         if not (left is right):
             raise AssertionError("%r is not %r." % (left, right))
 
+    def assertNone(self, obj, msg):
+        """Fail if obj is not None"""
+        if (obj is not None):
+            raise AssertionError(msg)
+
+    def assertNotNone(self, obj, msg):
+        """Fail if obj is None"""
+        if (obj is None):
+            raise AssertionError(msg)
+
     def assertTransportMode(self, transport, path, mode):
         """Fail if a path does not have mode mode.
         
@@ -1539,11 +1549,11 @@
 
     def failUnlessExists(self, path):
         """Fail unless path, which may be abs or relative, exists."""
-        self.failUnless(osutils.lexists(path))
+        self.failUnless(osutils.lexists(path),path+" does not exist")
 
     def failIfExists(self, path):
         """Fail if path, which may be abs or relative, exists."""
-        self.failIf(osutils.lexists(path))
+        self.failIf(osutils.lexists(path),path+" exists")
 
 
 class TestCaseWithTransport(TestCaseInTempDir):

=== modified file 'bzrlib/tests/blackbox/test_mv.py'
--- bzrlib/tests/blackbox/test_mv.py	2006-12-18 12:10:57 +0000
+++ bzrlib/tests/blackbox/test_mv.py	2007-01-11 17:20:22 +0000
@@ -26,10 +26,28 @@
     TestCaseWithTransport,
     TestSkipped,
     )
-
+from bzrlib.osutils import (
+    splitpath
+    )
 
 class TestMove(TestCaseWithTransport):
 
+    def assertInWorkingTree(self,path):
+        tree = workingtree.WorkingTree.open('.')
+        self.assertNotNone(tree.path2id(path),path+' not in working tree.')
+
+    def assertNotInWorkingTree(self,path):
+        tree = workingtree.WorkingTree.open('.')
+        self.assertNone(tree.path2id(path),path+' in working tree.')
+
+    def assertMoved(self,from_path,to_path):
+        """Assert that to_path is existing and versioned but from_path not. """
+        self.failIfExists(from_path)
+        self.assertNotInWorkingTree(from_path)
+
+        self.failUnlessExists(to_path)
+        self.assertInWorkingTree(to_path)
+
     def test_mv_modes(self):
         """Test two modes of operation for mv"""
         tree = self.make_branch_and_tree('.')
@@ -37,94 +55,86 @@
         tree.add(['a', 'c', 'subdir'])
 
         self.run_bzr('mv', 'a', 'b')
-        self.failUnlessExists('b')
-        self.failIfExists('a')
+        self.assertMoved('a','b')
 
         self.run_bzr('mv', 'b', 'subdir')
-        self.failUnlessExists('subdir/b')
-        self.failIfExists('b')
+        self.assertMoved('b','subdir/b')
 
         self.run_bzr('mv', 'subdir/b', 'a')
-        self.failUnlessExists('a')
-        self.failIfExists('subdir/b')
+        self.assertMoved('subdir/b','a')
 
         self.run_bzr('mv', 'a', 'c', 'subdir')
-        self.failUnlessExists('subdir/a')
-        self.failUnlessExists('subdir/c')
-        self.failIfExists('a')
-        self.failIfExists('c')
+        self.assertMoved('a','subdir/a')
+        self.assertMoved('c','subdir/c')
 
         self.run_bzr('mv', 'subdir/a', 'subdir/newa')
-        self.failUnlessExists('subdir/newa')
-        self.failIfExists('subdir/a')
+        self.assertMoved('subdir/a','subdir/newa')
 
     def test_mv_unversioned(self):
         self.build_tree(['unversioned.txt'])
         self.run_bzr_error(
-            ["^bzr: ERROR: can't rename: old name .* is not versioned$"],
+            ["^bzr: ERROR: Could not rename unversioned.txt => elsewhere."
+             " .*unversioned.txt is not versioned$"],
             'mv', 'unversioned.txt', 'elsewhere')
 
     def test_mv_nonexisting(self):
         self.run_bzr_error(
-            ["^bzr: ERROR: can't rename: old working file .* does not exist$"],
+            ["^bzr: ERROR: Could not rename doesnotexist => somewhereelse."
+             " .*doesnotexist is not versioned$"],
             'mv', 'doesnotexist', 'somewhereelse')
 
     def test_mv_unqualified(self):
         self.run_bzr_error(['^bzr: ERROR: missing file argument$'], 'mv')
-        
+
     def test_mv_invalid(self):
         tree = self.make_branch_and_tree('.')
         self.build_tree(['test.txt', 'sub1/'])
         tree.add(['test.txt'])
 
         self.run_bzr_error(
-            ["^bzr: ERROR: destination u'sub1' is not a versioned directory$"],
+            ["^bzr: ERROR: Could not move to sub1: sub1 is not versioned$"],
             'mv', 'test.txt', 'sub1')
-        
+
         self.run_bzr_error(
-            ["^bzr: ERROR: can't determine destination directory id for u'sub1'$"],
+            ["^bzr: ERROR: Could not move test.txt => .*hello.txt: "
+             "sub1 is not versioned$"],
             'mv', 'test.txt', 'sub1/hello.txt')
-        
+
     def test_mv_dirs(self):
         tree = self.make_branch_and_tree('.')
         self.build_tree(['hello.txt', 'sub1/'])
         tree.add(['hello.txt', 'sub1'])
 
         self.run_bzr('mv', 'sub1', 'sub2')
-        self.failUnlessExists('sub2')
-        self.failIfExists('sub1')
+        self.assertMoved('sub1','sub2')
+
         self.run_bzr('mv', 'hello.txt', 'sub2')
-        self.failUnlessExists("sub2/hello.txt")
-        self.failIfExists("hello.txt")
+        self.assertMoved('hello.txt','sub2/hello.txt')
 
         tree.read_working_inventory()
-        tree.commit('commit with some things moved to subdirs')
 
         self.build_tree(['sub1/'])
         tree.add(['sub1'])
         self.run_bzr('mv', 'sub2/hello.txt', 'sub1')
-        self.failIfExists('sub2/hello.txt')
-        self.failUnlessExists('sub1/hello.txt')
+        self.assertMoved('sub2/hello.txt','sub1/hello.txt')
+
         self.run_bzr('mv', 'sub2', 'sub1')
-        self.failIfExists('sub2')
-        self.failUnlessExists('sub1/sub2')
+        self.assertMoved('sub2','sub1/sub2')
 
     def test_mv_relative(self):
         self.build_tree(['sub1/', 'sub1/sub2/', 'sub1/hello.txt'])
         tree = self.make_branch_and_tree('.')
         tree.add(['sub1', 'sub1/sub2', 'sub1/hello.txt'])
-        tree.commit('initial tree')
 
         os.chdir('sub1/sub2')
         self.run_bzr('mv', '../hello.txt', '.')
         self.failUnlessExists('./hello.txt')
         tree.read_working_inventory()
-        tree.commit('move to parent directory')
 
         os.chdir('..')
-
         self.run_bzr('mv', 'sub2/hello.txt', '.')
-        self.failUnlessExists('hello.txt')
+        os.chdir('..')
+        self.assertMoved('sub1/sub2/hello.txt','sub1/hello.txt')
 
     def test_mv_smoke_aliases(self):
         # just test that aliases for mv exist, if their behaviour is changed in
@@ -147,3 +157,225 @@
         self.run_bzr('mv', 'c/b', 'b')
         tree = workingtree.WorkingTree.open('.')
         self.assertEqual('b-id', tree.path2id('b'))
+
+    def test_mv_already_moved_file(self):
+        """Test bzr mv original_file to moved_file.
+
+        Tests if a file which has allready been moved by an external tool,
+        is handled correctly by bzr mv.
+        Setup: a is in the working tree, b does not exist.
+        User does: mv a b; bzr mv a b
+
+        """
+        self.build_tree(['a'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a'])
+
+        os.rename('a', 'b')
+        self.run_bzr('mv', 'a', 'b')
+        self.assertMoved('a','b')
+
+    def test_mv_already_moved_file_to_versioned_target(self):
+        """Test bzr mv existing_file to versioned_file.
+
+        Tests if an attempt to move an existing versioned file
+        to another versiond file will fail.
+        Setup: a and b are in the working tree.
+        User does: rm b; mv a b; bzr mv a b
+
+        """
+        self.build_tree(['a', 'b'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a', 'b'])
+
+        os.remove('b')
+        os.rename('a', 'b')
+        self.run_bzr_error(
+            ["^bzr: ERROR: Could not move a => b. b is already versioned$"],
+            'mv', 'a', 'b')
+        #check that nothing changed
+        self.failIfExists('a')
+        self.failUnlessExists('b')
+
+    def test_mv_already_moved_file_into_subdir(self):
+        """Test bzr mv original_file to versioned_directory/file.
+
+        Tests if a file which has already been moved into a versioned
+        directory by an external tool, is handled correctly by bzr mv.
+        Setup: a and sub/ are in the working tree.
+        User does: mv a sub/a; bzr mv a sub/a
+
+        """
+        self.build_tree(['a', 'sub/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a', 'sub'])
+
+        os.rename('a', 'sub/a')
+        self.run_bzr('mv', 'a', 'sub/a')
+        self.assertMoved('a','sub/a')
+
+    def test_mv_already_moved_file_into_unversioned_subdir(self):
+        """Test bzr mv original_file to unversioned_directory/file.
+
+        Tests if an attempt to move an existing versioned file
+        into an unversioned directory will fail.
+        Setup: a is in the working tree, sub/ is not.
+        User does: mv a sub/a; bzr mv a sub/a
+
+        """
+        self.build_tree(['a', 'sub/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a'])
+
+        os.rename('a', 'sub/a')
+        self.run_bzr_error(
+            ["^bzr: ERROR: Could not move a => a: sub is not versioned$"],
+            'mv', 'a', 'sub/a')
+        self.failIfExists('a')
+        self.failUnlessExists('sub/a')
+
+    def test_mv_already_moved_files_into_subdir(self):
+        """Test bzr mv original_files to versioned_directory.
+
+        Tests if files which has already been moved into a versioned
+        directory by an external tool, is handled correctly by bzr mv.
+        Setup: a1, a2, sub are in the working tree.
+        User does: mv a1 sub/.; bzr mv a1 a2 sub
+
+        """
+        self.build_tree(['a1', 'a2', 'sub/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'a2', 'sub'])
+
+        os.rename('a1', 'sub/a1')
+        self.run_bzr('mv', 'a1', 'a2', 'sub')
+        self.assertMoved('a1','sub/a1')
+        self.assertMoved('a2','sub/a2')
+
+    def test_mv_already_moved_files_into_unversioned_subdir(self):
+        """Test bzr mv original_file to unversioned_directory.
+
+        Tests if an attempt to move existing versioned file
+        into an unversioned directory will fail.
+        Setup: a1, a2 are in the working tree, sub is not.
+        User does: mv a1 sub/.; bzr mv a1 a2 sub
+
+        """
+        self.build_tree(['a1', 'a2', 'sub/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'a2'])
+
+        os.rename('a1', 'sub/a1')
+        self.run_bzr_error(
+            ["^bzr: ERROR: Could not move to sub. sub is not versioned$"],
+            'mv', 'a1', 'a2', 'sub')
+        self.failIfExists('a1')
+        self.failUnlessExists('sub/a1')
+        self.failUnlessExists('a2')
+        self.failIfExists('sub/a2')
+
+    def test_mv_already_moved_file_forcing_after(self):
+        """Test bzr mv versioned_file to unversioned_file.
+
+        Tests if an attempt to move an existing versioned file to an existing
+        unversioned file will fail, informing the user to use the --after
+        option to force this.
+        Setup: a is in the working tree, b not versioned.
+        User does: mv a b; touch a; bzr mv a b
+
+        """
+        self.build_tree(['a', 'b'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a'])
+
+        os.rename('a', 'b')
+        self.build_tree(['a']) #touch a
+        self.run_bzr_error(
+            ["^bzr: ERROR: Could not rename a => b: Files exist: a b:"
+             " \(Use --after to update the Bazaar id\)$"],
+            'mv', 'a', 'b')
+        self.failUnlessExists('a')
+        self.failUnlessExists('b')
+
+    def test_mv_already_moved_file_using_after(self):
+        """Test bzr mv --after versioned_file to unversioned_file.
+
+        Tests if an existing versioned file can be forced to move to an
+        existing unversioned file using the --after option. With the result
+        that bazaar considers the unversioned_file to be moved from
+        versioned_file and versioned_file will become unversioned.
+        Setup: a is in the working tree and b exists.
+        User does: mv a b; touch a; bzr mv a b --after
+        Resulting in a => b and a is unknown.
+
+        """
+        self.build_tree(['a', 'b'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a'])
+        os.rename('a', 'b')
+        self.build_tree(['a']) #touch a
+
+        self.run_bzr('mv', 'a', 'b', '--after')
+        self.failUnlessExists('a')
+        self.assertNotInWorkingTree('a')#a should be unknown now.
+        self.failUnlessExists('b')
+        self.assertInWorkingTree('b')
+
+    def test_mv_already_moved_files_forcing_after(self):
+        """Test bzr mv versioned_files to directory/unversioned_file.
+
+        Tests if an attempt to move an existing versioned file to an existing
+        unversioned file in some other directory will fail, informing the user
+        to use the --after option to force this.
+
+        Setup: a1, a2, sub are versioned and in the working tree,
+               sub/a1, sub/a2 are in working tree.
+        User does: mv a* sub; touch a1; touch a2; bzr mv a1 a2 sub
+
+        """
+        self.build_tree(['a1', 'a2', 'sub/', 'sub/a1', 'sub/a2'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'a2', 'sub'])
+        os.rename('a1', 'sub/a1')
+        os.rename('a2', 'sub/a2')
+        self.build_tree(['a1']) #touch a1
+        self.build_tree(['a2']) #touch a2
+
+        self.run_bzr_error(
+            ["^bzr: ERROR: Could not rename a1 => a1: Files exist: a1 .*a1:"
+             " \(Use --after to update the Bazaar id\)$"],
+            'mv', 'a1', 'a2', 'sub')
+        self.failUnlessExists('a1')
+        self.failUnlessExists('a2')
+        self.failUnlessExists('sub/a1')
+        self.failUnlessExists('sub/a2')
+
+    def test_mv_already_moved_files_using_after(self):
+        """Test bzr mv --after versioned_file to directory/unversioned_file.
+
+        Tests if an existing versioned file can be forced to move to an
+        existing unversioned file in some other directory using the --after
+        option. With the result that bazaar considers
+        directory/unversioned_file to be moved from versioned_file and
+        versioned_file will become unversioned.
+
+        Setup: a1, a2, sub are versioned and in the working tree,
+               sub/a1, sub/a2 are in working tree.
+        User does: mv a* sub; touch a1; touch a2; bzr mv a1 a2 sub --after
+
+        """
+        self.build_tree(['a1', 'a2', 'sub/', 'sub/a1', 'sub/a2'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'a2', 'sub'])
+        os.rename('a1', 'sub/a1')
+        os.rename('a2', 'sub/a2')
+        self.build_tree(['a1']) #touch a1
+        self.build_tree(['a2']) #touch a2
+
+        self.run_bzr('mv', 'a1', 'a2', 'sub', '--after')
+        self.failUnlessExists('a1')
+        self.failUnlessExists('a2')
+        self.failUnlessExists('sub/a1')
+        self.failUnlessExists('sub/a2')
+        self.assertInWorkingTree('sub/a1')
+        self.assertInWorkingTree('sub/a2')
\ No newline at end of file

=== modified file 'bzrlib/tests/workingtree_implementations/test_workingtree.py'
--- bzrlib/tests/workingtree_implementations/test_workingtree.py	2006-10-20 11:00:25 +0000
+++ bzrlib/tests/workingtree_implementations/test_workingtree.py	2007-01-11 17:47:23 +0000
@@ -681,3 +681,50 @@
                 tree.add, [u'a\u030a'])
         finally:
             osutils.normalized_filename = orig
+
+    def test_move_deprecated_correct_call_named(self):
+        """tree.move has the deprecated parameter 'to_name'.
+        It has been replaced by 'to_dir' for consistency.
+        Test the new API using named parameter"""
+        self.build_tree(['a1', 'sub1/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'sub1'])
+        tree.commit('initial commit')
+        tree.move(['a1'], to_dir='sub1', after=False)
+
+    def test_move_deprecated_correct_call_unnamed(self):
+        """tree.move has the deprecated parameter 'to_name'.
+        It has been replaced by 'to_dir' for consistency.
+        Test the new API using unnamed parameter"""
+        self.build_tree(['a1', 'sub1/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'sub1'])
+        tree.commit('initial commit')
+        tree.move(['a1'], 'sub1', after=False)
+
+    def test_move_deprecated_wrong_call(self):
+        """tree.move has the deprecated parameter 'to_name'.
+        It has been replaced by 'to_dir' for consistency.
+        Test the new API using wrong parameter"""
+        self.build_tree(['a1', 'sub1/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'sub1'])
+        tree.commit('initial commit')
+        self.assertRaises(TypeError, tree.move, ['a1'],
+                          to_this_parameter_does_not_exist='sub1',
+                          after=False)
+
+    def test_move_deprecated_deprecated_call(self):
+        """tree.move has the deprecated parameter 'to_name'.
+        It has been replaced by 'to_dir' for consistency.
+        Test the new API using deprecated parameter"""
+        self.build_tree(['a1', 'sub1/'])
+        tree = self.make_branch_and_tree('.')
+        tree.add(['a1', 'sub1'])
+        tree.commit('initial commit')
+
+        #tree.move(['a1'], to_name='sub1', after=False)
+        self.callDeprecated(['The parameter to_name was deprecated'
+                             ' in version 0.13. Use to_dir instead'],
+                            tree.move, ['a1'], to_name='sub1',
+                            after=False)

=== modified file 'bzrlib/workingtree.py'
--- bzrlib/workingtree.py	2006-12-20 11:53:30 +0000
+++ bzrlib/workingtree.py	2007-01-11 17:58:38 +0000
@@ -72,16 +72,7 @@
 
 from bzrlib import symbol_versioning
 from bzrlib.decorators import needs_read_lock, needs_write_lock
-from bzrlib.errors import (BzrCheckError,
-                           BzrError,
-                           ConflictFormatError,
-                           WeaveRevisionNotPresent,
-                           NotBranchError,
-                           NoSuchFile,
-                           NotVersionedError,
-                           MergeModifiedFormatError,
-                           UnsupportedOperation,
-                           )
+
 from bzrlib.inventory import InventoryEntry, Inventory, ROOT_ID
 from bzrlib.lockable_files import LockableFiles, TransportLock
 from bzrlib.lockdir import LockDir
@@ -91,12 +82,12 @@
     compact_date,
     file_kind,
     isdir,
+    normpath,
     pathjoin,
+    rand_chars,
+    realpath,
     safe_unicode,
     splitpath,
-    rand_chars,
-    normpath,
-    realpath,
     supports_executable,
     )
 from bzrlib.trace import mutter, note
@@ -239,8 +230,8 @@
         mutter("opening working tree %r", basedir)
         if deprecated_passed(branch):
             if not _internal:
-                warnings.warn("WorkingTree(..., branch=XXX) is deprecated as of bzr 0.8."
-                     " Please use bzrdir.open_workingtree() or"
+                warnings.warn("WorkingTree(..., branch=XXX) is deprecated"
+                     " as of bzr 0.8. Please use bzrdir.open_workingtree() or"
                      " WorkingTree.open().",
                      DeprecationWarning,
                      stacklevel=2
@@ -265,7 +256,8 @@
         # if needed, or, when the cache sees a change, append it to the hash
         # cache file, and have the parser take the most recent entry for a
         # given path only.
-        cache_filename = self.bzrdir.get_workingtree_transport(None).local_abspath('stat-cache')
+        wt_trans = self.bzrdir.get_workingtree_transport(None)
+        cache_filename = wt_trans.local_abspath('stat-cache')
         self._hashcache = hashcache.HashCache(basedir, cache_filename,
                                               self._control_files._file_mode)
         hc = self._hashcache
@@ -395,7 +387,7 @@
                 if inv is not None and inv.revision_id == revision_id:
                     return bzrlib.revisiontree.RevisionTree(
                         self.branch.repository, inv, revision_id)
-            except (NoSuchFile, errors.BadInventoryFormat):
+            except (errors.NoSuchFile, errors.BadInventoryFormat):
                 pass
         # No cached copy available, retrieve from the repository.
         # FIXME? RBC 20060403 should we cache the inventory locally
@@ -513,7 +505,7 @@
             parents = [last_rev]
         try:
             merges_file = self._control_files.get_utf8('pending-merges')
-        except NoSuchFile:
+        except errors.NoSuchFile:
             pass
         else:
             for l in merges_file.readlines():
@@ -632,7 +624,7 @@
                     kinds[pos] = file_kind(fullpath)
                 except OSError, e:
                     if e.errno == errno.ENOENT:
-                        raise NoSuchFile(fullpath)
+                        raise errors.NoSuchFile(fullpath)
 
     @needs_write_lock
     def add_parent_tree_id(self, revision_id, allow_leftmost_as_ghost=False):
@@ -647,8 +639,8 @@
         :param allow_leftmost_as_ghost: Allow the first parent to be a ghost.
         """
         parents = self.get_parent_ids() + [revision_id]
-        self.set_parent_ids(parents,
-            allow_leftmost_as_ghost=len(parents) > 1 or allow_leftmost_as_ghost)
+        self.set_parent_ids(parents, allow_leftmost_as_ghost=len(parents) > 1
+            or allow_leftmost_as_ghost)
 
     @needs_tree_write_lock
     def add_parent_tree(self, parent_tuple, allow_leftmost_as_ghost=False):
@@ -788,9 +780,9 @@
         """Merge from a branch into this working tree.
 
         :param branch: The branch to merge from.
-        :param to_revision: If non-None, the merge will merge to to_revision, but 
-            not beyond it. to_revision does not need to be in the history of
-            the branch when it is supplied. If None, to_revision defaults to
+        :param to_revision: If non-None, the merge will merge to to_revision,
+            but not beyond it. to_revision does not need to be in the history
+            of the branch when it is supplied. If None, to_revision defaults to
             branch.last_revision().
         """
         from bzrlib.merge import Merger, Merge3Merger
@@ -830,14 +822,14 @@
     def merge_modified(self):
         try:
             hashfile = self._control_files.get('merge-hashes')
-        except NoSuchFile:
+        except errors.NoSuchFile:
             return {}
         merge_hashes = {}
         try:
             if hashfile.next() != MERGE_MODIFIED_HEADER_1 + '\n':
-                raise MergeModifiedFormatError()
+                raise errors.MergeModifiedFormatError()
         except StopIteration:
-            raise MergeModifiedFormatError()
+            raise errors.MergeModifiedFormatError()
         for s in RioReader(hashfile):
             file_id = s.get("file_id")
             if file_id not in self.inventory:
@@ -961,9 +953,9 @@
 
                 if f_ie:
                     if f_ie.kind != fk:
-                        raise BzrCheckError("file %r entered as kind %r id %r, "
-                                            "now of kind %r"
-                                            % (fap, f_ie.kind, f_ie.file_id, fk))
+                        raise errors.BzrCheckError(
+                            "file %r entered as kind %r id %r, now of kind %r"
+                            % (fap, f_ie.kind, f_ie.file_id, fk))
 
                 # make a last minute entry
                 if f_ie:
@@ -983,130 +975,299 @@
                 new_children.sort()
                 new_children = collections.deque(new_children)
                 stack.append((f_ie.file_id, fp, fap, new_children))
-                # Break out of inner loop, so that we start outer loop with child
+                # Break out of inner loop,
+                # so that we start outer loop with child
                 break
             else:
                 # if we finished all children, pop it off the stack
                 stack.pop()
 
     @needs_tree_write_lock
-    def move(self, from_paths, to_name):
+    def move(self, from_paths, to_dir=None, after=False, **kwargs):
         """Rename files.
 
-        to_name must exist in the inventory.
+        to_dir must exist in the inventory.
 
-        If to_name exists and is a directory, the files are moved into
+        If to_dir exists and is a directory, the files are moved into
         it, keeping their old names.  
 
-        Note that to_name is only the last component of the new name;
+        Note that to_dir is only the last component of the new name;
         this doesn't change the directory.
 
+        For each entry in from_paths the move mode will be determined
+        independently.
+
+        The first mode moves the file in the filesystem and updates the
+        inventory. The second mode only updates the inventory without
+        touching the file on the filesystem. This is the new mode introduced
+        in version 0.15.
+
+        move uses the second mode if 'after == True' and the target is not
+        versioned but present in the working tree.
+
+        move uses the second mode if 'after == False' and the source is
+        versioned but no longer in the working tree, and the target is not
+        versioned but present in the working tree.
+
+        move uses the first mode if 'after == False' and the source is
+        versioned and present in the working tree, and the target is not
+        versioned and not present in the working tree.
+
+        Everything else results in an error.
+
         This returns a list of (from_path, to_path) pairs for each
         entry that is moved.
         """
-        result = []
-        ## TODO: Option to move IDs only
+        rename_entries = []
+        rename_tuples = []
+
+        # check for deprecated use of signature
+        if to_dir is None:
+            to_dir = kwargs.get('to_name', None)
+            if to_dir is None:
+                raise TypeError('You must supply a target directory')
+            else:
+                symbol_versioning.warn('The parameter to_name was deprecated'
+                                       ' in version 0.13. Use to_dir instead',
+                                       DeprecationWarning)
+
+        # check destination directory
         assert not isinstance(from_paths, basestring)
         inv = self.inventory
-        to_abs = self.abspath(to_name)
+        to_abs = self.abspath(to_dir)
         if not isdir(to_abs):
-            raise BzrError("destination %r is not a directory" % to_abs)
-        if not self.has_filename(to_name):
-            raise BzrError("destination %r not in working directory" % to_abs)
-        to_dir_id = inv.path2id(to_name)
-        if to_dir_id is None and to_name != '':
-            raise BzrError("destination %r is not a versioned directory" % to_name)
+            raise errors.BzrMoveFailedError('',to_dir,
+                errors.NotADirectory(to_abs))
+        if not self.has_filename(to_dir):
+            raise errors.BzrMoveFailedError('',to_dir,
+                errors.NotInWorkingDirectory(to_dir))
+        to_dir_id = inv.path2id(to_dir)
+        if to_dir_id is None:
+            raise errors.BzrMoveFailedError('',to_dir,
+                errors.NotVersionedError(path=str(to_dir)))
+
         to_dir_ie = inv[to_dir_id]
         if to_dir_ie.kind != 'directory':
-            raise BzrError("destination %r is not a directory" % to_abs)
-
-        to_idpath = inv.get_idpath(to_dir_id)
-
-        for f in from_paths:
-            if not self.has_filename(f):
-                raise BzrError("%r does not exist in working tree" % f)
-            f_id = inv.path2id(f)
-            if f_id is None:
-                raise BzrError("%r is not versioned" % f)
-            name_tail = splitpath(f)[-1]
-            dest_path = pathjoin(to_name, name_tail)
-            if self.has_filename(dest_path):
-                raise BzrError("destination %r already exists" % dest_path)
-            if f_id in to_idpath:
-                raise BzrError("can't move %r to a subdirectory of itself" % f)
-
-        # OK, so there's a race here, it's possible that someone will
-        # create a file in this interval and then the rename might be
-        # left half-done.  But we should have caught most problems.
-        orig_inv = deepcopy(self.inventory)
+            raise errors.BzrMoveFailedError('',to_dir,
+                errors.NotADirectory(to_abs))
+
+        # create rename entries and tuples
+        for from_rel in from_paths:
+            from_tail = splitpath(from_rel)[-1]
+            from_id = inv.path2id(from_rel)
+            if from_id is None:
+                raise errors.BzrMoveFailedError(from_rel,to_dir,
+                    errors.NotVersionedError(path=str(from_rel)))
+
+            from_entry = inv[from_id]
+            from_parent_id = from_entry.parent_id
+            to_rel = pathjoin(to_dir, from_tail)
+            rename_entry = WorkingTree._RenameEntry(from_rel=from_rel,
+                                         from_id=from_id,
+                                         from_tail=from_tail,
+                                         from_parent_id=from_parent_id,
+                                         to_rel=to_rel, to_tail=from_tail,
+                                         to_parent_id=to_dir_id)
+            rename_entries.append(rename_entry)
+            rename_tuples.append((from_rel, to_rel))
+
+        # determine which move mode to use. checks also for movability
+        rename_entries = self._determine_mv_mode(rename_entries, after)
+
         original_modified = self._inventory_is_modified
         try:
             if len(from_paths):
                 self._inventory_is_modified = True
-            for f in from_paths:
-                name_tail = splitpath(f)[-1]
-                dest_path = pathjoin(to_name, name_tail)
-                result.append((f, dest_path))
-                inv.rename(inv.path2id(f), to_dir_id, name_tail)
-                try:
-                    osutils.rename(self.abspath(f), self.abspath(dest_path))
-                except OSError, e:
-                    raise BzrError("failed to rename %r to %r: %s" %
-                                   (f, dest_path, e[1]))
+            self._move(rename_entries)
         except:
             # restore the inventory on error
-            self._set_inventory(orig_inv, dirty=original_modified)
+            self._inventory_is_modified = original_modified
             raise
         self._write_inventory(inv)
-        return result
+        return rename_tuples
+
+    def _determine_mv_mode(self, rename_entries, after=False):
+        """Determines for each from-to pair if both inventory and working tree
+        or only the inventory has to be changed.
+
+        Also does basic plausability tests.
+        """
+        inv = self.inventory
+
+        for rename_entry in rename_entries:
+            # store to local variables for easier reference
+            from_rel = rename_entry.from_rel
+            from_id = rename_entry.from_id
+            to_rel = rename_entry.to_rel
+            to_id = inv.path2id(to_rel)
+            only_change_inv = False
+
+            # check the inventory for source and destination
+            if from_id is None:
+                raise errors.BzrMoveFailedError(from_rel,to_rel,
+                    errors.NotVersionedError(path=str(from_rel)))
+            if to_id is not None:
+                raise errors.BzrMoveFailedError(from_rel,to_rel,
+                    errors.AlreadyVersionedError(path=str(to_rel)))
+
+            # try to determine the mode for rename (only change inv or change
+            # inv and file system)
+            if after:
+                if not self.has_filename(to_rel):
+                    raise errors.BzrMoveFailedError(from_id,to_rel,
+                        errors.NoSuchFile(path=str(to_rel),
+                        extra="New file has not been created yet"))
+                only_change_inv = True
+            elif not self.has_filename(from_rel) and self.has_filename(to_rel):
+                only_change_inv = True
+            elif self.has_filename(from_rel) and not self.has_filename(to_rel):
+                only_change_inv = False
+            else:
+                # something is wrong, so lets determine what exactly
+                if not self.has_filename(from_rel) and \
+                   not self.has_filename(to_rel):
+                    raise errors.BzrRenameFailedError(from_rel,to_rel,
+                        errors.PathsDoNotExist(paths=(str(from_rel),
+                        str(to_rel))))
+                else:
+                    raise errors.BzrRenameFailedError(from_rel,to_rel,
+                        errors.FilesExist(paths=(str(from_rel), str(to_rel)),
+                        extra="(Use --after to update the Bazaar id)"))
+            rename_entry.only_change_inv = only_change_inv
+        return rename_entries
+
+    def _move(self, rename_entries):
+        """Moves a list of files.
+
+        Depending on the value of the flag 'only_change_inv', the
+        file will be moved on the file system or not.
+        """
+        inv = self.inventory
+        moved = []
+
+        for entry in rename_entries:
+            try:
+                self._move_entry(entry)
+            except OSError, e:
+                self._rollback_move(moved)
+                raise errors.BzrRenameFailedError(entry.from_rel, entry.to_rel,
+                    e[1])
+            except errors.BzrError, e:
+                self._rollback_move(moved)
+                raise
+            moved.append(entry)
+
+    def _rollback_move(self, moved):
+        """Try to rollback a previous move in case of an error in the
+        filesystem.
+        """
+        inv = self.inventory
+        for entry in moved:
+            try:
+                self._move_entry(entry, inverse=True)
+            except OSError, e:
+                raise errors.BzrMoveFailedError( '', '',
+                    errors.BzrError("Rollback failed."
+                        " The working tree is in an inconsistent state."
+                        " Please consider doing a 'bzr revert'."
+                        " Error message is: %s" % e[1]))
+
+    def _move_entry(self, entry, inverse=False):
+        inv = self.inventory
+        from_rel_abs = self.abspath(entry.from_rel)
+        to_rel_abs = self.abspath(entry.to_rel)
+
+        if from_rel_abs == to_rel_abs:
+            raise errors.BzrMoveFailedError(from_rel, to_rel,
+                "Source and target are identical.")
+
+        if inverse:
+            if not entry.only_change_inv:
+                osutils.rename(to_rel_abs, from_rel_abs)
+            inv.rename(entry.from_id, entry.from_parent_id, entry.from_tail)
+        else:
+            if not entry.only_change_inv:
+                osutils.rename(from_rel_abs, to_rel_abs)
+            inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
 
     @needs_tree_write_lock
-    def rename_one(self, from_rel, to_rel):
+    def rename_one(self, from_rel, to_rel, after=False):
         """Rename one file.
 
         This can change the directory or the filename or both.
+
+        rename_one has several 'modes' to work. First, it can rename a physical
+        file and change the file_id. That is the normal mode. Second, it can
+        only change the file_id without touching any physical file. This is
+        the new mode introduced in version 0.15.
+
+        rename_one uses the second mode if 'after == True' and 'to_rel' is not
+        versioned but present in the working tree.
+
+        rename_one uses the second mode if 'after == False' and 'from_rel' is
+        versioned but no longer in the working tree, and 'to_rel' is not
+        versioned but present in the working tree.
+
+        rename_one uses the first mode if 'after == False' and 'from_rel' is
+        versioned and present in the working tree, and 'to_rel' is not
+        versioned and not present in the working tree.
+
+        Everything else results in an error.
         """
         inv = self.inventory
-        if not self.has_filename(from_rel):
-            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
-        if self.has_filename(to_rel):
-            raise BzrError("can't rename: new working file %r already exists" % to_rel)
-
-        file_id = inv.path2id(from_rel)
-        if file_id is None:
-            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
-
-        entry = inv[file_id]
-        from_parent = entry.parent_id
-        from_name = entry.name
-        
-        if inv.path2id(to_rel):
-            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
-
+        rename_entries = []
+
+        # create rename entries and tuples
+        from_tail = splitpath(from_rel)[-1]
+        from_id = inv.path2id(from_rel)
+        if from_id is None:
+            raise errors.BzrRenameFailedError(from_rel,to_rel,
+                errors.NotVersionedError(path=str(from_rel)))
+        from_entry = inv[from_id]
+        from_parent_id = from_entry.parent_id
         to_dir, to_tail = os.path.split(to_rel)
         to_dir_id = inv.path2id(to_dir)
-        if to_dir_id is None and to_dir != '':
-            raise BzrError("can't determine destination directory id for %r" % to_dir)
-
-        mutter("rename_one:")
-        mutter("  file_id    {%s}" % file_id)
-        mutter("  from_rel   %r" % from_rel)
-        mutter("  to_rel     %r" % to_rel)
-        mutter("  to_dir     %r" % to_dir)
-        mutter("  to_dir_id  {%s}" % to_dir_id)
-
-        inv.rename(file_id, to_dir_id, to_tail)
-
-        from_abs = self.abspath(from_rel)
-        to_abs = self.abspath(to_rel)
-        try:
-            osutils.rename(from_abs, to_abs)
-        except OSError, e:
-            inv.rename(file_id, from_parent, from_name)
-            raise BzrError("failed to rename %r to %r: %s"
-                    % (from_abs, to_abs, e[1]))
+        rename_entry = WorkingTree._RenameEntry(from_rel=from_rel,
+                                     from_id=from_id,
+                                     from_tail=from_tail,
+                                     from_parent_id=from_parent_id,
+                                     to_rel=to_rel, to_tail=to_tail,
+                                     to_parent_id=to_dir_id)
+        rename_entries.append(rename_entry)
+
+        # determine which move mode to use. checks also for movability
+        rename_entries = self._determine_mv_mode(rename_entries, after)
+
+        # check if the target changed directory and if the target directory is
+        # versioned
+        if to_dir_id is None:
+            raise errors.BzrMoveFailedError(from_rel,to_rel,
+                errors.NotVersionedError(path=str(to_dir)))
+
+        # all checks done. now we can continue with our actual work
+        mutter('rename_one:\n'
+               '  from_id   {%s}\n'
+               '  from_rel: %r\n'
+               '  to_rel:   %r\n'
+               '  to_dir    %r\n'
+               '  to_dir_id {%s}\n',
+               from_id, from_rel, to_rel, to_dir, to_dir_id)
+
+        self._move(rename_entries)
         self._write_inventory(inv)
 
+    class _RenameEntry(object):
+        def __init__(self, from_rel, from_id, from_tail, from_parent_id,
+                     to_rel, to_tail, to_parent_id):
+            self.from_rel = from_rel
+            self.from_id = from_id
+            self.from_tail = from_tail
+            self.from_parent_id = from_parent_id
+            self.to_rel = to_rel
+            self.to_tail = to_tail
+            self.to_parent_id = to_parent_id
+            self.only_change_inv = False
+
     @needs_read_lock
     def unknowns(self):
         """Return all unknown files.
@@ -1490,7 +1651,7 @@
             if not fid:
                 # TODO: Perhaps make this just a warning, and continue?
                 # This tends to happen when 
-                raise NotVersionedError(path=f)
+                raise errors.NotVersionedError(path=f)
             if verbose:
                 # having remove it, it must be either ignored or unknown
                 if self.is_ignored(f):
@@ -1539,7 +1700,7 @@
             elif kind == 'symlink':
                 inv.add(InventoryLink(file_id, name, parent))
             else:
-                raise BzrError("unknown kind %r" % kind)
+                raise errors.BzrError("unknown kind %r" % kind)
         self._write_inventory(inv)
 
     @needs_tree_write_lock
@@ -1720,10 +1881,10 @@
         self.flush()
 
     def set_conflicts(self, arg):
-        raise UnsupportedOperation(self.set_conflicts, self)
+        raise errors.UnsupportedOperation(self.set_conflicts, self)
 
     def add_conflicts(self, arg):
-        raise UnsupportedOperation(self.add_conflicts, self)
+        raise errors.UnsupportedOperation(self.add_conflicts, self)
 
     @needs_read_lock
     def conflicts(self):
@@ -1803,7 +1964,7 @@
         """See Mutable.last_revision."""
         try:
             return self._control_files.get_utf8('last-revision').read()
-        except NoSuchFile:
+        except errors.NoSuchFile:
             return None
 
     def _change_last_revision(self, revision_id):
@@ -1834,13 +1995,13 @@
     def conflicts(self):
         try:
             confile = self._control_files.get('conflicts')
-        except NoSuchFile:
+        except errors.NoSuchFile:
             return _mod_conflicts.ConflictList()
         try:
             if confile.next() != CONFLICT_HEADER_1 + '\n':
-                raise ConflictFormatError()
+                raise errors.ConflictFormatError()
         except StopIteration:
-            raise ConflictFormatError()
+            raise errors.ConflictFormatError()
         return _mod_conflicts.ConflictList.from_stanzas(RioReader(confile))
 
     def unlock(self):
@@ -1908,7 +2069,7 @@
             transport = a_bzrdir.get_workingtree_transport(None)
             format_string = transport.get("format").read()
             return klass._formats[format_string]
-        except NoSuchFile:
+        except errors.NoSuchFile:
             raise errors.NoWorkingTree(base=transport.base)
         except KeyError:
             raise errors.UnknownFormatError(format=format_string)


More information about the bazaar mailing list