34
32
revision as _mod_revision,
38
35
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
39
ReusingTransform, CantMoveRoot,
36
ReusingTransform, NotVersionedError, CantMoveRoot,
40
37
ExistingLimbo, ImmortalLimbo, NoFinalPath,
41
38
UnableCreateSymlink)
42
39
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
81
78
class TreeTransformBase(object):
82
79
"""The base class for TreeTransform and its kin."""
84
def __init__(self, tree, pb=None,
81
def __init__(self, tree, pb=DummyProgress(),
85
82
case_sensitive=True):
88
85
:param tree: The tree that will be transformed, but not necessarily
87
:param pb: A ProgressTask indicating how much progress is being made
91
88
:param case_sensitive: If True, the target of the transform is
92
89
case sensitive, not just case preserving.
165
162
def adjust_path(self, name, parent, trans_id):
166
163
"""Change the path that is assigned to a transaction id."""
168
raise ValueError("Parent trans-id may not be None")
169
164
if trans_id == self._new_root:
170
165
raise CantMoveRoot
171
166
self._new_name[trans_id] = name
172
167
self._new_parent[trans_id] = parent
168
if parent == ROOT_PARENT:
169
if self._new_root is not None:
170
raise ValueError("Cannot have multiple roots.")
171
self._new_root = trans_id
174
173
def adjust_root_path(self, name, parent):
175
174
"""Emulate moving the root by moving all children, instead.
203
202
self.version_file(old_root_file_id, old_root)
204
203
self.unversion_file(self._new_root)
206
def fixup_new_roots(self):
207
"""Reinterpret requests to change the root directory
209
Instead of creating a root directory, or moving an existing directory,
210
all the attributes and children of the new root are applied to the
211
existing root directory.
213
This means that the old root trans-id becomes obsolete, so it is
214
recommended only to invoke this after the root trans-id has become
217
new_roots = [k for k, v in self._new_parent.iteritems() if v is
219
if len(new_roots) < 1:
221
if len(new_roots) != 1:
222
raise ValueError('A tree cannot have two roots!')
223
if self._new_root is None:
224
self._new_root = new_roots[0]
226
old_new_root = new_roots[0]
227
# TODO: What to do if a old_new_root is present, but self._new_root is
228
# not listed as being removed? This code explicitly unversions
229
# the old root and versions it with the new file_id. Though that
230
# seems like an incomplete delta
232
# unversion the new root's directory.
233
file_id = self.final_file_id(old_new_root)
234
if old_new_root in self._new_id:
235
self.cancel_versioning(old_new_root)
237
self.unversion_file(old_new_root)
238
# if, at this stage, root still has an old file_id, zap it so we can
239
# stick a new one in.
240
if (self.tree_file_id(self._new_root) is not None and
241
self._new_root not in self._removed_id):
242
self.unversion_file(self._new_root)
243
self.version_file(file_id, self._new_root)
245
# Now move children of new root into old root directory.
246
# Ensure all children are registered with the transaction, but don't
247
# use directly-- some tree children have new parents
248
list(self.iter_tree_children(old_new_root))
249
# Move all children of new root into old root directory.
250
for child in self.by_parent().get(old_new_root, []):
251
self.adjust_path(self.final_name(child), self._new_root, child)
253
# Ensure old_new_root has no directory.
254
if old_new_root in self._new_contents:
255
self.cancel_creation(old_new_root)
257
self.delete_contents(old_new_root)
259
# prevent deletion of root directory.
260
if self._new_root in self._removed_contents:
261
self.cancel_deletion(self._new_root)
263
# destroy path info for old_new_root.
264
del self._new_parent[old_new_root]
265
del self._new_name[old_new_root]
267
205
def trans_id_tree_file_id(self, inventory_id):
268
206
"""Determine the transaction id of a working tree file.
316
254
def delete_contents(self, trans_id):
317
255
"""Schedule the contents of a path entry for deletion"""
318
kind = self.tree_kind(trans_id)
320
self._removed_contents.add(trans_id)
256
self.tree_kind(trans_id)
257
self._removed_contents.add(trans_id)
322
259
def cancel_deletion(self, trans_id):
323
260
"""Cancel a scheduled deletion"""
388
325
changed_kind = set(self._removed_contents)
389
326
changed_kind.intersection_update(self._new_contents)
390
327
changed_kind.difference_update(new_ids)
391
changed_kind = (t for t in changed_kind
392
if self.tree_kind(t) != self.final_kind(t))
328
changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
393
330
new_ids.update(changed_kind)
394
331
return sorted(FinalPaths(self).get_paths(new_ids))
396
333
def final_kind(self, trans_id):
397
334
"""Determine the final file kind, after any changes applied.
399
:return: None if the file does not exist/has no contents. (It is
400
conceivable that a path would be created without the corresponding
401
contents insertion command)
336
Raises NoSuchFile if the file does not exist/has no contents.
337
(It is conceivable that a path would be created without the
338
corresponding contents insertion command)
403
340
if trans_id in self._new_contents:
404
341
return self._new_contents[trans_id]
405
342
elif trans_id in self._removed_contents:
343
raise NoSuchFile(None)
408
345
return self.tree_kind(trans_id)
686
641
def _any_contents(self, trans_ids):
687
642
"""Return true if any of the trans_ids, will have contents."""
688
643
for trans_id in trans_ids:
689
if self.final_kind(trans_id) is not None:
645
kind = self.final_kind(trans_id)
693
651
def _set_executability(self, path, trans_id):
823
781
Return a (name, parent, kind, executable) tuple
825
783
to_name = self.final_name(to_trans_id)
826
to_kind = self.final_kind(to_trans_id)
785
to_kind = self.final_kind(to_trans_id)
827
788
to_parent = self.final_file_id(self.final_parent(to_trans_id))
828
789
if to_trans_id in self._new_executability:
829
790
to_executable = self._new_executability[to_trans_id]
904
865
return _PreviewTree(self)
906
def commit(self, branch, message, merge_parents=None, strict=False,
907
timestamp=None, timezone=None, committer=None, authors=None,
908
revprops=None, revision_id=None):
867
def commit(self, branch, message, merge_parents=None, strict=False):
909
868
"""Commit the result of this TreeTransform to a branch.
911
870
:param branch: The branch to commit to.
912
871
:param message: The message to attach to the commit.
913
:param merge_parents: Additional parent revision-ids specified by
915
:param strict: If True, abort the commit if there are unversioned
917
:param timestamp: if not None, seconds-since-epoch for the time and
918
date. (May be a float.)
919
:param timezone: Optional timezone for timestamp, as an offset in
921
:param committer: Optional committer in email-id format.
922
(e.g. "J Random Hacker <jrandom@example.com>")
923
:param authors: Optional list of authors in email-id format.
924
:param revprops: Optional dictionary of revision properties.
925
:param revision_id: Optional revision id. (Specifying a revision-id
926
may reduce performance for some non-native formats.)
872
:param merge_parents: Additional parents specified by pending merges.
927
873
:return: The revision_id of the revision committed.
929
875
self._check_malformed()
946
892
if self._tree.get_revision_id() != last_rev_id:
947
893
raise ValueError('TreeTransform not based on branch basis: %s' %
948
894
self._tree.get_revision_id())
949
revprops = commit.Commit.update_revprops(revprops, branch, authors)
950
builder = branch.get_commit_builder(parent_ids,
955
revision_id=revision_id)
895
builder = branch.get_commit_builder(parent_ids)
956
896
preview = self.get_preview_tree()
957
897
list(builder.record_iter_changes(preview, last_rev_id,
958
898
self.iter_changes()))
1060
1000
class DiskTreeTransform(TreeTransformBase):
1061
1001
"""Tree transform storing its contents on disk."""
1063
def __init__(self, tree, limbodir, pb=None,
1003
def __init__(self, tree, limbodir, pb=DummyProgress(),
1064
1004
case_sensitive=True):
1065
1005
"""Constructor.
1066
1006
:param tree: The tree that will be transformed, but not necessarily
1067
1007
the output tree.
1068
1008
:param limbodir: A directory where new files can be stored until
1069
1009
they are installed in their proper places
1010
:param pb: A ProgressBar indicating how much progress is being made
1071
1011
:param case_sensitive: If True, the target of the transform is
1072
1012
case sensitive, not just case preserving.
1136
1075
if (trans_id in self._limbo_files and
1137
1076
trans_id not in self._needs_rename):
1138
1077
self._rename_in_limbo([trans_id])
1139
if previous_parent != parent:
1140
self._limbo_children[previous_parent].remove(trans_id)
1141
if previous_parent != parent or previous_name != name:
1142
del self._limbo_children_names[previous_parent][previous_name]
1078
self._limbo_children[previous_parent].remove(trans_id)
1079
del self._limbo_children_names[previous_parent][previous_name]
1144
1081
def _rename_in_limbo(self, trans_ids):
1145
1082
"""Fix limbo names so that the right final path is produced.
1159
1096
new_path = self._limbo_name(trans_id)
1160
1097
os.rename(old_path, new_path)
1161
for descendant in self._limbo_descendants(trans_id):
1162
desc_path = self._limbo_files[descendant]
1163
desc_path = new_path + desc_path[len(old_path):]
1164
self._limbo_files[descendant] = desc_path
1166
def _limbo_descendants(self, trans_id):
1167
"""Return the set of trans_ids whose limbo paths descend from this."""
1168
descendants = set(self._limbo_children.get(trans_id, []))
1169
for descendant in list(descendants):
1170
descendants.update(self._limbo_descendants(descendant))
1173
1099
def create_file(self, contents, trans_id, mode_id=None):
1174
1100
"""Schedule creation of a new file.
1209
1134
def _read_symlink_target(self, trans_id):
1210
1135
return os.readlink(self._limbo_name(trans_id))
1212
def _set_mtime(self, path):
1213
"""All files that are created get the same mtime.
1215
This time is set by the first object to be created.
1217
if self._creation_mtime is None:
1218
self._creation_mtime = time.time()
1219
os.utime(path, (self._creation_mtime, self._creation_mtime))
1221
1137
def create_hardlink(self, path, trans_id):
1222
1138
"""Schedule creation of a hard link"""
1223
1139
name = self._limbo_name(trans_id)
1394
1310
def tree_kind(self, trans_id):
1395
1311
"""Determine the file kind in the working tree.
1397
:returns: The file kind or None if the file does not exist
1313
Raises NoSuchFile if the file does not exist
1399
1315
path = self._tree_id_paths.get(trans_id)
1400
1316
if path is None:
1317
raise NoSuchFile(None)
1403
1319
return file_kind(self._tree.abspath(path))
1404
except errors.NoSuchFile:
1321
if e.errno != errno.ENOENT:
1324
raise NoSuchFile(path)
1407
1326
def _set_mode(self, trans_id, mode_id, typefunc):
1408
1327
"""Set the mode of new file contents.
1622
1542
child_pb.update('removing file', num, len(tree_paths))
1623
1543
full_path = self._tree.abspath(path)
1624
1544
if trans_id in self._removed_contents:
1625
delete_path = os.path.join(self._deletiondir, trans_id)
1626
mover.pre_delete(full_path, delete_path)
1627
elif (trans_id in self._new_name
1628
or trans_id in self._new_parent):
1545
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1547
elif trans_id in self._new_name or trans_id in \
1630
1550
mover.rename(full_path, self._limbo_name(trans_id))
1631
except errors.TransformRenameFailed, e:
1632
1552
if e.errno != errno.ENOENT:
1685
1605
unversioned files in the input tree.
1688
def __init__(self, tree, pb=None, case_sensitive=True):
1608
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1689
1609
tree.lock_read()
1690
1610
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1691
1611
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1696
1616
def tree_kind(self, trans_id):
1697
1617
path = self._tree_id_paths.get(trans_id)
1698
1618
if path is None:
1619
raise NoSuchFile(None)
1700
1620
file_id = self._tree.path2id(path)
1702
return self._tree.kind(file_id)
1703
except errors.NoSuchFile:
1621
return self._tree.kind(file_id)
1706
1623
def _set_mode(self, trans_id, mode_id, typefunc):
1707
1624
"""Set the mode of new file contents.
1766
1683
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1767
1684
self._iter_parent_trees()]
1768
1685
vf.add_lines((file_id, tree_revision), parent_keys,
1769
self.get_file_lines(file_id))
1686
self.get_file(file_id).readlines())
1770
1687
repo = self._get_repository()
1771
1688
base_vf = repo.texts
1772
1689
if base_vf not in vf.fallback_versionedfiles:
1987
1902
def get_file_mtime(self, file_id, path=None):
1988
1903
"""See Tree.get_file_mtime"""
1989
1904
if not self._content_change(file_id):
1990
return self._transform._tree.get_file_mtime(file_id)
1905
return self._transform._tree.get_file_mtime(file_id, path)
1991
1906
return self._stat_limbo_file(file_id).st_mtime
1993
1908
def _file_size(self, entry, stat_value):
2055
1970
executable = None
2056
1971
if kind == 'symlink':
2057
1972
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2058
executable = tt._new_executability.get(trans_id, executable)
1973
if supports_executable():
1974
executable = tt._new_executability.get(trans_id, executable)
2059
1975
return kind, size, executable, link_or_sha1
2061
1977
def iter_changes(self, from_tree, include_unchanged=False,
2264
2180
for num, _unused in enumerate(wt.all_file_ids()):
2265
2181
if num > 0: # more than just a root
2266
2182
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2183
existing_files = set()
2184
for dir, files in wt.walkdirs():
2185
existing_files.update(f[0] for f in files)
2267
2186
file_trans_id = {}
2268
2187
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2269
2188
pp = ProgressPhase("Build phase", 2, top_pb)
2293
2212
precomputed_delta = []
2295
2214
precomputed_delta = None
2296
# Check if tree inventory has content. If so, we populate
2297
# existing_files with the directory content. If there are no
2298
# entries we skip populating existing_files as its not used.
2299
# This improves performance and unncessary work on large
2300
# directory trees. (#501307)
2302
existing_files = set()
2303
for dir, files in wt.walkdirs():
2304
existing_files.update(f[0] for f in files)
2305
2215
for num, (tree_path, entry) in \
2306
2216
enumerate(tree.inventory.iter_entries_by_dir()):
2307
2217
pb.update("Building tree", num - len(deferred_contents), total)
2380
2290
new_desired_files = desired_files
2382
2292
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2383
unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
2384
in iter if not (c or e[0] != e[1])]
2385
if accelerator_tree.supports_content_filtering():
2386
unchanged = [(f, p) for (f, p) in unchanged
2387
if not accelerator_tree.iter_search_rules([p]).next()]
2388
unchanged = dict(unchanged)
2293
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2294
in iter if not (c or e[0] != e[1]))
2389
2295
new_desired_files = []
2391
2297
for file_id, (trans_id, tree_path) in desired_files:
2502
2404
raise errors.BadFileKindError(name, kind)
2407
@deprecated_function(deprecated_in((1, 9, 0)))
2408
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2409
"""Create new file contents according to an inventory entry.
2411
DEPRECATED. Use create_from_tree instead.
2413
if entry.kind == "file":
2415
lines = tree.get_file(entry.file_id).readlines()
2416
tt.create_file(lines, trans_id, mode_id=mode_id)
2417
elif entry.kind == "symlink":
2418
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2419
elif entry.kind == "directory":
2420
tt.create_directory(trans_id)
2505
2423
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2506
2424
filter_tree_path=None):
2507
2425
"""Create new file contents according to tree contents.
2584
2502
def revert(working_tree, target_tree, filenames, backups=False,
2585
pb=None, change_reporter=None):
2503
pb=DummyProgress(), change_reporter=None):
2586
2504
"""Revert a working tree's contents to those of a target tree."""
2587
2505
target_tree.lock_read()
2588
pb = ui.ui_factory.nested_progress_bar()
2589
2506
tt = TreeTransform(working_tree, pb)
2591
2508
pp = ProgressPhase("Revert phase", 3, pb)
2717
2636
parent_trans = ROOT_PARENT
2719
2638
parent_trans = tt.trans_id_file_id(parent[1])
2720
if parent[0] is None and versioned[0]:
2721
tt.adjust_root_path(name[1], parent_trans)
2723
tt.adjust_path(name[1], parent_trans, trans_id)
2639
tt.adjust_path(name[1], parent_trans, trans_id)
2724
2640
if executable[0] != executable[1] and kind[1] == "file":
2725
2641
tt.set_executability(executable[1], trans_id)
2726
2642
if working_tree.supports_content_filtering():
2739
2655
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2740
2656
deferred_files):
2741
2657
tt.create_file(bytes, trans_id, mode_id)
2742
tt.fixup_new_roots()
2744
2659
if basis_tree is not None:
2745
2660
basis_tree.unlock()
2746
2661
return merge_modified
2749
def resolve_conflicts(tt, pb=None, pass_func=None):
2664
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2750
2665
"""Make many conflict-resolution attempts, but die if they fail"""
2751
2666
if pass_func is None:
2752
2667
pass_func = conflict_pass
2753
2668
new_conflicts = set()
2754
pb = ui.ui_factory.nested_progress_bar()
2756
2670
for n in range(10):
2757
2671
pb.update('Resolution pass', n+1, 10)
2890
2804
self.pending_deletions = []
2892
2806
def rename(self, from_, to):
2893
"""Rename a file from one path to another."""
2807
"""Rename a file from one path to another. Functions like os.rename"""
2895
2809
os.rename(from_, to)
2896
2810
except OSError, e:
2897
2811
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2898
2812
raise errors.FileExists(to, str(e))
2899
# normal OSError doesn't include filenames so it's hard to see where
2900
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
2901
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
2902
2814
self.past_renames.append((from_, to))
2904
2816
def pre_delete(self, from_, to):