34
34
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
36
36
from bzrlib.progress import DummyProgress, ProgressPhase
37
from bzrlib.symbol_versioning import deprecated_function, zero_fifteen
37
from bzrlib.symbol_versioning import (
38
42
from bzrlib.trace import mutter, warning
39
43
from bzrlib import tree
53
57
class _TransformResults(object):
54
def __init__(self, modified_paths):
58
def __init__(self, modified_paths, rename_count):
55
59
object.__init__(self)
56
60
self.modified_paths = modified_paths
61
self.rename_count = rename_count
59
64
class TreeTransform(object):
60
65
"""Represent a tree transformation.
62
67
This object is designed to support incremental generation of the transform,
70
However, it gives optimum performance when parent directories are created
71
before their contents. The transform is then able to put child files
72
directly in their parent directory, avoiding later renames.
65
74
It is easy to produce malformed transforms, but they are generally
66
75
harmless. Attempting to apply a malformed transform will cause an
84
93
def __init__(self, tree, pb=DummyProgress()):
85
94
"""Note: a tree_write lock is taken on the tree.
87
Use TreeTransform.finalize() to release the lock
96
Use TreeTransform.finalize() to release the lock (can be omitted if
97
TreeTransform.apply() called).
89
99
object.__init__(self)
106
116
self._new_name = {}
107
117
self._new_parent = {}
108
118
self._new_contents = {}
119
# A mapping of transform ids to their limbo filename
120
self._limbo_files = {}
121
# A mapping of transform ids to a set of the transform ids of children
122
# that their limbo directory has
123
self._limbo_children = {}
124
# Map transform ids to maps of child filename to child transform id
125
self._limbo_children_names = {}
126
# List of transform ids that need to be renamed from limbo into place
127
self._needs_rename = set()
109
128
self._removed_contents = set()
110
129
self._new_executability = {}
111
130
self._new_reference_revision = {}
115
134
self._removed_id = set()
116
135
self._tree_path_ids = {}
117
136
self._tree_id_paths = {}
137
# Cache of realpath results, to speed up canonical_path
118
138
self._realpaths = {}
119
# Cache of realpath results, to speed up canonical_path
139
# Cache of relpath results, to speed up canonical_path
120
140
self._relpaths = {}
121
# Cache of relpath results, to speed up canonical_path
122
141
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
123
142
self.__done = False
144
self.rename_count = 0
126
146
def __get_root(self):
127
147
return self._new_root
129
149
root = property(__get_root)
131
151
def finalize(self):
132
"""Release the working tree lock, if held, clean up limbo dir."""
152
"""Release the working tree lock, if held, clean up limbo dir.
154
This is required if apply has not been invoked, but can be invoked
133
157
if self._tree is None:
136
for trans_id, kind in self._new_contents.iteritems():
137
path = self._limbo_name(trans_id)
160
entries = [(self._limbo_name(t), t, k) for t, k in
161
self._new_contents.iteritems()]
162
entries.sort(reverse=True)
163
for path, trans_id, kind in entries:
138
164
if kind == "directory":
165
191
"""Change the path that is assigned to a transaction id."""
166
192
if trans_id == self._new_root:
167
193
raise CantMoveRoot
194
previous_parent = self._new_parent.get(trans_id)
195
previous_name = self._new_name.get(trans_id)
168
196
self._new_name[trans_id] = name
169
197
self._new_parent[trans_id] = parent
198
if (trans_id in self._limbo_files and
199
trans_id not in self._needs_rename):
200
self._rename_in_limbo([trans_id])
201
self._limbo_children[previous_parent].remove(trans_id)
202
del self._limbo_children_names[previous_parent][previous_name]
204
def _rename_in_limbo(self, trans_ids):
205
"""Fix limbo names so that the right final path is produced.
207
This means we outsmarted ourselves-- we tried to avoid renaming
208
these files later by creating them with their final names in their
209
final parents. But now the previous name or parent is no longer
210
suitable, so we have to rename them.
212
Even for trans_ids that have no new contents, we must remove their
213
entries from _limbo_files, because they are now stale.
215
for trans_id in trans_ids:
216
old_path = self._limbo_files.pop(trans_id)
217
if trans_id not in self._new_contents:
219
new_path = self._limbo_name(trans_id)
220
os.rename(old_path, new_path)
171
222
def adjust_root_path(self, name, parent):
172
223
"""Emulate moving the root by moving all children, instead.
330
381
def cancel_creation(self, trans_id):
331
382
"""Cancel the creation of new file contents."""
332
383
del self._new_contents[trans_id]
384
children = self._limbo_children.get(trans_id)
385
# if this is a limbo directory with children, move them before removing
387
if children is not None:
388
self._rename_in_limbo(children)
389
del self._limbo_children[trans_id]
390
del self._limbo_children_names[trans_id]
333
391
delete_any(self._limbo_name(trans_id))
335
393
def delete_contents(self, trans_id):
663
721
def _duplicate_entries(self, by_parent):
664
722
"""No directory may have two entries with the same name."""
724
if (self._new_name, self._new_parent) == ({}, {}):
666
726
for children in by_parent.itervalues():
667
727
name_ids = [(self.final_name(t), t) for t in children]
792
def apply(self, no_conflicts=False):
733
793
"""Apply all changes to the inventory and filesystem.
735
795
If filesystem or inventory conflicts are present, MalformedTransform
798
If apply succeeds, finalize is not necessary.
800
:param no_conflicts: if True, the caller guarantees there are no
801
conflicts, so no check is made.
738
conflicts = self.find_conflicts()
739
if len(conflicts) != 0:
740
raise MalformedTransform(conflicts=conflicts)
804
conflicts = self.find_conflicts()
805
if len(conflicts) != 0:
806
raise MalformedTransform(conflicts=conflicts)
741
807
inv = self._tree.inventory
742
808
inventory_delta = []
743
809
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
751
817
self._tree.apply_inventory_delta(inventory_delta)
752
818
self.__done = True
754
return _TransformResults(modified_paths)
820
return _TransformResults(modified_paths, self.rename_count)
756
822
def _limbo_name(self, trans_id):
757
823
"""Generate the limbo name of a file"""
758
return pathjoin(self._limbodir, trans_id)
824
limbo_name = self._limbo_files.get(trans_id)
825
if limbo_name is not None:
827
parent = self._new_parent.get(trans_id)
828
# if the parent directory is already in limbo (e.g. when building a
829
# tree), choose a limbo name inside the parent, to reduce further
831
use_direct_path = False
832
if self._new_contents.get(parent) == 'directory':
833
filename = self._new_name.get(trans_id)
834
if filename is not None:
835
if parent not in self._limbo_children:
836
self._limbo_children[parent] = set()
837
self._limbo_children_names[parent] = {}
838
use_direct_path = True
839
# the direct path can only be used if no other file has
840
# already taken this pathname, i.e. if the name is unused, or
841
# if it is already associated with this trans_id.
842
elif (self._limbo_children_names[parent].get(filename)
843
in (trans_id, None)):
844
use_direct_path = True
846
limbo_name = pathjoin(self._limbo_files[parent], filename)
847
self._limbo_children[parent].add(trans_id)
848
self._limbo_children_names[parent][filename] = trans_id
850
limbo_name = pathjoin(self._limbodir, trans_id)
851
self._needs_rename.add(trans_id)
852
self._limbo_files[trans_id] = limbo_name
760
855
def _apply_removals(self, inv, inventory_delta):
761
856
"""Perform tree operations that remove directory/inventory names.
812
909
if trans_id in self._new_contents or \
813
910
self.path_changed(trans_id):
814
911
full_path = self._tree.abspath(path)
816
os.rename(self._limbo_name(trans_id), full_path)
818
# We may be renaming a dangling inventory id
819
if e.errno != errno.ENOENT:
912
if trans_id in self._needs_rename:
914
os.rename(self._limbo_name(trans_id), full_path)
916
# We may be renaming a dangling inventory id
917
if e.errno != errno.ENOENT:
920
self.rename_count += 1
821
921
if trans_id in self._new_contents:
822
922
modified_paths.append(full_path)
823
923
del self._new_contents[trans_id]
1151
1251
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1152
1252
pp = ProgressPhase("Build phase", 2, top_pb)
1153
1253
if tree.inventory.root is not None:
1154
# this is kindof a hack: we should be altering the root
1155
# as partof the regular tree shape diff logic.
1156
# the conditional test hereis to avoid doing an
1254
# This is kind of a hack: we should be altering the root
1255
# as part of the regular tree shape diff logic.
1256
# The conditional test here is to avoid doing an
1157
1257
# expensive operation (flush) every time the root id
1158
1258
# is set within the tree, nor setting the root and thus
1159
1259
# marking the tree as dirty, because we use two different
1327
1428
working_tree.unlock()
1431
@deprecated_function(zero_ninety)
1330
1432
def change_entry(tt, file_id, working_tree, target_tree,
1331
1433
trans_id_file_id, backups, trans_id, by_parent):
1332
1434
"""Replace a file_id's contents with those from a target tree."""
1435
if file_id is None and target_tree is None:
1436
# skip the logic altogether in the deprecation test
1333
1438
e_trans_id = trans_id_file_id(file_id)
1334
1439
entry = target_tree.inventory[file_id]
1335
1440
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1420
1525
pp.next_phase()
1421
1526
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1423
_alter_files(working_tree, target_tree, tt, child_pb,
1528
merge_modified = _alter_files(working_tree, target_tree, tt,
1529
child_pb, filenames, backups)
1426
1531
child_pb.finished()
1427
1532
pp.next_phase()
1469
1574
if kind[0] == 'file' and (backups or kind[1] is None):
1470
1575
wt_sha1 = working_tree.get_file_sha1(file_id)
1471
1576
if merge_modified.get(file_id) != wt_sha1:
1472
# acquire the basis tree lazyily to prevent the expense
1473
# of accessing it when its not needed ? (Guessing, RBC,
1577
# acquire the basis tree lazily to prevent the
1578
# expense of accessing it when it's not needed ?
1579
# (Guessing, RBC, 200702)
1475
1580
if basis_tree is None:
1476
1581
basis_tree = working_tree.basis_tree()
1477
1582
basis_tree.lock_read()
1505
1610
elif kind[1] == 'file':
1506
1611
tt.create_file(target_tree.get_file_lines(file_id),
1507
1612
trans_id, mode_id)
1613
if basis_tree is None:
1614
basis_tree = working_tree.basis_tree()
1615
basis_tree.lock_read()
1616
new_sha1 = target_tree.get_file_sha1(file_id)
1617
if (file_id in basis_tree and new_sha1 ==
1618
basis_tree.get_file_sha1(file_id)):
1619
if file_id in merge_modified:
1620
del merge_modified[file_id]
1622
merge_modified[file_id] = new_sha1
1508
1624
# preserve the execute bit when backing up
1509
1625
if keep_content and executable[0] == executable[1]:
1510
1626
tt.set_executability(executable[1], trans_id)
1545
def conflict_pass(tt, conflicts):
1546
"""Resolve some classes of conflicts."""
1662
def conflict_pass(tt, conflicts, path_tree=None):
1663
"""Resolve some classes of conflicts.
1665
:param tt: The transform to resolve conflicts in
1666
:param conflicts: The conflicts to resolve
1667
:param path_tree: A Tree to get supplemental paths from
1547
1669
new_conflicts = set()
1548
1670
for c_type, conflict in ((c[0], c) for c in conflicts):
1549
1671
if c_type == 'duplicate id':
1580
1702
except KeyError:
1581
1703
tt.create_directory(trans_id)
1582
1704
new_conflicts.add((c_type, 'Created directory', trans_id))
1706
tt.final_name(trans_id)
1708
file_id = tt.final_file_id(trans_id)
1709
entry = path_tree.inventory[file_id]
1710
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1711
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1583
1712
elif c_type == 'unversioned parent':
1584
1713
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1585
1714
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))