Merge lp:~jelmer/brz/brz-git-renames into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/brz-git-renames
Merge into: lp:brz
Diff against target: 503 lines (+218/-170)
8 files modified
breezy/builtins.py (+4/-4)
breezy/bzr/inventorytree.py (+1/-84)
breezy/bzr/workingtree.py (+23/-0)
breezy/tests/per_tree/test_inv.py (+0/-82)
breezy/tests/per_workingtree/__init__.py (+1/-0)
breezy/tests/per_workingtree/test_canonical_path.py (+109/-0)
breezy/tree.py (+44/-0)
breezy/workingtree.py (+36/-0)
To merge this branch: bzr merge lp:~jelmer/brz/brz-git-renames
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+354952@code.launchpad.net

Description of the change

Some refactoring around get_canonical_inventory_paths.

* Drop the 'inventory' bit since it doesn't apply to non-inventory trees
* Move to working trees; canonicalization currently only matters there, where
  files can be stored on case-insensitive (VFAT) or normalizing filesystems
  (i.e. Mac OS X)
* Run tests against all working tree formats, including Git, not just InventoryTree ones

This fixes 'bzr mv' and 'bzr rename' for Git.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Okay, moves seem reasonable to me. The logic is not all correct, but we'll need to do another pass anyway to deal with filesystem mapping issues.

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
https://ci.breezy-vcs.org/job/land-brz/477/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'breezy/builtins.py'
--- breezy/builtins.py 2018-08-12 23:18:19 +0000
+++ breezy/builtins.py 2018-09-14 14:37:48 +0000
@@ -1043,7 +1043,7 @@
1043 into_existing = False1043 into_existing = False
1044 else:1044 else:
1045 # 'fix' the case of a potential 'from'1045 # 'fix' the case of a potential 'from'
1046 from_path = tree.get_canonical_inventory_path(rel_names[0])1046 from_path = tree.get_canonical_path(rel_names[0])
1047 if (not osutils.lexists(names_list[0]) and1047 if (not osutils.lexists(names_list[0]) and
1048 tree.is_versioned(from_path) and1048 tree.is_versioned(from_path) and
1049 tree.stored_kind(from_path) == "directory"):1049 tree.stored_kind(from_path) == "directory"):
@@ -1053,7 +1053,7 @@
1053 # move into existing directory1053 # move into existing directory
1054 # All entries reference existing inventory items, so fix them up1054 # All entries reference existing inventory items, so fix them up
1055 # for cicp file-systems.1055 # for cicp file-systems.
1056 rel_names = tree.get_canonical_inventory_paths(rel_names)1056 rel_names = list(tree.get_canonical_paths(rel_names))
1057 for src, dest in tree.move(rel_names[:-1], rel_names[-1], after=after):1057 for src, dest in tree.move(rel_names[:-1], rel_names[-1], after=after):
1058 if not is_quiet():1058 if not is_quiet():
1059 self.outf.write("%s => %s\n" % (src, dest))1059 self.outf.write("%s => %s\n" % (src, dest))
@@ -1065,13 +1065,13 @@
10651065
1066 # for cicp file-systems: the src references an existing inventory1066 # for cicp file-systems: the src references an existing inventory
1067 # item:1067 # item:
1068 src = tree.get_canonical_inventory_path(rel_names[0])1068 src = tree.get_canonical_path(rel_names[0])
1069 # Find the canonical version of the destination: In all cases, the1069 # Find the canonical version of the destination: In all cases, the
1070 # parent of the target must be in the inventory, so we fetch the1070 # parent of the target must be in the inventory, so we fetch the
1071 # canonical version from there (we do not always *use* the1071 # canonical version from there (we do not always *use* the
1072 # canonicalized tail portion - we may be attempting to rename the1072 # canonicalized tail portion - we may be attempting to rename the
1073 # case of the tail)1073 # case of the tail)
1074 canon_dest = tree.get_canonical_inventory_path(rel_names[1])1074 canon_dest = tree.get_canonical_path(rel_names[1])
1075 dest_parent = osutils.dirname(canon_dest)1075 dest_parent = osutils.dirname(canon_dest)
1076 spec_tail = osutils.basename(rel_names[1])1076 spec_tail = osutils.basename(rel_names[1])
1077 # For a CICP file-system, we need to avoid creating 2 inventory1077 # For a CICP file-system, we need to avoid creating 2 inventory
10781078
=== modified file 'breezy/bzr/inventorytree.py'
--- breezy/bzr/inventorytree.py 2018-07-13 00:25:33 +0000
+++ breezy/bzr/inventorytree.py 2018-09-14 14:37:48 +0000
@@ -69,89 +69,6 @@
69 private to external API users.69 private to external API users.
70 """70 """
7171
72 def get_canonical_inventory_paths(self, paths):
73 """Like get_canonical_inventory_path() but works on multiple items.
74
75 :param paths: A sequence of paths relative to the root of the tree.
76 :return: A list of paths, with each item the corresponding input path
77 adjusted to account for existing elements that match case
78 insensitively.
79 """
80 with self.lock_read():
81 return list(self._yield_canonical_inventory_paths(paths))
82
83 def get_canonical_inventory_path(self, path):
84 """Returns the first inventory item that case-insensitively matches path.
85
86 If a path matches exactly, it is returned. If no path matches exactly
87 but more than one path matches case-insensitively, it is implementation
88 defined which is returned.
89
90 If no path matches case-insensitively, the input path is returned, but
91 with as many path entries that do exist changed to their canonical
92 form.
93
94 If you need to resolve many names from the same tree, you should
95 use get_canonical_inventory_paths() to avoid O(N) behaviour.
96
97 :param path: A paths relative to the root of the tree.
98 :return: The input path adjusted to account for existing elements
99 that match case insensitively.
100 """
101 with self.lock_read():
102 return next(self._yield_canonical_inventory_paths([path]))
103
104 def _yield_canonical_inventory_paths(self, paths):
105 for path in paths:
106 # First, if the path as specified exists exactly, just use it.
107 if self.path2id(path) is not None:
108 yield path
109 continue
110 # go walkin...
111 cur_id = self.get_root_id()
112 cur_path = ''
113 bit_iter = iter(path.split("/"))
114 for elt in bit_iter:
115 lelt = elt.lower()
116 new_path = None
117 try:
118 for child in self.iter_child_entries(self.id2path(cur_id), cur_id):
119 try:
120 # XXX: it seem like if the child is known to be in the
121 # tree, we shouldn't need to go from its id back to
122 # its path -- mbp 2010-02-11
123 #
124 # XXX: it seems like we could be more efficient
125 # by just directly looking up the original name and
126 # only then searching all children; also by not
127 # chopping paths so much. -- mbp 2010-02-11
128 child_base = os.path.basename(self.id2path(child.file_id))
129 if (child_base == elt):
130 # if we found an exact match, we can stop now; if
131 # we found an approximate match we need to keep
132 # searching because there might be an exact match
133 # later.
134 cur_id = child.file_id
135 new_path = osutils.pathjoin(cur_path, child_base)
136 break
137 elif child_base.lower() == lelt:
138 cur_id = child.file_id
139 new_path = osutils.pathjoin(cur_path, child_base)
140 except errors.NoSuchId:
141 # before a change is committed we can see this error...
142 continue
143 except errors.NotADirectory:
144 pass
145 if new_path:
146 cur_path = new_path
147 else:
148 # got to the end of this directory and no entries matched.
149 # Return what matched so far, plus the rest as specified.
150 cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter))
151 break
152 yield cur_path
153 # all done.
154
155 def _get_root_inventory(self):72 def _get_root_inventory(self):
156 return self._inventory73 return self._inventory
15774
@@ -451,7 +368,7 @@
451 def _fix_case_of_inventory_path(self, path):368 def _fix_case_of_inventory_path(self, path):
452 """If our tree isn't case sensitive, return the canonical path"""369 """If our tree isn't case sensitive, return the canonical path"""
453 if not self.case_sensitive:370 if not self.case_sensitive:
454 path = self.get_canonical_inventory_path(path)371 path = self.get_canonical_path(path)
455 return path372 return path
456373
457 def smart_add(self, file_list, recurse=True, action=None, save=True):374 def smart_add(self, file_list, recurse=True, action=None, save=True):
458375
=== modified file 'breezy/bzr/workingtree.py'
--- breezy/bzr/workingtree.py 2018-08-26 01:29:46 +0000
+++ breezy/bzr/workingtree.py 2018-09-14 14:37:48 +0000
@@ -78,6 +78,7 @@
78 )78 )
79from ..trace import mutter, note79from ..trace import mutter, note
80from ..tree import (80from ..tree import (
81 get_canonical_path,
81 FileTimestampUnavailable,82 FileTimestampUnavailable,
82 TreeDirectory,83 TreeDirectory,
83 TreeFile,84 TreeFile,
@@ -1710,6 +1711,28 @@
1710 else:1711 else:
1711 return self._check_for_tree_references(iterator)1712 return self._check_for_tree_references(iterator)
17121713
1714 def get_canonical_paths(self, paths):
1715 """Look up canonical paths for multiple items.
1716
1717 :param paths: A sequence of paths relative to the root of the tree.
1718 :return: A iterator over paths, with each item the corresponding input path
1719 adjusted to account for existing elements that match case
1720 insensitively.
1721 """
1722 with self.lock_read():
1723 if not self.case_sensitive:
1724 normalize = lambda x: x.lower()
1725 elif sys.platform == 'darwin':
1726 import unicodedata
1727 normalize = lambda x: unicodedata.normalize('NFC', x)
1728 else:
1729 normalize = None
1730 for path in paths:
1731 if normalize is None or self.is_versioned(path):
1732 yield path
1733 else:
1734 yield get_canonical_path(self, path, normalize)
1735
17131736
1714class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):1737class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
1715 """Base class for working trees that live in bzr meta directories."""1738 """Base class for working trees that live in bzr meta directories."""
17161739
=== modified file 'breezy/tests/per_tree/test_inv.py'
--- breezy/tests/per_tree/test_inv.py 2018-07-03 02:40:57 +0000
+++ breezy/tests/per_tree/test_inv.py 2018-09-14 14:37:48 +0000
@@ -27,7 +27,6 @@
27from breezy.mutabletree import MutableTree27from breezy.mutabletree import MutableTree
28from breezy.tests import TestSkipped28from breezy.tests import TestSkipped
29from breezy.transform import _PreviewTree29from breezy.transform import _PreviewTree
30from breezy.uncommit import uncommit
31from breezy.tests import (30from breezy.tests import (
32 features,31 features,
33 )32 )
@@ -98,84 +97,3 @@
98 self.addCleanup(tree.unlock)97 self.addCleanup(tree.unlock)
99 self.assertEqual(set([]), tree.paths2ids(['file'],98 self.assertEqual(set([]), tree.paths2ids(['file'],
100 require_versioned=False))99 require_versioned=False))
101
102 def _make_canonical_test_tree(self, commit=True):
103 # make a tree used by all the 'canonical' tests below.
104 work_tree = self.make_branch_and_tree('tree')
105 self.build_tree(['tree/dir/', 'tree/dir/file'])
106 work_tree.add(['dir', 'dir/file'])
107 if commit:
108 work_tree.commit('commit 1')
109 # XXX: this isn't actually guaranteed to return the class we want to
110 # test -- mbp 2010-02-12
111 return work_tree
112
113 def test_canonical_path(self):
114 work_tree = self._make_canonical_test_tree()
115 if not isinstance(work_tree, InventoryTree):
116 raise tests.TestNotApplicable(
117 "test not applicable on non-inventory tests")
118 self.assertEqual('dir/file',
119 work_tree.get_canonical_inventory_path('Dir/File'))
120
121 def test_canonical_path_before_commit(self):
122 work_tree = self._make_canonical_test_tree(False)
123 if not isinstance(work_tree, InventoryTree):
124 # note: not committed.
125 raise tests.TestNotApplicable(
126 "test not applicable on non-inventory tests")
127 self.assertEqual('dir/file',
128 work_tree.get_canonical_inventory_path('Dir/File'))
129
130 def test_canonical_path_dir(self):
131 # check it works when asked for just the directory portion.
132 work_tree = self._make_canonical_test_tree()
133 if not isinstance(work_tree, InventoryTree):
134 raise tests.TestNotApplicable(
135 "test not applicable on non-inventory tests")
136 self.assertEqual('dir', work_tree.get_canonical_inventory_path('Dir'))
137
138 def test_canonical_path_root(self):
139 work_tree = self._make_canonical_test_tree()
140 if not isinstance(work_tree, InventoryTree):
141 raise tests.TestNotApplicable(
142 "test not applicable on non-inventory tests")
143 self.assertEqual('', work_tree.get_canonical_inventory_path(''))
144 self.assertEqual('/', work_tree.get_canonical_inventory_path('/'))
145
146 def test_canonical_path_invalid_all(self):
147 work_tree = self._make_canonical_test_tree()
148 if not isinstance(work_tree, InventoryTree):
149 raise tests.TestNotApplicable(
150 "test not applicable on non-inventory tests")
151 self.assertEqual('foo/bar',
152 work_tree.get_canonical_inventory_path('foo/bar'))
153
154 def test_canonical_invalid_child(self):
155 work_tree = self._make_canonical_test_tree()
156 if not isinstance(work_tree, InventoryTree):
157 raise tests.TestNotApplicable(
158 "test not applicable on non-inventory tests")
159 self.assertEqual('dir/None',
160 work_tree.get_canonical_inventory_path('Dir/None'))
161
162 def test_canonical_tree_name_mismatch(self):
163 # see <https://bugs.launchpad.net/bzr/+bug/368931>
164 # some of the trees we want to use can only exist on a disk, not in
165 # memory - therefore we can only test this if the filesystem is
166 # case-sensitive.
167 self.requireFeature(features.case_sensitive_filesystem_feature)
168 work_tree = self.make_branch_and_tree('.')
169 self.build_tree(['test/', 'test/file', 'Test'])
170 work_tree.add(['test/', 'test/file', 'Test'])
171
172 test_tree = self._convert_tree(work_tree)
173 if not isinstance(test_tree, InventoryTree):
174 raise tests.TestNotApplicable(
175 "test not applicable on non-inventory tests")
176 test_tree.lock_read()
177 self.addCleanup(test_tree.unlock)
178
179 self.assertEqual(['test', 'test/file', 'Test', 'test/foo', 'Test/foo'],
180 test_tree.get_canonical_inventory_paths(
181 ['test', 'test/file', 'Test', 'test/foo', 'Test/foo']))
182100
=== modified file 'breezy/tests/per_workingtree/__init__.py'
--- breezy/tests/per_workingtree/__init__.py 2017-09-10 20:57:03 +0000
+++ breezy/tests/per_workingtree/__init__.py 2018-09-14 14:37:48 +0000
@@ -123,6 +123,7 @@
123 'basis_inventory',123 'basis_inventory',
124 'basis_tree',124 'basis_tree',
125 'break_lock',125 'break_lock',
126 'canonical_path',
126 'changes_from',127 'changes_from',
127 'check',128 'check',
128 'check_state',129 'check_state',
129130
=== added file 'breezy/tests/per_workingtree/test_canonical_path.py'
--- breezy/tests/per_workingtree/test_canonical_path.py 1970-01-01 00:00:00 +0000
+++ breezy/tests/per_workingtree/test_canonical_path.py 2018-09-14 14:37:48 +0000
@@ -0,0 +1,109 @@
1# Copyright (C) 2007-2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17"""Tests for interface conformance of canonical paths of trees."""
18
19
20from breezy import (
21 tests,
22 )
23from breezy.tests.per_workingtree import (
24 TestCaseWithWorkingTree,
25 )
26from breezy.tests import (
27 features,
28 )
29
30
31class TestCanonicalPaths(TestCaseWithWorkingTree):
32
33 def _make_canonical_test_tree(self, commit=True):
34 # make a tree used by all the 'canonical' tests below.
35 work_tree = self.make_branch_and_tree('tree')
36 self.build_tree(['tree/dir/', 'tree/dir/file'])
37 work_tree.add(['dir', 'dir/file'])
38 if commit:
39 work_tree.commit('commit 1')
40 # XXX: this isn't actually guaranteed to return the class we want to
41 # test -- mbp 2010-02-12
42 return work_tree
43
44 def test_canonical_path(self):
45 work_tree = self._make_canonical_test_tree()
46 if features.CaseInsensitiveFilesystemFeature.available():
47 self.assertEqual('dir/file',
48 work_tree.get_canonical_path('Dir/File'))
49 else:
50 self.assertEqual('Dir/File',
51 work_tree.get_canonical_path('Dir/File'))
52
53 def test_canonical_path_before_commit(self):
54 work_tree = self._make_canonical_test_tree(False)
55 if features.CaseInsensitiveFilesystemFeature.available():
56 self.assertEqual('dir/file',
57 work_tree.get_canonical_path('Dir/File'))
58 else:
59 self.assertEqual('Dir/File',
60 work_tree.get_canonical_path('Dir/File'))
61
62 def test_canonical_path_dir(self):
63 # check it works when asked for just the directory portion.
64 work_tree = self._make_canonical_test_tree()
65 if features.CaseInsensitiveFilesystemFeature.available():
66 self.assertEqual('dir', work_tree.get_canonical_path('Dir'))
67 else:
68 self.assertEqual('Dir', work_tree.get_canonical_path('Dir'))
69
70 def test_canonical_path_root(self):
71 work_tree = self._make_canonical_test_tree()
72 self.assertEqual('', work_tree.get_canonical_path(''))
73 self.assertEqual('/', work_tree.get_canonical_path('/'))
74
75 def test_canonical_path_invalid_all(self):
76 work_tree = self._make_canonical_test_tree()
77 self.assertEqual('foo/bar',
78 work_tree.get_canonical_path('foo/bar'))
79
80 def test_canonical_invalid_child(self):
81 work_tree = self._make_canonical_test_tree()
82 if features.CaseInsensitiveFilesystemFeature.available():
83 self.assertEqual('dir/None',
84 work_tree.get_canonical_path('Dir/None'))
85 else:
86 self.assertEqual('Dir/None',
87 work_tree.get_canonical_path('Dir/None'))
88
89 def test_canonical_tree_name_mismatch(self):
90 # see <https://bugs.launchpad.net/bzr/+bug/368931>
91 # some of the trees we want to use can only exist on a disk, not in
92 # memory - therefore we can only test this if the filesystem is
93 # case-sensitive.
94 self.requireFeature(features.case_sensitive_filesystem_feature)
95 work_tree = self.make_branch_and_tree('.')
96 self.build_tree(['test/', 'test/file', 'Test'])
97 work_tree.add(['test/', 'test/file', 'Test'])
98
99 self.assertEqual(['test', 'Test', 'test/file', 'Test/file'],
100 list(work_tree.get_canonical_paths(
101 ['test', 'Test', 'test/file', 'Test/file'])))
102
103 test_revid = work_tree.commit('commit')
104 test_tree = work_tree.branch.repository.revision_tree(test_revid)
105 test_tree.lock_read()
106 self.addCleanup(test_tree.unlock)
107
108 self.assertEqual(['', 'Test', 'test', 'test/file'],
109 [p for p, e in test_tree.iter_entries_by_dir()])
0110
=== modified file 'breezy/tree.py'
--- breezy/tree.py 2018-07-26 22:12:29 +0000
+++ breezy/tree.py 2018-09-14 14:37:48 +0000
@@ -1364,3 +1364,47 @@
1364 return to_tree.id2path(file_id)1364 return to_tree.id2path(file_id)
1365 except errors.NoSuchId:1365 except errors.NoSuchId:
1366 return None1366 return None
1367
1368
1369def get_canonical_path(tree, path, normalize):
1370 """Find the canonical path of an item, ignoring case.
1371
1372 :param tree: Tree to traverse
1373 :param path: Case-insensitive path to look up
1374 :param normalize: Function to normalize a filename for comparison
1375 :return: The canonical path
1376 """
1377 # go walkin...
1378 cur_id = self.get_root_id()
1379 cur_path = ''
1380 bit_iter = iter(path.split("/"))
1381 for elt in bit_iter:
1382 lelt = normalize(elt)
1383 new_path = None
1384 try:
1385 for child in self.iter_child_entries(cur_path, cur_id):
1386 try:
1387 if child.name == elt:
1388 # if we found an exact match, we can stop now; if
1389 # we found an approximate match we need to keep
1390 # searching because there might be an exact match
1391 # later.
1392 cur_id = child.file_id
1393 new_path = osutils.pathjoin(cur_path, child.name)
1394 break
1395 elif normalize(child.name) == lelt:
1396 cur_id = child.file_id
1397 new_path = osutils.pathjoin(cur_path, child.name)
1398 except errors.NoSuchId:
1399 # before a change is committed we can see this error...
1400 continue
1401 except errors.NotADirectory:
1402 pass
1403 if new_path:
1404 cur_path = new_path
1405 else:
1406 # got to the end of this directory and no entries matched.
1407 # Return what matched so far, plus the rest as specified.
1408 cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter))
1409 break
1410 return cur_path
13671411
=== modified file 'breezy/workingtree.py'
--- breezy/workingtree.py 2018-08-07 19:23:46 +0000
+++ breezy/workingtree.py 2018-09-14 14:37:48 +0000
@@ -1337,6 +1337,42 @@
1337 """Return the ShelfManager for this WorkingTree."""1337 """Return the ShelfManager for this WorkingTree."""
1338 raise NotImplementedError(self.get_shelf_manager)1338 raise NotImplementedError(self.get_shelf_manager)
13391339
1340 def get_canonical_paths(self, paths):
1341 """Like get_canonical_path() but works on multiple items.
1342
1343 :param paths: A sequence of paths relative to the root of the tree.
1344 :return: A list of paths, with each item the corresponding input path
1345 adjusted to account for existing elements that match case
1346 insensitively.
1347 """
1348 with self.lock_read():
1349 for path in paths:
1350 yield path
1351
1352 def get_canonical_path(self, path):
1353 """Returns the first item in the tree that matches a path.
1354
1355 This is meant to allow case-insensitive path lookups on e.g.
1356 FAT filesystems.
1357
1358 If a path matches exactly, it is returned. If no path matches exactly
1359 but more than one path matches according to the underlying file system,
1360 it is implementation defined which is returned.
1361
1362 If no path matches according to the file system, the input path is
1363 returned, but with as many path entries that do exist changed to their
1364 canonical form.
1365
1366 If you need to resolve many names from the same tree, you should
1367 use get_canonical_paths() to avoid O(N) behaviour.
1368
1369 :param path: A paths relative to the root of the tree.
1370 :return: The input path adjusted to account for existing elements
1371 that match case insensitively.
1372 """
1373 with self.lock_read():
1374 return next(self.get_canonical_paths([path]))
1375
13401376
1341class WorkingTreeFormatRegistry(controldir.ControlComponentFormatRegistry):1377class WorkingTreeFormatRegistry(controldir.ControlComponentFormatRegistry):
1342 """Registry for working tree formats."""1378 """Registry for working tree formats."""

Subscribers

People subscribed via source and target branches