1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
21
from stat import S_ISREG, S_IEXEC
25
config as _mod_config,
32
lazy_import.lazy_import(globals(), """
42
revision as _mod_revision,
46
from breezy.bzr import (
49
from breezy.i18n import gettext
51
from .errors import (DuplicateKey, MalformedTransform,
52
ReusingTransform, CantMoveRoot,
53
ImmortalLimbo, NoFinalPath,
55
from .filters import filtered_output_bytes, ContentFilterContext
56
from .mutabletree import MutableTree
57
from .osutils import (
65
from .progress import ProgressPhase
72
ROOT_PARENT = "root-parent"
74
def unique_add(map, key, value):
76
raise DuplicateKey(key=key)
81
class _TransformResults(object):
82
def __init__(self, modified_paths, rename_count):
84
self.modified_paths = modified_paths
85
self.rename_count = rename_count
88
class TreeTransformBase(object):
89
"""The base class for TreeTransform and its kin."""
91
def __init__(self, tree, pb=None,
95
:param tree: The tree that will be transformed, but not necessarily
98
:param case_sensitive: If True, the target of the transform is
99
case sensitive, not just case preserving.
101
object.__init__(self)
104
# mapping of trans_id -> new basename
106
# mapping of trans_id -> new parent trans_id
107
self._new_parent = {}
108
# mapping of trans_id with new contents -> new file_kind
109
self._new_contents = {}
110
# mapping of trans_id => (sha1 of content, stat_value)
111
self._observed_sha1s = {}
112
# Set of trans_ids whose contents will be removed
113
self._removed_contents = set()
114
# Mapping of trans_id -> new execute-bit value
115
self._new_executability = {}
116
# Mapping of trans_id -> new tree-reference value
117
self._new_reference_revision = {}
118
# Mapping of trans_id -> new file_id
120
# Mapping of old file-id -> trans_id
121
self._non_present_ids = {}
122
# Mapping of new file_id -> trans_id
124
# Set of trans_ids that will be removed
125
self._removed_id = set()
126
# Mapping of path in old tree -> trans_id
127
self._tree_path_ids = {}
128
# Mapping trans_id -> path in old tree
129
self._tree_id_paths = {}
130
# The trans_id that will be used as the tree root
131
root_id = tree.get_root_id()
132
if root_id is not None:
133
self._new_root = self.trans_id_tree_file_id(root_id)
135
self._new_root = None
136
# Indicator of whether the transform has been applied
140
# Whether the target is case sensitive
141
self._case_sensitive_target = case_sensitive
142
# A counter of how many files have been renamed
143
self.rename_count = 0
146
"""Support Context Manager API."""
149
def __exit__(self, exc_type, exc_val, exc_tb):
150
"""Support Context Manager API."""
154
"""Release the working tree lock, if held.
156
This is required if apply has not been invoked, but can be invoked
159
if self._tree is None:
161
for hook in MutableTree.hooks['post_transform']:
162
hook(self._tree, self)
166
def __get_root(self):
167
return self._new_root
169
root = property(__get_root)
171
def _assign_id(self):
172
"""Produce a new tranform id"""
173
new_id = "new-%s" % self._id_number
177
def create_path(self, name, parent):
178
"""Assign a transaction id to a new path"""
179
trans_id = self._assign_id()
180
unique_add(self._new_name, trans_id, name)
181
unique_add(self._new_parent, trans_id, parent)
184
def adjust_path(self, name, parent, trans_id):
185
"""Change the path that is assigned to a transaction id."""
187
raise ValueError("Parent trans-id may not be None")
188
if trans_id == self._new_root:
190
self._new_name[trans_id] = name
191
self._new_parent[trans_id] = parent
193
def adjust_root_path(self, name, parent):
194
"""Emulate moving the root by moving all children, instead.
196
We do this by undoing the association of root's transaction id with the
197
current tree. This allows us to create a new directory with that
198
transaction id. We unversion the root directory and version the
199
physically new directory, and hope someone versions the tree root
202
old_root = self._new_root
203
old_root_file_id = self.final_file_id(old_root)
204
# force moving all children of root
205
for child_id in self.iter_tree_children(old_root):
206
if child_id != parent:
207
self.adjust_path(self.final_name(child_id),
208
self.final_parent(child_id), child_id)
209
file_id = self.final_file_id(child_id)
210
if file_id is not None:
211
self.unversion_file(child_id)
212
self.version_file(file_id, child_id)
214
# the physical root needs a new transaction id
215
self._tree_path_ids.pop("")
216
self._tree_id_paths.pop(old_root)
217
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
218
if parent == old_root:
219
parent = self._new_root
220
self.adjust_path(name, parent, old_root)
221
self.create_directory(old_root)
222
self.version_file(old_root_file_id, old_root)
223
self.unversion_file(self._new_root)
225
def fixup_new_roots(self):
226
"""Reinterpret requests to change the root directory
228
Instead of creating a root directory, or moving an existing directory,
229
all the attributes and children of the new root are applied to the
230
existing root directory.
232
This means that the old root trans-id becomes obsolete, so it is
233
recommended only to invoke this after the root trans-id has become
237
new_roots = [k for k, v in viewitems(self._new_parent)
239
if len(new_roots) < 1:
241
if len(new_roots) != 1:
242
raise ValueError('A tree cannot have two roots!')
243
if self._new_root is None:
244
self._new_root = new_roots[0]
246
old_new_root = new_roots[0]
247
# unversion the new root's directory.
248
if self.final_kind(self._new_root) is None:
249
file_id = self.final_file_id(old_new_root)
251
file_id = self.final_file_id(self._new_root)
252
if old_new_root in self._new_id:
253
self.cancel_versioning(old_new_root)
255
self.unversion_file(old_new_root)
256
# if, at this stage, root still has an old file_id, zap it so we can
257
# stick a new one in.
258
if (self.tree_file_id(self._new_root) is not None and
259
self._new_root not in self._removed_id):
260
self.unversion_file(self._new_root)
261
if file_id is not None:
262
self.version_file(file_id, self._new_root)
264
# Now move children of new root into old root directory.
265
# Ensure all children are registered with the transaction, but don't
266
# use directly-- some tree children have new parents
267
list(self.iter_tree_children(old_new_root))
268
# Move all children of new root into old root directory.
269
for child in self.by_parent().get(old_new_root, []):
270
self.adjust_path(self.final_name(child), self._new_root, child)
272
# Ensure old_new_root has no directory.
273
if old_new_root in self._new_contents:
274
self.cancel_creation(old_new_root)
276
self.delete_contents(old_new_root)
278
# prevent deletion of root directory.
279
if self._new_root in self._removed_contents:
280
self.cancel_deletion(self._new_root)
282
# destroy path info for old_new_root.
283
del self._new_parent[old_new_root]
284
del self._new_name[old_new_root]
286
def trans_id_tree_file_id(self, inventory_id):
287
"""Determine the transaction id of a working tree file.
289
This reflects only files that already exist, not ones that will be
290
added by transactions.
292
if inventory_id is None:
293
raise ValueError('None is not a valid file id')
294
path = self._tree.id2path(inventory_id)
295
return self.trans_id_tree_path(path)
297
def trans_id_file_id(self, file_id):
298
"""Determine or set the transaction id associated with a file ID.
299
A new id is only created for file_ids that were never present. If
300
a transaction has been unversioned, it is deliberately still returned.
301
(this will likely lead to an unversioned parent conflict.)
304
raise ValueError('None is not a valid file id')
305
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
306
return self._r_new_id[file_id]
309
next(self._tree.iter_entries_by_dir([file_id]))
310
except StopIteration:
311
if file_id in self._non_present_ids:
312
return self._non_present_ids[file_id]
314
trans_id = self._assign_id()
315
self._non_present_ids[file_id] = trans_id
318
return self.trans_id_tree_file_id(file_id)
320
def trans_id_tree_path(self, path):
321
"""Determine (and maybe set) the transaction ID for a tree path."""
322
path = self.canonical_path(path)
323
if path not in self._tree_path_ids:
324
self._tree_path_ids[path] = self._assign_id()
325
self._tree_id_paths[self._tree_path_ids[path]] = path
326
return self._tree_path_ids[path]
328
def get_tree_parent(self, trans_id):
329
"""Determine id of the parent in the tree."""
330
path = self._tree_id_paths[trans_id]
333
return self.trans_id_tree_path(os.path.dirname(path))
335
def delete_contents(self, trans_id):
336
"""Schedule the contents of a path entry for deletion"""
337
kind = self.tree_kind(trans_id)
339
self._removed_contents.add(trans_id)
341
def cancel_deletion(self, trans_id):
342
"""Cancel a scheduled deletion"""
343
self._removed_contents.remove(trans_id)
345
def unversion_file(self, trans_id):
346
"""Schedule a path entry to become unversioned"""
347
self._removed_id.add(trans_id)
349
def delete_versioned(self, trans_id):
350
"""Delete and unversion a versioned file"""
351
self.delete_contents(trans_id)
352
self.unversion_file(trans_id)
354
def set_executability(self, executability, trans_id):
355
"""Schedule setting of the 'execute' bit
356
To unschedule, set to None
358
if executability is None:
359
del self._new_executability[trans_id]
361
unique_add(self._new_executability, trans_id, executability)
363
def set_tree_reference(self, revision_id, trans_id):
364
"""Set the reference associated with a directory"""
365
unique_add(self._new_reference_revision, trans_id, revision_id)
367
def version_file(self, file_id, trans_id):
368
"""Schedule a file to become versioned."""
371
unique_add(self._new_id, trans_id, file_id)
372
unique_add(self._r_new_id, file_id, trans_id)
374
def cancel_versioning(self, trans_id):
375
"""Undo a previous versioning of a file"""
376
file_id = self._new_id[trans_id]
377
del self._new_id[trans_id]
378
del self._r_new_id[file_id]
380
def new_paths(self, filesystem_only=False):
381
"""Determine the paths of all new and changed files.
383
:param filesystem_only: if True, only calculate values for files
384
that require renames or execute bit changes.
388
stale_ids = self._needs_rename.difference(self._new_name)
389
stale_ids.difference_update(self._new_parent)
390
stale_ids.difference_update(self._new_contents)
391
stale_ids.difference_update(self._new_id)
392
needs_rename = self._needs_rename.difference(stale_ids)
393
id_sets = (needs_rename, self._new_executability)
395
id_sets = (self._new_name, self._new_parent, self._new_contents,
396
self._new_id, self._new_executability)
397
for id_set in id_sets:
398
new_ids.update(id_set)
399
return sorted(FinalPaths(self).get_paths(new_ids))
401
def _inventory_altered(self):
402
"""Determine which trans_ids need new Inventory entries.
404
An new entry is needed when anything that would be reflected by an
405
inventory entry changes, including file name, file_id, parent file_id,
406
file kind, and the execute bit.
408
Some care is taken to return entries with real changes, not cases
409
where the value is deleted and then restored to its original value,
410
but some actually unchanged values may be returned.
412
:returns: A list of (path, trans_id) for all items requiring an
413
inventory change. Ordered by path.
416
# Find entries whose file_ids are new (or changed).
417
new_file_id = set(t for t in self._new_id
418
if self._new_id[t] != self.tree_file_id(t))
419
for id_set in [self._new_name, self._new_parent, new_file_id,
420
self._new_executability]:
421
changed_ids.update(id_set)
422
# removing implies a kind change
423
changed_kind = set(self._removed_contents)
425
changed_kind.intersection_update(self._new_contents)
426
# Ignore entries that are already known to have changed.
427
changed_kind.difference_update(changed_ids)
428
# to keep only the truly changed ones
429
changed_kind = (t for t in changed_kind
430
if self.tree_kind(t) != self.final_kind(t))
431
# all kind changes will alter the inventory
432
changed_ids.update(changed_kind)
433
# To find entries with changed parent_ids, find parents which existed,
434
# but changed file_id.
435
changed_file_id = set(t for t in new_file_id if t in self._removed_id)
436
# Now add all their children to the set.
437
for parent_trans_id in new_file_id:
438
changed_ids.update(self.iter_tree_children(parent_trans_id))
439
return sorted(FinalPaths(self).get_paths(changed_ids))
441
def final_kind(self, trans_id):
442
"""Determine the final file kind, after any changes applied.
444
:return: None if the file does not exist/has no contents. (It is
445
conceivable that a path would be created without the corresponding
446
contents insertion command)
448
if trans_id in self._new_contents:
449
return self._new_contents[trans_id]
450
elif trans_id in self._removed_contents:
453
return self.tree_kind(trans_id)
455
def tree_file_id(self, trans_id):
456
"""Determine the file id associated with the trans_id in the tree"""
458
path = self._tree_id_paths[trans_id]
460
# the file is a new, unversioned file, or invalid trans_id
462
# the file is old; the old id is still valid
463
if self._new_root == trans_id:
464
return self._tree.get_root_id()
465
return self._tree.path2id(path)
467
def final_file_id(self, trans_id):
468
"""Determine the file id after any changes are applied, or None.
470
None indicates that the file will not be versioned after changes are
474
return self._new_id[trans_id]
476
if trans_id in self._removed_id:
478
return self.tree_file_id(trans_id)
480
def inactive_file_id(self, trans_id):
481
"""Return the inactive file_id associated with a transaction id.
482
That is, the one in the tree or in non_present_ids.
483
The file_id may actually be active, too.
485
file_id = self.tree_file_id(trans_id)
486
if file_id is not None:
488
for key, value in viewitems(self._non_present_ids):
489
if value == trans_id:
492
def final_parent(self, trans_id):
493
"""Determine the parent file_id, after any changes are applied.
495
ROOT_PARENT is returned for the tree root.
498
return self._new_parent[trans_id]
500
return self.get_tree_parent(trans_id)
502
def final_name(self, trans_id):
503
"""Determine the final filename, after all changes are applied."""
505
return self._new_name[trans_id]
508
return os.path.basename(self._tree_id_paths[trans_id])
510
raise NoFinalPath(trans_id, self)
513
"""Return a map of parent: children for known parents.
515
Only new paths and parents of tree files with assigned ids are used.
518
items = list(viewitems(self._new_parent))
519
items.extend((t, self.final_parent(t))
520
for t in list(self._tree_id_paths))
521
for trans_id, parent_id in items:
522
if parent_id not in by_parent:
523
by_parent[parent_id] = set()
524
by_parent[parent_id].add(trans_id)
527
def path_changed(self, trans_id):
528
"""Return True if a trans_id's path has changed."""
529
return (trans_id in self._new_name) or (trans_id in self._new_parent)
531
def new_contents(self, trans_id):
532
return (trans_id in self._new_contents)
534
def find_conflicts(self):
535
"""Find any violations of inventory or filesystem invariants"""
536
if self._done is True:
537
raise ReusingTransform()
539
# ensure all children of all existent parents are known
540
# all children of non-existent parents are known, by definition.
541
self._add_tree_children()
542
by_parent = self.by_parent()
543
conflicts.extend(self._unversioned_parents(by_parent))
544
conflicts.extend(self._parent_loops())
545
conflicts.extend(self._duplicate_entries(by_parent))
546
conflicts.extend(self._duplicate_ids())
547
conflicts.extend(self._parent_type_conflicts(by_parent))
548
conflicts.extend(self._improper_versioning())
549
conflicts.extend(self._executability_conflicts())
550
conflicts.extend(self._overwrite_conflicts())
553
def _check_malformed(self):
554
conflicts = self.find_conflicts()
555
if len(conflicts) != 0:
556
raise MalformedTransform(conflicts=conflicts)
558
def _add_tree_children(self):
559
"""Add all the children of all active parents to the known paths.
561
Active parents are those which gain children, and those which are
562
removed. This is a necessary first step in detecting conflicts.
564
parents = list(self.by_parent())
565
parents.extend([t for t in self._removed_contents if
566
self.tree_kind(t) == 'directory'])
567
for trans_id in self._removed_id:
568
file_id = self.tree_file_id(trans_id)
569
if file_id is not None:
570
if self._tree.stored_kind(file_id) == 'directory':
571
parents.append(trans_id)
572
elif self.tree_kind(trans_id) == 'directory':
573
parents.append(trans_id)
575
for parent_id in parents:
576
# ensure that all children are registered with the transaction
577
list(self.iter_tree_children(parent_id))
579
def _has_named_child(self, name, parent_id, known_children):
580
"""Does a parent already have a name child.
582
:param name: The searched for name.
584
:param parent_id: The parent for which the check is made.
586
:param known_children: The already known children. This should have
587
been recently obtained from `self.by_parent.get(parent_id)`
588
(or will be if None is passed).
590
if known_children is None:
591
known_children = self.by_parent().get(parent_id, [])
592
for child in known_children:
593
if self.final_name(child) == name:
595
parent_path = self._tree_id_paths.get(parent_id, None)
596
if parent_path is None:
597
# No parent... no children
599
child_path = joinpath(parent_path, name)
600
child_id = self._tree_path_ids.get(child_path, None)
602
# Not known by the tree transform yet, check the filesystem
603
return osutils.lexists(self._tree.abspath(child_path))
605
raise AssertionError('child_id is missing: %s, %s, %s'
606
% (name, parent_id, child_id))
608
def _available_backup_name(self, name, target_id):
609
"""Find an available backup name.
611
:param name: The basename of the file.
613
:param target_id: The directory trans_id where the backup should
616
known_children = self.by_parent().get(target_id, [])
617
return osutils.available_backup_name(
619
lambda base: self._has_named_child(
620
base, target_id, known_children))
622
def _parent_loops(self):
623
"""No entry should be its own ancestor"""
625
for trans_id in self._new_parent:
628
while parent_id != ROOT_PARENT:
631
parent_id = self.final_parent(parent_id)
634
if parent_id == trans_id:
635
conflicts.append(('parent loop', trans_id))
636
if parent_id in seen:
640
def _unversioned_parents(self, by_parent):
641
"""If parent directories are versioned, children must be versioned."""
643
for parent_id, children in viewitems(by_parent):
644
if parent_id == ROOT_PARENT:
646
if self.final_file_id(parent_id) is not None:
648
for child_id in children:
649
if self.final_file_id(child_id) is not None:
650
conflicts.append(('unversioned parent', parent_id))
654
def _improper_versioning(self):
655
"""Cannot version a file with no contents, or a bad type.
657
However, existing entries with no contents are okay.
660
for trans_id in self._new_id:
661
kind = self.final_kind(trans_id)
663
conflicts.append(('versioning no contents', trans_id))
665
if not inventory.InventoryEntry.versionable_kind(kind):
666
conflicts.append(('versioning bad kind', trans_id, kind))
669
def _executability_conflicts(self):
670
"""Check for bad executability changes.
672
Only versioned files may have their executability set, because
673
1. only versioned entries can have executability under windows
674
2. only files can be executable. (The execute bit on a directory
675
does not indicate searchability)
678
for trans_id in self._new_executability:
679
if self.final_file_id(trans_id) is None:
680
conflicts.append(('unversioned executability', trans_id))
682
if self.final_kind(trans_id) != "file":
683
conflicts.append(('non-file executability', trans_id))
686
def _overwrite_conflicts(self):
687
"""Check for overwrites (not permitted on Win32)"""
689
for trans_id in self._new_contents:
690
if self.tree_kind(trans_id) is None:
692
if trans_id not in self._removed_contents:
693
conflicts.append(('overwrite', trans_id,
694
self.final_name(trans_id)))
697
def _duplicate_entries(self, by_parent):
698
"""No directory may have two entries with the same name."""
700
if (self._new_name, self._new_parent) == ({}, {}):
702
for children in viewvalues(by_parent):
704
for child_tid in children:
705
name = self.final_name(child_tid)
707
# Keep children only if they still exist in the end
708
if not self._case_sensitive_target:
710
name_ids.append((name, child_tid))
714
for name, trans_id in name_ids:
715
kind = self.final_kind(trans_id)
716
file_id = self.final_file_id(trans_id)
717
if kind is None and file_id is None:
719
if name == last_name:
720
conflicts.append(('duplicate', last_trans_id, trans_id,
723
last_trans_id = trans_id
726
def _duplicate_ids(self):
727
"""Each inventory id may only be used once"""
729
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
731
all_ids = self._tree.all_file_ids()
732
active_tree_ids = all_ids.difference(removed_tree_ids)
733
for trans_id, file_id in viewitems(self._new_id):
734
if file_id in active_tree_ids:
735
old_trans_id = self.trans_id_tree_file_id(file_id)
736
conflicts.append(('duplicate id', old_trans_id, trans_id))
739
def _parent_type_conflicts(self, by_parent):
740
"""Children must have a directory parent"""
742
for parent_id, children in viewitems(by_parent):
743
if parent_id == ROOT_PARENT:
746
for child_id in children:
747
if self.final_kind(child_id) is not None:
752
# There is at least a child, so we need an existing directory to
754
kind = self.final_kind(parent_id)
756
# The directory will be deleted
757
conflicts.append(('missing parent', parent_id))
758
elif kind != "directory":
759
# Meh, we need a *directory* to put something in it
760
conflicts.append(('non-directory parent', parent_id))
763
def _set_executability(self, path, trans_id):
764
"""Set the executability of versioned files """
765
if self._tree._supports_executable():
766
new_executability = self._new_executability[trans_id]
767
abspath = self._tree.abspath(path)
768
current_mode = os.stat(abspath).st_mode
769
if new_executability:
772
to_mode = current_mode | (0o100 & ~umask)
773
# Enable x-bit for others only if they can read it.
774
if current_mode & 0o004:
775
to_mode |= 0o001 & ~umask
776
if current_mode & 0o040:
777
to_mode |= 0o010 & ~umask
779
to_mode = current_mode & ~0o111
780
osutils.chmod_if_possible(abspath, to_mode)
782
def _new_entry(self, name, parent_id, file_id):
783
"""Helper function to create a new filesystem entry."""
784
trans_id = self.create_path(name, parent_id)
785
if file_id is not None:
786
self.version_file(file_id, trans_id)
789
def new_file(self, name, parent_id, contents, file_id=None,
790
executable=None, sha1=None):
791
"""Convenience method to create files.
793
name is the name of the file to create.
794
parent_id is the transaction id of the parent directory of the file.
795
contents is an iterator of bytestrings, which will be used to produce
797
:param file_id: The inventory ID of the file, if it is to be versioned.
798
:param executable: Only valid when a file_id has been supplied.
800
trans_id = self._new_entry(name, parent_id, file_id)
801
# TODO: rather than scheduling a set_executable call,
802
# have create_file create the file with the right mode.
803
self.create_file(contents, trans_id, sha1=sha1)
804
if executable is not None:
805
self.set_executability(executable, trans_id)
808
def new_directory(self, name, parent_id, file_id=None):
809
"""Convenience method to create directories.
811
name is the name of the directory to create.
812
parent_id is the transaction id of the parent directory of the
814
file_id is the inventory ID of the directory, if it is to be versioned.
816
trans_id = self._new_entry(name, parent_id, file_id)
817
self.create_directory(trans_id)
820
def new_symlink(self, name, parent_id, target, file_id=None):
821
"""Convenience method to create symbolic link.
823
name is the name of the symlink to create.
824
parent_id is the transaction id of the parent directory of the symlink.
825
target is a bytestring of the target of the symlink.
826
file_id is the inventory ID of the file, if it is to be versioned.
828
trans_id = self._new_entry(name, parent_id, file_id)
829
self.create_symlink(target, trans_id)
832
def new_orphan(self, trans_id, parent_id):
833
"""Schedule an item to be orphaned.
835
When a directory is about to be removed, its children, if they are not
836
versioned are moved out of the way: they don't have a parent anymore.
838
:param trans_id: The trans_id of the existing item.
839
:param parent_id: The parent trans_id of the item.
841
raise NotImplementedError(self.new_orphan)
843
def _get_potential_orphans(self, dir_id):
844
"""Find the potential orphans in a directory.
846
A directory can't be safely deleted if there are versioned files in it.
847
If all the contained files are unversioned then they can be orphaned.
849
The 'None' return value means that the directory contains at least one
850
versioned file and should not be deleted.
852
:param dir_id: The directory trans id.
854
:return: A list of the orphan trans ids or None if at least one
855
versioned file is present.
858
# Find the potential orphans, stop if one item should be kept
859
for child_tid in self.by_parent()[dir_id]:
860
if child_tid in self._removed_contents:
861
# The child is removed as part of the transform. Since it was
862
# versioned before, it's not an orphan
864
elif self.final_file_id(child_tid) is None:
865
# The child is not versioned
866
orphans.append(child_tid)
868
# We have a versioned file here, searching for orphans is
874
def _affected_ids(self):
875
"""Return the set of transform ids affected by the transform"""
876
trans_ids = set(self._removed_id)
877
trans_ids.update(self._new_id)
878
trans_ids.update(self._removed_contents)
879
trans_ids.update(self._new_contents)
880
trans_ids.update(self._new_executability)
881
trans_ids.update(self._new_name)
882
trans_ids.update(self._new_parent)
885
def _get_file_id_maps(self):
886
"""Return mapping of file_ids to trans_ids in the to and from states"""
887
trans_ids = self._affected_ids()
890
# Build up two dicts: trans_ids associated with file ids in the
891
# FROM state, vs the TO state.
892
for trans_id in trans_ids:
893
from_file_id = self.tree_file_id(trans_id)
894
if from_file_id is not None:
895
from_trans_ids[from_file_id] = trans_id
896
to_file_id = self.final_file_id(trans_id)
897
if to_file_id is not None:
898
to_trans_ids[to_file_id] = trans_id
899
return from_trans_ids, to_trans_ids
901
def _from_file_data(self, from_trans_id, from_versioned, file_id):
902
"""Get data about a file in the from (tree) state
904
Return a (name, parent, kind, executable) tuple
906
from_path = self._tree_id_paths.get(from_trans_id)
908
# get data from working tree if versioned
909
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
910
from_name = from_entry.name
911
from_parent = from_entry.parent_id
914
if from_path is None:
915
# File does not exist in FROM state
919
# File exists, but is not versioned. Have to use path-
921
from_name = os.path.basename(from_path)
922
tree_parent = self.get_tree_parent(from_trans_id)
923
from_parent = self.tree_file_id(tree_parent)
924
if from_path is not None:
925
from_kind, from_executable, from_stats = \
926
self._tree._comparison_data(from_entry, from_path)
929
from_executable = False
930
return from_name, from_parent, from_kind, from_executable
932
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
933
"""Get data about a file in the to (target) state
935
Return a (name, parent, kind, executable) tuple
937
to_name = self.final_name(to_trans_id)
938
to_kind = self.final_kind(to_trans_id)
939
to_parent = self.final_file_id(self.final_parent(to_trans_id))
940
if to_trans_id in self._new_executability:
941
to_executable = self._new_executability[to_trans_id]
942
elif to_trans_id == from_trans_id:
943
to_executable = from_executable
945
to_executable = False
946
return to_name, to_parent, to_kind, to_executable
948
def iter_changes(self):
949
"""Produce output in the same format as Tree.iter_changes.
951
Will produce nonsensical results if invoked while inventory/filesystem
952
conflicts (as reported by TreeTransform.find_conflicts()) are present.
954
This reads the Transform, but only reproduces changes involving a
955
file_id. Files that are not versioned in either of the FROM or TO
956
states are not reflected.
958
final_paths = FinalPaths(self)
959
from_trans_ids, to_trans_ids = self._get_file_id_maps()
961
# Now iterate through all active file_ids
962
for file_id in set(from_trans_ids).union(to_trans_ids):
964
from_trans_id = from_trans_ids.get(file_id)
965
# find file ids, and determine versioning state
966
if from_trans_id is None:
967
from_versioned = False
968
from_trans_id = to_trans_ids[file_id]
970
from_versioned = True
971
to_trans_id = to_trans_ids.get(file_id)
972
if to_trans_id is None:
974
to_trans_id = from_trans_id
978
from_name, from_parent, from_kind, from_executable = \
979
self._from_file_data(from_trans_id, from_versioned, file_id)
981
to_name, to_parent, to_kind, to_executable = \
982
self._to_file_data(to_trans_id, from_trans_id, from_executable)
984
if not from_versioned:
987
from_path = self._tree_id_paths.get(from_trans_id)
991
to_path = final_paths.get_path(to_trans_id)
992
if from_kind != to_kind:
994
elif to_kind in ('file', 'symlink') and (
995
to_trans_id != from_trans_id or
996
to_trans_id in self._new_contents):
998
if (not modified and from_versioned == to_versioned and
999
from_parent==to_parent and from_name == to_name and
1000
from_executable == to_executable):
1002
results.append((file_id, (from_path, to_path), modified,
1003
(from_versioned, to_versioned),
1004
(from_parent, to_parent),
1005
(from_name, to_name),
1006
(from_kind, to_kind),
1007
(from_executable, to_executable)))
1008
return iter(sorted(results, key=lambda x:x[1]))
1010
def get_preview_tree(self):
1011
"""Return a tree representing the result of the transform.
1013
The tree is a snapshot, and altering the TreeTransform will invalidate
1016
return _PreviewTree(self)
1018
def commit(self, branch, message, merge_parents=None, strict=False,
1019
timestamp=None, timezone=None, committer=None, authors=None,
1020
revprops=None, revision_id=None):
1021
"""Commit the result of this TreeTransform to a branch.
1023
:param branch: The branch to commit to.
1024
:param message: The message to attach to the commit.
1025
:param merge_parents: Additional parent revision-ids specified by
1027
:param strict: If True, abort the commit if there are unversioned
1029
:param timestamp: if not None, seconds-since-epoch for the time and
1030
date. (May be a float.)
1031
:param timezone: Optional timezone for timestamp, as an offset in
1033
:param committer: Optional committer in email-id format.
1034
(e.g. "J Random Hacker <jrandom@example.com>")
1035
:param authors: Optional list of authors in email-id format.
1036
:param revprops: Optional dictionary of revision properties.
1037
:param revision_id: Optional revision id. (Specifying a revision-id
1038
may reduce performance for some non-native formats.)
1039
:return: The revision_id of the revision committed.
1041
self._check_malformed()
1043
unversioned = set(self._new_contents).difference(set(self._new_id))
1044
for trans_id in unversioned:
1045
if self.final_file_id(trans_id) is None:
1046
raise errors.StrictCommitFailed()
1048
revno, last_rev_id = branch.last_revision_info()
1049
if last_rev_id == _mod_revision.NULL_REVISION:
1050
if merge_parents is not None:
1051
raise ValueError('Cannot supply merge parents for first'
1055
parent_ids = [last_rev_id]
1056
if merge_parents is not None:
1057
parent_ids.extend(merge_parents)
1058
if self._tree.get_revision_id() != last_rev_id:
1059
raise ValueError('TreeTransform not based on branch basis: %s' %
1060
self._tree.get_revision_id())
1061
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1062
builder = branch.get_commit_builder(parent_ids,
1063
timestamp=timestamp,
1065
committer=committer,
1067
revision_id=revision_id)
1068
preview = self.get_preview_tree()
1069
list(builder.record_iter_changes(preview, last_rev_id,
1070
self.iter_changes()))
1071
builder.finish_inventory()
1072
revision_id = builder.commit(message)
1073
branch.set_last_revision_info(revno + 1, revision_id)
1076
def _text_parent(self, trans_id):
1077
file_id = self.tree_file_id(trans_id)
1079
if file_id is None or self._tree.kind(file_id) != 'file':
1081
except errors.NoSuchFile:
1085
def _get_parents_texts(self, trans_id):
1086
"""Get texts for compression parents of this file."""
1087
file_id = self._text_parent(trans_id)
1090
return (self._tree.get_file_text(file_id),)
1092
def _get_parents_lines(self, trans_id):
1093
"""Get lines for compression parents of this file."""
1094
file_id = self._text_parent(trans_id)
1097
return (self._tree.get_file_lines(file_id),)
1099
def serialize(self, serializer):
1100
"""Serialize this TreeTransform.
1102
:param serializer: A Serialiser like pack.ContainerSerializer.
1104
new_name = dict((k, v.encode('utf-8')) for k, v in
1105
viewitems(self._new_name))
1106
new_executability = dict((k, int(v)) for k, v in
1107
viewitems(self._new_executability))
1108
tree_path_ids = dict((k.encode('utf-8'), v)
1109
for k, v in viewitems(self._tree_path_ids))
1111
'_id_number': self._id_number,
1112
'_new_name': new_name,
1113
'_new_parent': self._new_parent,
1114
'_new_executability': new_executability,
1115
'_new_id': self._new_id,
1116
'_tree_path_ids': tree_path_ids,
1117
'_removed_id': list(self._removed_id),
1118
'_removed_contents': list(self._removed_contents),
1119
'_non_present_ids': self._non_present_ids,
1121
yield serializer.bytes_record(bencode.bencode(attribs),
1123
for trans_id, kind in viewitems(self._new_contents):
1125
lines = osutils.chunks_to_lines(
1126
self._read_file_chunks(trans_id))
1127
parents = self._get_parents_lines(trans_id)
1128
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1129
content = ''.join(mpdiff.to_patch())
1130
if kind == 'directory':
1132
if kind == 'symlink':
1133
content = self._read_symlink_target(trans_id)
1134
yield serializer.bytes_record(content, ((trans_id, kind),))
1136
def deserialize(self, records):
1137
"""Deserialize a stored TreeTransform.
1139
:param records: An iterable of (names, content) tuples, as per
1140
pack.ContainerPushParser.
1142
names, content = next(records)
1143
attribs = bencode.bdecode(content)
1144
self._id_number = attribs['_id_number']
1145
self._new_name = dict((k, v.decode('utf-8'))
1146
for k, v in viewitems(attribs['_new_name']))
1147
self._new_parent = attribs['_new_parent']
1148
self._new_executability = dict((k, bool(v))
1149
for k, v in viewitems(attribs['_new_executability']))
1150
self._new_id = attribs['_new_id']
1151
self._r_new_id = dict((v, k) for k, v in viewitems(self._new_id))
1152
self._tree_path_ids = {}
1153
self._tree_id_paths = {}
1154
for bytepath, trans_id in viewitems(attribs['_tree_path_ids']):
1155
path = bytepath.decode('utf-8')
1156
self._tree_path_ids[path] = trans_id
1157
self._tree_id_paths[trans_id] = path
1158
self._removed_id = set(attribs['_removed_id'])
1159
self._removed_contents = set(attribs['_removed_contents'])
1160
self._non_present_ids = attribs['_non_present_ids']
1161
for ((trans_id, kind),), content in records:
1163
mpdiff = multiparent.MultiParent.from_patch(content)
1164
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1165
self.create_file(lines, trans_id)
1166
if kind == 'directory':
1167
self.create_directory(trans_id)
1168
if kind == 'symlink':
1169
self.create_symlink(content.decode('utf-8'), trans_id)
1172
class DiskTreeTransform(TreeTransformBase):
1173
"""Tree transform storing its contents on disk."""
1175
def __init__(self, tree, limbodir, pb=None,
1176
case_sensitive=True):
1178
:param tree: The tree that will be transformed, but not necessarily
1180
:param limbodir: A directory where new files can be stored until
1181
they are installed in their proper places
1183
:param case_sensitive: If True, the target of the transform is
1184
case sensitive, not just case preserving.
1186
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1187
self._limbodir = limbodir
1188
self._deletiondir = None
1189
# A mapping of transform ids to their limbo filename
1190
self._limbo_files = {}
1191
self._possibly_stale_limbo_files = set()
1192
# A mapping of transform ids to a set of the transform ids of children
1193
# that their limbo directory has
1194
self._limbo_children = {}
1195
# Map transform ids to maps of child filename to child transform id
1196
self._limbo_children_names = {}
1197
# List of transform ids that need to be renamed from limbo into place
1198
self._needs_rename = set()
1199
self._creation_mtime = None
1202
"""Release the working tree lock, if held, clean up limbo dir.
1204
This is required if apply has not been invoked, but can be invoked
1207
if self._tree is None:
1210
limbo_paths = list(viewvalues(self._limbo_files))
1211
limbo_paths.extend(self._possibly_stale_limbo_files)
1212
limbo_paths.sort(reverse=True)
1213
for path in limbo_paths:
1216
except OSError as e:
1217
if e.errno != errno.ENOENT:
1219
# XXX: warn? perhaps we just got interrupted at an
1220
# inconvenient moment, but perhaps files are disappearing
1223
delete_any(self._limbodir)
1225
# We don't especially care *why* the dir is immortal.
1226
raise ImmortalLimbo(self._limbodir)
1228
if self._deletiondir is not None:
1229
delete_any(self._deletiondir)
1231
raise errors.ImmortalPendingDeletion(self._deletiondir)
1233
TreeTransformBase.finalize(self)
1235
def _limbo_supports_executable(self):
1236
"""Check if the limbo path supports the executable bit."""
1237
# FIXME: Check actual file system capabilities of limbodir
1238
return osutils.supports_executable()
1240
def _limbo_name(self, trans_id):
1241
"""Generate the limbo name of a file"""
1242
limbo_name = self._limbo_files.get(trans_id)
1243
if limbo_name is None:
1244
limbo_name = self._generate_limbo_path(trans_id)
1245
self._limbo_files[trans_id] = limbo_name
1248
def _generate_limbo_path(self, trans_id):
1249
"""Generate a limbo path using the trans_id as the relative path.
1251
This is suitable as a fallback, and when the transform should not be
1252
sensitive to the path encoding of the limbo directory.
1254
self._needs_rename.add(trans_id)
1255
return pathjoin(self._limbodir, trans_id)
1257
def adjust_path(self, name, parent, trans_id):
1258
previous_parent = self._new_parent.get(trans_id)
1259
previous_name = self._new_name.get(trans_id)
1260
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1261
if (trans_id in self._limbo_files and
1262
trans_id not in self._needs_rename):
1263
self._rename_in_limbo([trans_id])
1264
if previous_parent != parent:
1265
self._limbo_children[previous_parent].remove(trans_id)
1266
if previous_parent != parent or previous_name != name:
1267
del self._limbo_children_names[previous_parent][previous_name]
1269
def _rename_in_limbo(self, trans_ids):
1270
"""Fix limbo names so that the right final path is produced.
1272
This means we outsmarted ourselves-- we tried to avoid renaming
1273
these files later by creating them with their final names in their
1274
final parents. But now the previous name or parent is no longer
1275
suitable, so we have to rename them.
1277
Even for trans_ids that have no new contents, we must remove their
1278
entries from _limbo_files, because they are now stale.
1280
for trans_id in trans_ids:
1281
old_path = self._limbo_files[trans_id]
1282
self._possibly_stale_limbo_files.add(old_path)
1283
del self._limbo_files[trans_id]
1284
if trans_id not in self._new_contents:
1286
new_path = self._limbo_name(trans_id)
1287
os.rename(old_path, new_path)
1288
self._possibly_stale_limbo_files.remove(old_path)
1289
for descendant in self._limbo_descendants(trans_id):
1290
desc_path = self._limbo_files[descendant]
1291
desc_path = new_path + desc_path[len(old_path):]
1292
self._limbo_files[descendant] = desc_path
1294
def _limbo_descendants(self, trans_id):
1295
"""Return the set of trans_ids whose limbo paths descend from this."""
1296
descendants = set(self._limbo_children.get(trans_id, []))
1297
for descendant in list(descendants):
1298
descendants.update(self._limbo_descendants(descendant))
1301
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1302
"""Schedule creation of a new file.
1306
:param contents: an iterator of strings, all of which will be written
1307
to the target destination.
1308
:param trans_id: TreeTransform handle
1309
:param mode_id: If not None, force the mode of the target file to match
1310
the mode of the object referenced by mode_id.
1311
Otherwise, we will try to preserve mode bits of an existing file.
1312
:param sha1: If the sha1 of this content is already known, pass it in.
1313
We can use it to prevent future sha1 computations.
1315
name = self._limbo_name(trans_id)
1316
f = open(name, 'wb')
1318
unique_add(self._new_contents, trans_id, 'file')
1319
f.writelines(contents)
1322
self._set_mtime(name)
1323
self._set_mode(trans_id, mode_id, S_ISREG)
1324
# It is unfortunate we have to use lstat instead of fstat, but we just
1325
# used utime and chmod on the file, so we need the accurate final
1327
if sha1 is not None:
1328
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1330
def _read_file_chunks(self, trans_id):
1331
cur_file = open(self._limbo_name(trans_id), 'rb')
1333
return cur_file.readlines()
1337
def _read_symlink_target(self, trans_id):
1338
return os.readlink(self._limbo_name(trans_id))
1340
def _set_mtime(self, path):
1341
"""All files that are created get the same mtime.
1343
This time is set by the first object to be created.
1345
if self._creation_mtime is None:
1346
self._creation_mtime = time.time()
1347
os.utime(path, (self._creation_mtime, self._creation_mtime))
1349
def create_hardlink(self, path, trans_id):
1350
"""Schedule creation of a hard link"""
1351
name = self._limbo_name(trans_id)
1354
except OSError as e:
1355
if e.errno != errno.EPERM:
1357
raise errors.HardLinkNotSupported(path)
1359
unique_add(self._new_contents, trans_id, 'file')
1361
# Clean up the file, it never got registered so
1362
# TreeTransform.finalize() won't clean it up.
1366
def create_directory(self, trans_id):
1367
"""Schedule creation of a new directory.
1369
See also new_directory.
1371
os.mkdir(self._limbo_name(trans_id))
1372
unique_add(self._new_contents, trans_id, 'directory')
1374
def create_symlink(self, target, trans_id):
1375
"""Schedule creation of a new symbolic link.
1377
target is a bytestring.
1378
See also new_symlink.
1381
os.symlink(target, self._limbo_name(trans_id))
1382
unique_add(self._new_contents, trans_id, 'symlink')
1385
path = FinalPaths(self).get_path(trans_id)
1388
raise UnableCreateSymlink(path=path)
1390
def cancel_creation(self, trans_id):
1391
"""Cancel the creation of new file contents."""
1392
del self._new_contents[trans_id]
1393
if trans_id in self._observed_sha1s:
1394
del self._observed_sha1s[trans_id]
1395
children = self._limbo_children.get(trans_id)
1396
# if this is a limbo directory with children, move them before removing
1398
if children is not None:
1399
self._rename_in_limbo(children)
1400
del self._limbo_children[trans_id]
1401
del self._limbo_children_names[trans_id]
1402
delete_any(self._limbo_name(trans_id))
1404
def new_orphan(self, trans_id, parent_id):
1405
conf = self._tree.get_config_stack()
1406
handle_orphan = conf.get('bzr.transform.orphan_policy')
1407
handle_orphan(self, trans_id, parent_id)
1410
class OrphaningError(errors.BzrError):
1412
# Only bugs could lead to such exception being seen by the user
1413
internal_error = True
1414
_fmt = "Error while orphaning %s in %s directory"
1416
def __init__(self, orphan, parent):
1417
errors.BzrError.__init__(self)
1418
self.orphan = orphan
1419
self.parent = parent
1422
class OrphaningForbidden(OrphaningError):
1424
_fmt = "Policy: %s doesn't allow creating orphans."
1426
def __init__(self, policy):
1427
errors.BzrError.__init__(self)
1428
self.policy = policy
1431
def move_orphan(tt, orphan_id, parent_id):
1432
"""See TreeTransformBase.new_orphan.
1434
This creates a new orphan in the `bzr-orphans` dir at the root of the
1437
:param tt: The TreeTransform orphaning `trans_id`.
1439
:param orphan_id: The trans id that should be orphaned.
1441
:param parent_id: The orphan parent trans id.
1443
# Add the orphan dir if it doesn't exist
1444
orphan_dir_basename = 'bzr-orphans'
1445
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1446
if tt.final_kind(od_id) is None:
1447
tt.create_directory(od_id)
1448
parent_path = tt._tree_id_paths[parent_id]
1449
# Find a name that doesn't exist yet in the orphan dir
1450
actual_name = tt.final_name(orphan_id)
1451
new_name = tt._available_backup_name(actual_name, od_id)
1452
tt.adjust_path(new_name, od_id, orphan_id)
1453
trace.warning('%s has been orphaned in %s'
1454
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1457
def refuse_orphan(tt, orphan_id, parent_id):
1458
"""See TreeTransformBase.new_orphan.
1460
This refuses to create orphan, letting the caller handle the conflict.
1462
raise OrphaningForbidden('never')
1465
orphaning_registry = registry.Registry()
1466
orphaning_registry.register(
1467
'conflict', refuse_orphan,
1468
'Leave orphans in place and create a conflict on the directory.')
1469
orphaning_registry.register(
1470
'move', move_orphan,
1471
'Move orphans into the bzr-orphans directory.')
1472
orphaning_registry._set_default_key('conflict')
1475
opt_transform_orphan = _mod_config.RegistryOption(
1476
'bzr.transform.orphan_policy', orphaning_registry,
1477
help='Policy for orphaned files during transform operations.',
1481
class TreeTransform(DiskTreeTransform):
1482
"""Represent a tree transformation.
1484
This object is designed to support incremental generation of the transform,
1487
However, it gives optimum performance when parent directories are created
1488
before their contents. The transform is then able to put child files
1489
directly in their parent directory, avoiding later renames.
1491
It is easy to produce malformed transforms, but they are generally
1492
harmless. Attempting to apply a malformed transform will cause an
1493
exception to be raised before any modifications are made to the tree.
1495
Many kinds of malformed transforms can be corrected with the
1496
resolve_conflicts function. The remaining ones indicate programming error,
1497
such as trying to create a file with no path.
1499
Two sets of file creation methods are supplied. Convenience methods are:
1504
These are composed of the low-level methods:
1506
* create_file or create_directory or create_symlink
1510
Transform/Transaction ids
1511
-------------------------
1512
trans_ids are temporary ids assigned to all files involved in a transform.
1513
It's possible, even common, that not all files in the Tree have trans_ids.
1515
trans_ids are used because filenames and file_ids are not good enough
1516
identifiers; filenames change, and not all files have file_ids. File-ids
1517
are also associated with trans-ids, so that moving a file moves its
1520
trans_ids are only valid for the TreeTransform that generated them.
1524
Limbo is a temporary directory use to hold new versions of files.
1525
Files are added to limbo by create_file, create_directory, create_symlink,
1526
and their convenience variants (new_*). Files may be removed from limbo
1527
using cancel_creation. Files are renamed from limbo into their final
1528
location as part of TreeTransform.apply
1530
Limbo must be cleaned up, by either calling TreeTransform.apply or
1531
calling TreeTransform.finalize.
1533
Files are placed into limbo inside their parent directories, where
1534
possible. This reduces subsequent renames, and makes operations involving
1535
lots of files faster. This optimization is only possible if the parent
1536
directory is created *before* creating any of its children, so avoid
1537
creating children before parents, where possible.
1541
This temporary directory is used by _FileMover for storing files that are
1542
about to be deleted. In case of rollback, the files will be restored.
1543
FileMover does not delete files until it is sure that a rollback will not
1546
def __init__(self, tree, pb=None):
1547
"""Note: a tree_write lock is taken on the tree.
1549
Use TreeTransform.finalize() to release the lock (can be omitted if
1550
TreeTransform.apply() called).
1552
tree.lock_tree_write()
1555
limbodir = urlutils.local_path_from_url(
1556
tree._transport.abspath('limbo'))
1557
osutils.ensure_empty_directory_exists(
1559
errors.ExistingLimbo)
1560
deletiondir = urlutils.local_path_from_url(
1561
tree._transport.abspath('pending-deletion'))
1562
osutils.ensure_empty_directory_exists(
1564
errors.ExistingPendingDeletion)
1569
# Cache of realpath results, to speed up canonical_path
1570
self._realpaths = {}
1571
# Cache of relpath results, to speed up canonical_path
1573
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1574
tree.case_sensitive)
1575
self._deletiondir = deletiondir
1577
def canonical_path(self, path):
1578
"""Get the canonical tree-relative path"""
1579
# don't follow final symlinks
1580
abs = self._tree.abspath(path)
1581
if abs in self._relpaths:
1582
return self._relpaths[abs]
1583
dirname, basename = os.path.split(abs)
1584
if dirname not in self._realpaths:
1585
self._realpaths[dirname] = os.path.realpath(dirname)
1586
dirname = self._realpaths[dirname]
1587
abs = pathjoin(dirname, basename)
1588
if dirname in self._relpaths:
1589
relpath = pathjoin(self._relpaths[dirname], basename)
1590
relpath = relpath.rstrip('/\\')
1592
relpath = self._tree.relpath(abs)
1593
self._relpaths[abs] = relpath
1596
def tree_kind(self, trans_id):
1597
"""Determine the file kind in the working tree.
1599
:returns: The file kind or None if the file does not exist
1601
path = self._tree_id_paths.get(trans_id)
1605
return file_kind(self._tree.abspath(path))
1606
except errors.NoSuchFile:
1609
def _set_mode(self, trans_id, mode_id, typefunc):
1610
"""Set the mode of new file contents.
1611
The mode_id is the existing file to get the mode from (often the same
1612
as trans_id). The operation is only performed if there's a mode match
1613
according to typefunc.
1618
old_path = self._tree_id_paths[mode_id]
1622
mode = os.stat(self._tree.abspath(old_path)).st_mode
1623
except OSError as e:
1624
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1625
# Either old_path doesn't exist, or the parent of the
1626
# target is not a directory (but will be one eventually)
1627
# Either way, we know it doesn't exist *right now*
1628
# See also bug #248448
1633
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1635
def iter_tree_children(self, parent_id):
1636
"""Iterate through the entry's tree children, if any"""
1638
path = self._tree_id_paths[parent_id]
1642
children = os.listdir(self._tree.abspath(path))
1643
except OSError as e:
1644
if not (osutils._is_error_enotdir(e)
1645
or e.errno in (errno.ENOENT, errno.ESRCH)):
1649
for child in children:
1650
childpath = joinpath(path, child)
1651
if self._tree.is_control_filename(childpath):
1653
yield self.trans_id_tree_path(childpath)
1655
def _generate_limbo_path(self, trans_id):
1656
"""Generate a limbo path using the final path if possible.
1658
This optimizes the performance of applying the tree transform by
1659
avoiding renames. These renames can be avoided only when the parent
1660
directory is already scheduled for creation.
1662
If the final path cannot be used, falls back to using the trans_id as
1665
parent = self._new_parent.get(trans_id)
1666
# if the parent directory is already in limbo (e.g. when building a
1667
# tree), choose a limbo name inside the parent, to reduce further
1669
use_direct_path = False
1670
if self._new_contents.get(parent) == 'directory':
1671
filename = self._new_name.get(trans_id)
1672
if filename is not None:
1673
if parent not in self._limbo_children:
1674
self._limbo_children[parent] = set()
1675
self._limbo_children_names[parent] = {}
1676
use_direct_path = True
1677
# the direct path can only be used if no other file has
1678
# already taken this pathname, i.e. if the name is unused, or
1679
# if it is already associated with this trans_id.
1680
elif self._case_sensitive_target:
1681
if (self._limbo_children_names[parent].get(filename)
1682
in (trans_id, None)):
1683
use_direct_path = True
1685
for l_filename, l_trans_id in viewitems(
1686
self._limbo_children_names[parent]):
1687
if l_trans_id == trans_id:
1689
if l_filename.lower() == filename.lower():
1692
use_direct_path = True
1694
if not use_direct_path:
1695
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1697
limbo_name = pathjoin(self._limbo_files[parent], filename)
1698
self._limbo_children[parent].add(trans_id)
1699
self._limbo_children_names[parent][filename] = trans_id
1703
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1704
"""Apply all changes to the inventory and filesystem.
1706
If filesystem or inventory conflicts are present, MalformedTransform
1709
If apply succeeds, finalize is not necessary.
1711
:param no_conflicts: if True, the caller guarantees there are no
1712
conflicts, so no check is made.
1713
:param precomputed_delta: An inventory delta to use instead of
1715
:param _mover: Supply an alternate FileMover, for testing
1717
for hook in MutableTree.hooks['pre_transform']:
1718
hook(self._tree, self)
1719
if not no_conflicts:
1720
self._check_malformed()
1721
child_pb = ui.ui_factory.nested_progress_bar()
1723
if precomputed_delta is None:
1724
child_pb.update(gettext('Apply phase'), 0, 2)
1725
inventory_delta = self._generate_inventory_delta()
1728
inventory_delta = precomputed_delta
1731
mover = _FileMover()
1735
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1736
self._apply_removals(mover)
1737
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1738
modified_paths = self._apply_insertions(mover)
1743
mover.apply_deletions()
1746
if self.final_file_id(self.root) is None:
1747
inventory_delta = [e for e in inventory_delta if e[0] != '']
1748
self._tree.apply_inventory_delta(inventory_delta)
1749
self._apply_observed_sha1s()
1752
return _TransformResults(modified_paths, self.rename_count)
1754
def _generate_inventory_delta(self):
1755
"""Generate an inventory delta for the current transform."""
1756
inventory_delta = []
1757
child_pb = ui.ui_factory.nested_progress_bar()
1758
new_paths = self._inventory_altered()
1759
total_entries = len(new_paths) + len(self._removed_id)
1761
for num, trans_id in enumerate(self._removed_id):
1763
child_pb.update(gettext('removing file'), num, total_entries)
1764
if trans_id == self._new_root:
1765
file_id = self._tree.get_root_id()
1767
file_id = self.tree_file_id(trans_id)
1768
# File-id isn't really being deleted, just moved
1769
if file_id in self._r_new_id:
1771
path = self._tree_id_paths[trans_id]
1772
inventory_delta.append((path, None, file_id, None))
1773
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1775
entries = self._tree.iter_entries_by_dir(
1776
viewvalues(new_path_file_ids))
1777
old_paths = dict((e.file_id, p) for p, e in entries)
1779
for num, (path, trans_id) in enumerate(new_paths):
1781
child_pb.update(gettext('adding file'),
1782
num + len(self._removed_id), total_entries)
1783
file_id = new_path_file_ids[trans_id]
1787
kind = self.final_kind(trans_id)
1789
kind = self._tree.stored_kind(file_id)
1790
parent_trans_id = self.final_parent(trans_id)
1791
parent_file_id = new_path_file_ids.get(parent_trans_id)
1792
if parent_file_id is None:
1793
parent_file_id = self.final_file_id(parent_trans_id)
1794
if trans_id in self._new_reference_revision:
1795
new_entry = inventory.TreeReference(
1797
self._new_name[trans_id],
1798
self.final_file_id(self._new_parent[trans_id]),
1799
None, self._new_reference_revision[trans_id])
1801
new_entry = inventory.make_entry(kind,
1802
self.final_name(trans_id),
1803
parent_file_id, file_id)
1804
old_path = old_paths.get(new_entry.file_id)
1805
new_executability = self._new_executability.get(trans_id)
1806
if new_executability is not None:
1807
new_entry.executable = new_executability
1808
inventory_delta.append(
1809
(old_path, path, new_entry.file_id, new_entry))
1812
return inventory_delta
1814
def _apply_removals(self, mover):
1815
"""Perform tree operations that remove directory/inventory names.
1817
That is, delete files that are to be deleted, and put any files that
1818
need renaming into limbo. This must be done in strict child-to-parent
1821
If inventory_delta is None, no inventory delta generation is performed.
1823
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1824
child_pb = ui.ui_factory.nested_progress_bar()
1826
for num, (path, trans_id) in enumerate(tree_paths):
1827
# do not attempt to move root into a subdirectory of itself.
1830
child_pb.update(gettext('removing file'), num, len(tree_paths))
1831
full_path = self._tree.abspath(path)
1832
if trans_id in self._removed_contents:
1833
delete_path = os.path.join(self._deletiondir, trans_id)
1834
mover.pre_delete(full_path, delete_path)
1835
elif (trans_id in self._new_name
1836
or trans_id in self._new_parent):
1838
mover.rename(full_path, self._limbo_name(trans_id))
1839
except errors.TransformRenameFailed as e:
1840
if e.errno != errno.ENOENT:
1843
self.rename_count += 1
1847
def _apply_insertions(self, mover):
1848
"""Perform tree operations that insert directory/inventory names.
1850
That is, create any files that need to be created, and restore from
1851
limbo any files that needed renaming. This must be done in strict
1852
parent-to-child order.
1854
If inventory_delta is None, no inventory delta is calculated, and
1855
no list of modified paths is returned.
1857
new_paths = self.new_paths(filesystem_only=True)
1859
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1861
child_pb = ui.ui_factory.nested_progress_bar()
1863
for num, (path, trans_id) in enumerate(new_paths):
1865
child_pb.update(gettext('adding file'), num, len(new_paths))
1866
full_path = self._tree.abspath(path)
1867
if trans_id in self._needs_rename:
1869
mover.rename(self._limbo_name(trans_id), full_path)
1870
except errors.TransformRenameFailed as e:
1871
# We may be renaming a dangling inventory id
1872
if e.errno != errno.ENOENT:
1875
self.rename_count += 1
1876
# TODO: if trans_id in self._observed_sha1s, we should
1877
# re-stat the final target, since ctime will be
1878
# updated by the change.
1879
if (trans_id in self._new_contents or
1880
self.path_changed(trans_id)):
1881
if trans_id in self._new_contents:
1882
modified_paths.append(full_path)
1883
if trans_id in self._new_executability:
1884
self._set_executability(path, trans_id)
1885
if trans_id in self._observed_sha1s:
1886
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1887
st = osutils.lstat(full_path)
1888
self._observed_sha1s[trans_id] = (o_sha1, st)
1891
for path, trans_id in new_paths:
1892
# new_paths includes stuff like workingtree conflicts. Only the
1893
# stuff in new_contents actually comes from limbo.
1894
if trans_id in self._limbo_files:
1895
del self._limbo_files[trans_id]
1896
self._new_contents.clear()
1897
return modified_paths
1899
def _apply_observed_sha1s(self):
1900
"""After we have finished renaming everything, update observed sha1s
1902
This has to be done after self._tree.apply_inventory_delta, otherwise
1903
it doesn't know anything about the files we are updating. Also, we want
1904
to do this as late as possible, so that most entries end up cached.
1906
# TODO: this doesn't update the stat information for directories. So
1907
# the first 'bzr status' will still need to rewrite
1908
# .bzr/checkout/dirstate. However, we at least don't need to
1909
# re-read all of the files.
1910
# TODO: If the operation took a while, we could do a time.sleep(3) here
1911
# to allow the clock to tick over and ensure we won't have any
1912
# problems. (we could observe start time, and finish time, and if
1913
# it is less than eg 10% overhead, add a sleep call.)
1914
paths = FinalPaths(self)
1915
for trans_id, observed in viewitems(self._observed_sha1s):
1916
path = paths.get_path(trans_id)
1917
# We could get the file_id, but dirstate prefers to use the path
1918
# anyway, and it is 'cheaper' to determine.
1919
# file_id = self._new_id[trans_id]
1920
self._tree._observed_sha1(None, path, observed)
1923
class TransformPreview(DiskTreeTransform):
1924
"""A TreeTransform for generating preview trees.
1926
Unlike TreeTransform, this version works when the input tree is a
1927
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1928
unversioned files in the input tree.
1931
def __init__(self, tree, pb=None, case_sensitive=True):
1933
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1934
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1936
def canonical_path(self, path):
1939
def tree_kind(self, trans_id):
1940
path = self._tree_id_paths.get(trans_id)
1943
kind = self._tree.path_content_summary(path)[0]
1944
if kind == 'missing':
1948
def _set_mode(self, trans_id, mode_id, typefunc):
1949
"""Set the mode of new file contents.
1950
The mode_id is the existing file to get the mode from (often the same
1951
as trans_id). The operation is only performed if there's a mode match
1952
according to typefunc.
1954
# is it ok to ignore this? probably
1957
def iter_tree_children(self, parent_id):
1958
"""Iterate through the entry's tree children, if any"""
1960
path = self._tree_id_paths[parent_id]
1963
file_id = self.tree_file_id(parent_id)
1966
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1967
children = getattr(entry, 'children', {})
1968
for child in children:
1969
childpath = joinpath(path, child)
1970
yield self.trans_id_tree_path(childpath)
1972
def new_orphan(self, trans_id, parent_id):
1973
raise NotImplementedError(self.new_orphan)
1976
class _PreviewTree(tree.InventoryTree):
1977
"""Partial implementation of Tree to support show_diff_trees"""
1979
def __init__(self, transform):
1980
self._transform = transform
1981
self._final_paths = FinalPaths(transform)
1982
self.__by_parent = None
1983
self._parent_ids = []
1984
self._all_children_cache = {}
1985
self._path2trans_id_cache = {}
1986
self._final_name_cache = {}
1987
self._iter_changes_cache = dict((c[0], c) for c in
1988
self._transform.iter_changes())
1990
def _content_change(self, file_id):
1991
"""Return True if the content of this file changed"""
1992
changes = self._iter_changes_cache.get(file_id)
1993
# changes[2] is true if the file content changed. See
1994
# InterTree.iter_changes.
1995
return (changes is not None and changes[2])
1997
def _get_repository(self):
1998
repo = getattr(self._transform._tree, '_repository', None)
2000
repo = self._transform._tree.branch.repository
2003
def _iter_parent_trees(self):
2004
for revision_id in self.get_parent_ids():
2006
yield self.revision_tree(revision_id)
2007
except errors.NoSuchRevisionInTree:
2008
yield self._get_repository().revision_tree(revision_id)
2010
def _get_file_revision(self, file_id, vf, tree_revision):
2011
parent_keys = [(file_id, t.get_file_revision(file_id)) for t in
2012
self._iter_parent_trees()]
2013
vf.add_lines((file_id, tree_revision), parent_keys,
2014
self.get_file_lines(file_id))
2015
repo = self._get_repository()
2016
base_vf = repo.texts
2017
if base_vf not in vf.fallback_versionedfiles:
2018
vf.fallback_versionedfiles.append(base_vf)
2019
return tree_revision
2021
def _stat_limbo_file(self, file_id=None, trans_id=None):
2022
if trans_id is None:
2023
trans_id = self._transform.trans_id_file_id(file_id)
2024
name = self._transform._limbo_name(trans_id)
2025
return os.lstat(name)
2028
def _by_parent(self):
2029
if self.__by_parent is None:
2030
self.__by_parent = self._transform.by_parent()
2031
return self.__by_parent
2033
def _comparison_data(self, entry, path):
2034
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2035
if kind == 'missing':
2039
file_id = self._transform.final_file_id(self._path2trans_id(path))
2040
executable = self.is_executable(file_id, path)
2041
return kind, executable, None
2043
def is_locked(self):
2046
def lock_read(self):
2047
# Perhaps in theory, this should lock the TreeTransform?
2054
def root_inventory(self):
2055
"""This Tree does not use inventory as its backing data."""
2056
raise NotImplementedError(_PreviewTree.root_inventory)
2058
def get_root_id(self):
2059
return self._transform.final_file_id(self._transform.root)
2061
def all_file_ids(self):
2062
tree_ids = set(self._transform._tree.all_file_ids())
2063
tree_ids.difference_update(self._transform.tree_file_id(t)
2064
for t in self._transform._removed_id)
2065
tree_ids.update(viewvalues(self._transform._new_id))
2069
return iter(self.all_file_ids())
2071
def _has_id(self, file_id, fallback_check):
2072
if file_id in self._transform._r_new_id:
2074
elif file_id in {self._transform.tree_file_id(trans_id) for
2075
trans_id in self._transform._removed_id}:
2078
return fallback_check(file_id)
2080
def has_id(self, file_id):
2081
return self._has_id(file_id, self._transform._tree.has_id)
2083
def has_or_had_id(self, file_id):
2084
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2086
def _path2trans_id(self, path):
2087
# We must not use None here, because that is a valid value to store.
2088
trans_id = self._path2trans_id_cache.get(path, object)
2089
if trans_id is not object:
2091
segments = splitpath(path)
2092
cur_parent = self._transform.root
2093
for cur_segment in segments:
2094
for child in self._all_children(cur_parent):
2095
final_name = self._final_name_cache.get(child)
2096
if final_name is None:
2097
final_name = self._transform.final_name(child)
2098
self._final_name_cache[child] = final_name
2099
if final_name == cur_segment:
2103
self._path2trans_id_cache[path] = None
2105
self._path2trans_id_cache[path] = cur_parent
2108
def path2id(self, path):
2109
if isinstance(path, list):
2112
path = osutils.pathjoin(*path)
2113
return self._transform.final_file_id(self._path2trans_id(path))
2115
def id2path(self, file_id):
2116
trans_id = self._transform.trans_id_file_id(file_id)
2118
return self._final_paths._determine_path(trans_id)
2120
raise errors.NoSuchId(self, file_id)
2122
def _all_children(self, trans_id):
2123
children = self._all_children_cache.get(trans_id)
2124
if children is not None:
2126
children = set(self._transform.iter_tree_children(trans_id))
2127
# children in the _new_parent set are provided by _by_parent.
2128
children.difference_update(self._transform._new_parent)
2129
children.update(self._by_parent.get(trans_id, []))
2130
self._all_children_cache[trans_id] = children
2133
def iter_children(self, file_id):
2134
trans_id = self._transform.trans_id_file_id(file_id)
2135
for child_trans_id in self._all_children(trans_id):
2136
yield self._transform.final_file_id(child_trans_id)
2139
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2140
in self._transform._tree.extras())
2141
possible_extras.update(self._transform._new_contents)
2142
possible_extras.update(self._transform._removed_id)
2143
for trans_id in possible_extras:
2144
if self._transform.final_file_id(trans_id) is None:
2145
yield self._final_paths._determine_path(trans_id)
2147
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
2148
yield_parents=False):
2149
for trans_id, parent_file_id in ordered_entries:
2150
file_id = self._transform.final_file_id(trans_id)
2153
if (specific_file_ids is not None
2154
and file_id not in specific_file_ids):
2156
kind = self._transform.final_kind(trans_id)
2158
kind = self._transform._tree.stored_kind(file_id)
2159
new_entry = inventory.make_entry(
2161
self._transform.final_name(trans_id),
2162
parent_file_id, file_id)
2163
yield new_entry, trans_id
2165
def _list_files_by_dir(self):
2166
todo = [ROOT_PARENT]
2168
while len(todo) > 0:
2170
parent_file_id = self._transform.final_file_id(parent)
2171
children = list(self._all_children(parent))
2172
paths = dict(zip(children, self._final_paths.get_paths(children)))
2173
children.sort(key=paths.get)
2174
todo.extend(reversed(children))
2175
for trans_id in children:
2176
ordered_ids.append((trans_id, parent_file_id))
2179
def iter_child_entries(self, file_id, path=None):
2180
self.id2path(file_id)
2181
trans_id = self._transform.trans_id_file_id(file_id)
2182
todo = [(child_trans_id, trans_id) for child_trans_id in
2183
self._all_children(trans_id)]
2184
for entry, trans_id in self._make_inv_entries(todo):
2187
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
2188
# This may not be a maximally efficient implementation, but it is
2189
# reasonably straightforward. An implementation that grafts the
2190
# TreeTransform changes onto the tree's iter_entries_by_dir results
2191
# might be more efficient, but requires tricky inferences about stack
2193
ordered_ids = self._list_files_by_dir()
2194
for entry, trans_id in self._make_inv_entries(ordered_ids,
2195
specific_file_ids, yield_parents=yield_parents):
2196
yield unicode(self._final_paths.get_path(trans_id)), entry
2198
def _iter_entries_for_dir(self, dir_path):
2199
"""Return path, entry for items in a directory without recursing down."""
2200
dir_file_id = self.path2id(dir_path)
2202
for file_id in self.iter_children(dir_file_id):
2203
trans_id = self._transform.trans_id_file_id(file_id)
2204
ordered_ids.append((trans_id, file_id))
2205
for entry, trans_id in self._make_inv_entries(ordered_ids):
2206
yield unicode(self._final_paths.get_path(trans_id)), entry
2208
def list_files(self, include_root=False, from_dir=None, recursive=True):
2209
"""See WorkingTree.list_files."""
2210
# XXX This should behave like WorkingTree.list_files, but is really
2211
# more like RevisionTree.list_files.
2215
prefix = from_dir + '/'
2216
entries = self.iter_entries_by_dir()
2217
for path, entry in entries:
2218
if entry.name == '' and not include_root:
2221
if not path.startswith(prefix):
2223
path = path[len(prefix):]
2224
yield path, 'V', entry.kind, entry.file_id, entry
2226
if from_dir is None and include_root is True:
2227
root_entry = inventory.make_entry('directory', '',
2228
ROOT_PARENT, self.get_root_id())
2229
yield '', 'V', 'directory', root_entry.file_id, root_entry
2230
entries = self._iter_entries_for_dir(from_dir or '')
2231
for path, entry in entries:
2232
yield path, 'V', entry.kind, entry.file_id, entry
2234
def kind(self, file_id):
2235
trans_id = self._transform.trans_id_file_id(file_id)
2236
return self._transform.final_kind(trans_id)
2238
def stored_kind(self, file_id):
2239
trans_id = self._transform.trans_id_file_id(file_id)
2241
return self._transform._new_contents[trans_id]
2243
return self._transform._tree.stored_kind(file_id)
2245
def get_file_mtime(self, file_id, path=None):
2246
"""See Tree.get_file_mtime"""
2247
if not self._content_change(file_id):
2248
return self._transform._tree.get_file_mtime(file_id)
2249
return self._stat_limbo_file(file_id).st_mtime
2251
def _file_size(self, entry, stat_value):
2252
return self.get_file_size(entry.file_id)
2254
def get_file_size(self, file_id):
2255
"""See Tree.get_file_size"""
2256
trans_id = self._transform.trans_id_file_id(file_id)
2257
kind = self._transform.final_kind(trans_id)
2260
if trans_id in self._transform._new_contents:
2261
return self._stat_limbo_file(trans_id=trans_id).st_size
2262
if self.kind(file_id) == 'file':
2263
return self._transform._tree.get_file_size(file_id)
2267
def get_file_verifier(self, file_id, path=None, stat_value=None):
2268
trans_id = self._transform.trans_id_file_id(file_id)
2269
kind = self._transform._new_contents.get(trans_id)
2271
return self._transform._tree.get_file_verifier(file_id)
2273
fileobj = self.get_file(file_id)
2275
return ("SHA1", sha_file(fileobj))
2279
def get_file_sha1(self, file_id, path=None, stat_value=None):
2280
trans_id = self._transform.trans_id_file_id(file_id)
2281
kind = self._transform._new_contents.get(trans_id)
2283
return self._transform._tree.get_file_sha1(file_id)
2285
fileobj = self.get_file(file_id)
2287
return sha_file(fileobj)
2291
def is_executable(self, file_id, path=None):
2294
trans_id = self._transform.trans_id_file_id(file_id)
2296
return self._transform._new_executability[trans_id]
2299
return self._transform._tree.is_executable(file_id, path)
2300
except OSError as e:
2301
if e.errno == errno.ENOENT:
2304
except errors.NoSuchId:
2307
def has_filename(self, path):
2308
trans_id = self._path2trans_id(path)
2309
if trans_id in self._transform._new_contents:
2311
elif trans_id in self._transform._removed_contents:
2314
return self._transform._tree.has_filename(path)
2316
def path_content_summary(self, path):
2317
trans_id = self._path2trans_id(path)
2318
tt = self._transform
2319
tree_path = tt._tree_id_paths.get(trans_id)
2320
kind = tt._new_contents.get(trans_id)
2322
if tree_path is None or trans_id in tt._removed_contents:
2323
return 'missing', None, None, None
2324
summary = tt._tree.path_content_summary(tree_path)
2325
kind, size, executable, link_or_sha1 = summary
2328
limbo_name = tt._limbo_name(trans_id)
2329
if trans_id in tt._new_reference_revision:
2330
kind = 'tree-reference'
2332
statval = os.lstat(limbo_name)
2333
size = statval.st_size
2334
if not tt._limbo_supports_executable():
2337
executable = statval.st_mode & S_IEXEC
2341
if kind == 'symlink':
2342
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2343
executable = tt._new_executability.get(trans_id, executable)
2344
return kind, size, executable, link_or_sha1
2346
def iter_changes(self, from_tree, include_unchanged=False,
2347
specific_files=None, pb=None, extra_trees=None,
2348
require_versioned=True, want_unversioned=False):
2349
"""See InterTree.iter_changes.
2351
This has a fast path that is only used when the from_tree matches
2352
the transform tree, and no fancy options are supplied.
2354
if (from_tree is not self._transform._tree or include_unchanged or
2355
specific_files or want_unversioned):
2356
return tree.InterTree(from_tree, self).iter_changes(
2357
include_unchanged=include_unchanged,
2358
specific_files=specific_files,
2360
extra_trees=extra_trees,
2361
require_versioned=require_versioned,
2362
want_unversioned=want_unversioned)
2363
if want_unversioned:
2364
raise ValueError('want_unversioned is not supported')
2365
return self._transform.iter_changes()
2367
def get_file(self, file_id, path=None):
2368
"""See Tree.get_file"""
2369
if not self._content_change(file_id):
2370
return self._transform._tree.get_file(file_id, path)
2371
trans_id = self._transform.trans_id_file_id(file_id)
2372
name = self._transform._limbo_name(trans_id)
2373
return open(name, 'rb')
2375
def get_file_with_stat(self, file_id, path=None):
2376
return self.get_file(file_id, path), None
2378
def annotate_iter(self, file_id,
2379
default_revision=_mod_revision.CURRENT_REVISION):
2380
changes = self._iter_changes_cache.get(file_id)
2384
changed_content, versioned, kind = (changes[2], changes[3],
2388
get_old = (kind[0] == 'file' and versioned[0])
2390
old_annotation = self._transform._tree.annotate_iter(file_id,
2391
default_revision=default_revision)
2395
return old_annotation
2396
if not changed_content:
2397
return old_annotation
2398
# TODO: This is doing something similar to what WT.annotate_iter is
2399
# doing, however it fails slightly because it doesn't know what
2400
# the *other* revision_id is, so it doesn't know how to give the
2401
# other as the origin for some lines, they all get
2402
# 'default_revision'
2403
# It would be nice to be able to use the new Annotator based
2404
# approach, as well.
2405
return annotate.reannotate([old_annotation],
2406
self.get_file(file_id).readlines(),
2409
def get_symlink_target(self, file_id, path=None):
2410
"""See Tree.get_symlink_target"""
2411
if not self._content_change(file_id):
2412
return self._transform._tree.get_symlink_target(file_id)
2413
trans_id = self._transform.trans_id_file_id(file_id)
2414
name = self._transform._limbo_name(trans_id)
2415
return osutils.readlink(name)
2417
def walkdirs(self, prefix=''):
2418
pending = [self._transform.root]
2419
while len(pending) > 0:
2420
parent_id = pending.pop()
2423
prefix = prefix.rstrip('/')
2424
parent_path = self._final_paths.get_path(parent_id)
2425
parent_file_id = self._transform.final_file_id(parent_id)
2426
for child_id in self._all_children(parent_id):
2427
path_from_root = self._final_paths.get_path(child_id)
2428
basename = self._transform.final_name(child_id)
2429
file_id = self._transform.final_file_id(child_id)
2430
kind = self._transform.final_kind(child_id)
2431
if kind is not None:
2432
versioned_kind = kind
2435
versioned_kind = self._transform._tree.stored_kind(file_id)
2436
if versioned_kind == 'directory':
2437
subdirs.append(child_id)
2438
children.append((path_from_root, basename, kind, None,
2439
file_id, versioned_kind))
2441
if parent_path.startswith(prefix):
2442
yield (parent_path, parent_file_id), children
2443
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2446
def get_parent_ids(self):
2447
return self._parent_ids
2449
def set_parent_ids(self, parent_ids):
2450
self._parent_ids = parent_ids
2452
def get_revision_tree(self, revision_id):
2453
return self._transform._tree.get_revision_tree(revision_id)
2456
def joinpath(parent, child):
2457
"""Join tree-relative paths, handling the tree root specially"""
2458
if parent is None or parent == "":
2461
return pathjoin(parent, child)
2464
class FinalPaths(object):
2465
"""Make path calculation cheap by memoizing paths.
2467
The underlying tree must not be manipulated between calls, or else
2468
the results will likely be incorrect.
2470
def __init__(self, transform):
2471
object.__init__(self)
2472
self._known_paths = {}
2473
self.transform = transform
2475
def _determine_path(self, trans_id):
2476
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2478
name = self.transform.final_name(trans_id)
2479
parent_id = self.transform.final_parent(trans_id)
2480
if parent_id == self.transform.root:
2483
return pathjoin(self.get_path(parent_id), name)
2485
def get_path(self, trans_id):
2486
"""Find the final path associated with a trans_id"""
2487
if trans_id not in self._known_paths:
2488
self._known_paths[trans_id] = self._determine_path(trans_id)
2489
return self._known_paths[trans_id]
2491
def get_paths(self, trans_ids):
2492
return [(self.get_path(t), t) for t in trans_ids]
2496
def topology_sorted_ids(tree):
2497
"""Determine the topological order of the ids in a tree"""
2498
file_ids = list(tree)
2499
file_ids.sort(key=tree.id2path)
2503
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2504
delta_from_tree=False):
2505
"""Create working tree for a branch, using a TreeTransform.
2507
This function should be used on empty trees, having a tree root at most.
2508
(see merge and revert functionality for working with existing trees)
2510
Existing files are handled like so:
2512
- Existing bzrdirs take precedence over creating new items. They are
2513
created as '%s.diverted' % name.
2514
- Otherwise, if the content on disk matches the content we are building,
2515
it is silently replaced.
2516
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2518
:param tree: The tree to convert wt into a copy of
2519
:param wt: The working tree that files will be placed into
2520
:param accelerator_tree: A tree which can be used for retrieving file
2521
contents more quickly than tree itself, i.e. a workingtree. tree
2522
will be used for cases where accelerator_tree's content is different.
2523
:param hardlink: If true, hard-link files to accelerator_tree, where
2524
possible. accelerator_tree must implement abspath, i.e. be a
2526
:param delta_from_tree: If true, build_tree may use the input Tree to
2527
generate the inventory delta.
2529
wt.lock_tree_write()
2533
if accelerator_tree is not None:
2534
accelerator_tree.lock_read()
2536
return _build_tree(tree, wt, accelerator_tree, hardlink,
2539
if accelerator_tree is not None:
2540
accelerator_tree.unlock()
2547
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2548
"""See build_tree."""
2549
for num, _unused in enumerate(wt.all_file_ids()):
2550
if num > 0: # more than just a root
2551
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2553
top_pb = ui.ui_factory.nested_progress_bar()
2554
pp = ProgressPhase("Build phase", 2, top_pb)
2555
if tree.get_root_id() is not None:
2556
# This is kind of a hack: we should be altering the root
2557
# as part of the regular tree shape diff logic.
2558
# The conditional test here is to avoid doing an
2559
# expensive operation (flush) every time the root id
2560
# is set within the tree, nor setting the root and thus
2561
# marking the tree as dirty, because we use two different
2562
# idioms here: tree interfaces and inventory interfaces.
2563
if wt.get_root_id() != tree.get_root_id():
2564
wt.set_root_id(tree.get_root_id())
2566
tt = TreeTransform(wt)
2570
file_trans_id[wt.get_root_id()] = \
2571
tt.trans_id_tree_file_id(wt.get_root_id())
2572
pb = ui.ui_factory.nested_progress_bar()
2574
deferred_contents = []
2576
total = len(tree.all_file_ids())
2578
precomputed_delta = []
2580
precomputed_delta = None
2581
# Check if tree inventory has content. If so, we populate
2582
# existing_files with the directory content. If there are no
2583
# entries we skip populating existing_files as its not used.
2584
# This improves performance and unncessary work on large
2585
# directory trees. (#501307)
2587
existing_files = set()
2588
for dir, files in wt.walkdirs():
2589
existing_files.update(f[0] for f in files)
2590
for num, (tree_path, entry) in \
2591
enumerate(tree.iter_entries_by_dir()):
2592
pb.update(gettext("Building tree"), num - len(deferred_contents), total)
2593
if entry.parent_id is None:
2596
file_id = entry.file_id
2598
precomputed_delta.append((None, tree_path, file_id, entry))
2599
if tree_path in existing_files:
2600
target_path = wt.abspath(tree_path)
2601
kind = file_kind(target_path)
2602
if kind == "directory":
2604
controldir.ControlDir.open(target_path)
2605
except errors.NotBranchError:
2609
if (file_id not in divert and
2610
_content_match(tree, entry, file_id, kind,
2612
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2613
if kind == 'directory':
2615
parent_id = file_trans_id[entry.parent_id]
2616
if entry.kind == 'file':
2617
# We *almost* replicate new_by_entry, so that we can defer
2618
# getting the file text, and get them all at once.
2619
trans_id = tt.create_path(entry.name, parent_id)
2620
file_trans_id[file_id] = trans_id
2621
tt.version_file(file_id, trans_id)
2622
executable = tree.is_executable(file_id, tree_path)
2624
tt.set_executability(executable, trans_id)
2625
trans_data = (trans_id, tree_path, entry.text_sha1)
2626
deferred_contents.append((file_id, trans_data))
2628
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2631
new_trans_id = file_trans_id[file_id]
2632
old_parent = tt.trans_id_tree_path(tree_path)
2633
_reparent_children(tt, old_parent, new_trans_id)
2634
offset = num + 1 - len(deferred_contents)
2635
_create_files(tt, tree, deferred_contents, pb, offset,
2636
accelerator_tree, hardlink)
2640
divert_trans = set(file_trans_id[f] for f in divert)
2641
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2642
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2643
if len(raw_conflicts) > 0:
2644
precomputed_delta = None
2645
conflicts = cook_conflicts(raw_conflicts, tt)
2646
for conflict in conflicts:
2647
trace.warning(unicode(conflict))
2649
wt.add_conflicts(conflicts)
2650
except errors.UnsupportedOperation:
2652
result = tt.apply(no_conflicts=True,
2653
precomputed_delta=precomputed_delta)
2660
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2662
total = len(desired_files) + offset
2664
if accelerator_tree is None:
2665
new_desired_files = desired_files
2667
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2668
unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
2669
in iter if not (c or e[0] != e[1])]
2670
if accelerator_tree.supports_content_filtering():
2671
unchanged = [(f, p) for (f, p) in unchanged
2672
if not next(accelerator_tree.iter_search_rules([p]))]
2673
unchanged = dict(unchanged)
2674
new_desired_files = []
2676
for file_id, (trans_id, tree_path, text_sha1) in desired_files:
2677
accelerator_path = unchanged.get(file_id)
2678
if accelerator_path is None:
2679
new_desired_files.append((file_id,
2680
(trans_id, tree_path, text_sha1)))
2682
pb.update(gettext('Adding file contents'), count + offset, total)
2684
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2687
contents = accelerator_tree.get_file(file_id, accelerator_path)
2688
if wt.supports_content_filtering():
2689
filters = wt._content_filter_stack(tree_path)
2690
contents = filtered_output_bytes(contents, filters,
2691
ContentFilterContext(tree_path, tree))
2693
tt.create_file(contents, trans_id, sha1=text_sha1)
2697
except AttributeError:
2698
# after filtering, contents may no longer be file-like
2702
for count, ((trans_id, tree_path, text_sha1), contents) in enumerate(
2703
tree.iter_files_bytes(new_desired_files)):
2704
if wt.supports_content_filtering():
2705
filters = wt._content_filter_stack(tree_path)
2706
contents = filtered_output_bytes(contents, filters,
2707
ContentFilterContext(tree_path, tree))
2708
tt.create_file(contents, trans_id, sha1=text_sha1)
2709
pb.update(gettext('Adding file contents'), count + offset, total)
2712
def _reparent_children(tt, old_parent, new_parent):
2713
for child in tt.iter_tree_children(old_parent):
2714
tt.adjust_path(tt.final_name(child), new_parent, child)
2717
def _reparent_transform_children(tt, old_parent, new_parent):
2718
by_parent = tt.by_parent()
2719
for child in by_parent[old_parent]:
2720
tt.adjust_path(tt.final_name(child), new_parent, child)
2721
return by_parent[old_parent]
2724
def _content_match(tree, entry, file_id, kind, target_path):
2725
if entry.kind != kind:
2727
if entry.kind == "directory":
2729
if entry.kind == "file":
2730
f = file(target_path, 'rb')
2732
if tree.get_file_text(file_id) == f.read():
2736
elif entry.kind == "symlink":
2737
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2742
def resolve_checkout(tt, conflicts, divert):
2743
new_conflicts = set()
2744
for c_type, conflict in ((c[0], c) for c in conflicts):
2745
# Anything but a 'duplicate' would indicate programmer error
2746
if c_type != 'duplicate':
2747
raise AssertionError(c_type)
2748
# Now figure out which is new and which is old
2749
if tt.new_contents(conflict[1]):
2750
new_file = conflict[1]
2751
old_file = conflict[2]
2753
new_file = conflict[2]
2754
old_file = conflict[1]
2756
# We should only get here if the conflict wasn't completely
2758
final_parent = tt.final_parent(old_file)
2759
if new_file in divert:
2760
new_name = tt.final_name(old_file)+'.diverted'
2761
tt.adjust_path(new_name, final_parent, new_file)
2762
new_conflicts.add((c_type, 'Diverted to',
2763
new_file, old_file))
2765
new_name = tt.final_name(old_file)+'.moved'
2766
tt.adjust_path(new_name, final_parent, old_file)
2767
new_conflicts.add((c_type, 'Moved existing file to',
2768
old_file, new_file))
2769
return new_conflicts
2772
def new_by_entry(tt, entry, parent_id, tree):
2773
"""Create a new file according to its inventory entry"""
2777
contents = tree.get_file(entry.file_id).readlines()
2778
executable = tree.is_executable(entry.file_id)
2779
return tt.new_file(name, parent_id, contents, entry.file_id,
2781
elif kind in ('directory', 'tree-reference'):
2782
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2783
if kind == 'tree-reference':
2784
tt.set_tree_reference(entry.reference_revision, trans_id)
2786
elif kind == 'symlink':
2787
target = tree.get_symlink_target(entry.file_id)
2788
return tt.new_symlink(name, parent_id, target, entry.file_id)
2790
raise errors.BadFileKindError(name, kind)
2793
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2794
filter_tree_path=None):
2795
"""Create new file contents according to tree contents.
2797
:param filter_tree_path: the tree path to use to lookup
2798
content filters to apply to the bytes output in the working tree.
2799
This only applies if the working tree supports content filtering.
2801
kind = tree.kind(file_id)
2802
if kind == 'directory':
2803
tt.create_directory(trans_id)
2804
elif kind == "file":
2806
tree_file = tree.get_file(file_id)
2808
bytes = tree_file.readlines()
2812
if wt.supports_content_filtering() and filter_tree_path is not None:
2813
filters = wt._content_filter_stack(filter_tree_path)
2814
bytes = filtered_output_bytes(bytes, filters,
2815
ContentFilterContext(filter_tree_path, tree))
2816
tt.create_file(bytes, trans_id)
2817
elif kind == "symlink":
2818
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2820
raise AssertionError('Unknown kind %r' % kind)
2823
def create_entry_executability(tt, entry, trans_id):
2824
"""Set the executability of a trans_id according to an inventory entry"""
2825
if entry.kind == "file":
2826
tt.set_executability(entry.executable, trans_id)
2829
def revert(working_tree, target_tree, filenames, backups=False,
2830
pb=None, change_reporter=None):
2831
"""Revert a working tree's contents to those of a target tree."""
2832
target_tree.lock_read()
2833
pb = ui.ui_factory.nested_progress_bar()
2834
tt = TreeTransform(working_tree, pb)
2836
pp = ProgressPhase("Revert phase", 3, pb)
2837
conflicts, merge_modified = _prepare_revert_transform(
2838
working_tree, target_tree, tt, filenames, backups, pp)
2840
change_reporter = delta._ChangeReporter(
2841
unversioned_filter=working_tree.is_ignored)
2842
delta.report_changes(tt.iter_changes(), change_reporter)
2843
for conflict in conflicts:
2844
trace.warning(unicode(conflict))
2847
working_tree.set_merge_modified(merge_modified)
2849
target_tree.unlock()
2855
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2856
backups, pp, basis_tree=None,
2857
merge_modified=None):
2858
child_pb = ui.ui_factory.nested_progress_bar()
2860
if merge_modified is None:
2861
merge_modified = working_tree.merge_modified()
2862
merge_modified = _alter_files(working_tree, target_tree, tt,
2863
child_pb, filenames, backups,
2864
merge_modified, basis_tree)
2867
child_pb = ui.ui_factory.nested_progress_bar()
2869
raw_conflicts = resolve_conflicts(tt, child_pb,
2870
lambda t, c: conflict_pass(t, c, target_tree))
2873
conflicts = cook_conflicts(raw_conflicts, tt)
2874
return conflicts, merge_modified
2877
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2878
backups, merge_modified, basis_tree=None):
2879
if basis_tree is not None:
2880
basis_tree.lock_read()
2881
# We ask the working_tree for its changes relative to the target, rather
2882
# than the target changes relative to the working tree. Because WT4 has an
2883
# optimizer to compare itself to a target, but no optimizer for the
2885
change_list = working_tree.iter_changes(target_tree,
2886
specific_files=specific_files, pb=pb)
2887
if target_tree.get_root_id() is None:
2893
for id_num, (file_id, path, changed_content, versioned, parent, name,
2894
kind, executable) in enumerate(change_list):
2895
target_path, wt_path = path
2896
target_versioned, wt_versioned = versioned
2897
target_parent, wt_parent = parent
2898
target_name, wt_name = name
2899
target_kind, wt_kind = kind
2900
target_executable, wt_executable = executable
2901
if skip_root and wt_parent is None:
2903
trans_id = tt.trans_id_file_id(file_id)
2906
keep_content = False
2907
if wt_kind == 'file' and (backups or target_kind is None):
2908
wt_sha1 = working_tree.get_file_sha1(file_id)
2909
if merge_modified.get(file_id) != wt_sha1:
2910
# acquire the basis tree lazily to prevent the
2911
# expense of accessing it when it's not needed ?
2912
# (Guessing, RBC, 200702)
2913
if basis_tree is None:
2914
basis_tree = working_tree.basis_tree()
2915
basis_tree.lock_read()
2916
if basis_tree.has_id(file_id):
2917
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2919
elif target_kind is None and not target_versioned:
2921
if wt_kind is not None:
2922
if not keep_content:
2923
tt.delete_contents(trans_id)
2924
elif target_kind is not None:
2925
parent_trans_id = tt.trans_id_file_id(wt_parent)
2926
backup_name = tt._available_backup_name(
2927
wt_name, parent_trans_id)
2928
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2929
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2930
if wt_versioned and target_versioned:
2931
tt.unversion_file(trans_id)
2932
tt.version_file(file_id, new_trans_id)
2933
# New contents should have the same unix perms as old
2936
trans_id = new_trans_id
2937
if target_kind in ('directory', 'tree-reference'):
2938
tt.create_directory(trans_id)
2939
if target_kind == 'tree-reference':
2940
revision = target_tree.get_reference_revision(file_id,
2942
tt.set_tree_reference(revision, trans_id)
2943
elif target_kind == 'symlink':
2944
tt.create_symlink(target_tree.get_symlink_target(file_id),
2946
elif target_kind == 'file':
2947
deferred_files.append((file_id, (trans_id, mode_id)))
2948
if basis_tree is None:
2949
basis_tree = working_tree.basis_tree()
2950
basis_tree.lock_read()
2951
new_sha1 = target_tree.get_file_sha1(file_id)
2952
if (basis_tree.has_id(file_id) and
2953
new_sha1 == basis_tree.get_file_sha1(file_id)):
2954
if file_id in merge_modified:
2955
del merge_modified[file_id]
2957
merge_modified[file_id] = new_sha1
2959
# preserve the execute bit when backing up
2960
if keep_content and wt_executable == target_executable:
2961
tt.set_executability(target_executable, trans_id)
2962
elif target_kind is not None:
2963
raise AssertionError(target_kind)
2964
if not wt_versioned and target_versioned:
2965
tt.version_file(file_id, trans_id)
2966
if wt_versioned and not target_versioned:
2967
tt.unversion_file(trans_id)
2968
if (target_name is not None and
2969
(wt_name != target_name or wt_parent != target_parent)):
2970
if target_name == '' and target_parent is None:
2971
parent_trans = ROOT_PARENT
2973
parent_trans = tt.trans_id_file_id(target_parent)
2974
if wt_parent is None and wt_versioned:
2975
tt.adjust_root_path(target_name, parent_trans)
2977
tt.adjust_path(target_name, parent_trans, trans_id)
2978
if wt_executable != target_executable and target_kind == "file":
2979
tt.set_executability(target_executable, trans_id)
2980
if working_tree.supports_content_filtering():
2981
for index, ((trans_id, mode_id), bytes) in enumerate(
2982
target_tree.iter_files_bytes(deferred_files)):
2983
file_id = deferred_files[index][0]
2984
# We're reverting a tree to the target tree so using the
2985
# target tree to find the file path seems the best choice
2986
# here IMO - Ian C 27/Oct/2009
2987
filter_tree_path = target_tree.id2path(file_id)
2988
filters = working_tree._content_filter_stack(filter_tree_path)
2989
bytes = filtered_output_bytes(bytes, filters,
2990
ContentFilterContext(filter_tree_path, working_tree))
2991
tt.create_file(bytes, trans_id, mode_id)
2993
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2995
tt.create_file(bytes, trans_id, mode_id)
2996
tt.fixup_new_roots()
2998
if basis_tree is not None:
3000
return merge_modified
3003
def resolve_conflicts(tt, pb=None, pass_func=None):
3004
"""Make many conflict-resolution attempts, but die if they fail"""
3005
if pass_func is None:
3006
pass_func = conflict_pass
3007
new_conflicts = set()
3008
pb = ui.ui_factory.nested_progress_bar()
3011
pb.update(gettext('Resolution pass'), n+1, 10)
3012
conflicts = tt.find_conflicts()
3013
if len(conflicts) == 0:
3014
return new_conflicts
3015
new_conflicts.update(pass_func(tt, conflicts))
3016
raise MalformedTransform(conflicts=conflicts)
3021
def conflict_pass(tt, conflicts, path_tree=None):
3022
"""Resolve some classes of conflicts.
3024
:param tt: The transform to resolve conflicts in
3025
:param conflicts: The conflicts to resolve
3026
:param path_tree: A Tree to get supplemental paths from
3028
new_conflicts = set()
3029
for c_type, conflict in ((c[0], c) for c in conflicts):
3030
if c_type == 'duplicate id':
3031
tt.unversion_file(conflict[1])
3032
new_conflicts.add((c_type, 'Unversioned existing file',
3033
conflict[1], conflict[2], ))
3034
elif c_type == 'duplicate':
3035
# files that were renamed take precedence
3036
final_parent = tt.final_parent(conflict[1])
3037
if tt.path_changed(conflict[1]):
3038
existing_file, new_file = conflict[2], conflict[1]
3040
existing_file, new_file = conflict[1], conflict[2]
3041
new_name = tt.final_name(existing_file) + '.moved'
3042
tt.adjust_path(new_name, final_parent, existing_file)
3043
new_conflicts.add((c_type, 'Moved existing file to',
3044
existing_file, new_file))
3045
elif c_type == 'parent loop':
3046
# break the loop by undoing one of the ops that caused the loop
3048
while not tt.path_changed(cur):
3049
cur = tt.final_parent(cur)
3050
new_conflicts.add((c_type, 'Cancelled move', cur,
3051
tt.final_parent(cur),))
3052
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3054
elif c_type == 'missing parent':
3055
trans_id = conflict[1]
3056
if trans_id in tt._removed_contents:
3057
cancel_deletion = True
3058
orphans = tt._get_potential_orphans(trans_id)
3060
cancel_deletion = False
3061
# All children are orphans
3064
tt.new_orphan(o, trans_id)
3065
except OrphaningError:
3066
# Something bad happened so we cancel the directory
3067
# deletion which will leave it in place with a
3068
# conflict. The user can deal with it from there.
3069
# Note that this also catch the case where we don't
3070
# want to create orphans and leave the directory in
3072
cancel_deletion = True
3075
# Cancel the directory deletion
3076
tt.cancel_deletion(trans_id)
3077
new_conflicts.add(('deleting parent', 'Not deleting',
3082
tt.final_name(trans_id)
3084
if path_tree is not None:
3085
file_id = tt.final_file_id(trans_id)
3087
file_id = tt.inactive_file_id(trans_id)
3088
_, entry = next(path_tree.iter_entries_by_dir(
3090
# special-case the other tree root (move its
3091
# children to current root)
3092
if entry.parent_id is None:
3094
moved = _reparent_transform_children(
3095
tt, trans_id, tt.root)
3097
new_conflicts.add((c_type, 'Moved to root',
3100
parent_trans_id = tt.trans_id_file_id(
3102
tt.adjust_path(entry.name, parent_trans_id,
3105
tt.create_directory(trans_id)
3106
new_conflicts.add((c_type, 'Created directory', trans_id))
3107
elif c_type == 'unversioned parent':
3108
file_id = tt.inactive_file_id(conflict[1])
3109
# special-case the other tree root (move its children instead)
3110
if path_tree and path_tree.has_id(file_id):
3111
if path_tree.path2id('') == file_id:
3112
# This is the root entry, skip it
3114
tt.version_file(file_id, conflict[1])
3115
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3116
elif c_type == 'non-directory parent':
3117
parent_id = conflict[1]
3118
parent_parent = tt.final_parent(parent_id)
3119
parent_name = tt.final_name(parent_id)
3120
parent_file_id = tt.final_file_id(parent_id)
3121
new_parent_id = tt.new_directory(parent_name + '.new',
3122
parent_parent, parent_file_id)
3123
_reparent_transform_children(tt, parent_id, new_parent_id)
3124
if parent_file_id is not None:
3125
tt.unversion_file(parent_id)
3126
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3127
elif c_type == 'versioning no contents':
3128
tt.cancel_versioning(conflict[1])
3129
return new_conflicts
3132
def cook_conflicts(raw_conflicts, tt):
3133
"""Generate a list of cooked conflicts, sorted by file path"""
3134
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3135
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3138
def iter_cook_conflicts(raw_conflicts, tt):
3140
for conflict in raw_conflicts:
3141
c_type = conflict[0]
3142
action = conflict[1]
3143
modified_path = fp.get_path(conflict[2])
3144
modified_id = tt.final_file_id(conflict[2])
3145
if len(conflict) == 3:
3146
yield conflicts.Conflict.factory(
3147
c_type, action=action, path=modified_path, file_id=modified_id)
3150
conflicting_path = fp.get_path(conflict[3])
3151
conflicting_id = tt.final_file_id(conflict[3])
3152
yield conflicts.Conflict.factory(
3153
c_type, action=action, path=modified_path,
3154
file_id=modified_id,
3155
conflict_path=conflicting_path,
3156
conflict_file_id=conflicting_id)
3159
class _FileMover(object):
3160
"""Moves and deletes files for TreeTransform, tracking operations"""
3163
self.past_renames = []
3164
self.pending_deletions = []
3166
def rename(self, from_, to):
3167
"""Rename a file from one path to another."""
3169
os.rename(from_, to)
3170
except OSError as e:
3171
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3172
raise errors.FileExists(to, str(e))
3173
# normal OSError doesn't include filenames so it's hard to see where
3174
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3175
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3176
self.past_renames.append((from_, to))
3178
def pre_delete(self, from_, to):
3179
"""Rename a file out of the way and mark it for deletion.
3181
Unlike os.unlink, this works equally well for files and directories.
3182
:param from_: The current file path
3183
:param to: A temporary path for the file
3185
self.rename(from_, to)
3186
self.pending_deletions.append(to)
3189
"""Reverse all renames that have been performed"""
3190
for from_, to in reversed(self.past_renames):
3192
os.rename(to, from_)
3193
except OSError as e:
3194
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3195
# after rollback, don't reuse _FileMover
3197
pending_deletions = None
3199
def apply_deletions(self):
3200
"""Apply all marked deletions"""
3201
for path in self.pending_deletions:
3203
# after apply_deletions, don't reuse _FileMover
3205
pending_deletions = None