Merge lp:~jelmer/brz/brz-git-renames into lp:brz
- brz-git-renames
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Approve | ||
Review via email: mp+354952@code.launchpad.net |
Commit message
Description of the change
Some refactoring around get_canonical_
* 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
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
https:/
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/builtins.py' | |||
2 | --- breezy/builtins.py 2018-08-12 23:18:19 +0000 | |||
3 | +++ breezy/builtins.py 2018-09-14 14:37:48 +0000 | |||
4 | @@ -1043,7 +1043,7 @@ | |||
5 | 1043 | into_existing = False | 1043 | into_existing = False |
6 | 1044 | else: | 1044 | else: |
7 | 1045 | # 'fix' the case of a potential 'from' | 1045 | # 'fix' the case of a potential 'from' |
9 | 1046 | from_path = tree.get_canonical_inventory_path(rel_names[0]) | 1046 | from_path = tree.get_canonical_path(rel_names[0]) |
10 | 1047 | if (not osutils.lexists(names_list[0]) and | 1047 | if (not osutils.lexists(names_list[0]) and |
11 | 1048 | tree.is_versioned(from_path) and | 1048 | tree.is_versioned(from_path) and |
12 | 1049 | tree.stored_kind(from_path) == "directory"): | 1049 | tree.stored_kind(from_path) == "directory"): |
13 | @@ -1053,7 +1053,7 @@ | |||
14 | 1053 | # move into existing directory | 1053 | # move into existing directory |
15 | 1054 | # All entries reference existing inventory items, so fix them up | 1054 | # All entries reference existing inventory items, so fix them up |
16 | 1055 | # for cicp file-systems. | 1055 | # for cicp file-systems. |
18 | 1056 | rel_names = tree.get_canonical_inventory_paths(rel_names) | 1056 | rel_names = list(tree.get_canonical_paths(rel_names)) |
19 | 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): |
20 | 1058 | if not is_quiet(): | 1058 | if not is_quiet(): |
21 | 1059 | self.outf.write("%s => %s\n" % (src, dest)) | 1059 | self.outf.write("%s => %s\n" % (src, dest)) |
22 | @@ -1065,13 +1065,13 @@ | |||
23 | 1065 | 1065 | ||
24 | 1066 | # for cicp file-systems: the src references an existing inventory | 1066 | # for cicp file-systems: the src references an existing inventory |
25 | 1067 | # item: | 1067 | # item: |
27 | 1068 | src = tree.get_canonical_inventory_path(rel_names[0]) | 1068 | src = tree.get_canonical_path(rel_names[0]) |
28 | 1069 | # Find the canonical version of the destination: In all cases, the | 1069 | # Find the canonical version of the destination: In all cases, the |
29 | 1070 | # parent of the target must be in the inventory, so we fetch the | 1070 | # parent of the target must be in the inventory, so we fetch the |
30 | 1071 | # canonical version from there (we do not always *use* the | 1071 | # canonical version from there (we do not always *use* the |
31 | 1072 | # canonicalized tail portion - we may be attempting to rename the | 1072 | # canonicalized tail portion - we may be attempting to rename the |
32 | 1073 | # case of the tail) | 1073 | # case of the tail) |
34 | 1074 | canon_dest = tree.get_canonical_inventory_path(rel_names[1]) | 1074 | canon_dest = tree.get_canonical_path(rel_names[1]) |
35 | 1075 | dest_parent = osutils.dirname(canon_dest) | 1075 | dest_parent = osutils.dirname(canon_dest) |
36 | 1076 | spec_tail = osutils.basename(rel_names[1]) | 1076 | spec_tail = osutils.basename(rel_names[1]) |
37 | 1077 | # For a CICP file-system, we need to avoid creating 2 inventory | 1077 | # For a CICP file-system, we need to avoid creating 2 inventory |
38 | 1078 | 1078 | ||
39 | === modified file 'breezy/bzr/inventorytree.py' | |||
40 | --- breezy/bzr/inventorytree.py 2018-07-13 00:25:33 +0000 | |||
41 | +++ breezy/bzr/inventorytree.py 2018-09-14 14:37:48 +0000 | |||
42 | @@ -69,89 +69,6 @@ | |||
43 | 69 | private to external API users. | 69 | private to external API users. |
44 | 70 | """ | 70 | """ |
45 | 71 | 71 | ||
46 | 72 | def get_canonical_inventory_paths(self, paths): | ||
47 | 73 | """Like get_canonical_inventory_path() but works on multiple items. | ||
48 | 74 | |||
49 | 75 | :param paths: A sequence of paths relative to the root of the tree. | ||
50 | 76 | :return: A list of paths, with each item the corresponding input path | ||
51 | 77 | adjusted to account for existing elements that match case | ||
52 | 78 | insensitively. | ||
53 | 79 | """ | ||
54 | 80 | with self.lock_read(): | ||
55 | 81 | return list(self._yield_canonical_inventory_paths(paths)) | ||
56 | 82 | |||
57 | 83 | def get_canonical_inventory_path(self, path): | ||
58 | 84 | """Returns the first inventory item that case-insensitively matches path. | ||
59 | 85 | |||
60 | 86 | If a path matches exactly, it is returned. If no path matches exactly | ||
61 | 87 | but more than one path matches case-insensitively, it is implementation | ||
62 | 88 | defined which is returned. | ||
63 | 89 | |||
64 | 90 | If no path matches case-insensitively, the input path is returned, but | ||
65 | 91 | with as many path entries that do exist changed to their canonical | ||
66 | 92 | form. | ||
67 | 93 | |||
68 | 94 | If you need to resolve many names from the same tree, you should | ||
69 | 95 | use get_canonical_inventory_paths() to avoid O(N) behaviour. | ||
70 | 96 | |||
71 | 97 | :param path: A paths relative to the root of the tree. | ||
72 | 98 | :return: The input path adjusted to account for existing elements | ||
73 | 99 | that match case insensitively. | ||
74 | 100 | """ | ||
75 | 101 | with self.lock_read(): | ||
76 | 102 | return next(self._yield_canonical_inventory_paths([path])) | ||
77 | 103 | |||
78 | 104 | def _yield_canonical_inventory_paths(self, paths): | ||
79 | 105 | for path in paths: | ||
80 | 106 | # First, if the path as specified exists exactly, just use it. | ||
81 | 107 | if self.path2id(path) is not None: | ||
82 | 108 | yield path | ||
83 | 109 | continue | ||
84 | 110 | # go walkin... | ||
85 | 111 | cur_id = self.get_root_id() | ||
86 | 112 | cur_path = '' | ||
87 | 113 | bit_iter = iter(path.split("/")) | ||
88 | 114 | for elt in bit_iter: | ||
89 | 115 | lelt = elt.lower() | ||
90 | 116 | new_path = None | ||
91 | 117 | try: | ||
92 | 118 | for child in self.iter_child_entries(self.id2path(cur_id), cur_id): | ||
93 | 119 | try: | ||
94 | 120 | # XXX: it seem like if the child is known to be in the | ||
95 | 121 | # tree, we shouldn't need to go from its id back to | ||
96 | 122 | # its path -- mbp 2010-02-11 | ||
97 | 123 | # | ||
98 | 124 | # XXX: it seems like we could be more efficient | ||
99 | 125 | # by just directly looking up the original name and | ||
100 | 126 | # only then searching all children; also by not | ||
101 | 127 | # chopping paths so much. -- mbp 2010-02-11 | ||
102 | 128 | child_base = os.path.basename(self.id2path(child.file_id)) | ||
103 | 129 | if (child_base == elt): | ||
104 | 130 | # if we found an exact match, we can stop now; if | ||
105 | 131 | # we found an approximate match we need to keep | ||
106 | 132 | # searching because there might be an exact match | ||
107 | 133 | # later. | ||
108 | 134 | cur_id = child.file_id | ||
109 | 135 | new_path = osutils.pathjoin(cur_path, child_base) | ||
110 | 136 | break | ||
111 | 137 | elif child_base.lower() == lelt: | ||
112 | 138 | cur_id = child.file_id | ||
113 | 139 | new_path = osutils.pathjoin(cur_path, child_base) | ||
114 | 140 | except errors.NoSuchId: | ||
115 | 141 | # before a change is committed we can see this error... | ||
116 | 142 | continue | ||
117 | 143 | except errors.NotADirectory: | ||
118 | 144 | pass | ||
119 | 145 | if new_path: | ||
120 | 146 | cur_path = new_path | ||
121 | 147 | else: | ||
122 | 148 | # got to the end of this directory and no entries matched. | ||
123 | 149 | # Return what matched so far, plus the rest as specified. | ||
124 | 150 | cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter)) | ||
125 | 151 | break | ||
126 | 152 | yield cur_path | ||
127 | 153 | # all done. | ||
128 | 154 | |||
129 | 155 | def _get_root_inventory(self): | 72 | def _get_root_inventory(self): |
130 | 156 | return self._inventory | 73 | return self._inventory |
131 | 157 | 74 | ||
132 | @@ -451,7 +368,7 @@ | |||
133 | 451 | def _fix_case_of_inventory_path(self, path): | 368 | def _fix_case_of_inventory_path(self, path): |
134 | 452 | """If our tree isn't case sensitive, return the canonical path""" | 369 | """If our tree isn't case sensitive, return the canonical path""" |
135 | 453 | if not self.case_sensitive: | 370 | if not self.case_sensitive: |
137 | 454 | path = self.get_canonical_inventory_path(path) | 371 | path = self.get_canonical_path(path) |
138 | 455 | return path | 372 | return path |
139 | 456 | 373 | ||
140 | 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): |
141 | 458 | 375 | ||
142 | === modified file 'breezy/bzr/workingtree.py' | |||
143 | --- breezy/bzr/workingtree.py 2018-08-26 01:29:46 +0000 | |||
144 | +++ breezy/bzr/workingtree.py 2018-09-14 14:37:48 +0000 | |||
145 | @@ -78,6 +78,7 @@ | |||
146 | 78 | ) | 78 | ) |
147 | 79 | from ..trace import mutter, note | 79 | from ..trace import mutter, note |
148 | 80 | from ..tree import ( | 80 | from ..tree import ( |
149 | 81 | get_canonical_path, | ||
150 | 81 | FileTimestampUnavailable, | 82 | FileTimestampUnavailable, |
151 | 82 | TreeDirectory, | 83 | TreeDirectory, |
152 | 83 | TreeFile, | 84 | TreeFile, |
153 | @@ -1710,6 +1711,28 @@ | |||
154 | 1710 | else: | 1711 | else: |
155 | 1711 | return self._check_for_tree_references(iterator) | 1712 | return self._check_for_tree_references(iterator) |
156 | 1712 | 1713 | ||
157 | 1714 | def get_canonical_paths(self, paths): | ||
158 | 1715 | """Look up canonical paths for multiple items. | ||
159 | 1716 | |||
160 | 1717 | :param paths: A sequence of paths relative to the root of the tree. | ||
161 | 1718 | :return: A iterator over paths, with each item the corresponding input path | ||
162 | 1719 | adjusted to account for existing elements that match case | ||
163 | 1720 | insensitively. | ||
164 | 1721 | """ | ||
165 | 1722 | with self.lock_read(): | ||
166 | 1723 | if not self.case_sensitive: | ||
167 | 1724 | normalize = lambda x: x.lower() | ||
168 | 1725 | elif sys.platform == 'darwin': | ||
169 | 1726 | import unicodedata | ||
170 | 1727 | normalize = lambda x: unicodedata.normalize('NFC', x) | ||
171 | 1728 | else: | ||
172 | 1729 | normalize = None | ||
173 | 1730 | for path in paths: | ||
174 | 1731 | if normalize is None or self.is_versioned(path): | ||
175 | 1732 | yield path | ||
176 | 1733 | else: | ||
177 | 1734 | yield get_canonical_path(self, path, normalize) | ||
178 | 1735 | |||
179 | 1713 | 1736 | ||
180 | 1714 | class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat): | 1737 | class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat): |
181 | 1715 | """Base class for working trees that live in bzr meta directories.""" | 1738 | """Base class for working trees that live in bzr meta directories.""" |
182 | 1716 | 1739 | ||
183 | === modified file 'breezy/tests/per_tree/test_inv.py' | |||
184 | --- breezy/tests/per_tree/test_inv.py 2018-07-03 02:40:57 +0000 | |||
185 | +++ breezy/tests/per_tree/test_inv.py 2018-09-14 14:37:48 +0000 | |||
186 | @@ -27,7 +27,6 @@ | |||
187 | 27 | from breezy.mutabletree import MutableTree | 27 | from breezy.mutabletree import MutableTree |
188 | 28 | from breezy.tests import TestSkipped | 28 | from breezy.tests import TestSkipped |
189 | 29 | from breezy.transform import _PreviewTree | 29 | from breezy.transform import _PreviewTree |
190 | 30 | from breezy.uncommit import uncommit | ||
191 | 31 | from breezy.tests import ( | 30 | from breezy.tests import ( |
192 | 32 | features, | 31 | features, |
193 | 33 | ) | 32 | ) |
194 | @@ -98,84 +97,3 @@ | |||
195 | 98 | self.addCleanup(tree.unlock) | 97 | self.addCleanup(tree.unlock) |
196 | 99 | self.assertEqual(set([]), tree.paths2ids(['file'], | 98 | self.assertEqual(set([]), tree.paths2ids(['file'], |
197 | 100 | require_versioned=False)) | 99 | require_versioned=False)) |
198 | 101 | |||
199 | 102 | def _make_canonical_test_tree(self, commit=True): | ||
200 | 103 | # make a tree used by all the 'canonical' tests below. | ||
201 | 104 | work_tree = self.make_branch_and_tree('tree') | ||
202 | 105 | self.build_tree(['tree/dir/', 'tree/dir/file']) | ||
203 | 106 | work_tree.add(['dir', 'dir/file']) | ||
204 | 107 | if commit: | ||
205 | 108 | work_tree.commit('commit 1') | ||
206 | 109 | # XXX: this isn't actually guaranteed to return the class we want to | ||
207 | 110 | # test -- mbp 2010-02-12 | ||
208 | 111 | return work_tree | ||
209 | 112 | |||
210 | 113 | def test_canonical_path(self): | ||
211 | 114 | work_tree = self._make_canonical_test_tree() | ||
212 | 115 | if not isinstance(work_tree, InventoryTree): | ||
213 | 116 | raise tests.TestNotApplicable( | ||
214 | 117 | "test not applicable on non-inventory tests") | ||
215 | 118 | self.assertEqual('dir/file', | ||
216 | 119 | work_tree.get_canonical_inventory_path('Dir/File')) | ||
217 | 120 | |||
218 | 121 | def test_canonical_path_before_commit(self): | ||
219 | 122 | work_tree = self._make_canonical_test_tree(False) | ||
220 | 123 | if not isinstance(work_tree, InventoryTree): | ||
221 | 124 | # note: not committed. | ||
222 | 125 | raise tests.TestNotApplicable( | ||
223 | 126 | "test not applicable on non-inventory tests") | ||
224 | 127 | self.assertEqual('dir/file', | ||
225 | 128 | work_tree.get_canonical_inventory_path('Dir/File')) | ||
226 | 129 | |||
227 | 130 | def test_canonical_path_dir(self): | ||
228 | 131 | # check it works when asked for just the directory portion. | ||
229 | 132 | work_tree = self._make_canonical_test_tree() | ||
230 | 133 | if not isinstance(work_tree, InventoryTree): | ||
231 | 134 | raise tests.TestNotApplicable( | ||
232 | 135 | "test not applicable on non-inventory tests") | ||
233 | 136 | self.assertEqual('dir', work_tree.get_canonical_inventory_path('Dir')) | ||
234 | 137 | |||
235 | 138 | def test_canonical_path_root(self): | ||
236 | 139 | work_tree = self._make_canonical_test_tree() | ||
237 | 140 | if not isinstance(work_tree, InventoryTree): | ||
238 | 141 | raise tests.TestNotApplicable( | ||
239 | 142 | "test not applicable on non-inventory tests") | ||
240 | 143 | self.assertEqual('', work_tree.get_canonical_inventory_path('')) | ||
241 | 144 | self.assertEqual('/', work_tree.get_canonical_inventory_path('/')) | ||
242 | 145 | |||
243 | 146 | def test_canonical_path_invalid_all(self): | ||
244 | 147 | work_tree = self._make_canonical_test_tree() | ||
245 | 148 | if not isinstance(work_tree, InventoryTree): | ||
246 | 149 | raise tests.TestNotApplicable( | ||
247 | 150 | "test not applicable on non-inventory tests") | ||
248 | 151 | self.assertEqual('foo/bar', | ||
249 | 152 | work_tree.get_canonical_inventory_path('foo/bar')) | ||
250 | 153 | |||
251 | 154 | def test_canonical_invalid_child(self): | ||
252 | 155 | work_tree = self._make_canonical_test_tree() | ||
253 | 156 | if not isinstance(work_tree, InventoryTree): | ||
254 | 157 | raise tests.TestNotApplicable( | ||
255 | 158 | "test not applicable on non-inventory tests") | ||
256 | 159 | self.assertEqual('dir/None', | ||
257 | 160 | work_tree.get_canonical_inventory_path('Dir/None')) | ||
258 | 161 | |||
259 | 162 | def test_canonical_tree_name_mismatch(self): | ||
260 | 163 | # see <https://bugs.launchpad.net/bzr/+bug/368931> | ||
261 | 164 | # some of the trees we want to use can only exist on a disk, not in | ||
262 | 165 | # memory - therefore we can only test this if the filesystem is | ||
263 | 166 | # case-sensitive. | ||
264 | 167 | self.requireFeature(features.case_sensitive_filesystem_feature) | ||
265 | 168 | work_tree = self.make_branch_and_tree('.') | ||
266 | 169 | self.build_tree(['test/', 'test/file', 'Test']) | ||
267 | 170 | work_tree.add(['test/', 'test/file', 'Test']) | ||
268 | 171 | |||
269 | 172 | test_tree = self._convert_tree(work_tree) | ||
270 | 173 | if not isinstance(test_tree, InventoryTree): | ||
271 | 174 | raise tests.TestNotApplicable( | ||
272 | 175 | "test not applicable on non-inventory tests") | ||
273 | 176 | test_tree.lock_read() | ||
274 | 177 | self.addCleanup(test_tree.unlock) | ||
275 | 178 | |||
276 | 179 | self.assertEqual(['test', 'test/file', 'Test', 'test/foo', 'Test/foo'], | ||
277 | 180 | test_tree.get_canonical_inventory_paths( | ||
278 | 181 | ['test', 'test/file', 'Test', 'test/foo', 'Test/foo'])) | ||
279 | 182 | 100 | ||
280 | === modified file 'breezy/tests/per_workingtree/__init__.py' | |||
281 | --- breezy/tests/per_workingtree/__init__.py 2017-09-10 20:57:03 +0000 | |||
282 | +++ breezy/tests/per_workingtree/__init__.py 2018-09-14 14:37:48 +0000 | |||
283 | @@ -123,6 +123,7 @@ | |||
284 | 123 | 'basis_inventory', | 123 | 'basis_inventory', |
285 | 124 | 'basis_tree', | 124 | 'basis_tree', |
286 | 125 | 'break_lock', | 125 | 'break_lock', |
287 | 126 | 'canonical_path', | ||
288 | 126 | 'changes_from', | 127 | 'changes_from', |
289 | 127 | 'check', | 128 | 'check', |
290 | 128 | 'check_state', | 129 | 'check_state', |
291 | 129 | 130 | ||
292 | === added file 'breezy/tests/per_workingtree/test_canonical_path.py' | |||
293 | --- breezy/tests/per_workingtree/test_canonical_path.py 1970-01-01 00:00:00 +0000 | |||
294 | +++ breezy/tests/per_workingtree/test_canonical_path.py 2018-09-14 14:37:48 +0000 | |||
295 | @@ -0,0 +1,109 @@ | |||
296 | 1 | # Copyright (C) 2007-2010 Canonical Ltd | ||
297 | 2 | # | ||
298 | 3 | # This program is free software; you can redistribute it and/or modify | ||
299 | 4 | # it under the terms of the GNU General Public License as published by | ||
300 | 5 | # the Free Software Foundation; either version 2 of the License, or | ||
301 | 6 | # (at your option) any later version. | ||
302 | 7 | # | ||
303 | 8 | # This program is distributed in the hope that it will be useful, | ||
304 | 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
305 | 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
306 | 11 | # GNU General Public License for more details. | ||
307 | 12 | # | ||
308 | 13 | # You should have received a copy of the GNU General Public License | ||
309 | 14 | # along with this program; if not, write to the Free Software | ||
310 | 15 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
311 | 16 | |||
312 | 17 | """Tests for interface conformance of canonical paths of trees.""" | ||
313 | 18 | |||
314 | 19 | |||
315 | 20 | from breezy import ( | ||
316 | 21 | tests, | ||
317 | 22 | ) | ||
318 | 23 | from breezy.tests.per_workingtree import ( | ||
319 | 24 | TestCaseWithWorkingTree, | ||
320 | 25 | ) | ||
321 | 26 | from breezy.tests import ( | ||
322 | 27 | features, | ||
323 | 28 | ) | ||
324 | 29 | |||
325 | 30 | |||
326 | 31 | class TestCanonicalPaths(TestCaseWithWorkingTree): | ||
327 | 32 | |||
328 | 33 | def _make_canonical_test_tree(self, commit=True): | ||
329 | 34 | # make a tree used by all the 'canonical' tests below. | ||
330 | 35 | work_tree = self.make_branch_and_tree('tree') | ||
331 | 36 | self.build_tree(['tree/dir/', 'tree/dir/file']) | ||
332 | 37 | work_tree.add(['dir', 'dir/file']) | ||
333 | 38 | if commit: | ||
334 | 39 | work_tree.commit('commit 1') | ||
335 | 40 | # XXX: this isn't actually guaranteed to return the class we want to | ||
336 | 41 | # test -- mbp 2010-02-12 | ||
337 | 42 | return work_tree | ||
338 | 43 | |||
339 | 44 | def test_canonical_path(self): | ||
340 | 45 | work_tree = self._make_canonical_test_tree() | ||
341 | 46 | if features.CaseInsensitiveFilesystemFeature.available(): | ||
342 | 47 | self.assertEqual('dir/file', | ||
343 | 48 | work_tree.get_canonical_path('Dir/File')) | ||
344 | 49 | else: | ||
345 | 50 | self.assertEqual('Dir/File', | ||
346 | 51 | work_tree.get_canonical_path('Dir/File')) | ||
347 | 52 | |||
348 | 53 | def test_canonical_path_before_commit(self): | ||
349 | 54 | work_tree = self._make_canonical_test_tree(False) | ||
350 | 55 | if features.CaseInsensitiveFilesystemFeature.available(): | ||
351 | 56 | self.assertEqual('dir/file', | ||
352 | 57 | work_tree.get_canonical_path('Dir/File')) | ||
353 | 58 | else: | ||
354 | 59 | self.assertEqual('Dir/File', | ||
355 | 60 | work_tree.get_canonical_path('Dir/File')) | ||
356 | 61 | |||
357 | 62 | def test_canonical_path_dir(self): | ||
358 | 63 | # check it works when asked for just the directory portion. | ||
359 | 64 | work_tree = self._make_canonical_test_tree() | ||
360 | 65 | if features.CaseInsensitiveFilesystemFeature.available(): | ||
361 | 66 | self.assertEqual('dir', work_tree.get_canonical_path('Dir')) | ||
362 | 67 | else: | ||
363 | 68 | self.assertEqual('Dir', work_tree.get_canonical_path('Dir')) | ||
364 | 69 | |||
365 | 70 | def test_canonical_path_root(self): | ||
366 | 71 | work_tree = self._make_canonical_test_tree() | ||
367 | 72 | self.assertEqual('', work_tree.get_canonical_path('')) | ||
368 | 73 | self.assertEqual('/', work_tree.get_canonical_path('/')) | ||
369 | 74 | |||
370 | 75 | def test_canonical_path_invalid_all(self): | ||
371 | 76 | work_tree = self._make_canonical_test_tree() | ||
372 | 77 | self.assertEqual('foo/bar', | ||
373 | 78 | work_tree.get_canonical_path('foo/bar')) | ||
374 | 79 | |||
375 | 80 | def test_canonical_invalid_child(self): | ||
376 | 81 | work_tree = self._make_canonical_test_tree() | ||
377 | 82 | if features.CaseInsensitiveFilesystemFeature.available(): | ||
378 | 83 | self.assertEqual('dir/None', | ||
379 | 84 | work_tree.get_canonical_path('Dir/None')) | ||
380 | 85 | else: | ||
381 | 86 | self.assertEqual('Dir/None', | ||
382 | 87 | work_tree.get_canonical_path('Dir/None')) | ||
383 | 88 | |||
384 | 89 | def test_canonical_tree_name_mismatch(self): | ||
385 | 90 | # see <https://bugs.launchpad.net/bzr/+bug/368931> | ||
386 | 91 | # some of the trees we want to use can only exist on a disk, not in | ||
387 | 92 | # memory - therefore we can only test this if the filesystem is | ||
388 | 93 | # case-sensitive. | ||
389 | 94 | self.requireFeature(features.case_sensitive_filesystem_feature) | ||
390 | 95 | work_tree = self.make_branch_and_tree('.') | ||
391 | 96 | self.build_tree(['test/', 'test/file', 'Test']) | ||
392 | 97 | work_tree.add(['test/', 'test/file', 'Test']) | ||
393 | 98 | |||
394 | 99 | self.assertEqual(['test', 'Test', 'test/file', 'Test/file'], | ||
395 | 100 | list(work_tree.get_canonical_paths( | ||
396 | 101 | ['test', 'Test', 'test/file', 'Test/file']))) | ||
397 | 102 | |||
398 | 103 | test_revid = work_tree.commit('commit') | ||
399 | 104 | test_tree = work_tree.branch.repository.revision_tree(test_revid) | ||
400 | 105 | test_tree.lock_read() | ||
401 | 106 | self.addCleanup(test_tree.unlock) | ||
402 | 107 | |||
403 | 108 | self.assertEqual(['', 'Test', 'test', 'test/file'], | ||
404 | 109 | [p for p, e in test_tree.iter_entries_by_dir()]) | ||
405 | 0 | 110 | ||
406 | === modified file 'breezy/tree.py' | |||
407 | --- breezy/tree.py 2018-07-26 22:12:29 +0000 | |||
408 | +++ breezy/tree.py 2018-09-14 14:37:48 +0000 | |||
409 | @@ -1364,3 +1364,47 @@ | |||
410 | 1364 | return to_tree.id2path(file_id) | 1364 | return to_tree.id2path(file_id) |
411 | 1365 | except errors.NoSuchId: | 1365 | except errors.NoSuchId: |
412 | 1366 | return None | 1366 | return None |
413 | 1367 | |||
414 | 1368 | |||
415 | 1369 | def get_canonical_path(tree, path, normalize): | ||
416 | 1370 | """Find the canonical path of an item, ignoring case. | ||
417 | 1371 | |||
418 | 1372 | :param tree: Tree to traverse | ||
419 | 1373 | :param path: Case-insensitive path to look up | ||
420 | 1374 | :param normalize: Function to normalize a filename for comparison | ||
421 | 1375 | :return: The canonical path | ||
422 | 1376 | """ | ||
423 | 1377 | # go walkin... | ||
424 | 1378 | cur_id = self.get_root_id() | ||
425 | 1379 | cur_path = '' | ||
426 | 1380 | bit_iter = iter(path.split("/")) | ||
427 | 1381 | for elt in bit_iter: | ||
428 | 1382 | lelt = normalize(elt) | ||
429 | 1383 | new_path = None | ||
430 | 1384 | try: | ||
431 | 1385 | for child in self.iter_child_entries(cur_path, cur_id): | ||
432 | 1386 | try: | ||
433 | 1387 | if child.name == elt: | ||
434 | 1388 | # if we found an exact match, we can stop now; if | ||
435 | 1389 | # we found an approximate match we need to keep | ||
436 | 1390 | # searching because there might be an exact match | ||
437 | 1391 | # later. | ||
438 | 1392 | cur_id = child.file_id | ||
439 | 1393 | new_path = osutils.pathjoin(cur_path, child.name) | ||
440 | 1394 | break | ||
441 | 1395 | elif normalize(child.name) == lelt: | ||
442 | 1396 | cur_id = child.file_id | ||
443 | 1397 | new_path = osutils.pathjoin(cur_path, child.name) | ||
444 | 1398 | except errors.NoSuchId: | ||
445 | 1399 | # before a change is committed we can see this error... | ||
446 | 1400 | continue | ||
447 | 1401 | except errors.NotADirectory: | ||
448 | 1402 | pass | ||
449 | 1403 | if new_path: | ||
450 | 1404 | cur_path = new_path | ||
451 | 1405 | else: | ||
452 | 1406 | # got to the end of this directory and no entries matched. | ||
453 | 1407 | # Return what matched so far, plus the rest as specified. | ||
454 | 1408 | cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter)) | ||
455 | 1409 | break | ||
456 | 1410 | return cur_path | ||
457 | 1367 | 1411 | ||
458 | === modified file 'breezy/workingtree.py' | |||
459 | --- breezy/workingtree.py 2018-08-07 19:23:46 +0000 | |||
460 | +++ breezy/workingtree.py 2018-09-14 14:37:48 +0000 | |||
461 | @@ -1337,6 +1337,42 @@ | |||
462 | 1337 | """Return the ShelfManager for this WorkingTree.""" | 1337 | """Return the ShelfManager for this WorkingTree.""" |
463 | 1338 | raise NotImplementedError(self.get_shelf_manager) | 1338 | raise NotImplementedError(self.get_shelf_manager) |
464 | 1339 | 1339 | ||
465 | 1340 | def get_canonical_paths(self, paths): | ||
466 | 1341 | """Like get_canonical_path() but works on multiple items. | ||
467 | 1342 | |||
468 | 1343 | :param paths: A sequence of paths relative to the root of the tree. | ||
469 | 1344 | :return: A list of paths, with each item the corresponding input path | ||
470 | 1345 | adjusted to account for existing elements that match case | ||
471 | 1346 | insensitively. | ||
472 | 1347 | """ | ||
473 | 1348 | with self.lock_read(): | ||
474 | 1349 | for path in paths: | ||
475 | 1350 | yield path | ||
476 | 1351 | |||
477 | 1352 | def get_canonical_path(self, path): | ||
478 | 1353 | """Returns the first item in the tree that matches a path. | ||
479 | 1354 | |||
480 | 1355 | This is meant to allow case-insensitive path lookups on e.g. | ||
481 | 1356 | FAT filesystems. | ||
482 | 1357 | |||
483 | 1358 | If a path matches exactly, it is returned. If no path matches exactly | ||
484 | 1359 | but more than one path matches according to the underlying file system, | ||
485 | 1360 | it is implementation defined which is returned. | ||
486 | 1361 | |||
487 | 1362 | If no path matches according to the file system, the input path is | ||
488 | 1363 | returned, but with as many path entries that do exist changed to their | ||
489 | 1364 | canonical form. | ||
490 | 1365 | |||
491 | 1366 | If you need to resolve many names from the same tree, you should | ||
492 | 1367 | use get_canonical_paths() to avoid O(N) behaviour. | ||
493 | 1368 | |||
494 | 1369 | :param path: A paths relative to the root of the tree. | ||
495 | 1370 | :return: The input path adjusted to account for existing elements | ||
496 | 1371 | that match case insensitively. | ||
497 | 1372 | """ | ||
498 | 1373 | with self.lock_read(): | ||
499 | 1374 | return next(self.get_canonical_paths([path])) | ||
500 | 1375 | |||
501 | 1340 | 1376 | ||
502 | 1341 | class WorkingTreeFormatRegistry(controldir.ControlComponentFormatRegistry): | 1377 | class WorkingTreeFormatRegistry(controldir.ControlComponentFormatRegistry): |
503 | 1342 | """Registry for working tree formats.""" | 1378 | """Registry for working tree formats.""" |
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.