Merge lp:~jelmer/brz/transform-file-id into lp:brz/3.1
- transform-file-id
- Merge into 3.1
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/transform-file-id |
Merge into: | lp:brz/3.1 |
Prerequisite: | lp:~jelmer/brz/transform |
Diff against target: |
2570 lines (+1291/-910) 16 files modified
breezy/bzr/inventorytree.py (+3/-3) breezy/bzr/tests/__init__.py (+1/-0) breezy/bzr/tests/test_transform.py (+49/-0) breezy/bzr/transform.py (+903/-0) breezy/bzr/workingtree.py (+2/-2) breezy/errors.py (+0/-16) breezy/git/transform.py (+275/-0) breezy/git/tree.py (+6/-7) breezy/git/workingtree.py (+2/-6) breezy/merge.py (+6/-11) breezy/tests/per_tree/test_path_content_summary.py (+4/-2) breezy/tests/per_tree/test_symlinks.py (+1/-1) breezy/tests/per_workingtree/test_transform.py (+3/-2) breezy/tests/test_errors.py (+0/-6) breezy/tests/test_transform.py (+13/-34) breezy/transform.py (+23/-820) |
To merge this branch: | bzr merge lp:~jelmer/brz/transform-file-id |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij | Approve | ||
Review via email: mp+386859@code.launchpad.net |
This proposal supersedes a proposal from 2020-06-29.
Commit message
Split out git and bzr-specific transforms.
Description of the change
Split out git and bzr-specific transforms.
To post a comment you must log in.
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Revision history for this message
Jelmer Vernooij (jelmer) : Posted in a previous version of this proposal | # |
review:
Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Merging failed
https:/
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Merging failed
https:/
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Running landing tests failed
https:/
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Running landing tests failed
https:/
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : Posted in a previous version of this proposal | # |
Running landing tests failed
https:/
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote : | # |
Voting criteria not met
https:/
Revision history for this message
Jelmer Vernooij (jelmer) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/bzr/inventorytree.py' |
2 | --- breezy/bzr/inventorytree.py 2020-07-04 01:15:59 +0000 |
3 | +++ breezy/bzr/inventorytree.py 2020-07-05 14:44:34 +0000 |
4 | @@ -325,7 +325,7 @@ |
5 | return last_revision |
6 | |
7 | def preview_transform(self, pb=None): |
8 | - from ..transform import TransformPreview |
9 | + from .transform import TransformPreview |
10 | return TransformPreview(self, pb=pb) |
11 | |
12 | |
13 | @@ -510,8 +510,8 @@ |
14 | self.set_parent_trees([(new_revid, rev_tree)]) |
15 | |
16 | def transform(self, pb=None): |
17 | - from ..transform import TreeTransform |
18 | - return TreeTransform(self, pb=pb) |
19 | + from .transform import InventoryTreeTransform |
20 | + return InventoryTreeTransform(self, pb=pb) |
21 | |
22 | |
23 | class _SmartAddHelper(object): |
24 | |
25 | === modified file 'breezy/bzr/tests/__init__.py' |
26 | --- breezy/bzr/tests/__init__.py 2020-06-25 21:13:23 +0000 |
27 | +++ breezy/bzr/tests/__init__.py 2020-07-05 14:44:34 +0000 |
28 | @@ -81,6 +81,7 @@ |
29 | 'test_serializer', |
30 | 'test_tag', |
31 | 'test_testament', |
32 | + 'test_transform', |
33 | 'test_versionedfile', |
34 | 'test_vf_search', |
35 | 'test_vfs_ratchet', |
36 | |
37 | === added file 'breezy/bzr/tests/test_transform.py' |
38 | --- breezy/bzr/tests/test_transform.py 1970-01-01 00:00:00 +0000 |
39 | +++ breezy/bzr/tests/test_transform.py 2020-07-05 14:44:34 +0000 |
40 | @@ -0,0 +1,49 @@ |
41 | +# Copyright (C) 2006-2012, 2016 Canonical Ltd |
42 | +# |
43 | +# This program is free software; you can redistribute it and/or modify |
44 | +# it under the terms of the GNU General Public License as published by |
45 | +# the Free Software Foundation; either version 2 of the License, or |
46 | +# (at your option) any later version. |
47 | +# |
48 | +# This program is distributed in the hope that it will be useful, |
49 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
50 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
51 | +# GNU General Public License for more details. |
52 | +# |
53 | +# You should have received a copy of the GNU General Public License |
54 | +# along with this program; if not, write to the Free Software |
55 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
56 | + |
57 | +from . import TestCaseWithTransport |
58 | + |
59 | + |
60 | +class TestInventoryAltered(TestCaseWithTransport): |
61 | + |
62 | + def test_inventory_altered_unchanged(self): |
63 | + tree = self.make_branch_and_tree('tree') |
64 | + self.build_tree(['tree/foo']) |
65 | + tree.add('foo', b'foo-id') |
66 | + with tree.preview_transform() as tt: |
67 | + self.assertEqual([], tt._inventory_altered()) |
68 | + |
69 | + def test_inventory_altered_changed_parent_id(self): |
70 | + tree = self.make_branch_and_tree('tree') |
71 | + self.build_tree(['tree/foo']) |
72 | + tree.add('foo', b'foo-id') |
73 | + with tree.preview_transform() as tt: |
74 | + tt.unversion_file(tt.root) |
75 | + tt.version_file(tt.root, file_id=b'new-id') |
76 | + foo_trans_id = tt.trans_id_tree_path('foo') |
77 | + foo_tuple = ('foo', foo_trans_id) |
78 | + root_tuple = ('', tt.root) |
79 | + self.assertEqual([root_tuple, foo_tuple], tt._inventory_altered()) |
80 | + |
81 | + def test_inventory_altered_noop_changed_parent_id(self): |
82 | + tree = self.make_branch_and_tree('tree') |
83 | + self.build_tree(['tree/foo']) |
84 | + tree.add('foo', b'foo-id') |
85 | + with tree.preview_transform() as tt: |
86 | + tt.unversion_file(tt.root) |
87 | + tt.version_file(tt.root, file_id=tree.path2id('')) |
88 | + tt.trans_id_tree_path('foo') |
89 | + self.assertEqual([], tt._inventory_altered()) |
90 | |
91 | === added file 'breezy/bzr/transform.py' |
92 | --- breezy/bzr/transform.py 1970-01-01 00:00:00 +0000 |
93 | +++ breezy/bzr/transform.py 2020-07-05 14:44:34 +0000 |
94 | @@ -0,0 +1,903 @@ |
95 | +# Copyright (C) 2006-2011 Canonical Ltd |
96 | +# Copyright (C) 2020 Breezy Developers |
97 | +# |
98 | +# This program is free software; you can redistribute it and/or modify |
99 | +# it under the terms of the GNU General Public License as published by |
100 | +# the Free Software Foundation; either version 2 of the License, or |
101 | +# (at your option) any later version. |
102 | +# |
103 | +# This program is distributed in the hope that it will be useful, |
104 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
105 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
106 | +# GNU General Public License for more details. |
107 | +# |
108 | +# You should have received a copy of the GNU General Public License |
109 | +# along with this program; if not, write to the Free Software |
110 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
111 | + |
112 | +from __future__ import absolute_import |
113 | + |
114 | +import errno |
115 | +import os |
116 | +from stat import S_IEXEC |
117 | + |
118 | +from .. import ( |
119 | + annotate, |
120 | + errors, |
121 | + lock, |
122 | + osutils, |
123 | + revision as _mod_revision, |
124 | + tree, |
125 | + ui, |
126 | + ) |
127 | + |
128 | +from ..i18n import gettext |
129 | +from ..mutabletree import MutableTree |
130 | +from ..sixish import text_type, viewvalues, viewitems |
131 | +from ..transform import ( |
132 | + ROOT_PARENT, |
133 | + TreeTransform, |
134 | + _FileMover, |
135 | + _TransformResults, |
136 | + DiskTreeTransform, |
137 | + joinpath, |
138 | + NoFinalPath, |
139 | + FinalPaths, |
140 | + unique_add, |
141 | + TransformRenameFailed, |
142 | + ) |
143 | +from . import ( |
144 | + inventory, |
145 | + inventorytree, |
146 | + ) |
147 | + |
148 | + |
149 | +class InventoryTreeTransform(TreeTransform): |
150 | + """Tree transform for Bazaar trees.""" |
151 | + |
152 | + def version_file(self, trans_id, file_id=None): |
153 | + """Schedule a file to become versioned.""" |
154 | + if file_id is None: |
155 | + raise ValueError() |
156 | + unique_add(self._new_id, trans_id, file_id) |
157 | + unique_add(self._r_new_id, file_id, trans_id) |
158 | + |
159 | + def cancel_versioning(self, trans_id): |
160 | + """Undo a previous versioning of a file""" |
161 | + file_id = self._new_id[trans_id] |
162 | + del self._new_id[trans_id] |
163 | + del self._r_new_id[file_id] |
164 | + |
165 | + def _duplicate_ids(self): |
166 | + """Each inventory id may only be used once""" |
167 | + conflicts = [] |
168 | + try: |
169 | + all_ids = self._tree.all_file_ids() |
170 | + except errors.UnsupportedOperation: |
171 | + # it's okay for non-file-id trees to raise UnsupportedOperation. |
172 | + return [] |
173 | + removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in |
174 | + self._removed_id)) |
175 | + active_tree_ids = all_ids.difference(removed_tree_ids) |
176 | + for trans_id, file_id in viewitems(self._new_id): |
177 | + if file_id in active_tree_ids: |
178 | + path = self._tree.id2path(file_id) |
179 | + old_trans_id = self.trans_id_tree_path(path) |
180 | + conflicts.append(('duplicate id', old_trans_id, trans_id)) |
181 | + return conflicts |
182 | + |
183 | + def find_conflicts(self): |
184 | + conflicts = super(InventoryTreeTransform, self).find_conflicts() |
185 | + conflicts.extend(self._duplicate_ids()) |
186 | + return conflicts |
187 | + |
188 | + def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): |
189 | + """Apply all changes to the inventory and filesystem. |
190 | + |
191 | + If filesystem or inventory conflicts are present, MalformedTransform |
192 | + will be thrown. |
193 | + |
194 | + If apply succeeds, finalize is not necessary. |
195 | + |
196 | + :param no_conflicts: if True, the caller guarantees there are no |
197 | + conflicts, so no check is made. |
198 | + :param precomputed_delta: An inventory delta to use instead of |
199 | + calculating one. |
200 | + :param _mover: Supply an alternate FileMover, for testing |
201 | + """ |
202 | + for hook in MutableTree.hooks['pre_transform']: |
203 | + hook(self._tree, self) |
204 | + if not no_conflicts: |
205 | + self._check_malformed() |
206 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
207 | + if precomputed_delta is None: |
208 | + child_pb.update(gettext('Apply phase'), 0, 2) |
209 | + inventory_delta = self._generate_inventory_delta() |
210 | + offset = 1 |
211 | + else: |
212 | + inventory_delta = precomputed_delta |
213 | + offset = 0 |
214 | + if _mover is None: |
215 | + mover = _FileMover() |
216 | + else: |
217 | + mover = _mover |
218 | + try: |
219 | + child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset) |
220 | + self._apply_removals(mover) |
221 | + child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset) |
222 | + modified_paths = self._apply_insertions(mover) |
223 | + except BaseException: |
224 | + mover.rollback() |
225 | + raise |
226 | + else: |
227 | + mover.apply_deletions() |
228 | + if self.final_file_id(self.root) is None: |
229 | + inventory_delta = [e for e in inventory_delta if e[0] != ''] |
230 | + self._tree.apply_inventory_delta(inventory_delta) |
231 | + self._apply_observed_sha1s() |
232 | + self._done = True |
233 | + self.finalize() |
234 | + return _TransformResults(modified_paths, self.rename_count) |
235 | + |
236 | + def _apply_removals(self, mover): |
237 | + """Perform tree operations that remove directory/inventory names. |
238 | + |
239 | + That is, delete files that are to be deleted, and put any files that |
240 | + need renaming into limbo. This must be done in strict child-to-parent |
241 | + order. |
242 | + |
243 | + If inventory_delta is None, no inventory delta generation is performed. |
244 | + """ |
245 | + tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True) |
246 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
247 | + for num, (path, trans_id) in enumerate(tree_paths): |
248 | + # do not attempt to move root into a subdirectory of itself. |
249 | + if path == '': |
250 | + continue |
251 | + child_pb.update(gettext('removing file'), num, len(tree_paths)) |
252 | + full_path = self._tree.abspath(path) |
253 | + if trans_id in self._removed_contents: |
254 | + delete_path = os.path.join(self._deletiondir, trans_id) |
255 | + mover.pre_delete(full_path, delete_path) |
256 | + elif (trans_id in self._new_name or |
257 | + trans_id in self._new_parent): |
258 | + try: |
259 | + mover.rename(full_path, self._limbo_name(trans_id)) |
260 | + except TransformRenameFailed as e: |
261 | + if e.errno != errno.ENOENT: |
262 | + raise |
263 | + else: |
264 | + self.rename_count += 1 |
265 | + |
266 | + def _apply_insertions(self, mover): |
267 | + """Perform tree operations that insert directory/inventory names. |
268 | + |
269 | + That is, create any files that need to be created, and restore from |
270 | + limbo any files that needed renaming. This must be done in strict |
271 | + parent-to-child order. |
272 | + |
273 | + If inventory_delta is None, no inventory delta is calculated, and |
274 | + no list of modified paths is returned. |
275 | + """ |
276 | + new_paths = self.new_paths(filesystem_only=True) |
277 | + modified_paths = [] |
278 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
279 | + for num, (path, trans_id) in enumerate(new_paths): |
280 | + if (num % 10) == 0: |
281 | + child_pb.update(gettext('adding file'), |
282 | + num, len(new_paths)) |
283 | + full_path = self._tree.abspath(path) |
284 | + if trans_id in self._needs_rename: |
285 | + try: |
286 | + mover.rename(self._limbo_name(trans_id), full_path) |
287 | + except TransformRenameFailed as e: |
288 | + # We may be renaming a dangling inventory id |
289 | + if e.errno != errno.ENOENT: |
290 | + raise |
291 | + else: |
292 | + self.rename_count += 1 |
293 | + # TODO: if trans_id in self._observed_sha1s, we should |
294 | + # re-stat the final target, since ctime will be |
295 | + # updated by the change. |
296 | + if (trans_id in self._new_contents |
297 | + or self.path_changed(trans_id)): |
298 | + if trans_id in self._new_contents: |
299 | + modified_paths.append(full_path) |
300 | + if trans_id in self._new_executability: |
301 | + self._set_executability(path, trans_id) |
302 | + if trans_id in self._observed_sha1s: |
303 | + o_sha1, o_st_val = self._observed_sha1s[trans_id] |
304 | + st = osutils.lstat(full_path) |
305 | + self._observed_sha1s[trans_id] = (o_sha1, st) |
306 | + for path, trans_id in new_paths: |
307 | + # new_paths includes stuff like workingtree conflicts. Only the |
308 | + # stuff in new_contents actually comes from limbo. |
309 | + if trans_id in self._limbo_files: |
310 | + del self._limbo_files[trans_id] |
311 | + self._new_contents.clear() |
312 | + return modified_paths |
313 | + |
314 | + def _apply_observed_sha1s(self): |
315 | + """After we have finished renaming everything, update observed sha1s |
316 | + |
317 | + This has to be done after self._tree.apply_inventory_delta, otherwise |
318 | + it doesn't know anything about the files we are updating. Also, we want |
319 | + to do this as late as possible, so that most entries end up cached. |
320 | + """ |
321 | + # TODO: this doesn't update the stat information for directories. So |
322 | + # the first 'bzr status' will still need to rewrite |
323 | + # .bzr/checkout/dirstate. However, we at least don't need to |
324 | + # re-read all of the files. |
325 | + # TODO: If the operation took a while, we could do a time.sleep(3) here |
326 | + # to allow the clock to tick over and ensure we won't have any |
327 | + # problems. (we could observe start time, and finish time, and if |
328 | + # it is less than eg 10% overhead, add a sleep call.) |
329 | + paths = FinalPaths(self) |
330 | + for trans_id, observed in viewitems(self._observed_sha1s): |
331 | + path = paths.get_path(trans_id) |
332 | + self._tree._observed_sha1(path, observed) |
333 | + |
334 | + def get_preview_tree(self): |
335 | + """Return a tree representing the result of the transform. |
336 | + |
337 | + The tree is a snapshot, and altering the TreeTransform will invalidate |
338 | + it. |
339 | + """ |
340 | + return _PreviewTree(self) |
341 | + |
342 | + def _inventory_altered(self): |
343 | + """Determine which trans_ids need new Inventory entries. |
344 | + |
345 | + An new entry is needed when anything that would be reflected by an |
346 | + inventory entry changes, including file name, file_id, parent file_id, |
347 | + file kind, and the execute bit. |
348 | + |
349 | + Some care is taken to return entries with real changes, not cases |
350 | + where the value is deleted and then restored to its original value, |
351 | + but some actually unchanged values may be returned. |
352 | + |
353 | + :returns: A list of (path, trans_id) for all items requiring an |
354 | + inventory change. Ordered by path. |
355 | + """ |
356 | + changed_ids = set() |
357 | + # Find entries whose file_ids are new (or changed). |
358 | + new_file_id = set(t for t in self._new_id |
359 | + if self._new_id[t] != self.tree_file_id(t)) |
360 | + for id_set in [self._new_name, self._new_parent, new_file_id, |
361 | + self._new_executability]: |
362 | + changed_ids.update(id_set) |
363 | + # removing implies a kind change |
364 | + changed_kind = set(self._removed_contents) |
365 | + # so does adding |
366 | + changed_kind.intersection_update(self._new_contents) |
367 | + # Ignore entries that are already known to have changed. |
368 | + changed_kind.difference_update(changed_ids) |
369 | + # to keep only the truly changed ones |
370 | + changed_kind = (t for t in changed_kind |
371 | + if self.tree_kind(t) != self.final_kind(t)) |
372 | + # all kind changes will alter the inventory |
373 | + changed_ids.update(changed_kind) |
374 | + # To find entries with changed parent_ids, find parents which existed, |
375 | + # but changed file_id. |
376 | + # Now add all their children to the set. |
377 | + for parent_trans_id in new_file_id: |
378 | + changed_ids.update(self.iter_tree_children(parent_trans_id)) |
379 | + return sorted(FinalPaths(self).get_paths(changed_ids)) |
380 | + |
381 | + def _generate_inventory_delta(self): |
382 | + """Generate an inventory delta for the current transform.""" |
383 | + inventory_delta = [] |
384 | + new_paths = self._inventory_altered() |
385 | + total_entries = len(new_paths) + len(self._removed_id) |
386 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
387 | + for num, trans_id in enumerate(self._removed_id): |
388 | + if (num % 10) == 0: |
389 | + child_pb.update(gettext('removing file'), |
390 | + num, total_entries) |
391 | + if trans_id == self._new_root: |
392 | + file_id = self._tree.path2id('') |
393 | + else: |
394 | + file_id = self.tree_file_id(trans_id) |
395 | + # File-id isn't really being deleted, just moved |
396 | + if file_id in self._r_new_id: |
397 | + continue |
398 | + path = self._tree_id_paths[trans_id] |
399 | + inventory_delta.append((path, None, file_id, None)) |
400 | + new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in |
401 | + new_paths) |
402 | + for num, (path, trans_id) in enumerate(new_paths): |
403 | + if (num % 10) == 0: |
404 | + child_pb.update(gettext('adding file'), |
405 | + num + len(self._removed_id), total_entries) |
406 | + file_id = new_path_file_ids[trans_id] |
407 | + if file_id is None: |
408 | + continue |
409 | + kind = self.final_kind(trans_id) |
410 | + if kind is None: |
411 | + kind = self._tree.stored_kind(self._tree.id2path(file_id)) |
412 | + parent_trans_id = self.final_parent(trans_id) |
413 | + parent_file_id = new_path_file_ids.get(parent_trans_id) |
414 | + if parent_file_id is None: |
415 | + parent_file_id = self.final_file_id(parent_trans_id) |
416 | + if trans_id in self._new_reference_revision: |
417 | + new_entry = inventory.TreeReference( |
418 | + file_id, |
419 | + self._new_name[trans_id], |
420 | + self.final_file_id(self._new_parent[trans_id]), |
421 | + None, self._new_reference_revision[trans_id]) |
422 | + else: |
423 | + new_entry = inventory.make_entry(kind, |
424 | + self.final_name(trans_id), |
425 | + parent_file_id, file_id) |
426 | + try: |
427 | + old_path = self._tree.id2path(new_entry.file_id) |
428 | + except errors.NoSuchId: |
429 | + old_path = None |
430 | + new_executability = self._new_executability.get(trans_id) |
431 | + if new_executability is not None: |
432 | + new_entry.executable = new_executability |
433 | + inventory_delta.append( |
434 | + (old_path, path, new_entry.file_id, new_entry)) |
435 | + return inventory_delta |
436 | + |
437 | + |
438 | +class TransformPreview(InventoryTreeTransform): |
439 | + """A TreeTransform for generating preview trees. |
440 | + |
441 | + Unlike TreeTransform, this version works when the input tree is a |
442 | + RevisionTree, rather than a WorkingTree. As a result, it tends to ignore |
443 | + unversioned files in the input tree. |
444 | + """ |
445 | + |
446 | + def __init__(self, tree, pb=None, case_sensitive=True): |
447 | + tree.lock_read() |
448 | + limbodir = osutils.mkdtemp(prefix='bzr-limbo-') |
449 | + DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive) |
450 | + |
451 | + def canonical_path(self, path): |
452 | + return path |
453 | + |
454 | + def tree_kind(self, trans_id): |
455 | + path = self._tree_id_paths.get(trans_id) |
456 | + if path is None: |
457 | + return None |
458 | + kind = self._tree.path_content_summary(path)[0] |
459 | + if kind == 'missing': |
460 | + kind = None |
461 | + return kind |
462 | + |
463 | + def _set_mode(self, trans_id, mode_id, typefunc): |
464 | + """Set the mode of new file contents. |
465 | + The mode_id is the existing file to get the mode from (often the same |
466 | + as trans_id). The operation is only performed if there's a mode match |
467 | + according to typefunc. |
468 | + """ |
469 | + # is it ok to ignore this? probably |
470 | + pass |
471 | + |
472 | + def iter_tree_children(self, parent_id): |
473 | + """Iterate through the entry's tree children, if any""" |
474 | + try: |
475 | + path = self._tree_id_paths[parent_id] |
476 | + except KeyError: |
477 | + return |
478 | + try: |
479 | + entry = next(self._tree.iter_entries_by_dir( |
480 | + specific_files=[path]))[1] |
481 | + except StopIteration: |
482 | + return |
483 | + children = getattr(entry, 'children', {}) |
484 | + for child in children: |
485 | + childpath = joinpath(path, child) |
486 | + yield self.trans_id_tree_path(childpath) |
487 | + |
488 | + def new_orphan(self, trans_id, parent_id): |
489 | + raise NotImplementedError(self.new_orphan) |
490 | + |
491 | + |
492 | +class _PreviewTree(inventorytree.InventoryTree): |
493 | + """Partial implementation of Tree to support show_diff_trees""" |
494 | + |
495 | + def __init__(self, transform): |
496 | + self._transform = transform |
497 | + self._final_paths = FinalPaths(transform) |
498 | + self.__by_parent = None |
499 | + self._parent_ids = [] |
500 | + self._all_children_cache = {} |
501 | + self._path2trans_id_cache = {} |
502 | + self._final_name_cache = {} |
503 | + self._iter_changes_cache = { |
504 | + c.file_id: c for c in self._transform.iter_changes()} |
505 | + |
506 | + def supports_tree_reference(self): |
507 | + # TODO(jelmer): Support tree references in _PreviewTree. |
508 | + # return self._transform._tree.supports_tree_reference() |
509 | + return False |
510 | + |
511 | + def _content_change(self, file_id): |
512 | + """Return True if the content of this file changed""" |
513 | + changes = self._iter_changes_cache.get(file_id) |
514 | + return (changes is not None and changes.changed_content) |
515 | + |
516 | + def _get_repository(self): |
517 | + repo = getattr(self._transform._tree, '_repository', None) |
518 | + if repo is None: |
519 | + repo = self._transform._tree.branch.repository |
520 | + return repo |
521 | + |
522 | + def _iter_parent_trees(self): |
523 | + for revision_id in self.get_parent_ids(): |
524 | + try: |
525 | + yield self.revision_tree(revision_id) |
526 | + except errors.NoSuchRevisionInTree: |
527 | + yield self._get_repository().revision_tree(revision_id) |
528 | + |
529 | + def _get_file_revision(self, path, file_id, vf, tree_revision): |
530 | + parent_keys = [ |
531 | + (file_id, t.get_file_revision(t.id2path(file_id))) |
532 | + for t in self._iter_parent_trees()] |
533 | + vf.add_lines((file_id, tree_revision), parent_keys, |
534 | + self.get_file_lines(path)) |
535 | + repo = self._get_repository() |
536 | + base_vf = repo.texts |
537 | + if base_vf not in vf.fallback_versionedfiles: |
538 | + vf.fallback_versionedfiles.append(base_vf) |
539 | + return tree_revision |
540 | + |
541 | + def _stat_limbo_file(self, trans_id): |
542 | + name = self._transform._limbo_name(trans_id) |
543 | + return os.lstat(name) |
544 | + |
545 | + @property |
546 | + def _by_parent(self): |
547 | + if self.__by_parent is None: |
548 | + self.__by_parent = self._transform.by_parent() |
549 | + return self.__by_parent |
550 | + |
551 | + def _comparison_data(self, entry, path): |
552 | + kind, size, executable, link_or_sha1 = self.path_content_summary(path) |
553 | + if kind == 'missing': |
554 | + kind = None |
555 | + executable = False |
556 | + else: |
557 | + file_id = self._transform.final_file_id(self._path2trans_id(path)) |
558 | + executable = self.is_executable(path) |
559 | + return kind, executable, None |
560 | + |
561 | + def is_locked(self): |
562 | + return False |
563 | + |
564 | + def lock_read(self): |
565 | + # Perhaps in theory, this should lock the TreeTransform? |
566 | + return lock.LogicalLockResult(self.unlock) |
567 | + |
568 | + def unlock(self): |
569 | + pass |
570 | + |
571 | + @property |
572 | + def root_inventory(self): |
573 | + """This Tree does not use inventory as its backing data.""" |
574 | + raise NotImplementedError(_PreviewTree.root_inventory) |
575 | + |
576 | + def all_file_ids(self): |
577 | + tree_ids = set(self._transform._tree.all_file_ids()) |
578 | + tree_ids.difference_update(self._transform.tree_file_id(t) |
579 | + for t in self._transform._removed_id) |
580 | + tree_ids.update(viewvalues(self._transform._new_id)) |
581 | + return tree_ids |
582 | + |
583 | + def all_versioned_paths(self): |
584 | + tree_paths = set(self._transform._tree.all_versioned_paths()) |
585 | + |
586 | + tree_paths.difference_update( |
587 | + self._transform.trans_id_tree_path(t) |
588 | + for t in self._transform._removed_id) |
589 | + |
590 | + tree_paths.update( |
591 | + self._final_paths._determine_path(t) |
592 | + for t in self._transform._new_id) |
593 | + |
594 | + return tree_paths |
595 | + |
596 | + def _path2trans_id(self, path): |
597 | + # We must not use None here, because that is a valid value to store. |
598 | + trans_id = self._path2trans_id_cache.get(path, object) |
599 | + if trans_id is not object: |
600 | + return trans_id |
601 | + segments = osutils.splitpath(path) |
602 | + cur_parent = self._transform.root |
603 | + for cur_segment in segments: |
604 | + for child in self._all_children(cur_parent): |
605 | + final_name = self._final_name_cache.get(child) |
606 | + if final_name is None: |
607 | + final_name = self._transform.final_name(child) |
608 | + self._final_name_cache[child] = final_name |
609 | + if final_name == cur_segment: |
610 | + cur_parent = child |
611 | + break |
612 | + else: |
613 | + self._path2trans_id_cache[path] = None |
614 | + return None |
615 | + self._path2trans_id_cache[path] = cur_parent |
616 | + return cur_parent |
617 | + |
618 | + def path2id(self, path): |
619 | + if isinstance(path, list): |
620 | + if path == []: |
621 | + path = [""] |
622 | + path = osutils.pathjoin(*path) |
623 | + return self._transform.final_file_id(self._path2trans_id(path)) |
624 | + |
625 | + def id2path(self, file_id, recurse='down'): |
626 | + trans_id = self._transform.trans_id_file_id(file_id) |
627 | + try: |
628 | + return self._final_paths._determine_path(trans_id) |
629 | + except NoFinalPath: |
630 | + raise errors.NoSuchId(self, file_id) |
631 | + |
632 | + def _all_children(self, trans_id): |
633 | + children = self._all_children_cache.get(trans_id) |
634 | + if children is not None: |
635 | + return children |
636 | + children = set(self._transform.iter_tree_children(trans_id)) |
637 | + # children in the _new_parent set are provided by _by_parent. |
638 | + children.difference_update(self._transform._new_parent) |
639 | + children.update(self._by_parent.get(trans_id, [])) |
640 | + self._all_children_cache[trans_id] = children |
641 | + return children |
642 | + |
643 | + def extras(self): |
644 | + possible_extras = set(self._transform.trans_id_tree_path(p) for p |
645 | + in self._transform._tree.extras()) |
646 | + possible_extras.update(self._transform._new_contents) |
647 | + possible_extras.update(self._transform._removed_id) |
648 | + for trans_id in possible_extras: |
649 | + if self._transform.final_file_id(trans_id) is None: |
650 | + yield self._final_paths._determine_path(trans_id) |
651 | + |
652 | + def _make_inv_entries(self, ordered_entries, specific_files=None): |
653 | + for trans_id, parent_file_id in ordered_entries: |
654 | + file_id = self._transform.final_file_id(trans_id) |
655 | + if file_id is None: |
656 | + continue |
657 | + if (specific_files is not None |
658 | + and self._final_paths.get_path(trans_id) not in specific_files): |
659 | + continue |
660 | + kind = self._transform.final_kind(trans_id) |
661 | + if kind is None: |
662 | + kind = self._transform._tree.stored_kind( |
663 | + self._transform._tree.id2path(file_id)) |
664 | + new_entry = inventory.make_entry( |
665 | + kind, |
666 | + self._transform.final_name(trans_id), |
667 | + parent_file_id, file_id) |
668 | + yield new_entry, trans_id |
669 | + |
670 | + def _list_files_by_dir(self): |
671 | + todo = [ROOT_PARENT] |
672 | + ordered_ids = [] |
673 | + while len(todo) > 0: |
674 | + parent = todo.pop() |
675 | + parent_file_id = self._transform.final_file_id(parent) |
676 | + children = list(self._all_children(parent)) |
677 | + paths = dict(zip(children, self._final_paths.get_paths(children))) |
678 | + children.sort(key=paths.get) |
679 | + todo.extend(reversed(children)) |
680 | + for trans_id in children: |
681 | + ordered_ids.append((trans_id, parent_file_id)) |
682 | + return ordered_ids |
683 | + |
684 | + def iter_child_entries(self, path): |
685 | + trans_id = self._path2trans_id(path) |
686 | + if trans_id is None: |
687 | + raise errors.NoSuchFile(path) |
688 | + todo = [(child_trans_id, trans_id) for child_trans_id in |
689 | + self._all_children(trans_id)] |
690 | + for entry, trans_id in self._make_inv_entries(todo): |
691 | + yield entry |
692 | + |
693 | + def iter_entries_by_dir(self, specific_files=None, recurse_nested=False): |
694 | + if recurse_nested: |
695 | + raise NotImplementedError( |
696 | + 'follow tree references not yet supported') |
697 | + |
698 | + # This may not be a maximally efficient implementation, but it is |
699 | + # reasonably straightforward. An implementation that grafts the |
700 | + # TreeTransform changes onto the tree's iter_entries_by_dir results |
701 | + # might be more efficient, but requires tricky inferences about stack |
702 | + # position. |
703 | + ordered_ids = self._list_files_by_dir() |
704 | + for entry, trans_id in self._make_inv_entries(ordered_ids, |
705 | + specific_files): |
706 | + yield self._final_paths.get_path(trans_id), entry |
707 | + |
708 | + def _iter_entries_for_dir(self, dir_path): |
709 | + """Return path, entry for items in a directory without recursing down.""" |
710 | + ordered_ids = [] |
711 | + dir_trans_id = self._path2trans_id(dir_path) |
712 | + dir_id = self._transform.final_file_id(dir_trans_id) |
713 | + for child_trans_id in self._all_children(dir_trans_id): |
714 | + ordered_ids.append((child_trans_id, dir_id)) |
715 | + path_entries = [] |
716 | + for entry, trans_id in self._make_inv_entries(ordered_ids): |
717 | + path_entries.append((self._final_paths.get_path(trans_id), entry)) |
718 | + path_entries.sort() |
719 | + return path_entries |
720 | + |
721 | + def list_files(self, include_root=False, from_dir=None, recursive=True, |
722 | + recurse_nested=False): |
723 | + """See WorkingTree.list_files.""" |
724 | + if recurse_nested: |
725 | + raise NotImplementedError( |
726 | + 'follow tree references not yet supported') |
727 | + |
728 | + # XXX This should behave like WorkingTree.list_files, but is really |
729 | + # more like RevisionTree.list_files. |
730 | + if from_dir == '.': |
731 | + from_dir = None |
732 | + if recursive: |
733 | + prefix = None |
734 | + if from_dir: |
735 | + prefix = from_dir + '/' |
736 | + entries = self.iter_entries_by_dir() |
737 | + for path, entry in entries: |
738 | + if entry.name == '' and not include_root: |
739 | + continue |
740 | + if prefix: |
741 | + if not path.startswith(prefix): |
742 | + continue |
743 | + path = path[len(prefix):] |
744 | + yield path, 'V', entry.kind, entry |
745 | + else: |
746 | + if from_dir is None and include_root is True: |
747 | + root_entry = inventory.make_entry( |
748 | + 'directory', '', ROOT_PARENT, self.path2id('')) |
749 | + yield '', 'V', 'directory', root_entry |
750 | + entries = self._iter_entries_for_dir(from_dir or '') |
751 | + for path, entry in entries: |
752 | + yield path, 'V', entry.kind, entry |
753 | + |
754 | + def kind(self, path): |
755 | + trans_id = self._path2trans_id(path) |
756 | + if trans_id is None: |
757 | + raise errors.NoSuchFile(path) |
758 | + return self._transform.final_kind(trans_id) |
759 | + |
760 | + def stored_kind(self, path): |
761 | + trans_id = self._path2trans_id(path) |
762 | + if trans_id is None: |
763 | + raise errors.NoSuchFile(path) |
764 | + try: |
765 | + return self._transform._new_contents[trans_id] |
766 | + except KeyError: |
767 | + return self._transform._tree.stored_kind(path) |
768 | + |
769 | + def get_file_mtime(self, path): |
770 | + """See Tree.get_file_mtime""" |
771 | + file_id = self.path2id(path) |
772 | + if file_id is None: |
773 | + raise errors.NoSuchFile(path) |
774 | + if not self._content_change(file_id): |
775 | + return self._transform._tree.get_file_mtime( |
776 | + self._transform._tree.id2path(file_id)) |
777 | + trans_id = self._path2trans_id(path) |
778 | + return self._stat_limbo_file(trans_id).st_mtime |
779 | + |
780 | + def get_file_size(self, path): |
781 | + """See Tree.get_file_size""" |
782 | + trans_id = self._path2trans_id(path) |
783 | + if trans_id is None: |
784 | + raise errors.NoSuchFile(path) |
785 | + kind = self._transform.final_kind(trans_id) |
786 | + if kind != 'file': |
787 | + return None |
788 | + if trans_id in self._transform._new_contents: |
789 | + return self._stat_limbo_file(trans_id).st_size |
790 | + if self.kind(path) == 'file': |
791 | + return self._transform._tree.get_file_size(path) |
792 | + else: |
793 | + return None |
794 | + |
795 | + def get_file_verifier(self, path, stat_value=None): |
796 | + trans_id = self._path2trans_id(path) |
797 | + if trans_id is None: |
798 | + raise errors.NoSuchFile(path) |
799 | + kind = self._transform._new_contents.get(trans_id) |
800 | + if kind is None: |
801 | + return self._transform._tree.get_file_verifier(path) |
802 | + if kind == 'file': |
803 | + with self.get_file(path) as fileobj: |
804 | + return ("SHA1", osutils.sha_file(fileobj)) |
805 | + |
806 | + def get_file_sha1(self, path, stat_value=None): |
807 | + trans_id = self._path2trans_id(path) |
808 | + if trans_id is None: |
809 | + raise errors.NoSuchFile(path) |
810 | + kind = self._transform._new_contents.get(trans_id) |
811 | + if kind is None: |
812 | + return self._transform._tree.get_file_sha1(path) |
813 | + if kind == 'file': |
814 | + with self.get_file(path) as fileobj: |
815 | + return osutils.sha_file(fileobj) |
816 | + |
817 | + def get_reference_revision(self, path): |
818 | + trans_id = self._path2trans_id(path) |
819 | + if trans_id is None: |
820 | + raise errors.NoSuchFile(path) |
821 | + reference_revision = self._transform._new_reference_revision.get(trans_id) |
822 | + if reference_revision is None: |
823 | + return self._transform._tree.get_reference_revision(path) |
824 | + return reference_revision |
825 | + |
826 | + def is_executable(self, path): |
827 | + trans_id = self._path2trans_id(path) |
828 | + if trans_id is None: |
829 | + return False |
830 | + try: |
831 | + return self._transform._new_executability[trans_id] |
832 | + except KeyError: |
833 | + try: |
834 | + return self._transform._tree.is_executable(path) |
835 | + except OSError as e: |
836 | + if e.errno == errno.ENOENT: |
837 | + return False |
838 | + raise |
839 | + except errors.NoSuchFile: |
840 | + return False |
841 | + |
842 | + def has_filename(self, path): |
843 | + trans_id = self._path2trans_id(path) |
844 | + if trans_id in self._transform._new_contents: |
845 | + return True |
846 | + elif trans_id in self._transform._removed_contents: |
847 | + return False |
848 | + else: |
849 | + return self._transform._tree.has_filename(path) |
850 | + |
851 | + def path_content_summary(self, path): |
852 | + trans_id = self._path2trans_id(path) |
853 | + tt = self._transform |
854 | + tree_path = tt._tree_id_paths.get(trans_id) |
855 | + kind = tt._new_contents.get(trans_id) |
856 | + if kind is None: |
857 | + if tree_path is None or trans_id in tt._removed_contents: |
858 | + return 'missing', None, None, None |
859 | + summary = tt._tree.path_content_summary(tree_path) |
860 | + kind, size, executable, link_or_sha1 = summary |
861 | + else: |
862 | + link_or_sha1 = None |
863 | + limbo_name = tt._limbo_name(trans_id) |
864 | + if trans_id in tt._new_reference_revision: |
865 | + kind = 'tree-reference' |
866 | + if kind == 'file': |
867 | + statval = os.lstat(limbo_name) |
868 | + size = statval.st_size |
869 | + if not tt._limbo_supports_executable(): |
870 | + executable = False |
871 | + else: |
872 | + executable = statval.st_mode & S_IEXEC |
873 | + else: |
874 | + size = None |
875 | + executable = None |
876 | + if kind == 'symlink': |
877 | + link_or_sha1 = os.readlink(limbo_name) |
878 | + if not isinstance(link_or_sha1, text_type): |
879 | + link_or_sha1 = link_or_sha1.decode(osutils._fs_enc) |
880 | + executable = tt._new_executability.get(trans_id, executable) |
881 | + return kind, size, executable, link_or_sha1 |
882 | + |
883 | + def iter_changes(self, from_tree, include_unchanged=False, |
884 | + specific_files=None, pb=None, extra_trees=None, |
885 | + require_versioned=True, want_unversioned=False): |
886 | + """See InterTree.iter_changes. |
887 | + |
888 | + This has a fast path that is only used when the from_tree matches |
889 | + the transform tree, and no fancy options are supplied. |
890 | + """ |
891 | + if (from_tree is not self._transform._tree or include_unchanged |
892 | + or specific_files or want_unversioned): |
893 | + return tree.InterTree.get(from_tree, self).iter_changes( |
894 | + include_unchanged=include_unchanged, |
895 | + specific_files=specific_files, |
896 | + pb=pb, |
897 | + extra_trees=extra_trees, |
898 | + require_versioned=require_versioned, |
899 | + want_unversioned=want_unversioned) |
900 | + if want_unversioned: |
901 | + raise ValueError('want_unversioned is not supported') |
902 | + return self._transform.iter_changes() |
903 | + |
904 | + def get_file(self, path): |
905 | + """See Tree.get_file""" |
906 | + file_id = self.path2id(path) |
907 | + if not self._content_change(file_id): |
908 | + return self._transform._tree.get_file(path) |
909 | + trans_id = self._path2trans_id(path) |
910 | + name = self._transform._limbo_name(trans_id) |
911 | + return open(name, 'rb') |
912 | + |
913 | + def get_file_with_stat(self, path): |
914 | + return self.get_file(path), None |
915 | + |
916 | + def annotate_iter(self, path, |
917 | + default_revision=_mod_revision.CURRENT_REVISION): |
918 | + file_id = self.path2id(path) |
919 | + changes = self._iter_changes_cache.get(file_id) |
920 | + if changes is None: |
921 | + get_old = True |
922 | + else: |
923 | + changed_content, versioned, kind = ( |
924 | + changes.changed_content, changes.versioned, changes.kind) |
925 | + if kind[1] is None: |
926 | + return None |
927 | + get_old = (kind[0] == 'file' and versioned[0]) |
928 | + if get_old: |
929 | + old_annotation = self._transform._tree.annotate_iter( |
930 | + path, default_revision=default_revision) |
931 | + else: |
932 | + old_annotation = [] |
933 | + if changes is None: |
934 | + return old_annotation |
935 | + if not changed_content: |
936 | + return old_annotation |
937 | + # TODO: This is doing something similar to what WT.annotate_iter is |
938 | + # doing, however it fails slightly because it doesn't know what |
939 | + # the *other* revision_id is, so it doesn't know how to give the |
940 | + # other as the origin for some lines, they all get |
941 | + # 'default_revision' |
942 | + # It would be nice to be able to use the new Annotator based |
943 | + # approach, as well. |
944 | + return annotate.reannotate([old_annotation], |
945 | + self.get_file(path).readlines(), |
946 | + default_revision) |
947 | + |
948 | + def get_symlink_target(self, path): |
949 | + """See Tree.get_symlink_target""" |
950 | + file_id = self.path2id(path) |
951 | + if not self._content_change(file_id): |
952 | + return self._transform._tree.get_symlink_target(path) |
953 | + trans_id = self._path2trans_id(path) |
954 | + name = self._transform._limbo_name(trans_id) |
955 | + return osutils.readlink(name) |
956 | + |
957 | + def walkdirs(self, prefix=''): |
958 | + pending = [self._transform.root] |
959 | + while len(pending) > 0: |
960 | + parent_id = pending.pop() |
961 | + children = [] |
962 | + subdirs = [] |
963 | + prefix = prefix.rstrip('/') |
964 | + parent_path = self._final_paths.get_path(parent_id) |
965 | + parent_file_id = self._transform.final_file_id(parent_id) |
966 | + for child_id in self._all_children(parent_id): |
967 | + path_from_root = self._final_paths.get_path(child_id) |
968 | + basename = self._transform.final_name(child_id) |
969 | + file_id = self._transform.final_file_id(child_id) |
970 | + kind = self._transform.final_kind(child_id) |
971 | + if kind is not None: |
972 | + versioned_kind = kind |
973 | + else: |
974 | + kind = 'unknown' |
975 | + versioned_kind = self._transform._tree.stored_kind( |
976 | + self._transform._tree.id2path(file_id)) |
977 | + if versioned_kind == 'directory': |
978 | + subdirs.append(child_id) |
979 | + children.append((path_from_root, basename, kind, None, |
980 | + file_id, versioned_kind)) |
981 | + children.sort() |
982 | + if parent_path.startswith(prefix): |
983 | + yield (parent_path, parent_file_id), children |
984 | + pending.extend(sorted(subdirs, key=self._final_paths.get_path, |
985 | + reverse=True)) |
986 | + |
987 | + def get_parent_ids(self): |
988 | + return self._parent_ids |
989 | + |
990 | + def set_parent_ids(self, parent_ids): |
991 | + self._parent_ids = parent_ids |
992 | + |
993 | + def get_revision_tree(self, revision_id): |
994 | + return self._transform._tree.get_revision_tree(revision_id) |
995 | + |
996 | + |
997 | + |
998 | |
999 | === modified file 'breezy/bzr/workingtree.py' |
1000 | --- breezy/bzr/workingtree.py 2020-07-04 00:38:37 +0000 |
1001 | +++ breezy/bzr/workingtree.py 2020-07-05 14:44:34 +0000 |
1002 | @@ -175,8 +175,8 @@ |
1003 | self.case_sensitive = False |
1004 | |
1005 | def transform(self, pb=None): |
1006 | - from ..transform import TreeTransform |
1007 | - return TreeTransform(self, pb=pb) |
1008 | + from .transform import InventoryTreeTransform |
1009 | + return InventoryTreeTransform(self, pb=pb) |
1010 | |
1011 | def _setup_directory_is_tree_reference(self): |
1012 | if self._branch.repository._format.supports_tree_reference: |
1013 | |
1014 | === modified file 'breezy/errors.py' |
1015 | --- breezy/errors.py 2020-06-25 21:17:44 +0000 |
1016 | +++ breezy/errors.py 2020-07-05 14:44:34 +0000 |
1017 | @@ -1508,22 +1508,6 @@ |
1018 | _fmt = "Parameter %(param)s is neither unicode nor utf8." |
1019 | |
1020 | |
1021 | -class CantMoveRoot(BzrError): |
1022 | - |
1023 | - _fmt = "Moving the root directory is not supported at this time" |
1024 | - |
1025 | - |
1026 | -class TransformRenameFailed(BzrError): |
1027 | - |
1028 | - _fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s" |
1029 | - |
1030 | - def __init__(self, from_path, to_path, why, errno): |
1031 | - self.from_path = from_path |
1032 | - self.to_path = to_path |
1033 | - self.why = why |
1034 | - self.errno = errno |
1035 | - |
1036 | - |
1037 | class BzrMoveFailedError(BzrError): |
1038 | |
1039 | _fmt = ("Could not move %(from_path)s%(operator)s %(to_path)s" |
1040 | |
1041 | === added file 'breezy/git/transform.py' |
1042 | --- breezy/git/transform.py 1970-01-01 00:00:00 +0000 |
1043 | +++ breezy/git/transform.py 2020-07-05 14:44:34 +0000 |
1044 | @@ -0,0 +1,275 @@ |
1045 | +# Copyright (C) 2006-2011 Canonical Ltd |
1046 | +# Copyright (C) 2020 Breezy Developers |
1047 | +# |
1048 | +# This program is free software; you can redistribute it and/or modify |
1049 | +# it under the terms of the GNU General Public License as published by |
1050 | +# the Free Software Foundation; either version 2 of the License, or |
1051 | +# (at your option) any later version. |
1052 | +# |
1053 | +# This program is distributed in the hope that it will be useful, |
1054 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1055 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1056 | +# GNU General Public License for more details. |
1057 | +# |
1058 | +# You should have received a copy of the GNU General Public License |
1059 | +# along with this program; if not, write to the Free Software |
1060 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1061 | + |
1062 | +from __future__ import absolute_import |
1063 | + |
1064 | +import errno |
1065 | +import os |
1066 | + |
1067 | +from .. import errors, ui |
1068 | +from ..i18n import gettext |
1069 | +from ..mutabletree import MutableTree |
1070 | +from ..sixish import viewitems |
1071 | +from ..transform import ( |
1072 | + TreeTransform, |
1073 | + _TransformResults, |
1074 | + _FileMover, |
1075 | + FinalPaths, |
1076 | + unique_add, |
1077 | + TransformRenameFailed, |
1078 | + ) |
1079 | + |
1080 | +from ..bzr import inventory |
1081 | +from ..bzr.transform import TransformPreview as GitTransformPreview |
1082 | + |
1083 | + |
1084 | +class GitTreeTransform(TreeTransform): |
1085 | + """Tree transform for Bazaar trees.""" |
1086 | + |
1087 | + def version_file(self, trans_id, file_id=None): |
1088 | + """Schedule a file to become versioned.""" |
1089 | + if file_id is None: |
1090 | + raise ValueError() |
1091 | + unique_add(self._new_id, trans_id, file_id) |
1092 | + unique_add(self._r_new_id, file_id, trans_id) |
1093 | + |
1094 | + def cancel_versioning(self, trans_id): |
1095 | + """Undo a previous versioning of a file""" |
1096 | + file_id = self._new_id[trans_id] |
1097 | + del self._new_id[trans_id] |
1098 | + del self._r_new_id[file_id] |
1099 | + |
1100 | + def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): |
1101 | + """Apply all changes to the inventory and filesystem. |
1102 | + |
1103 | + If filesystem or inventory conflicts are present, MalformedTransform |
1104 | + will be thrown. |
1105 | + |
1106 | + If apply succeeds, finalize is not necessary. |
1107 | + |
1108 | + :param no_conflicts: if True, the caller guarantees there are no |
1109 | + conflicts, so no check is made. |
1110 | + :param precomputed_delta: An inventory delta to use instead of |
1111 | + calculating one. |
1112 | + :param _mover: Supply an alternate FileMover, for testing |
1113 | + """ |
1114 | + for hook in MutableTree.hooks['pre_transform']: |
1115 | + hook(self._tree, self) |
1116 | + if not no_conflicts: |
1117 | + self._check_malformed() |
1118 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
1119 | + if precomputed_delta is None: |
1120 | + child_pb.update(gettext('Apply phase'), 0, 2) |
1121 | + changes = self._generate_transform_changes() |
1122 | + offset = 1 |
1123 | + else: |
1124 | + changes = [ |
1125 | + (op, np, ie) for (op, np, fid, ie) in precomputed_delta] |
1126 | + offset = 0 |
1127 | + if _mover is None: |
1128 | + mover = _FileMover() |
1129 | + else: |
1130 | + mover = _mover |
1131 | + try: |
1132 | + child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset) |
1133 | + self._apply_removals(mover) |
1134 | + child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset) |
1135 | + modified_paths = self._apply_insertions(mover) |
1136 | + except BaseException: |
1137 | + mover.rollback() |
1138 | + raise |
1139 | + else: |
1140 | + mover.apply_deletions() |
1141 | + if self.final_file_id(self.root) is None: |
1142 | + changes = [e for e in changes if e[0] != ''] |
1143 | + self._tree._apply_transform_delta(changes) |
1144 | + self._done = True |
1145 | + self.finalize() |
1146 | + return _TransformResults(modified_paths, self.rename_count) |
1147 | + |
1148 | + def _apply_removals(self, mover): |
1149 | + """Perform tree operations that remove directory/inventory names. |
1150 | + |
1151 | + That is, delete files that are to be deleted, and put any files that |
1152 | + need renaming into limbo. This must be done in strict child-to-parent |
1153 | + order. |
1154 | + |
1155 | + If inventory_delta is None, no inventory delta generation is performed. |
1156 | + """ |
1157 | + tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True) |
1158 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
1159 | + for num, (path, trans_id) in enumerate(tree_paths): |
1160 | + # do not attempt to move root into a subdirectory of itself. |
1161 | + if path == '': |
1162 | + continue |
1163 | + child_pb.update(gettext('removing file'), num, len(tree_paths)) |
1164 | + full_path = self._tree.abspath(path) |
1165 | + if trans_id in self._removed_contents: |
1166 | + delete_path = os.path.join(self._deletiondir, trans_id) |
1167 | + mover.pre_delete(full_path, delete_path) |
1168 | + elif (trans_id in self._new_name or |
1169 | + trans_id in self._new_parent): |
1170 | + try: |
1171 | + mover.rename(full_path, self._limbo_name(trans_id)) |
1172 | + except TransformRenameFailed as e: |
1173 | + if e.errno != errno.ENOENT: |
1174 | + raise |
1175 | + else: |
1176 | + self.rename_count += 1 |
1177 | + |
1178 | + def _apply_insertions(self, mover): |
1179 | + """Perform tree operations that insert directory/inventory names. |
1180 | + |
1181 | + That is, create any files that need to be created, and restore from |
1182 | + limbo any files that needed renaming. This must be done in strict |
1183 | + parent-to-child order. |
1184 | + |
1185 | + If inventory_delta is None, no inventory delta is calculated, and |
1186 | + no list of modified paths is returned. |
1187 | + """ |
1188 | + new_paths = self.new_paths(filesystem_only=True) |
1189 | + modified_paths = [] |
1190 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
1191 | + for num, (path, trans_id) in enumerate(new_paths): |
1192 | + if (num % 10) == 0: |
1193 | + child_pb.update(gettext('adding file'), |
1194 | + num, len(new_paths)) |
1195 | + full_path = self._tree.abspath(path) |
1196 | + if trans_id in self._needs_rename: |
1197 | + try: |
1198 | + mover.rename(self._limbo_name(trans_id), full_path) |
1199 | + except TransformRenameFailed as e: |
1200 | + # We may be renaming a dangling inventory id |
1201 | + if e.errno != errno.ENOENT: |
1202 | + raise |
1203 | + else: |
1204 | + self.rename_count += 1 |
1205 | + # TODO: if trans_id in self._observed_sha1s, we should |
1206 | + # re-stat the final target, since ctime will be |
1207 | + # updated by the change. |
1208 | + if (trans_id in self._new_contents |
1209 | + or self.path_changed(trans_id)): |
1210 | + if trans_id in self._new_contents: |
1211 | + modified_paths.append(full_path) |
1212 | + if trans_id in self._new_executability: |
1213 | + self._set_executability(path, trans_id) |
1214 | + if trans_id in self._observed_sha1s: |
1215 | + o_sha1, o_st_val = self._observed_sha1s[trans_id] |
1216 | + st = osutils.lstat(full_path) |
1217 | + self._observed_sha1s[trans_id] = (o_sha1, st) |
1218 | + for path, trans_id in new_paths: |
1219 | + # new_paths includes stuff like workingtree conflicts. Only the |
1220 | + # stuff in new_contents actually comes from limbo. |
1221 | + if trans_id in self._limbo_files: |
1222 | + del self._limbo_files[trans_id] |
1223 | + self._new_contents.clear() |
1224 | + return modified_paths |
1225 | + |
1226 | + def _inventory_altered(self): |
1227 | + """Determine which trans_ids need new Inventory entries. |
1228 | + |
1229 | + An new entry is needed when anything that would be reflected by an |
1230 | + inventory entry changes, including file name, file_id, parent file_id, |
1231 | + file kind, and the execute bit. |
1232 | + |
1233 | + Some care is taken to return entries with real changes, not cases |
1234 | + where the value is deleted and then restored to its original value, |
1235 | + but some actually unchanged values may be returned. |
1236 | + |
1237 | + :returns: A list of (path, trans_id) for all items requiring an |
1238 | + inventory change. Ordered by path. |
1239 | + """ |
1240 | + changed_ids = set() |
1241 | + # Find entries whose file_ids are new (or changed). |
1242 | + new_file_id = set(t for t in self._new_id |
1243 | + if self._new_id[t] != self.tree_file_id(t)) |
1244 | + for id_set in [self._new_name, self._new_parent, new_file_id, |
1245 | + self._new_executability]: |
1246 | + changed_ids.update(id_set) |
1247 | + # removing implies a kind change |
1248 | + changed_kind = set(self._removed_contents) |
1249 | + # so does adding |
1250 | + changed_kind.intersection_update(self._new_contents) |
1251 | + # Ignore entries that are already known to have changed. |
1252 | + changed_kind.difference_update(changed_ids) |
1253 | + # to keep only the truly changed ones |
1254 | + changed_kind = (t for t in changed_kind |
1255 | + if self.tree_kind(t) != self.final_kind(t)) |
1256 | + # all kind changes will alter the inventory |
1257 | + changed_ids.update(changed_kind) |
1258 | + # To find entries with changed parent_ids, find parents which existed, |
1259 | + # but changed file_id. |
1260 | + # Now add all their children to the set. |
1261 | + for parent_trans_id in new_file_id: |
1262 | + changed_ids.update(self.iter_tree_children(parent_trans_id)) |
1263 | + return sorted(FinalPaths(self).get_paths(changed_ids)) |
1264 | + |
1265 | + def _generate_transform_changes(self): |
1266 | + """Generate an inventory delta for the current transform.""" |
1267 | + changes = [] |
1268 | + new_paths = self._inventory_altered() |
1269 | + total_entries = len(new_paths) + len(self._removed_id) |
1270 | + with ui.ui_factory.nested_progress_bar() as child_pb: |
1271 | + for num, trans_id in enumerate(self._removed_id): |
1272 | + if (num % 10) == 0: |
1273 | + child_pb.update(gettext('removing file'), |
1274 | + num, total_entries) |
1275 | + if trans_id == self._new_root: |
1276 | + file_id = self._tree.path2id('') |
1277 | + else: |
1278 | + file_id = self.tree_file_id(trans_id) |
1279 | + # File-id isn't really being deleted, just moved |
1280 | + if file_id in self._r_new_id: |
1281 | + continue |
1282 | + path = self._tree_id_paths[trans_id] |
1283 | + changes.append((path, None, None)) |
1284 | + new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in |
1285 | + new_paths) |
1286 | + for num, (path, trans_id) in enumerate(new_paths): |
1287 | + if (num % 10) == 0: |
1288 | + child_pb.update(gettext('adding file'), |
1289 | + num + len(self._removed_id), total_entries) |
1290 | + file_id = new_path_file_ids[trans_id] |
1291 | + if file_id is None: |
1292 | + continue |
1293 | + kind = self.final_kind(trans_id) |
1294 | + if kind is None: |
1295 | + kind = self._tree.stored_kind(self._tree.id2path(file_id)) |
1296 | + parent_trans_id = self.final_parent(trans_id) |
1297 | + parent_file_id = new_path_file_ids.get(parent_trans_id) |
1298 | + if parent_file_id is None: |
1299 | + parent_file_id = self.final_file_id(parent_trans_id) |
1300 | + if trans_id in self._new_reference_revision: |
1301 | + new_entry = inventory.TreeReference( |
1302 | + file_id, |
1303 | + self._new_name[trans_id], |
1304 | + self.final_file_id(self._new_parent[trans_id]), |
1305 | + None, self._new_reference_revision[trans_id]) |
1306 | + else: |
1307 | + new_entry = inventory.make_entry(kind, |
1308 | + self.final_name(trans_id), |
1309 | + parent_file_id, file_id) |
1310 | + try: |
1311 | + old_path = self._tree.id2path(new_entry.file_id) |
1312 | + except errors.NoSuchId: |
1313 | + old_path = None |
1314 | + new_executability = self._new_executability.get(trans_id) |
1315 | + if new_executability is not None: |
1316 | + new_entry.executable = new_executability |
1317 | + changes.append( |
1318 | + (old_path, path, new_entry)) |
1319 | + return changes |
1320 | |
1321 | === modified file 'breezy/git/tree.py' |
1322 | --- breezy/git/tree.py 2020-07-04 23:16:47 +0000 |
1323 | +++ breezy/git/tree.py 2020-07-05 14:44:34 +0000 |
1324 | @@ -725,8 +725,8 @@ |
1325 | yield (path_decoded, parent_id), children |
1326 | |
1327 | def preview_transform(self, pb=None): |
1328 | - from ..transform import TransformPreview |
1329 | - return TransformPreview(self, pb=pb) |
1330 | + from .transform import GitTransformPreview |
1331 | + return GitTransformPreview(self, pb=pb) |
1332 | |
1333 | |
1334 | def tree_delta_from_git_changes(changes, mappings, |
1335 | @@ -1635,13 +1635,12 @@ |
1336 | raise NotImplementedError(self._live_entry) |
1337 | |
1338 | def transform(self, pb=None): |
1339 | - from ..transform import TreeTransform |
1340 | - return TreeTransform(self, pb=pb) |
1341 | + from .transform import GitTreeTransform |
1342 | + return GitTreeTransform(self, pb=pb) |
1343 | |
1344 | def preview_transform(self, pb=None): |
1345 | - from ..transform import TransformPreview |
1346 | - return TransformPreview(self, pb=pb) |
1347 | - |
1348 | + from ..transform import GitTransformPreview |
1349 | + return GitTransformPreview(self, pb=pb) |
1350 | |
1351 | |
1352 | class InterToIndexGitTree(InterGitTrees): |
1353 | |
1354 | === modified file 'breezy/git/workingtree.py' |
1355 | --- breezy/git/workingtree.py 2020-07-04 14:58:52 +0000 |
1356 | +++ breezy/git/workingtree.py 2020-07-05 14:44:34 +0000 |
1357 | @@ -224,10 +224,6 @@ |
1358 | else: |
1359 | self.case_sensitive = False |
1360 | |
1361 | - def transform(self, pb=None): |
1362 | - from ..transform import TreeTransform |
1363 | - return TreeTransform(self, pb=pb) |
1364 | - |
1365 | def merge_modified(self): |
1366 | return {} |
1367 | |
1368 | @@ -1075,8 +1071,8 @@ |
1369 | def store_uncommitted(self): |
1370 | raise errors.StoringUncommittedNotSupported(self) |
1371 | |
1372 | - def apply_inventory_delta(self, changes): |
1373 | - for (old_path, new_path, file_id, ie) in changes: |
1374 | + def _apply_transform_delta(self, changes): |
1375 | + for (old_path, new_path, ie) in changes: |
1376 | if old_path is not None: |
1377 | (index, old_subpath) = self._lookup_index( |
1378 | encode_git_path(old_path)) |
1379 | |
1380 | === modified file 'breezy/merge.py' |
1381 | --- breezy/merge.py 2020-07-04 18:40:42 +0000 |
1382 | +++ breezy/merge.py 2020-07-05 14:44:34 +0000 |
1383 | @@ -31,7 +31,6 @@ |
1384 | revision as _mod_revision, |
1385 | textfile, |
1386 | trace, |
1387 | - transform, |
1388 | tree as _mod_tree, |
1389 | tsort, |
1390 | ui, |
1391 | @@ -48,6 +47,7 @@ |
1392 | errors, |
1393 | hooks, |
1394 | registry, |
1395 | + transform, |
1396 | ) |
1397 | from .sixish import ( |
1398 | viewitems, |
1399 | @@ -780,10 +780,10 @@ |
1400 | |
1401 | def _compute_transform(self): |
1402 | if self._lca_trees is None: |
1403 | - entries = self._entries3() |
1404 | + entries = list(self._entries3()) |
1405 | resolver = self._three_way |
1406 | else: |
1407 | - entries = self._entries_lca() |
1408 | + entries = list(self._entries_lca()) |
1409 | resolver = self._lca_multi_way |
1410 | # Prepare merge hooks |
1411 | factories = Merger.hooks['merge_file_content'] |
1412 | @@ -794,7 +794,6 @@ |
1413 | for num, (file_id, changed, paths3, parents3, names3, |
1414 | executable3) in enumerate(entries): |
1415 | trans_id = self.tt.trans_id_file_id(file_id) |
1416 | - |
1417 | # Try merging each entry |
1418 | child_pb.update(gettext('Preparing file merge'), |
1419 | num, len(entries)) |
1420 | @@ -835,7 +834,6 @@ |
1421 | other and this. names3 is a tuple of names for base, other and this. |
1422 | executable3 is a tuple of execute-bit values for base, other and this. |
1423 | """ |
1424 | - result = [] |
1425 | iterator = self.other_tree.iter_changes(self.base_tree, |
1426 | specific_files=self.interesting_files, |
1427 | extra_trees=[self.this_tree]) |
1428 | @@ -863,10 +861,9 @@ |
1429 | names3 = change.name + (this_name,) |
1430 | paths3 = change.path + (this_path, ) |
1431 | executable3 = change.executable + (this_executable,) |
1432 | - result.append( |
1433 | + yield ( |
1434 | (change.file_id, change.changed_content, paths3, |
1435 | parents3, names3, executable3)) |
1436 | - return result |
1437 | |
1438 | def _entries_lca(self): |
1439 | """Gather data about files modified between multiple trees. |
1440 | @@ -895,7 +892,6 @@ |
1441 | self.interesting_files, lookup_trees) |
1442 | else: |
1443 | interesting_files = None |
1444 | - result = [] |
1445 | from .multiwalker import MultiWalker |
1446 | walker = MultiWalker(self.other_tree, self._lca_trees) |
1447 | |
1448 | @@ -1039,7 +1035,7 @@ |
1449 | raise AssertionError('unhandled kind: %s' % other_ie.kind) |
1450 | |
1451 | # If we have gotten this far, that means something has changed |
1452 | - result.append((file_id, content_changed, |
1453 | + yield (file_id, content_changed, |
1454 | ((base_path, lca_paths), |
1455 | other_path, this_path), |
1456 | ((base_ie.parent_id, lca_parent_ids), |
1457 | @@ -1048,8 +1044,7 @@ |
1458 | other_ie.name, this_ie.name), |
1459 | ((base_ie.executable, lca_executable), |
1460 | other_ie.executable, this_ie.executable) |
1461 | - )) |
1462 | - return result |
1463 | + ) |
1464 | |
1465 | def write_modified(self, results): |
1466 | if not self.working_tree.supports_merge_modified(): |
1467 | |
1468 | === modified file 'breezy/tests/per_tree/test_path_content_summary.py' |
1469 | --- breezy/tests/per_tree/test_path_content_summary.py 2020-07-04 00:38:37 +0000 |
1470 | +++ breezy/tests/per_tree/test_path_content_summary.py 2020-07-05 14:44:34 +0000 |
1471 | @@ -21,9 +21,11 @@ |
1472 | from breezy import ( |
1473 | osutils, |
1474 | tests, |
1475 | - transform, |
1476 | ) |
1477 | |
1478 | +from breezy.bzr.transform import ( |
1479 | + _PreviewTree, |
1480 | + ) |
1481 | from breezy.tests import ( |
1482 | features, |
1483 | per_tree, |
1484 | @@ -112,7 +114,7 @@ |
1485 | self.assertEqual('missing', summary[0]) |
1486 | self.assertIs(None, summary[2]) |
1487 | self.assertIs(None, summary[3]) |
1488 | - elif isinstance(tree, transform._PreviewTree): |
1489 | + elif isinstance(tree, _PreviewTree): |
1490 | self.expectFailure('PreviewTree returns "missing" for unversioned' |
1491 | 'files', self.assertEqual, 'file', summary[0]) |
1492 | self.assertEqual('file', summary[0]) |
1493 | |
1494 | === modified file 'breezy/tests/per_tree/test_symlinks.py' |
1495 | --- breezy/tests/per_tree/test_symlinks.py 2019-06-29 21:31:14 +0000 |
1496 | +++ breezy/tests/per_tree/test_symlinks.py 2020-07-05 14:44:34 +0000 |
1497 | @@ -27,7 +27,7 @@ |
1498 | ) |
1499 | from breezy.mutabletree import MutableTree |
1500 | from breezy.tests import TestSkipped |
1501 | -from breezy.transform import _PreviewTree |
1502 | +from breezy.bzr.transform import _PreviewTree |
1503 | from breezy.tests import ( |
1504 | features, |
1505 | ) |
1506 | |
1507 | === modified file 'breezy/tests/per_workingtree/test_transform.py' |
1508 | --- breezy/tests/per_workingtree/test_transform.py 2020-07-05 00:03:01 +0000 |
1509 | +++ breezy/tests/per_workingtree/test_transform.py 2020-07-05 14:44:34 +0000 |
1510 | @@ -70,6 +70,7 @@ |
1511 | MalformedTransform, |
1512 | NoFinalPath, |
1513 | ReusingTransform, |
1514 | + TransformRenameFailed, |
1515 | ) |
1516 | |
1517 | from breezy.tests.per_workingtree import TestCaseWithWorkingTree |
1518 | @@ -335,7 +336,7 @@ |
1519 | with transform: |
1520 | transform.delete_contents(root) |
1521 | e = self.assertRaises(AssertionError, self.assertRaises, |
1522 | - errors.TransformRenameFailed, |
1523 | + TransformRenameFailed, |
1524 | transform.apply) |
1525 | self.assertContainsRe('TransformRenameFailed not raised', str(e)) |
1526 | |
1527 | @@ -1032,7 +1033,7 @@ |
1528 | file_trans_id = rename_transform.trans_id_file_id(b'myfile-id') |
1529 | dir_id = rename_transform.trans_id_file_id(b'first-id') |
1530 | rename_transform.adjust_path('newname', dir_id, file_trans_id) |
1531 | - e = self.assertRaises(errors.TransformRenameFailed, |
1532 | + e = self.assertRaises(TransformRenameFailed, |
1533 | rename_transform.apply) |
1534 | # On nix looks like: |
1535 | # "Failed to rename .../work/.bzr/checkout/limbo/new-1 |
1536 | |
1537 | === modified file 'breezy/tests/test_errors.py' |
1538 | --- breezy/tests/test_errors.py 2020-06-25 21:17:44 +0000 |
1539 | +++ breezy/tests/test_errors.py 2020-07-05 14:44:34 +0000 |
1540 | @@ -515,12 +515,6 @@ |
1541 | str(e), |
1542 | r'Cannot bind address "example\.com:22":.*Permission denied') |
1543 | |
1544 | - def test_transform_rename_failed(self): |
1545 | - e = errors.TransformRenameFailed(u"from", u"to", "readonly file", 2) |
1546 | - self.assertEqual( |
1547 | - u"Failed to rename from to to: readonly file", |
1548 | - str(e)) |
1549 | - |
1550 | |
1551 | class TestErrorsUsingTransport(tests.TestCaseWithMemoryTransport): |
1552 | """Tests for errors that need to use a branch or repo.""" |
1553 | |
1554 | === modified file 'breezy/tests/test_transform.py' |
1555 | --- breezy/tests/test_transform.py 2020-07-05 00:03:01 +0000 |
1556 | +++ breezy/tests/test_transform.py 2020-07-05 14:44:34 +0000 |
1557 | @@ -87,9 +87,11 @@ |
1558 | MalformedTransform, |
1559 | NoFinalPath, |
1560 | ReusingTransform, |
1561 | - TransformPreview, |
1562 | - TreeTransform, |
1563 | + TransformRenameFailed, |
1564 | ) |
1565 | +from ..bzr.transform import ( |
1566 | + InventoryTreeTransform as TreeTransform, |
1567 | + ) |
1568 | |
1569 | |
1570 | class TransformGroup(object): |
1571 | @@ -109,38 +111,6 @@ |
1572 | return template % (b'<' * 7, tree, b'=' * 7, merge, b'>' * 7) |
1573 | |
1574 | |
1575 | -class TestInventoryAltered(tests.TestCaseWithTransport): |
1576 | - |
1577 | - def test_inventory_altered_unchanged(self): |
1578 | - tree = self.make_branch_and_tree('tree') |
1579 | - self.build_tree(['tree/foo']) |
1580 | - tree.add('foo', b'foo-id') |
1581 | - with tree.preview_transform() as tt: |
1582 | - self.assertEqual([], tt._inventory_altered()) |
1583 | - |
1584 | - def test_inventory_altered_changed_parent_id(self): |
1585 | - tree = self.make_branch_and_tree('tree') |
1586 | - self.build_tree(['tree/foo']) |
1587 | - tree.add('foo', b'foo-id') |
1588 | - with tree.preview_transform() as tt: |
1589 | - tt.unversion_file(tt.root) |
1590 | - tt.version_file(tt.root, file_id=b'new-id') |
1591 | - foo_trans_id = tt.trans_id_tree_path('foo') |
1592 | - foo_tuple = ('foo', foo_trans_id) |
1593 | - root_tuple = ('', tt.root) |
1594 | - self.assertEqual([root_tuple, foo_tuple], tt._inventory_altered()) |
1595 | - |
1596 | - def test_inventory_altered_noop_changed_parent_id(self): |
1597 | - tree = self.make_branch_and_tree('tree') |
1598 | - self.build_tree(['tree/foo']) |
1599 | - tree.add('foo', b'foo-id') |
1600 | - with tree.preview_transform() as tt: |
1601 | - tt.unversion_file(tt.root) |
1602 | - tt.version_file(tt.root, file_id=tree.path2id('')) |
1603 | - tt.trans_id_tree_path('foo') |
1604 | - self.assertEqual([], tt._inventory_altered()) |
1605 | - |
1606 | - |
1607 | class TestTransformMerge(TestCaseInTempDir): |
1608 | |
1609 | def test_text_merge(self): |
1610 | @@ -1603,3 +1573,12 @@ |
1611 | """If the file to be linked is unmodified, link""" |
1612 | transform.link_tree(self.child_tree, self.parent_tree) |
1613 | self.assertTrue(self.hardlinked()) |
1614 | + |
1615 | + |
1616 | +class ErrorTests(tests.TestCase): |
1617 | + |
1618 | + def test_transform_rename_failed(self): |
1619 | + e = TransformRenameFailed(u"from", u"to", "readonly file", 2) |
1620 | + self.assertEqual( |
1621 | + u"Failed to rename from to to: readonly file", |
1622 | + str(e)) |
1623 | |
1624 | === modified file 'breezy/transform.py' |
1625 | --- breezy/transform.py 2020-07-04 19:23:30 +0000 |
1626 | +++ breezy/transform.py 2020-07-05 14:44:34 +0000 |
1627 | @@ -29,29 +29,20 @@ |
1628 | osutils, |
1629 | registry, |
1630 | trace, |
1631 | - tree, |
1632 | ) |
1633 | lazy_import.lazy_import(globals(), """ |
1634 | from breezy import ( |
1635 | - annotate, |
1636 | cleanup, |
1637 | - commit, |
1638 | conflicts, |
1639 | - lock, |
1640 | multiparent, |
1641 | revision as _mod_revision, |
1642 | ui, |
1643 | urlutils, |
1644 | ) |
1645 | from breezy.i18n import gettext |
1646 | -from breezy.bzr import ( |
1647 | - inventory, |
1648 | - inventorytree, |
1649 | - ) |
1650 | """) |
1651 | |
1652 | from .errors import (DuplicateKey, |
1653 | - CantMoveRoot, |
1654 | BzrError, InternalBzrError) |
1655 | from .filters import filtered_output_bytes, ContentFilterContext |
1656 | from .mutabletree import MutableTree |
1657 | @@ -101,6 +92,11 @@ |
1658 | _fmt = "Tree transform is malformed %(conflicts)r" |
1659 | |
1660 | |
1661 | +class CantMoveRoot(BzrError): |
1662 | + |
1663 | + _fmt = "Moving the root directory is not supported at this time" |
1664 | + |
1665 | + |
1666 | class ImmortalLimbo(BzrError): |
1667 | |
1668 | _fmt = """Unable to delete transform temporary directory %(limbo_dir)s. |
1669 | @@ -112,6 +108,17 @@ |
1670 | self.limbo_dir = limbo_dir |
1671 | |
1672 | |
1673 | +class TransformRenameFailed(BzrError): |
1674 | + |
1675 | + _fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s" |
1676 | + |
1677 | + def __init__(self, from_path, to_path, why, errno): |
1678 | + self.from_path = from_path |
1679 | + self.to_path = to_path |
1680 | + self.why = why |
1681 | + self.errno = errno |
1682 | + |
1683 | + |
1684 | def unique_add(map, key, value): |
1685 | if key in map: |
1686 | raise DuplicateKey(key=key) |
1687 | @@ -410,16 +417,11 @@ |
1688 | |
1689 | def version_file(self, trans_id, file_id=None): |
1690 | """Schedule a file to become versioned.""" |
1691 | - if file_id is None: |
1692 | - raise ValueError() |
1693 | - unique_add(self._new_id, trans_id, file_id) |
1694 | - unique_add(self._r_new_id, file_id, trans_id) |
1695 | + raise NotImplementedError(self.version_file) |
1696 | |
1697 | def cancel_versioning(self, trans_id): |
1698 | """Undo a previous versioning of a file""" |
1699 | - file_id = self._new_id[trans_id] |
1700 | - del self._new_id[trans_id] |
1701 | - del self._r_new_id[file_id] |
1702 | + raise NotImplementedError(self.cancel_versioning) |
1703 | |
1704 | def new_paths(self, filesystem_only=False): |
1705 | """Determine the paths of all new and changed files. |
1706 | @@ -442,45 +444,6 @@ |
1707 | new_ids.update(id_set) |
1708 | return sorted(FinalPaths(self).get_paths(new_ids)) |
1709 | |
1710 | - def _inventory_altered(self): |
1711 | - """Determine which trans_ids need new Inventory entries. |
1712 | - |
1713 | - An new entry is needed when anything that would be reflected by an |
1714 | - inventory entry changes, including file name, file_id, parent file_id, |
1715 | - file kind, and the execute bit. |
1716 | - |
1717 | - Some care is taken to return entries with real changes, not cases |
1718 | - where the value is deleted and then restored to its original value, |
1719 | - but some actually unchanged values may be returned. |
1720 | - |
1721 | - :returns: A list of (path, trans_id) for all items requiring an |
1722 | - inventory change. Ordered by path. |
1723 | - """ |
1724 | - changed_ids = set() |
1725 | - # Find entries whose file_ids are new (or changed). |
1726 | - new_file_id = set(t for t in self._new_id |
1727 | - if self._new_id[t] != self.tree_file_id(t)) |
1728 | - for id_set in [self._new_name, self._new_parent, new_file_id, |
1729 | - self._new_executability]: |
1730 | - changed_ids.update(id_set) |
1731 | - # removing implies a kind change |
1732 | - changed_kind = set(self._removed_contents) |
1733 | - # so does adding |
1734 | - changed_kind.intersection_update(self._new_contents) |
1735 | - # Ignore entries that are already known to have changed. |
1736 | - changed_kind.difference_update(changed_ids) |
1737 | - # to keep only the truly changed ones |
1738 | - changed_kind = (t for t in changed_kind |
1739 | - if self.tree_kind(t) != self.final_kind(t)) |
1740 | - # all kind changes will alter the inventory |
1741 | - changed_ids.update(changed_kind) |
1742 | - # To find entries with changed parent_ids, find parents which existed, |
1743 | - # but changed file_id. |
1744 | - # Now add all their children to the set. |
1745 | - for parent_trans_id in new_file_id: |
1746 | - changed_ids.update(self.iter_tree_children(parent_trans_id)) |
1747 | - return sorted(FinalPaths(self).get_paths(changed_ids)) |
1748 | - |
1749 | def final_kind(self, trans_id): |
1750 | """Determine the final file kind, after any changes applied. |
1751 | |
1752 | @@ -591,7 +554,6 @@ |
1753 | conflicts.extend(self._unversioned_parents(by_parent)) |
1754 | conflicts.extend(self._parent_loops()) |
1755 | conflicts.extend(self._duplicate_entries(by_parent)) |
1756 | - conflicts.extend(self._duplicate_ids()) |
1757 | conflicts.extend(self._parent_type_conflicts(by_parent)) |
1758 | conflicts.extend(self._improper_versioning()) |
1759 | conflicts.extend(self._executability_conflicts()) |
1760 | @@ -773,24 +735,6 @@ |
1761 | last_trans_id = trans_id |
1762 | return conflicts |
1763 | |
1764 | - def _duplicate_ids(self): |
1765 | - """Each inventory id may only be used once""" |
1766 | - conflicts = [] |
1767 | - try: |
1768 | - all_ids = self._tree.all_file_ids() |
1769 | - except errors.UnsupportedOperation: |
1770 | - # it's okay for non-file-id trees to raise UnsupportedOperation. |
1771 | - return [] |
1772 | - removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in |
1773 | - self._removed_id)) |
1774 | - active_tree_ids = all_ids.difference(removed_tree_ids) |
1775 | - for trans_id, file_id in viewitems(self._new_id): |
1776 | - if file_id in active_tree_ids: |
1777 | - path = self._tree.id2path(file_id) |
1778 | - old_trans_id = self.trans_id_tree_path(path) |
1779 | - conflicts.append(('duplicate id', old_trans_id, trans_id)) |
1780 | - return conflicts |
1781 | - |
1782 | def _parent_type_conflicts(self, by_parent): |
1783 | """Children must have a directory parent""" |
1784 | conflicts = [] |
1785 | @@ -1075,7 +1019,7 @@ |
1786 | The tree is a snapshot, and altering the TreeTransform will invalidate |
1787 | it. |
1788 | """ |
1789 | - return _PreviewTree(self) |
1790 | + raise NotImplementedError(self.get_preview) |
1791 | |
1792 | def commit(self, branch, message, merge_parents=None, strict=False, |
1793 | timestamp=None, timezone=None, committer=None, authors=None, |
1794 | @@ -1120,6 +1064,7 @@ |
1795 | if self._tree.get_revision_id() != last_rev_id: |
1796 | raise ValueError('TreeTransform not based on branch basis: %s' % |
1797 | self._tree.get_revision_id().decode('utf-8')) |
1798 | + from . import commit |
1799 | revprops = commit.Commit.update_revprops(revprops, branch, authors) |
1800 | builder = branch.get_commit_builder(parent_ids, |
1801 | timestamp=timestamp, |
1802 | @@ -1836,749 +1781,7 @@ |
1803 | calculating one. |
1804 | :param _mover: Supply an alternate FileMover, for testing |
1805 | """ |
1806 | - for hook in MutableTree.hooks['pre_transform']: |
1807 | - hook(self._tree, self) |
1808 | - if not no_conflicts: |
1809 | - self._check_malformed() |
1810 | - with ui.ui_factory.nested_progress_bar() as child_pb: |
1811 | - if precomputed_delta is None: |
1812 | - child_pb.update(gettext('Apply phase'), 0, 2) |
1813 | - inventory_delta = self._generate_inventory_delta() |
1814 | - offset = 1 |
1815 | - else: |
1816 | - inventory_delta = precomputed_delta |
1817 | - offset = 0 |
1818 | - if _mover is None: |
1819 | - mover = _FileMover() |
1820 | - else: |
1821 | - mover = _mover |
1822 | - try: |
1823 | - child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset) |
1824 | - self._apply_removals(mover) |
1825 | - child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset) |
1826 | - modified_paths = self._apply_insertions(mover) |
1827 | - except BaseException: |
1828 | - mover.rollback() |
1829 | - raise |
1830 | - else: |
1831 | - mover.apply_deletions() |
1832 | - if not self.final_is_versioned(self.root): |
1833 | - inventory_delta = [e for e in inventory_delta if e[0] != ''] |
1834 | - self._tree.apply_inventory_delta(inventory_delta) |
1835 | - self._apply_observed_sha1s() |
1836 | - self._done = True |
1837 | - self.finalize() |
1838 | - return _TransformResults(modified_paths, self.rename_count) |
1839 | - |
1840 | - def _generate_inventory_delta(self): |
1841 | - """Generate an inventory delta for the current transform.""" |
1842 | - inventory_delta = [] |
1843 | - new_paths = self._inventory_altered() |
1844 | - total_entries = len(new_paths) + len(self._removed_id) |
1845 | - with ui.ui_factory.nested_progress_bar() as child_pb: |
1846 | - for num, trans_id in enumerate(self._removed_id): |
1847 | - if (num % 10) == 0: |
1848 | - child_pb.update(gettext('removing file'), |
1849 | - num, total_entries) |
1850 | - if trans_id == self._new_root: |
1851 | - file_id = self._tree.path2id('') |
1852 | - else: |
1853 | - file_id = self.tree_file_id(trans_id) |
1854 | - # File-id isn't really being deleted, just moved |
1855 | - if file_id in self._r_new_id: |
1856 | - continue |
1857 | - path = self._tree_id_paths[trans_id] |
1858 | - inventory_delta.append((path, None, file_id, None)) |
1859 | - new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in |
1860 | - new_paths) |
1861 | - for num, (path, trans_id) in enumerate(new_paths): |
1862 | - if (num % 10) == 0: |
1863 | - child_pb.update(gettext('adding file'), |
1864 | - num + len(self._removed_id), total_entries) |
1865 | - file_id = new_path_file_ids[trans_id] |
1866 | - if file_id is None: |
1867 | - continue |
1868 | - kind = self.final_kind(trans_id) |
1869 | - if kind is None: |
1870 | - kind = self._tree.stored_kind(self._tree.id2path(file_id)) |
1871 | - parent_trans_id = self.final_parent(trans_id) |
1872 | - parent_file_id = new_path_file_ids.get(parent_trans_id) |
1873 | - if parent_file_id is None: |
1874 | - parent_file_id = self.final_file_id(parent_trans_id) |
1875 | - if trans_id in self._new_reference_revision: |
1876 | - new_entry = inventory.TreeReference( |
1877 | - file_id, |
1878 | - self._new_name[trans_id], |
1879 | - self.final_file_id(self._new_parent[trans_id]), |
1880 | - None, self._new_reference_revision[trans_id]) |
1881 | - else: |
1882 | - new_entry = inventory.make_entry(kind, |
1883 | - self.final_name(trans_id), |
1884 | - parent_file_id, file_id) |
1885 | - try: |
1886 | - old_path = self._tree.id2path(new_entry.file_id) |
1887 | - except errors.NoSuchId: |
1888 | - old_path = None |
1889 | - new_executability = self._new_executability.get(trans_id) |
1890 | - if new_executability is not None: |
1891 | - new_entry.executable = new_executability |
1892 | - inventory_delta.append( |
1893 | - (old_path, path, new_entry.file_id, new_entry)) |
1894 | - return inventory_delta |
1895 | - |
1896 | - def _apply_removals(self, mover): |
1897 | - """Perform tree operations that remove directory/inventory names. |
1898 | - |
1899 | - That is, delete files that are to be deleted, and put any files that |
1900 | - need renaming into limbo. This must be done in strict child-to-parent |
1901 | - order. |
1902 | - |
1903 | - If inventory_delta is None, no inventory delta generation is performed. |
1904 | - """ |
1905 | - tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True) |
1906 | - with ui.ui_factory.nested_progress_bar() as child_pb: |
1907 | - for num, (path, trans_id) in enumerate(tree_paths): |
1908 | - # do not attempt to move root into a subdirectory of itself. |
1909 | - if path == '': |
1910 | - continue |
1911 | - child_pb.update(gettext('removing file'), num, len(tree_paths)) |
1912 | - full_path = self._tree.abspath(path) |
1913 | - if trans_id in self._removed_contents: |
1914 | - delete_path = os.path.join(self._deletiondir, trans_id) |
1915 | - mover.pre_delete(full_path, delete_path) |
1916 | - elif (trans_id in self._new_name or |
1917 | - trans_id in self._new_parent): |
1918 | - try: |
1919 | - mover.rename(full_path, self._limbo_name(trans_id)) |
1920 | - except errors.TransformRenameFailed as e: |
1921 | - if e.errno != errno.ENOENT: |
1922 | - raise |
1923 | - else: |
1924 | - self.rename_count += 1 |
1925 | - |
1926 | - def _apply_insertions(self, mover): |
1927 | - """Perform tree operations that insert directory/inventory names. |
1928 | - |
1929 | - That is, create any files that need to be created, and restore from |
1930 | - limbo any files that needed renaming. This must be done in strict |
1931 | - parent-to-child order. |
1932 | - |
1933 | - If inventory_delta is None, no inventory delta is calculated, and |
1934 | - no list of modified paths is returned. |
1935 | - """ |
1936 | - new_paths = self.new_paths(filesystem_only=True) |
1937 | - modified_paths = [] |
1938 | - with ui.ui_factory.nested_progress_bar() as child_pb: |
1939 | - for num, (path, trans_id) in enumerate(new_paths): |
1940 | - if (num % 10) == 0: |
1941 | - child_pb.update(gettext('adding file'), |
1942 | - num, len(new_paths)) |
1943 | - full_path = self._tree.abspath(path) |
1944 | - if trans_id in self._needs_rename: |
1945 | - try: |
1946 | - mover.rename(self._limbo_name(trans_id), full_path) |
1947 | - except errors.TransformRenameFailed as e: |
1948 | - # We may be renaming a dangling inventory id |
1949 | - if e.errno != errno.ENOENT: |
1950 | - raise |
1951 | - else: |
1952 | - self.rename_count += 1 |
1953 | - # TODO: if trans_id in self._observed_sha1s, we should |
1954 | - # re-stat the final target, since ctime will be |
1955 | - # updated by the change. |
1956 | - if (trans_id in self._new_contents |
1957 | - or self.path_changed(trans_id)): |
1958 | - if trans_id in self._new_contents: |
1959 | - modified_paths.append(full_path) |
1960 | - if trans_id in self._new_executability: |
1961 | - self._set_executability(path, trans_id) |
1962 | - if trans_id in self._observed_sha1s: |
1963 | - o_sha1, o_st_val = self._observed_sha1s[trans_id] |
1964 | - st = osutils.lstat(full_path) |
1965 | - self._observed_sha1s[trans_id] = (o_sha1, st) |
1966 | - for path, trans_id in new_paths: |
1967 | - # new_paths includes stuff like workingtree conflicts. Only the |
1968 | - # stuff in new_contents actually comes from limbo. |
1969 | - if trans_id in self._limbo_files: |
1970 | - del self._limbo_files[trans_id] |
1971 | - self._new_contents.clear() |
1972 | - return modified_paths |
1973 | - |
1974 | - def _apply_observed_sha1s(self): |
1975 | - """After we have finished renaming everything, update observed sha1s |
1976 | - |
1977 | - This has to be done after self._tree.apply_inventory_delta, otherwise |
1978 | - it doesn't know anything about the files we are updating. Also, we want |
1979 | - to do this as late as possible, so that most entries end up cached. |
1980 | - """ |
1981 | - # TODO: this doesn't update the stat information for directories. So |
1982 | - # the first 'bzr status' will still need to rewrite |
1983 | - # .bzr/checkout/dirstate. However, we at least don't need to |
1984 | - # re-read all of the files. |
1985 | - # TODO: If the operation took a while, we could do a time.sleep(3) here |
1986 | - # to allow the clock to tick over and ensure we won't have any |
1987 | - # problems. (we could observe start time, and finish time, and if |
1988 | - # it is less than eg 10% overhead, add a sleep call.) |
1989 | - paths = FinalPaths(self) |
1990 | - for trans_id, observed in viewitems(self._observed_sha1s): |
1991 | - path = paths.get_path(trans_id) |
1992 | - self._tree._observed_sha1(path, observed) |
1993 | - |
1994 | - |
1995 | -class TransformPreview(DiskTreeTransform): |
1996 | - """A TreeTransform for generating preview trees. |
1997 | - |
1998 | - Unlike TreeTransform, this version works when the input tree is a |
1999 | - RevisionTree, rather than a WorkingTree. As a result, it tends to ignore |
2000 | - unversioned files in the input tree. |
2001 | - """ |
2002 | - |
2003 | - def __init__(self, tree, pb=None, case_sensitive=True): |
2004 | - tree.lock_read() |
2005 | - limbodir = osutils.mkdtemp(prefix='bzr-limbo-') |
2006 | - DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive) |
2007 | - |
2008 | - def tree_kind(self, trans_id): |
2009 | - path = self._tree_id_paths.get(trans_id) |
2010 | - if path is None: |
2011 | - return None |
2012 | - kind = self._tree.path_content_summary(path)[0] |
2013 | - if kind == 'missing': |
2014 | - kind = None |
2015 | - return kind |
2016 | - |
2017 | - def _set_mode(self, trans_id, mode_id, typefunc): |
2018 | - """Set the mode of new file contents. |
2019 | - The mode_id is the existing file to get the mode from (often the same |
2020 | - as trans_id). The operation is only performed if there's a mode match |
2021 | - according to typefunc. |
2022 | - """ |
2023 | - # is it ok to ignore this? probably |
2024 | - pass |
2025 | - |
2026 | - def iter_tree_children(self, parent_id): |
2027 | - """Iterate through the entry's tree children, if any""" |
2028 | - try: |
2029 | - path = self._tree_id_paths[parent_id] |
2030 | - except KeyError: |
2031 | - return |
2032 | - try: |
2033 | - entry = next(self._tree.iter_entries_by_dir( |
2034 | - specific_files=[path]))[1] |
2035 | - except StopIteration: |
2036 | - return |
2037 | - children = getattr(entry, 'children', {}) |
2038 | - for child in children: |
2039 | - childpath = joinpath(path, child) |
2040 | - yield self.trans_id_tree_path(childpath) |
2041 | - |
2042 | - def new_orphan(self, trans_id, parent_id): |
2043 | - raise NotImplementedError(self.new_orphan) |
2044 | - |
2045 | - |
2046 | -class _PreviewTree(inventorytree.InventoryTree): |
2047 | - """Partial implementation of Tree to support show_diff_trees""" |
2048 | - |
2049 | - def __init__(self, transform): |
2050 | - self._transform = transform |
2051 | - self._final_paths = FinalPaths(transform) |
2052 | - self.__by_parent = None |
2053 | - self._parent_ids = [] |
2054 | - self._all_children_cache = {} |
2055 | - self._path2trans_id_cache = {} |
2056 | - self._final_name_cache = {} |
2057 | - self._iter_changes_cache = dict((c.file_id, c) for c in |
2058 | - self._transform.iter_changes()) |
2059 | - |
2060 | - def supports_tree_reference(self): |
2061 | - # TODO(jelmer): Support tree references in _PreviewTree. |
2062 | - # return self._transform._tree.supports_tree_reference() |
2063 | - return False |
2064 | - |
2065 | - def _content_change(self, file_id): |
2066 | - """Return True if the content of this file changed""" |
2067 | - changes = self._iter_changes_cache.get(file_id) |
2068 | - return (changes is not None and changes.changed_content) |
2069 | - |
2070 | - def _get_repository(self): |
2071 | - repo = getattr(self._transform._tree, '_repository', None) |
2072 | - if repo is None: |
2073 | - repo = self._transform._tree.branch.repository |
2074 | - return repo |
2075 | - |
2076 | - def _iter_parent_trees(self): |
2077 | - for revision_id in self.get_parent_ids(): |
2078 | - try: |
2079 | - yield self.revision_tree(revision_id) |
2080 | - except errors.NoSuchRevisionInTree: |
2081 | - yield self._get_repository().revision_tree(revision_id) |
2082 | - |
2083 | - def _get_file_revision(self, path, file_id, vf, tree_revision): |
2084 | - parent_keys = [ |
2085 | - (file_id, t.get_file_revision(t.id2path(file_id))) |
2086 | - for t in self._iter_parent_trees()] |
2087 | - vf.add_lines((file_id, tree_revision), parent_keys, |
2088 | - self.get_file_lines(path)) |
2089 | - repo = self._get_repository() |
2090 | - base_vf = repo.texts |
2091 | - if base_vf not in vf.fallback_versionedfiles: |
2092 | - vf.fallback_versionedfiles.append(base_vf) |
2093 | - return tree_revision |
2094 | - |
2095 | - def _stat_limbo_file(self, trans_id): |
2096 | - name = self._transform._limbo_name(trans_id) |
2097 | - return os.lstat(name) |
2098 | - |
2099 | - @property |
2100 | - def _by_parent(self): |
2101 | - if self.__by_parent is None: |
2102 | - self.__by_parent = self._transform.by_parent() |
2103 | - return self.__by_parent |
2104 | - |
2105 | - def _comparison_data(self, entry, path): |
2106 | - kind, size, executable, link_or_sha1 = self.path_content_summary(path) |
2107 | - if kind == 'missing': |
2108 | - kind = None |
2109 | - executable = False |
2110 | - else: |
2111 | - executable = self.is_executable(path) |
2112 | - return kind, executable, None |
2113 | - |
2114 | - def is_locked(self): |
2115 | - return False |
2116 | - |
2117 | - def lock_read(self): |
2118 | - # Perhaps in theory, this should lock the TreeTransform? |
2119 | - return lock.LogicalLockResult(self.unlock) |
2120 | - |
2121 | - def unlock(self): |
2122 | - pass |
2123 | - |
2124 | - @property |
2125 | - def root_inventory(self): |
2126 | - """This Tree does not use inventory as its backing data.""" |
2127 | - raise NotImplementedError(_PreviewTree.root_inventory) |
2128 | - |
2129 | - def all_file_ids(self): |
2130 | - tree_ids = set(self._transform._tree.all_file_ids()) |
2131 | - tree_ids.difference_update(self._transform.tree_file_id(t) |
2132 | - for t in self._transform._removed_id) |
2133 | - tree_ids.update(viewvalues(self._transform._new_id)) |
2134 | - return tree_ids |
2135 | - |
2136 | - def all_versioned_paths(self): |
2137 | - tree_paths = set(self._transform._tree.all_versioned_paths()) |
2138 | - |
2139 | - tree_paths.difference_update( |
2140 | - self._transform.trans_id_tree_path(t) |
2141 | - for t in self._transform._removed_id) |
2142 | - |
2143 | - tree_paths.update( |
2144 | - self._final_paths._determine_path(t) |
2145 | - for t in self._transform._new_id) |
2146 | - |
2147 | - return tree_paths |
2148 | - |
2149 | - def _path2trans_id(self, path): |
2150 | - # We must not use None here, because that is a valid value to store. |
2151 | - trans_id = self._path2trans_id_cache.get(path, object) |
2152 | - if trans_id is not object: |
2153 | - return trans_id |
2154 | - segments = splitpath(path) |
2155 | - cur_parent = self._transform.root |
2156 | - for cur_segment in segments: |
2157 | - for child in self._all_children(cur_parent): |
2158 | - final_name = self._final_name_cache.get(child) |
2159 | - if final_name is None: |
2160 | - final_name = self._transform.final_name(child) |
2161 | - self._final_name_cache[child] = final_name |
2162 | - if final_name == cur_segment: |
2163 | - cur_parent = child |
2164 | - break |
2165 | - else: |
2166 | - self._path2trans_id_cache[path] = None |
2167 | - return None |
2168 | - self._path2trans_id_cache[path] = cur_parent |
2169 | - return cur_parent |
2170 | - |
2171 | - def path2id(self, path): |
2172 | - if isinstance(path, list): |
2173 | - if path == []: |
2174 | - path = [""] |
2175 | - path = osutils.pathjoin(*path) |
2176 | - return self._transform.final_file_id(self._path2trans_id(path)) |
2177 | - |
2178 | - def id2path(self, file_id, recurse='down'): |
2179 | - trans_id = self._transform.trans_id_file_id(file_id) |
2180 | - try: |
2181 | - return self._final_paths._determine_path(trans_id) |
2182 | - except NoFinalPath: |
2183 | - raise errors.NoSuchId(self, file_id) |
2184 | - |
2185 | - def _all_children(self, trans_id): |
2186 | - children = self._all_children_cache.get(trans_id) |
2187 | - if children is not None: |
2188 | - return children |
2189 | - children = set(self._transform.iter_tree_children(trans_id)) |
2190 | - # children in the _new_parent set are provided by _by_parent. |
2191 | - children.difference_update(self._transform._new_parent) |
2192 | - children.update(self._by_parent.get(trans_id, [])) |
2193 | - self._all_children_cache[trans_id] = children |
2194 | - return children |
2195 | - |
2196 | - def extras(self): |
2197 | - possible_extras = set(self._transform.trans_id_tree_path(p) for p |
2198 | - in self._transform._tree.extras()) |
2199 | - possible_extras.update(self._transform._new_contents) |
2200 | - possible_extras.update(self._transform._removed_id) |
2201 | - for trans_id in possible_extras: |
2202 | - if not self._transform.final_is_versioned(trans_id): |
2203 | - yield self._final_paths._determine_path(trans_id) |
2204 | - |
2205 | - def _make_inv_entries(self, ordered_entries, specific_files=None): |
2206 | - for trans_id, parent_file_id in ordered_entries: |
2207 | - file_id = self._transform.final_file_id(trans_id) |
2208 | - if file_id is None: |
2209 | - continue |
2210 | - if (specific_files is not None |
2211 | - and self._final_paths.get_path(trans_id) not in specific_files): |
2212 | - continue |
2213 | - kind = self._transform.final_kind(trans_id) |
2214 | - if kind is None: |
2215 | - kind = self._transform._tree.stored_kind( |
2216 | - self._transform._tree.id2path(file_id)) |
2217 | - new_entry = inventory.make_entry( |
2218 | - kind, |
2219 | - self._transform.final_name(trans_id), |
2220 | - parent_file_id, file_id) |
2221 | - yield new_entry, trans_id |
2222 | - |
2223 | - def _list_files_by_dir(self): |
2224 | - todo = [ROOT_PARENT] |
2225 | - ordered_ids = [] |
2226 | - while len(todo) > 0: |
2227 | - parent = todo.pop() |
2228 | - parent_file_id = self._transform.final_file_id(parent) |
2229 | - children = list(self._all_children(parent)) |
2230 | - paths = dict(zip(children, self._final_paths.get_paths(children))) |
2231 | - children.sort(key=paths.get) |
2232 | - todo.extend(reversed(children)) |
2233 | - for trans_id in children: |
2234 | - ordered_ids.append((trans_id, parent_file_id)) |
2235 | - return ordered_ids |
2236 | - |
2237 | - def iter_child_entries(self, path): |
2238 | - trans_id = self._path2trans_id(path) |
2239 | - if trans_id is None: |
2240 | - raise errors.NoSuchFile(path) |
2241 | - todo = [(child_trans_id, trans_id) for child_trans_id in |
2242 | - self._all_children(trans_id)] |
2243 | - for entry, trans_id in self._make_inv_entries(todo): |
2244 | - yield entry |
2245 | - |
2246 | - def iter_entries_by_dir(self, specific_files=None, recurse_nested=False): |
2247 | - if recurse_nested: |
2248 | - raise NotImplementedError( |
2249 | - 'follow tree references not yet supported') |
2250 | - |
2251 | - # This may not be a maximally efficient implementation, but it is |
2252 | - # reasonably straightforward. An implementation that grafts the |
2253 | - # TreeTransform changes onto the tree's iter_entries_by_dir results |
2254 | - # might be more efficient, but requires tricky inferences about stack |
2255 | - # position. |
2256 | - ordered_ids = self._list_files_by_dir() |
2257 | - for entry, trans_id in self._make_inv_entries(ordered_ids, |
2258 | - specific_files): |
2259 | - yield self._final_paths.get_path(trans_id), entry |
2260 | - |
2261 | - def _iter_entries_for_dir(self, dir_path): |
2262 | - """Return path, entry for items in a directory without recursing down.""" |
2263 | - ordered_ids = [] |
2264 | - dir_trans_id = self._path2trans_id(dir_path) |
2265 | - dir_id = self._transform.final_file_id(dir_trans_id) |
2266 | - for child_trans_id in self._all_children(dir_trans_id): |
2267 | - ordered_ids.append((child_trans_id, dir_id)) |
2268 | - path_entries = [] |
2269 | - for entry, trans_id in self._make_inv_entries(ordered_ids): |
2270 | - path_entries.append((self._final_paths.get_path(trans_id), entry)) |
2271 | - path_entries.sort() |
2272 | - return path_entries |
2273 | - |
2274 | - def list_files(self, include_root=False, from_dir=None, recursive=True, |
2275 | - recurse_nested=False): |
2276 | - """See WorkingTree.list_files.""" |
2277 | - if recurse_nested: |
2278 | - raise NotImplementedError( |
2279 | - 'follow tree references not yet supported') |
2280 | - |
2281 | - # XXX This should behave like WorkingTree.list_files, but is really |
2282 | - # more like RevisionTree.list_files. |
2283 | - if from_dir == '.': |
2284 | - from_dir = None |
2285 | - if recursive: |
2286 | - prefix = None |
2287 | - if from_dir: |
2288 | - prefix = from_dir + '/' |
2289 | - entries = self.iter_entries_by_dir() |
2290 | - for path, entry in entries: |
2291 | - if entry.name == '' and not include_root: |
2292 | - continue |
2293 | - if prefix: |
2294 | - if not path.startswith(prefix): |
2295 | - continue |
2296 | - path = path[len(prefix):] |
2297 | - yield path, 'V', entry.kind, entry |
2298 | - else: |
2299 | - if from_dir is None and include_root is True: |
2300 | - root_entry = inventory.make_entry( |
2301 | - 'directory', '', ROOT_PARENT, self.path2id('')) |
2302 | - yield '', 'V', 'directory', root_entry |
2303 | - entries = self._iter_entries_for_dir(from_dir or '') |
2304 | - for path, entry in entries: |
2305 | - yield path, 'V', entry.kind, entry |
2306 | - |
2307 | - def kind(self, path): |
2308 | - trans_id = self._path2trans_id(path) |
2309 | - if trans_id is None: |
2310 | - raise errors.NoSuchFile(path) |
2311 | - return self._transform.final_kind(trans_id) |
2312 | - |
2313 | - def stored_kind(self, path): |
2314 | - trans_id = self._path2trans_id(path) |
2315 | - if trans_id is None: |
2316 | - raise errors.NoSuchFile(path) |
2317 | - try: |
2318 | - return self._transform._new_contents[trans_id] |
2319 | - except KeyError: |
2320 | - return self._transform._tree.stored_kind(path) |
2321 | - |
2322 | - def get_file_mtime(self, path): |
2323 | - """See Tree.get_file_mtime""" |
2324 | - file_id = self.path2id(path) |
2325 | - if file_id is None: |
2326 | - raise errors.NoSuchFile(path) |
2327 | - if not self._content_change(file_id): |
2328 | - return self._transform._tree.get_file_mtime( |
2329 | - self._transform._tree.id2path(file_id)) |
2330 | - trans_id = self._path2trans_id(path) |
2331 | - return self._stat_limbo_file(trans_id).st_mtime |
2332 | - |
2333 | - def get_file_size(self, path): |
2334 | - """See Tree.get_file_size""" |
2335 | - trans_id = self._path2trans_id(path) |
2336 | - if trans_id is None: |
2337 | - raise errors.NoSuchFile(path) |
2338 | - kind = self._transform.final_kind(trans_id) |
2339 | - if kind != 'file': |
2340 | - return None |
2341 | - if trans_id in self._transform._new_contents: |
2342 | - return self._stat_limbo_file(trans_id).st_size |
2343 | - if self.kind(path) == 'file': |
2344 | - return self._transform._tree.get_file_size(path) |
2345 | - else: |
2346 | - return None |
2347 | - |
2348 | - def get_file_verifier(self, path, stat_value=None): |
2349 | - trans_id = self._path2trans_id(path) |
2350 | - if trans_id is None: |
2351 | - raise errors.NoSuchFile(path) |
2352 | - kind = self._transform._new_contents.get(trans_id) |
2353 | - if kind is None: |
2354 | - return self._transform._tree.get_file_verifier(path) |
2355 | - if kind == 'file': |
2356 | - with self.get_file(path) as fileobj: |
2357 | - return ("SHA1", sha_file(fileobj)) |
2358 | - |
2359 | - def get_file_sha1(self, path, stat_value=None): |
2360 | - trans_id = self._path2trans_id(path) |
2361 | - if trans_id is None: |
2362 | - raise errors.NoSuchFile(path) |
2363 | - kind = self._transform._new_contents.get(trans_id) |
2364 | - if kind is None: |
2365 | - return self._transform._tree.get_file_sha1(path) |
2366 | - if kind == 'file': |
2367 | - with self.get_file(path) as fileobj: |
2368 | - return sha_file(fileobj) |
2369 | - |
2370 | - def get_reference_revision(self, path): |
2371 | - trans_id = self._path2trans_id(path) |
2372 | - if trans_id is None: |
2373 | - raise errors.NoSuchFile(path) |
2374 | - reference_revision = self._transform._new_reference_revision.get(trans_id) |
2375 | - if reference_revision is None: |
2376 | - return self._transform._tree.get_reference_revision(path) |
2377 | - return reference_revision |
2378 | - |
2379 | - def is_executable(self, path): |
2380 | - trans_id = self._path2trans_id(path) |
2381 | - if trans_id is None: |
2382 | - return False |
2383 | - try: |
2384 | - return self._transform._new_executability[trans_id] |
2385 | - except KeyError: |
2386 | - try: |
2387 | - return self._transform._tree.is_executable(path) |
2388 | - except OSError as e: |
2389 | - if e.errno == errno.ENOENT: |
2390 | - return False |
2391 | - raise |
2392 | - except errors.NoSuchFile: |
2393 | - return False |
2394 | - |
2395 | - def has_filename(self, path): |
2396 | - trans_id = self._path2trans_id(path) |
2397 | - if trans_id in self._transform._new_contents: |
2398 | - return True |
2399 | - elif trans_id in self._transform._removed_contents: |
2400 | - return False |
2401 | - else: |
2402 | - return self._transform._tree.has_filename(path) |
2403 | - |
2404 | - def path_content_summary(self, path): |
2405 | - trans_id = self._path2trans_id(path) |
2406 | - tt = self._transform |
2407 | - tree_path = tt._tree_id_paths.get(trans_id) |
2408 | - kind = tt._new_contents.get(trans_id) |
2409 | - if kind is None: |
2410 | - if tree_path is None or trans_id in tt._removed_contents: |
2411 | - return 'missing', None, None, None |
2412 | - summary = tt._tree.path_content_summary(tree_path) |
2413 | - kind, size, executable, link_or_sha1 = summary |
2414 | - else: |
2415 | - link_or_sha1 = None |
2416 | - limbo_name = tt._limbo_name(trans_id) |
2417 | - if trans_id in tt._new_reference_revision: |
2418 | - kind = 'tree-reference' |
2419 | - if kind == 'file': |
2420 | - statval = os.lstat(limbo_name) |
2421 | - size = statval.st_size |
2422 | - if not tt._limbo_supports_executable(): |
2423 | - executable = False |
2424 | - else: |
2425 | - executable = statval.st_mode & S_IEXEC |
2426 | - else: |
2427 | - size = None |
2428 | - executable = None |
2429 | - if kind == 'symlink': |
2430 | - link_or_sha1 = os.readlink(limbo_name) |
2431 | - if not isinstance(link_or_sha1, text_type): |
2432 | - link_or_sha1 = link_or_sha1.decode(osutils._fs_enc) |
2433 | - executable = tt._new_executability.get(trans_id, executable) |
2434 | - return kind, size, executable, link_or_sha1 |
2435 | - |
2436 | - def iter_changes(self, from_tree, include_unchanged=False, |
2437 | - specific_files=None, pb=None, extra_trees=None, |
2438 | - require_versioned=True, want_unversioned=False): |
2439 | - """See InterTree.iter_changes. |
2440 | - |
2441 | - This has a fast path that is only used when the from_tree matches |
2442 | - the transform tree, and no fancy options are supplied. |
2443 | - """ |
2444 | - if (from_tree is not self._transform._tree or include_unchanged |
2445 | - or specific_files or want_unversioned): |
2446 | - from .bzr.inventorytree import InterInventoryTree |
2447 | - return InterInventoryTree(from_tree, self).iter_changes( |
2448 | - include_unchanged=include_unchanged, |
2449 | - specific_files=specific_files, |
2450 | - pb=pb, |
2451 | - extra_trees=extra_trees, |
2452 | - require_versioned=require_versioned, |
2453 | - want_unversioned=want_unversioned) |
2454 | - if want_unversioned: |
2455 | - raise ValueError('want_unversioned is not supported') |
2456 | - return self._transform.iter_changes() |
2457 | - |
2458 | - def get_file(self, path): |
2459 | - """See Tree.get_file""" |
2460 | - file_id = self.path2id(path) |
2461 | - if not self._content_change(file_id): |
2462 | - return self._transform._tree.get_file(path) |
2463 | - trans_id = self._path2trans_id(path) |
2464 | - name = self._transform._limbo_name(trans_id) |
2465 | - return open(name, 'rb') |
2466 | - |
2467 | - def get_file_with_stat(self, path): |
2468 | - return self.get_file(path), None |
2469 | - |
2470 | - def annotate_iter(self, path, |
2471 | - default_revision=_mod_revision.CURRENT_REVISION): |
2472 | - file_id = self.path2id(path) |
2473 | - changes = self._iter_changes_cache.get(file_id) |
2474 | - if changes is None: |
2475 | - get_old = True |
2476 | - else: |
2477 | - changed_content, versioned, kind = ( |
2478 | - changes.changed_content, changes.versioned, changes.kind) |
2479 | - if kind[1] is None: |
2480 | - return None |
2481 | - get_old = (kind[0] == 'file' and versioned[0]) |
2482 | - if get_old: |
2483 | - old_annotation = self._transform._tree.annotate_iter( |
2484 | - path, default_revision=default_revision) |
2485 | - else: |
2486 | - old_annotation = [] |
2487 | - if changes is None: |
2488 | - return old_annotation |
2489 | - if not changed_content: |
2490 | - return old_annotation |
2491 | - # TODO: This is doing something similar to what WT.annotate_iter is |
2492 | - # doing, however it fails slightly because it doesn't know what |
2493 | - # the *other* revision_id is, so it doesn't know how to give the |
2494 | - # other as the origin for some lines, they all get |
2495 | - # 'default_revision' |
2496 | - # It would be nice to be able to use the new Annotator based |
2497 | - # approach, as well. |
2498 | - return annotate.reannotate([old_annotation], |
2499 | - self.get_file(path).readlines(), |
2500 | - default_revision) |
2501 | - |
2502 | - def get_symlink_target(self, path): |
2503 | - """See Tree.get_symlink_target""" |
2504 | - file_id = self.path2id(path) |
2505 | - if not self._content_change(file_id): |
2506 | - return self._transform._tree.get_symlink_target(path) |
2507 | - trans_id = self._path2trans_id(path) |
2508 | - name = self._transform._limbo_name(trans_id) |
2509 | - return osutils.readlink(name) |
2510 | - |
2511 | - def walkdirs(self, prefix=''): |
2512 | - pending = [self._transform.root] |
2513 | - while len(pending) > 0: |
2514 | - parent_id = pending.pop() |
2515 | - children = [] |
2516 | - subdirs = [] |
2517 | - prefix = prefix.rstrip('/') |
2518 | - parent_path = self._final_paths.get_path(parent_id) |
2519 | - parent_file_id = self._transform.final_file_id(parent_id) |
2520 | - for child_id in self._all_children(parent_id): |
2521 | - path_from_root = self._final_paths.get_path(child_id) |
2522 | - basename = self._transform.final_name(child_id) |
2523 | - file_id = self._transform.final_file_id(child_id) |
2524 | - kind = self._transform.final_kind(child_id) |
2525 | - if kind is not None: |
2526 | - versioned_kind = kind |
2527 | - else: |
2528 | - kind = 'unknown' |
2529 | - versioned_kind = self._transform._tree.stored_kind( |
2530 | - self._transform._tree.id2path(file_id)) |
2531 | - if versioned_kind == 'directory': |
2532 | - subdirs.append(child_id) |
2533 | - children.append((path_from_root, basename, kind, None, |
2534 | - file_id, versioned_kind)) |
2535 | - children.sort() |
2536 | - if parent_path.startswith(prefix): |
2537 | - yield (parent_path, parent_file_id), children |
2538 | - pending.extend(sorted(subdirs, key=self._final_paths.get_path, |
2539 | - reverse=True)) |
2540 | - |
2541 | - def get_parent_ids(self): |
2542 | - return self._parent_ids |
2543 | - |
2544 | - def set_parent_ids(self, parent_ids): |
2545 | - self._parent_ids = parent_ids |
2546 | - |
2547 | - def get_revision_tree(self, revision_id): |
2548 | - return self._transform._tree.get_revision_tree(revision_id) |
2549 | + raise NotImplementedError(self.apply) |
2550 | |
2551 | |
2552 | def joinpath(parent, child): |
2553 | @@ -3278,7 +2481,7 @@ |
2554 | raise errors.FileExists(to, str(e)) |
2555 | # normal OSError doesn't include filenames so it's hard to see where |
2556 | # the problem is, see https://bugs.launchpad.net/bzr/+bug/491763 |
2557 | - raise errors.TransformRenameFailed(from_, to, str(e), e.errno) |
2558 | + raise TransformRenameFailed(from_, to, str(e), e.errno) |
2559 | self.past_renames.append((from_, to)) |
2560 | |
2561 | def pre_delete(self, from_, to): |
2562 | @@ -3297,7 +2500,7 @@ |
2563 | try: |
2564 | os.rename(to, from_) |
2565 | except OSError as e: |
2566 | - raise errors.TransformRenameFailed(to, from_, str(e), e.errno) |
2567 | + raise TransformRenameFailed(to, from_, str(e), e.errno) |
2568 | # after rollback, don't reuse _FileMover |
2569 | self.past_renames = None |
2570 | self.pending_deletions = None |
Voting criteria not met /ci.breezy- vcs.org/ job/brz- 3.1/job/ brz-3.1- land/177/
https:/