[MERGE] enhanced mv command
Marius Kruger
amanic at gmail.com
Fri Jan 19 19:52:33 GMT 2007
strike 5
attached diff and zipped bundle again,
hope its more better this time.
On 1/18/07, Aaron Bentley <aaron.bentley at utoronto.ca> wrote:
> > Would you like me to address the comments lying around the list and
> > submit another patch.
>
> That's not necessary to me, since I've promised to address them when
> merging. I don't think you need to, but it might be a bit clearer.
>
here is the stuff I addressed:
1) Changed "FilesExist" to more specific "RenameFailedFilesExist" (John's
comments)
- I didn't realize that its introduced and thus only used by this
patch.
I did though it might be useful to have a generic exception,
but FilesExist isn't something which would often be a problem.
2) Fixed errors.py import order
3) Improved WorkingTree._move Exception catching as best I could
4) Removed extra blank lines from doc strings in test_mv.py
as suggested by martin. I thought it is neccessary after reading pep257
5) Removed assertNone,
from using other unittesting frameworks (c++& java),
I'm sort of used to have a asser*Null, but if you guys really don't
think its
nice, what can I say.
I still prefer
self.assertNone(tree.path2id(path))
self.assertNone(tree.path2id(path), path+' not in working tree.')
over
self.assertIsNot(tree.path2id(path), None)
self.assertIsNot(tree.path2id(path), None, path+' not in working
tree.')
--
I code therefore I am.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: https://lists.ubuntu.com/archives/bazaar/attachments/20070119/e33e9f1d/attachment-0001.htm
-------------- next part --------------
=== modified file 'NEWS'
--- NEWS 2007-01-18 03:58:02 +0000
+++ NEWS 2007-01-18 04:11:33 +0000
@@ -1,7 +1,17 @@
IN DEVELOPMENT
-
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)
+
* ``ls`` now works on treeless branches and remote branches.
(Aaron Bentley)
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2007-01-18 03:58:02 +0000
+++ bzrlib/builtins.py 2007-01-18 04:05:43 +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-18 03:58:02 +0000
+++ bzrlib/errors.py 2007-01-19 19:23:35 +0000
@@ -18,12 +18,17 @@
"""
-from bzrlib import symbol_versioning
-from bzrlib.patches import (PatchSyntax,
- PatchConflict,
- MalformedPatchHeader,
- MalformedHunkHeader,
- MalformedLine,)
+from bzrlib import (
+ osutils,
+ symbol_versioning,
+ )
+from bzrlib.patches import (
+ MalformedHunkHeader,
+ MalformedLine,
+ MalformedPatchHeader,
+ PatchConflict,
+ PatchSyntax,
+ )
# TODO: is there any value in providing the .args field used by standard
@@ -333,6 +338,32 @@
_fmt = "File exists: %(path)r%(extra)s"
+class RenameFailedFilesExist(BzrError):
+ """Used when renaming and both source and dest exist."""
+
+ _fmt = ("Could not rename %(source)s => %(dest)s because both files exist."
+ "%(extra)s")
+
+ def __init__(self, source, dest, extra=None):
+ BzrError.__init__(self)
+ self.source = str(source)
+ self.dest = str(dest)
+ if extra:
+ self.extra = ' ' + str(extra)
+ else:
+ self.extra = ''
+
+
+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"
@@ -505,17 +536,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"
@@ -528,17 +592,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):
@@ -1292,6 +1360,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-17 04:32:59 +0000
+++ bzrlib/tests/__init__.py 2007-01-19 19:40:18 +0000
@@ -661,9 +661,19 @@
excName = str(excClass)
raise self.failureException, "%s not raised" % excName
- def assertIs(self, left, right):
+ def assertIs(self, left, right, message=None):
if not (left is right):
- raise AssertionError("%r is not %r." % (left, right))
+ if message is not None:
+ raise AssertionError(message)
+ else:
+ raise AssertionError("%r is not %r." % (left, right))
+
+ def assertIsNot(self, left, right, message=None):
+ if (left is right):
+ if message is not None:
+ raise AssertionError(message)
+ else:
+ raise AssertionError("%r is %r." % (left, right))
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-19 19:39:39 +0000
@@ -26,10 +26,29 @@
TestCaseWithTransport,
TestSkipped,
)
-
+from bzrlib.osutils import (
+ splitpath
+ )
class TestMove(TestCaseWithTransport):
+ def assertInWorkingTree(self,path):
+ tree = workingtree.WorkingTree.open('.')
+ self.assertIsNot(tree.path2id(path), None,
+ path+' not in working tree.')
+
+ def assertNotInWorkingTree(self,path):
+ tree = workingtree.WorkingTree.open('.')
+ self.assertIs(tree.path2id(path), None, 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 +56,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 +158,215 @@
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 because both files exist."
+ " \(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 => sub/a1 because both files exist."
+ " \(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 2007-01-18 03:58:02 +0000
+++ bzrlib/tests/workingtree_implementations/test_workingtree.py 2007-01-18 04:05:43 +0000
@@ -682,3 +682,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-19 19:21:47 +0000
@@ -72,16 +72,6 @@
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 +81,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 +229,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 +255,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 +386,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 +504,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 +623,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 +638,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 +779,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 +821,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 +952,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 +974,292 @@
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.RenameFailedFilesExist(from_rel, 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:
+ self._rollback_move(moved)
+ raise
+ moved.append(entry)
+
+ def _rollback_move(self, moved):
+ """Try to rollback a previous move in case of an filesystem error."""
+ inv = self.inventory
+ for entry in moved:
+ try:
+ self._move_entry(_RenameEntry(entry.to_rel, entry.from_id,
+ entry.to_tail, entry.to_parent_id, entry.from_rel,
+ entry.from_tail, entry.from_parent_id,
+ entry.only_change_inv))
+ except OSError, e:
+ raise errors.BzrMoveFailedError( '', '', "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):
+ 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(entry.from_rel, entry.to_rel,
+ "Source and target are identical.")
+
+ if not entry.only_change_inv:
+ try:
+ osutils.rename(from_rel_abs, to_rel_abs)
+ except OSError, e:
+ raise errors.BzrMoveFailedError(entry.from_rel,
+ entry.to_rel, e[1])
+ 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, only_change_inv=False):
+ 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 = only_change_inv
+
@needs_read_lock
def unknowns(self):
"""Return all unknown files.
@@ -1490,7 +1643,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 +1692,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 +1873,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 +1956,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 +1987,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 +2061,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)
-------------- next part --------------
A non-text attachment was scrubbed...
Name: enhancedmv5.patch.zip
Type: application/zip
Size: 358504 bytes
Desc: not available
Url : https://lists.ubuntu.com/archives/bazaar/attachments/20070119/e33e9f1d/attachment-0001.zip
More information about the bazaar
mailing list