1
# Copyright (C) 2006-2010 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
19
from stat import S_ISREG, S_IEXEC
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
34
revision as _mod_revision,
38
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
39
ReusingTransform, CantMoveRoot,
40
ExistingLimbo, ImmortalLimbo, NoFinalPath,
42
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
43
from bzrlib.inventory import InventoryEntry
44
from bzrlib.osutils import (
54
from bzrlib.progress import ProgressPhase
55
from bzrlib.symbol_versioning import (
59
from bzrlib.trace import mutter, warning
60
from bzrlib import tree
62
import bzrlib.urlutils as urlutils
65
ROOT_PARENT = "root-parent"
68
def unique_add(map, key, value):
70
raise DuplicateKey(key=key)
74
class _TransformResults(object):
75
def __init__(self, modified_paths, rename_count):
77
self.modified_paths = modified_paths
78
self.rename_count = rename_count
81
class TreeTransformBase(object):
82
"""The base class for TreeTransform and its kin."""
84
def __init__(self, tree, pb=None,
88
:param tree: The tree that will be transformed, but not necessarily
91
:param case_sensitive: If True, the target of the transform is
92
case sensitive, not just case preserving.
97
# mapping of trans_id -> new basename
99
# mapping of trans_id -> new parent trans_id
100
self._new_parent = {}
101
# mapping of trans_id with new contents -> new file_kind
102
self._new_contents = {}
103
# Set of trans_ids whose contents will be removed
104
self._removed_contents = set()
105
# Mapping of trans_id -> new execute-bit value
106
self._new_executability = {}
107
# Mapping of trans_id -> new tree-reference value
108
self._new_reference_revision = {}
109
# Mapping of trans_id -> new file_id
111
# Mapping of old file-id -> trans_id
112
self._non_present_ids = {}
113
# Mapping of new file_id -> trans_id
115
# Set of trans_ids that will be removed
116
self._removed_id = set()
117
# Mapping of path in old tree -> trans_id
118
self._tree_path_ids = {}
119
# Mapping trans_id -> path in old tree
120
self._tree_id_paths = {}
121
# The trans_id that will be used as the tree root
122
root_id = tree.get_root_id()
123
if root_id is not None:
124
self._new_root = self.trans_id_tree_file_id(root_id)
126
self._new_root = None
127
# Indictor of whether the transform has been applied
131
# Whether the target is case sensitive
132
self._case_sensitive_target = case_sensitive
133
# A counter of how many files have been renamed
134
self.rename_count = 0
137
"""Release the working tree lock, if held.
139
This is required if apply has not been invoked, but can be invoked
142
if self._tree is None:
147
def __get_root(self):
148
return self._new_root
150
root = property(__get_root)
152
def _assign_id(self):
153
"""Produce a new tranform id"""
154
new_id = "new-%s" % self._id_number
158
def create_path(self, name, parent):
159
"""Assign a transaction id to a new path"""
160
trans_id = self._assign_id()
161
unique_add(self._new_name, trans_id, name)
162
unique_add(self._new_parent, trans_id, parent)
165
def adjust_path(self, name, parent, trans_id):
166
"""Change the path that is assigned to a transaction id."""
168
raise ValueError("Parent trans-id may not be None")
169
if trans_id == self._new_root:
171
self._new_name[trans_id] = name
172
self._new_parent[trans_id] = parent
174
def adjust_root_path(self, name, parent):
175
"""Emulate moving the root by moving all children, instead.
177
We do this by undoing the association of root's transaction id with the
178
current tree. This allows us to create a new directory with that
179
transaction id. We unversion the root directory and version the
180
physically new directory, and hope someone versions the tree root
183
old_root = self._new_root
184
old_root_file_id = self.final_file_id(old_root)
185
# force moving all children of root
186
for child_id in self.iter_tree_children(old_root):
187
if child_id != parent:
188
self.adjust_path(self.final_name(child_id),
189
self.final_parent(child_id), child_id)
190
file_id = self.final_file_id(child_id)
191
if file_id is not None:
192
self.unversion_file(child_id)
193
self.version_file(file_id, child_id)
195
# the physical root needs a new transaction id
196
self._tree_path_ids.pop("")
197
self._tree_id_paths.pop(old_root)
198
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
199
if parent == old_root:
200
parent = self._new_root
201
self.adjust_path(name, parent, old_root)
202
self.create_directory(old_root)
203
self.version_file(old_root_file_id, old_root)
204
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
def trans_id_tree_file_id(self, inventory_id):
268
"""Determine the transaction id of a working tree file.
270
This reflects only files that already exist, not ones that will be
271
added by transactions.
273
if inventory_id is None:
274
raise ValueError('None is not a valid file id')
275
path = self._tree.id2path(inventory_id)
276
return self.trans_id_tree_path(path)
278
def trans_id_file_id(self, file_id):
279
"""Determine or set the transaction id associated with a file ID.
280
A new id is only created for file_ids that were never present. If
281
a transaction has been unversioned, it is deliberately still returned.
282
(this will likely lead to an unversioned parent conflict.)
285
raise ValueError('None is not a valid file id')
286
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
287
return self._r_new_id[file_id]
290
self._tree.iter_entries_by_dir([file_id]).next()
291
except StopIteration:
292
if file_id in self._non_present_ids:
293
return self._non_present_ids[file_id]
295
trans_id = self._assign_id()
296
self._non_present_ids[file_id] = trans_id
299
return self.trans_id_tree_file_id(file_id)
301
def trans_id_tree_path(self, path):
302
"""Determine (and maybe set) the transaction ID for a tree path."""
303
path = self.canonical_path(path)
304
if path not in self._tree_path_ids:
305
self._tree_path_ids[path] = self._assign_id()
306
self._tree_id_paths[self._tree_path_ids[path]] = path
307
return self._tree_path_ids[path]
309
def get_tree_parent(self, trans_id):
310
"""Determine id of the parent in the tree."""
311
path = self._tree_id_paths[trans_id]
314
return self.trans_id_tree_path(os.path.dirname(path))
316
def delete_contents(self, trans_id):
317
"""Schedule the contents of a path entry for deletion"""
318
kind = self.tree_kind(trans_id)
320
self._removed_contents.add(trans_id)
322
def cancel_deletion(self, trans_id):
323
"""Cancel a scheduled deletion"""
324
self._removed_contents.remove(trans_id)
326
def unversion_file(self, trans_id):
327
"""Schedule a path entry to become unversioned"""
328
self._removed_id.add(trans_id)
330
def delete_versioned(self, trans_id):
331
"""Delete and unversion a versioned file"""
332
self.delete_contents(trans_id)
333
self.unversion_file(trans_id)
335
def set_executability(self, executability, trans_id):
336
"""Schedule setting of the 'execute' bit
337
To unschedule, set to None
339
if executability is None:
340
del self._new_executability[trans_id]
342
unique_add(self._new_executability, trans_id, executability)
344
def set_tree_reference(self, revision_id, trans_id):
345
"""Set the reference associated with a directory"""
346
unique_add(self._new_reference_revision, trans_id, revision_id)
348
def version_file(self, file_id, trans_id):
349
"""Schedule a file to become versioned."""
352
unique_add(self._new_id, trans_id, file_id)
353
unique_add(self._r_new_id, file_id, trans_id)
355
def cancel_versioning(self, trans_id):
356
"""Undo a previous versioning of a file"""
357
file_id = self._new_id[trans_id]
358
del self._new_id[trans_id]
359
del self._r_new_id[file_id]
361
def new_paths(self, filesystem_only=False):
362
"""Determine the paths of all new and changed files.
364
:param filesystem_only: if True, only calculate values for files
365
that require renames or execute bit changes.
369
stale_ids = self._needs_rename.difference(self._new_name)
370
stale_ids.difference_update(self._new_parent)
371
stale_ids.difference_update(self._new_contents)
372
stale_ids.difference_update(self._new_id)
373
needs_rename = self._needs_rename.difference(stale_ids)
374
id_sets = (needs_rename, self._new_executability)
376
id_sets = (self._new_name, self._new_parent, self._new_contents,
377
self._new_id, self._new_executability)
378
for id_set in id_sets:
379
new_ids.update(id_set)
380
return sorted(FinalPaths(self).get_paths(new_ids))
382
def _inventory_altered(self):
383
"""Get the trans_ids and paths of files needing new inv entries."""
385
for id_set in [self._new_name, self._new_parent, self._new_id,
386
self._new_executability]:
387
new_ids.update(id_set)
388
changed_kind = set(self._removed_contents)
389
changed_kind.intersection_update(self._new_contents)
390
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))
393
new_ids.update(changed_kind)
394
return sorted(FinalPaths(self).get_paths(new_ids))
396
def final_kind(self, trans_id):
397
"""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)
403
if trans_id in self._new_contents:
404
return self._new_contents[trans_id]
405
elif trans_id in self._removed_contents:
408
return self.tree_kind(trans_id)
410
def tree_file_id(self, trans_id):
411
"""Determine the file id associated with the trans_id in the tree"""
413
path = self._tree_id_paths[trans_id]
415
# the file is a new, unversioned file, or invalid trans_id
417
# the file is old; the old id is still valid
418
if self._new_root == trans_id:
419
return self._tree.get_root_id()
420
return self._tree.path2id(path)
422
def final_file_id(self, trans_id):
423
"""Determine the file id after any changes are applied, or None.
425
None indicates that the file will not be versioned after changes are
429
return self._new_id[trans_id]
431
if trans_id in self._removed_id:
433
return self.tree_file_id(trans_id)
435
def inactive_file_id(self, trans_id):
436
"""Return the inactive file_id associated with a transaction id.
437
That is, the one in the tree or in non_present_ids.
438
The file_id may actually be active, too.
440
file_id = self.tree_file_id(trans_id)
441
if file_id is not None:
443
for key, value in self._non_present_ids.iteritems():
444
if value == trans_id:
447
def final_parent(self, trans_id):
448
"""Determine the parent file_id, after any changes are applied.
450
ROOT_PARENT is returned for the tree root.
453
return self._new_parent[trans_id]
455
return self.get_tree_parent(trans_id)
457
def final_name(self, trans_id):
458
"""Determine the final filename, after all changes are applied."""
460
return self._new_name[trans_id]
463
return os.path.basename(self._tree_id_paths[trans_id])
465
raise NoFinalPath(trans_id, self)
468
"""Return a map of parent: children for known parents.
470
Only new paths and parents of tree files with assigned ids are used.
473
items = list(self._new_parent.iteritems())
474
items.extend((t, self.final_parent(t)) for t in
475
self._tree_id_paths.keys())
476
for trans_id, parent_id in items:
477
if parent_id not in by_parent:
478
by_parent[parent_id] = set()
479
by_parent[parent_id].add(trans_id)
482
def path_changed(self, trans_id):
483
"""Return True if a trans_id's path has changed."""
484
return (trans_id in self._new_name) or (trans_id in self._new_parent)
486
def new_contents(self, trans_id):
487
return (trans_id in self._new_contents)
489
def find_conflicts(self):
490
"""Find any violations of inventory or filesystem invariants"""
491
if self._done is True:
492
raise ReusingTransform()
494
# ensure all children of all existent parents are known
495
# all children of non-existent parents are known, by definition.
496
self._add_tree_children()
497
by_parent = self.by_parent()
498
conflicts.extend(self._unversioned_parents(by_parent))
499
conflicts.extend(self._parent_loops())
500
conflicts.extend(self._duplicate_entries(by_parent))
501
conflicts.extend(self._duplicate_ids())
502
conflicts.extend(self._parent_type_conflicts(by_parent))
503
conflicts.extend(self._improper_versioning())
504
conflicts.extend(self._executability_conflicts())
505
conflicts.extend(self._overwrite_conflicts())
508
def _check_malformed(self):
509
conflicts = self.find_conflicts()
510
if len(conflicts) != 0:
511
raise MalformedTransform(conflicts=conflicts)
513
def _add_tree_children(self):
514
"""Add all the children of all active parents to the known paths.
516
Active parents are those which gain children, and those which are
517
removed. This is a necessary first step in detecting conflicts.
519
parents = self.by_parent().keys()
520
parents.extend([t for t in self._removed_contents if
521
self.tree_kind(t) == 'directory'])
522
for trans_id in self._removed_id:
523
file_id = self.tree_file_id(trans_id)
524
if file_id is not None:
525
if self._tree.inventory[file_id].kind == 'directory':
526
parents.append(trans_id)
527
elif self.tree_kind(trans_id) == 'directory':
528
parents.append(trans_id)
530
for parent_id in parents:
531
# ensure that all children are registered with the transaction
532
list(self.iter_tree_children(parent_id))
534
def has_named_child(self, by_parent, parent_id, name):
536
children = by_parent[parent_id]
539
for child in children:
540
if self.final_name(child) == name:
543
path = self._tree_id_paths[parent_id]
546
childpath = joinpath(path, name)
547
child_id = self._tree_path_ids.get(childpath)
549
return lexists(self._tree.abspath(childpath))
551
if self.final_parent(child_id) != parent_id:
553
if child_id in self._removed_contents:
554
# XXX What about dangling file-ids?
559
def _parent_loops(self):
560
"""No entry should be its own ancestor"""
562
for trans_id in self._new_parent:
565
while parent_id is not ROOT_PARENT:
568
parent_id = self.final_parent(parent_id)
571
if parent_id == trans_id:
572
conflicts.append(('parent loop', trans_id))
573
if parent_id in seen:
577
def _unversioned_parents(self, by_parent):
578
"""If parent directories are versioned, children must be versioned."""
580
for parent_id, children in by_parent.iteritems():
581
if parent_id is ROOT_PARENT:
583
if self.final_file_id(parent_id) is not None:
585
for child_id in children:
586
if self.final_file_id(child_id) is not None:
587
conflicts.append(('unversioned parent', parent_id))
591
def _improper_versioning(self):
592
"""Cannot version a file with no contents, or a bad type.
594
However, existing entries with no contents are okay.
597
for trans_id in self._new_id.iterkeys():
598
kind = self.final_kind(trans_id)
600
conflicts.append(('versioning no contents', trans_id))
602
if not InventoryEntry.versionable_kind(kind):
603
conflicts.append(('versioning bad kind', trans_id, kind))
606
def _executability_conflicts(self):
607
"""Check for bad executability changes.
609
Only versioned files may have their executability set, because
610
1. only versioned entries can have executability under windows
611
2. only files can be executable. (The execute bit on a directory
612
does not indicate searchability)
615
for trans_id in self._new_executability:
616
if self.final_file_id(trans_id) is None:
617
conflicts.append(('unversioned executability', trans_id))
619
if self.final_kind(trans_id) != "file":
620
conflicts.append(('non-file executability', trans_id))
623
def _overwrite_conflicts(self):
624
"""Check for overwrites (not permitted on Win32)"""
626
for trans_id in self._new_contents:
627
if self.tree_kind(trans_id) is None:
629
if trans_id not in self._removed_contents:
630
conflicts.append(('overwrite', trans_id,
631
self.final_name(trans_id)))
634
def _duplicate_entries(self, by_parent):
635
"""No directory may have two entries with the same name."""
637
if (self._new_name, self._new_parent) == ({}, {}):
639
for children in by_parent.itervalues():
640
name_ids = [(self.final_name(t), t) for t in children]
641
if not self._case_sensitive_target:
642
name_ids = [(n.lower(), t) for n, t in name_ids]
646
for name, trans_id in name_ids:
647
kind = self.final_kind(trans_id)
648
file_id = self.final_file_id(trans_id)
649
if kind is None and file_id is None:
651
if name == last_name:
652
conflicts.append(('duplicate', last_trans_id, trans_id,
655
last_trans_id = trans_id
658
def _duplicate_ids(self):
659
"""Each inventory id may only be used once"""
661
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
663
all_ids = self._tree.all_file_ids()
664
active_tree_ids = all_ids.difference(removed_tree_ids)
665
for trans_id, file_id in self._new_id.iteritems():
666
if file_id in active_tree_ids:
667
old_trans_id = self.trans_id_tree_file_id(file_id)
668
conflicts.append(('duplicate id', old_trans_id, trans_id))
671
def _parent_type_conflicts(self, by_parent):
672
"""parents must have directory 'contents'."""
674
for parent_id, children in by_parent.iteritems():
675
if parent_id is ROOT_PARENT:
677
if not self._any_contents(children):
679
kind = self.final_kind(parent_id)
681
conflicts.append(('missing parent', parent_id))
682
elif kind != "directory":
683
conflicts.append(('non-directory parent', parent_id))
686
def _any_contents(self, trans_ids):
687
"""Return true if any of the trans_ids, will have contents."""
688
for trans_id in trans_ids:
689
if self.final_kind(trans_id) is not None:
693
def _set_executability(self, path, trans_id):
694
"""Set the executability of versioned files """
695
if supports_executable():
696
new_executability = self._new_executability[trans_id]
697
abspath = self._tree.abspath(path)
698
current_mode = os.stat(abspath).st_mode
699
if new_executability:
702
to_mode = current_mode | (0100 & ~umask)
703
# Enable x-bit for others only if they can read it.
704
if current_mode & 0004:
705
to_mode |= 0001 & ~umask
706
if current_mode & 0040:
707
to_mode |= 0010 & ~umask
709
to_mode = current_mode & ~0111
710
os.chmod(abspath, to_mode)
712
def _new_entry(self, name, parent_id, file_id):
713
"""Helper function to create a new filesystem entry."""
714
trans_id = self.create_path(name, parent_id)
715
if file_id is not None:
716
self.version_file(file_id, trans_id)
719
def new_file(self, name, parent_id, contents, file_id=None,
721
"""Convenience method to create files.
723
name is the name of the file to create.
724
parent_id is the transaction id of the parent directory of the file.
725
contents is an iterator of bytestrings, which will be used to produce
727
:param file_id: The inventory ID of the file, if it is to be versioned.
728
:param executable: Only valid when a file_id has been supplied.
730
trans_id = self._new_entry(name, parent_id, file_id)
731
# TODO: rather than scheduling a set_executable call,
732
# have create_file create the file with the right mode.
733
self.create_file(contents, trans_id)
734
if executable is not None:
735
self.set_executability(executable, trans_id)
738
def new_directory(self, name, parent_id, file_id=None):
739
"""Convenience method to create directories.
741
name is the name of the directory to create.
742
parent_id is the transaction id of the parent directory of the
744
file_id is the inventory ID of the directory, if it is to be versioned.
746
trans_id = self._new_entry(name, parent_id, file_id)
747
self.create_directory(trans_id)
750
def new_symlink(self, name, parent_id, target, file_id=None):
751
"""Convenience method to create symbolic link.
753
name is the name of the symlink to create.
754
parent_id is the transaction id of the parent directory of the symlink.
755
target is a bytestring of the target of the symlink.
756
file_id is the inventory ID of the file, if it is to be versioned.
758
trans_id = self._new_entry(name, parent_id, file_id)
759
self.create_symlink(target, trans_id)
762
def _affected_ids(self):
763
"""Return the set of transform ids affected by the transform"""
764
trans_ids = set(self._removed_id)
765
trans_ids.update(self._new_id.keys())
766
trans_ids.update(self._removed_contents)
767
trans_ids.update(self._new_contents.keys())
768
trans_ids.update(self._new_executability.keys())
769
trans_ids.update(self._new_name.keys())
770
trans_ids.update(self._new_parent.keys())
773
def _get_file_id_maps(self):
774
"""Return mapping of file_ids to trans_ids in the to and from states"""
775
trans_ids = self._affected_ids()
778
# Build up two dicts: trans_ids associated with file ids in the
779
# FROM state, vs the TO state.
780
for trans_id in trans_ids:
781
from_file_id = self.tree_file_id(trans_id)
782
if from_file_id is not None:
783
from_trans_ids[from_file_id] = trans_id
784
to_file_id = self.final_file_id(trans_id)
785
if to_file_id is not None:
786
to_trans_ids[to_file_id] = trans_id
787
return from_trans_ids, to_trans_ids
789
def _from_file_data(self, from_trans_id, from_versioned, file_id):
790
"""Get data about a file in the from (tree) state
792
Return a (name, parent, kind, executable) tuple
794
from_path = self._tree_id_paths.get(from_trans_id)
796
# get data from working tree if versioned
797
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
798
from_name = from_entry.name
799
from_parent = from_entry.parent_id
802
if from_path is None:
803
# File does not exist in FROM state
807
# File exists, but is not versioned. Have to use path-
809
from_name = os.path.basename(from_path)
810
tree_parent = self.get_tree_parent(from_trans_id)
811
from_parent = self.tree_file_id(tree_parent)
812
if from_path is not None:
813
from_kind, from_executable, from_stats = \
814
self._tree._comparison_data(from_entry, from_path)
817
from_executable = False
818
return from_name, from_parent, from_kind, from_executable
820
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
821
"""Get data about a file in the to (target) state
823
Return a (name, parent, kind, executable) tuple
825
to_name = self.final_name(to_trans_id)
826
to_kind = self.final_kind(to_trans_id)
827
to_parent = self.final_file_id(self.final_parent(to_trans_id))
828
if to_trans_id in self._new_executability:
829
to_executable = self._new_executability[to_trans_id]
830
elif to_trans_id == from_trans_id:
831
to_executable = from_executable
833
to_executable = False
834
return to_name, to_parent, to_kind, to_executable
836
def iter_changes(self):
837
"""Produce output in the same format as Tree.iter_changes.
839
Will produce nonsensical results if invoked while inventory/filesystem
840
conflicts (as reported by TreeTransform.find_conflicts()) are present.
842
This reads the Transform, but only reproduces changes involving a
843
file_id. Files that are not versioned in either of the FROM or TO
844
states are not reflected.
846
final_paths = FinalPaths(self)
847
from_trans_ids, to_trans_ids = self._get_file_id_maps()
849
# Now iterate through all active file_ids
850
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
852
from_trans_id = from_trans_ids.get(file_id)
853
# find file ids, and determine versioning state
854
if from_trans_id is None:
855
from_versioned = False
856
from_trans_id = to_trans_ids[file_id]
858
from_versioned = True
859
to_trans_id = to_trans_ids.get(file_id)
860
if to_trans_id is None:
862
to_trans_id = from_trans_id
866
from_name, from_parent, from_kind, from_executable = \
867
self._from_file_data(from_trans_id, from_versioned, file_id)
869
to_name, to_parent, to_kind, to_executable = \
870
self._to_file_data(to_trans_id, from_trans_id, from_executable)
872
if not from_versioned:
875
from_path = self._tree_id_paths.get(from_trans_id)
879
to_path = final_paths.get_path(to_trans_id)
880
if from_kind != to_kind:
882
elif to_kind in ('file', 'symlink') and (
883
to_trans_id != from_trans_id or
884
to_trans_id in self._new_contents):
886
if (not modified and from_versioned == to_versioned and
887
from_parent==to_parent and from_name == to_name and
888
from_executable == to_executable):
890
results.append((file_id, (from_path, to_path), modified,
891
(from_versioned, to_versioned),
892
(from_parent, to_parent),
893
(from_name, to_name),
894
(from_kind, to_kind),
895
(from_executable, to_executable)))
896
return iter(sorted(results, key=lambda x:x[1]))
898
def get_preview_tree(self):
899
"""Return a tree representing the result of the transform.
901
The tree is a snapshot, and altering the TreeTransform will invalidate
904
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):
909
"""Commit the result of this TreeTransform to a branch.
911
:param branch: The branch to commit to.
912
: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.)
927
:return: The revision_id of the revision committed.
929
self._check_malformed()
931
unversioned = set(self._new_contents).difference(set(self._new_id))
932
for trans_id in unversioned:
933
if self.final_file_id(trans_id) is None:
934
raise errors.StrictCommitFailed()
936
revno, last_rev_id = branch.last_revision_info()
937
if last_rev_id == _mod_revision.NULL_REVISION:
938
if merge_parents is not None:
939
raise ValueError('Cannot supply merge parents for first'
943
parent_ids = [last_rev_id]
944
if merge_parents is not None:
945
parent_ids.extend(merge_parents)
946
if self._tree.get_revision_id() != last_rev_id:
947
raise ValueError('TreeTransform not based on branch basis: %s' %
948
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)
956
preview = self.get_preview_tree()
957
list(builder.record_iter_changes(preview, last_rev_id,
958
self.iter_changes()))
959
builder.finish_inventory()
960
revision_id = builder.commit(message)
961
branch.set_last_revision_info(revno + 1, revision_id)
964
def _text_parent(self, trans_id):
965
file_id = self.tree_file_id(trans_id)
967
if file_id is None or self._tree.kind(file_id) != 'file':
969
except errors.NoSuchFile:
973
def _get_parents_texts(self, trans_id):
974
"""Get texts for compression parents of this file."""
975
file_id = self._text_parent(trans_id)
978
return (self._tree.get_file_text(file_id),)
980
def _get_parents_lines(self, trans_id):
981
"""Get lines for compression parents of this file."""
982
file_id = self._text_parent(trans_id)
985
return (self._tree.get_file_lines(file_id),)
987
def serialize(self, serializer):
988
"""Serialize this TreeTransform.
990
:param serializer: A Serialiser like pack.ContainerSerializer.
992
new_name = dict((k, v.encode('utf-8')) for k, v in
993
self._new_name.items())
994
new_executability = dict((k, int(v)) for k, v in
995
self._new_executability.items())
996
tree_path_ids = dict((k.encode('utf-8'), v)
997
for k, v in self._tree_path_ids.items())
999
'_id_number': self._id_number,
1000
'_new_name': new_name,
1001
'_new_parent': self._new_parent,
1002
'_new_executability': new_executability,
1003
'_new_id': self._new_id,
1004
'_tree_path_ids': tree_path_ids,
1005
'_removed_id': list(self._removed_id),
1006
'_removed_contents': list(self._removed_contents),
1007
'_non_present_ids': self._non_present_ids,
1009
yield serializer.bytes_record(bencode.bencode(attribs),
1011
for trans_id, kind in self._new_contents.items():
1013
lines = osutils.chunks_to_lines(
1014
self._read_file_chunks(trans_id))
1015
parents = self._get_parents_lines(trans_id)
1016
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1017
content = ''.join(mpdiff.to_patch())
1018
if kind == 'directory':
1020
if kind == 'symlink':
1021
content = self._read_symlink_target(trans_id)
1022
yield serializer.bytes_record(content, ((trans_id, kind),))
1024
def deserialize(self, records):
1025
"""Deserialize a stored TreeTransform.
1027
:param records: An iterable of (names, content) tuples, as per
1028
pack.ContainerPushParser.
1030
names, content = records.next()
1031
attribs = bencode.bdecode(content)
1032
self._id_number = attribs['_id_number']
1033
self._new_name = dict((k, v.decode('utf-8'))
1034
for k, v in attribs['_new_name'].items())
1035
self._new_parent = attribs['_new_parent']
1036
self._new_executability = dict((k, bool(v)) for k, v in
1037
attribs['_new_executability'].items())
1038
self._new_id = attribs['_new_id']
1039
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1040
self._tree_path_ids = {}
1041
self._tree_id_paths = {}
1042
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1043
path = bytepath.decode('utf-8')
1044
self._tree_path_ids[path] = trans_id
1045
self._tree_id_paths[trans_id] = path
1046
self._removed_id = set(attribs['_removed_id'])
1047
self._removed_contents = set(attribs['_removed_contents'])
1048
self._non_present_ids = attribs['_non_present_ids']
1049
for ((trans_id, kind),), content in records:
1051
mpdiff = multiparent.MultiParent.from_patch(content)
1052
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1053
self.create_file(lines, trans_id)
1054
if kind == 'directory':
1055
self.create_directory(trans_id)
1056
if kind == 'symlink':
1057
self.create_symlink(content.decode('utf-8'), trans_id)
1060
class DiskTreeTransform(TreeTransformBase):
1061
"""Tree transform storing its contents on disk."""
1063
def __init__(self, tree, limbodir, pb=None,
1064
case_sensitive=True):
1066
:param tree: The tree that will be transformed, but not necessarily
1068
:param limbodir: A directory where new files can be stored until
1069
they are installed in their proper places
1071
:param case_sensitive: If True, the target of the transform is
1072
case sensitive, not just case preserving.
1074
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1075
self._limbodir = limbodir
1076
self._deletiondir = None
1077
# A mapping of transform ids to their limbo filename
1078
self._limbo_files = {}
1079
# A mapping of transform ids to a set of the transform ids of children
1080
# that their limbo directory has
1081
self._limbo_children = {}
1082
# Map transform ids to maps of child filename to child transform id
1083
self._limbo_children_names = {}
1084
# List of transform ids that need to be renamed from limbo into place
1085
self._needs_rename = set()
1086
self._creation_mtime = None
1089
"""Release the working tree lock, if held, clean up limbo dir.
1091
This is required if apply has not been invoked, but can be invoked
1094
if self._tree is None:
1097
entries = [(self._limbo_name(t), t, k) for t, k in
1098
self._new_contents.iteritems()]
1099
entries.sort(reverse=True)
1100
for path, trans_id, kind in entries:
1103
delete_any(self._limbodir)
1105
# We don't especially care *why* the dir is immortal.
1106
raise ImmortalLimbo(self._limbodir)
1108
if self._deletiondir is not None:
1109
delete_any(self._deletiondir)
1111
raise errors.ImmortalPendingDeletion(self._deletiondir)
1113
TreeTransformBase.finalize(self)
1115
def _limbo_name(self, trans_id):
1116
"""Generate the limbo name of a file"""
1117
limbo_name = self._limbo_files.get(trans_id)
1118
if limbo_name is None:
1119
limbo_name = self._generate_limbo_path(trans_id)
1120
self._limbo_files[trans_id] = limbo_name
1123
def _generate_limbo_path(self, trans_id):
1124
"""Generate a limbo path using the trans_id as the relative path.
1126
This is suitable as a fallback, and when the transform should not be
1127
sensitive to the path encoding of the limbo directory.
1129
self._needs_rename.add(trans_id)
1130
return pathjoin(self._limbodir, trans_id)
1132
def adjust_path(self, name, parent, trans_id):
1133
previous_parent = self._new_parent.get(trans_id)
1134
previous_name = self._new_name.get(trans_id)
1135
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1136
if (trans_id in self._limbo_files and
1137
trans_id not in self._needs_rename):
1138
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]
1144
def _rename_in_limbo(self, trans_ids):
1145
"""Fix limbo names so that the right final path is produced.
1147
This means we outsmarted ourselves-- we tried to avoid renaming
1148
these files later by creating them with their final names in their
1149
final parents. But now the previous name or parent is no longer
1150
suitable, so we have to rename them.
1152
Even for trans_ids that have no new contents, we must remove their
1153
entries from _limbo_files, because they are now stale.
1155
for trans_id in trans_ids:
1156
old_path = self._limbo_files.pop(trans_id)
1157
if trans_id not in self._new_contents:
1159
new_path = self._limbo_name(trans_id)
1160
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
def create_file(self, contents, trans_id, mode_id=None):
1174
"""Schedule creation of a new file.
1178
Contents is an iterator of strings, all of which will be written
1179
to the target destination.
1181
New file takes the permissions of any existing file with that id,
1182
unless mode_id is specified.
1184
name = self._limbo_name(trans_id)
1185
f = open(name, 'wb')
1188
unique_add(self._new_contents, trans_id, 'file')
1190
# Clean up the file, it never got registered so
1191
# TreeTransform.finalize() won't clean it up.
1196
f.writelines(contents)
1199
self._set_mtime(name)
1200
self._set_mode(trans_id, mode_id, S_ISREG)
1202
def _read_file_chunks(self, trans_id):
1203
cur_file = open(self._limbo_name(trans_id), 'rb')
1205
return cur_file.readlines()
1209
def _read_symlink_target(self, trans_id):
1210
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
def create_hardlink(self, path, trans_id):
1222
"""Schedule creation of a hard link"""
1223
name = self._limbo_name(trans_id)
1227
if e.errno != errno.EPERM:
1229
raise errors.HardLinkNotSupported(path)
1231
unique_add(self._new_contents, trans_id, 'file')
1233
# Clean up the file, it never got registered so
1234
# TreeTransform.finalize() won't clean it up.
1238
def create_directory(self, trans_id):
1239
"""Schedule creation of a new directory.
1241
See also new_directory.
1243
os.mkdir(self._limbo_name(trans_id))
1244
unique_add(self._new_contents, trans_id, 'directory')
1246
def create_symlink(self, target, trans_id):
1247
"""Schedule creation of a new symbolic link.
1249
target is a bytestring.
1250
See also new_symlink.
1253
os.symlink(target, self._limbo_name(trans_id))
1254
unique_add(self._new_contents, trans_id, 'symlink')
1257
path = FinalPaths(self).get_path(trans_id)
1260
raise UnableCreateSymlink(path=path)
1262
def cancel_creation(self, trans_id):
1263
"""Cancel the creation of new file contents."""
1264
del self._new_contents[trans_id]
1265
children = self._limbo_children.get(trans_id)
1266
# if this is a limbo directory with children, move them before removing
1268
if children is not None:
1269
self._rename_in_limbo(children)
1270
del self._limbo_children[trans_id]
1271
del self._limbo_children_names[trans_id]
1272
delete_any(self._limbo_name(trans_id))
1275
class TreeTransform(DiskTreeTransform):
1276
"""Represent a tree transformation.
1278
This object is designed to support incremental generation of the transform,
1281
However, it gives optimum performance when parent directories are created
1282
before their contents. The transform is then able to put child files
1283
directly in their parent directory, avoiding later renames.
1285
It is easy to produce malformed transforms, but they are generally
1286
harmless. Attempting to apply a malformed transform will cause an
1287
exception to be raised before any modifications are made to the tree.
1289
Many kinds of malformed transforms can be corrected with the
1290
resolve_conflicts function. The remaining ones indicate programming error,
1291
such as trying to create a file with no path.
1293
Two sets of file creation methods are supplied. Convenience methods are:
1298
These are composed of the low-level methods:
1300
* create_file or create_directory or create_symlink
1304
Transform/Transaction ids
1305
-------------------------
1306
trans_ids are temporary ids assigned to all files involved in a transform.
1307
It's possible, even common, that not all files in the Tree have trans_ids.
1309
trans_ids are used because filenames and file_ids are not good enough
1310
identifiers; filenames change, and not all files have file_ids. File-ids
1311
are also associated with trans-ids, so that moving a file moves its
1314
trans_ids are only valid for the TreeTransform that generated them.
1318
Limbo is a temporary directory use to hold new versions of files.
1319
Files are added to limbo by create_file, create_directory, create_symlink,
1320
and their convenience variants (new_*). Files may be removed from limbo
1321
using cancel_creation. Files are renamed from limbo into their final
1322
location as part of TreeTransform.apply
1324
Limbo must be cleaned up, by either calling TreeTransform.apply or
1325
calling TreeTransform.finalize.
1327
Files are placed into limbo inside their parent directories, where
1328
possible. This reduces subsequent renames, and makes operations involving
1329
lots of files faster. This optimization is only possible if the parent
1330
directory is created *before* creating any of its children, so avoid
1331
creating children before parents, where possible.
1335
This temporary directory is used by _FileMover for storing files that are
1336
about to be deleted. In case of rollback, the files will be restored.
1337
FileMover does not delete files until it is sure that a rollback will not
1340
def __init__(self, tree, pb=None):
1341
"""Note: a tree_write lock is taken on the tree.
1343
Use TreeTransform.finalize() to release the lock (can be omitted if
1344
TreeTransform.apply() called).
1346
tree.lock_tree_write()
1349
limbodir = urlutils.local_path_from_url(
1350
tree._transport.abspath('limbo'))
1354
if e.errno == errno.EEXIST:
1355
raise ExistingLimbo(limbodir)
1356
deletiondir = urlutils.local_path_from_url(
1357
tree._transport.abspath('pending-deletion'))
1359
os.mkdir(deletiondir)
1361
if e.errno == errno.EEXIST:
1362
raise errors.ExistingPendingDeletion(deletiondir)
1367
# Cache of realpath results, to speed up canonical_path
1368
self._realpaths = {}
1369
# Cache of relpath results, to speed up canonical_path
1371
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1372
tree.case_sensitive)
1373
self._deletiondir = deletiondir
1375
def canonical_path(self, path):
1376
"""Get the canonical tree-relative path"""
1377
# don't follow final symlinks
1378
abs = self._tree.abspath(path)
1379
if abs in self._relpaths:
1380
return self._relpaths[abs]
1381
dirname, basename = os.path.split(abs)
1382
if dirname not in self._realpaths:
1383
self._realpaths[dirname] = os.path.realpath(dirname)
1384
dirname = self._realpaths[dirname]
1385
abs = pathjoin(dirname, basename)
1386
if dirname in self._relpaths:
1387
relpath = pathjoin(self._relpaths[dirname], basename)
1388
relpath = relpath.rstrip('/\\')
1390
relpath = self._tree.relpath(abs)
1391
self._relpaths[abs] = relpath
1394
def tree_kind(self, trans_id):
1395
"""Determine the file kind in the working tree.
1397
:returns: The file kind or None if the file does not exist
1399
path = self._tree_id_paths.get(trans_id)
1403
return file_kind(self._tree.abspath(path))
1404
except errors.NoSuchFile:
1407
def _set_mode(self, trans_id, mode_id, typefunc):
1408
"""Set the mode of new file contents.
1409
The mode_id is the existing file to get the mode from (often the same
1410
as trans_id). The operation is only performed if there's a mode match
1411
according to typefunc.
1416
old_path = self._tree_id_paths[mode_id]
1420
mode = os.stat(self._tree.abspath(old_path)).st_mode
1422
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1423
# Either old_path doesn't exist, or the parent of the
1424
# target is not a directory (but will be one eventually)
1425
# Either way, we know it doesn't exist *right now*
1426
# See also bug #248448
1431
os.chmod(self._limbo_name(trans_id), mode)
1433
def iter_tree_children(self, parent_id):
1434
"""Iterate through the entry's tree children, if any"""
1436
path = self._tree_id_paths[parent_id]
1440
children = os.listdir(self._tree.abspath(path))
1442
if not (osutils._is_error_enotdir(e)
1443
or e.errno in (errno.ENOENT, errno.ESRCH)):
1447
for child in children:
1448
childpath = joinpath(path, child)
1449
if self._tree.is_control_filename(childpath):
1451
yield self.trans_id_tree_path(childpath)
1453
def _generate_limbo_path(self, trans_id):
1454
"""Generate a limbo path using the final path if possible.
1456
This optimizes the performance of applying the tree transform by
1457
avoiding renames. These renames can be avoided only when the parent
1458
directory is already scheduled for creation.
1460
If the final path cannot be used, falls back to using the trans_id as
1463
parent = self._new_parent.get(trans_id)
1464
# if the parent directory is already in limbo (e.g. when building a
1465
# tree), choose a limbo name inside the parent, to reduce further
1467
use_direct_path = False
1468
if self._new_contents.get(parent) == 'directory':
1469
filename = self._new_name.get(trans_id)
1470
if filename is not None:
1471
if parent not in self._limbo_children:
1472
self._limbo_children[parent] = set()
1473
self._limbo_children_names[parent] = {}
1474
use_direct_path = True
1475
# the direct path can only be used if no other file has
1476
# already taken this pathname, i.e. if the name is unused, or
1477
# if it is already associated with this trans_id.
1478
elif self._case_sensitive_target:
1479
if (self._limbo_children_names[parent].get(filename)
1480
in (trans_id, None)):
1481
use_direct_path = True
1483
for l_filename, l_trans_id in\
1484
self._limbo_children_names[parent].iteritems():
1485
if l_trans_id == trans_id:
1487
if l_filename.lower() == filename.lower():
1490
use_direct_path = True
1492
if not use_direct_path:
1493
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1495
limbo_name = pathjoin(self._limbo_files[parent], filename)
1496
self._limbo_children[parent].add(trans_id)
1497
self._limbo_children_names[parent][filename] = trans_id
1501
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1502
"""Apply all changes to the inventory and filesystem.
1504
If filesystem or inventory conflicts are present, MalformedTransform
1507
If apply succeeds, finalize is not necessary.
1509
:param no_conflicts: if True, the caller guarantees there are no
1510
conflicts, so no check is made.
1511
:param precomputed_delta: An inventory delta to use instead of
1513
:param _mover: Supply an alternate FileMover, for testing
1515
if not no_conflicts:
1516
self._check_malformed()
1517
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1519
if precomputed_delta is None:
1520
child_pb.update('Apply phase', 0, 2)
1521
inventory_delta = self._generate_inventory_delta()
1524
inventory_delta = precomputed_delta
1527
mover = _FileMover()
1531
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1532
self._apply_removals(mover)
1533
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1534
modified_paths = self._apply_insertions(mover)
1539
mover.apply_deletions()
1542
self._tree.apply_inventory_delta(inventory_delta)
1545
return _TransformResults(modified_paths, self.rename_count)
1547
def _generate_inventory_delta(self):
1548
"""Generate an inventory delta for the current transform."""
1549
inventory_delta = []
1550
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1551
new_paths = self._inventory_altered()
1552
total_entries = len(new_paths) + len(self._removed_id)
1554
for num, trans_id in enumerate(self._removed_id):
1556
child_pb.update('removing file', num, total_entries)
1557
if trans_id == self._new_root:
1558
file_id = self._tree.get_root_id()
1560
file_id = self.tree_file_id(trans_id)
1561
# File-id isn't really being deleted, just moved
1562
if file_id in self._r_new_id:
1564
path = self._tree_id_paths[trans_id]
1565
inventory_delta.append((path, None, file_id, None))
1566
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1568
entries = self._tree.iter_entries_by_dir(
1569
new_path_file_ids.values())
1570
old_paths = dict((e.file_id, p) for p, e in entries)
1572
for num, (path, trans_id) in enumerate(new_paths):
1574
child_pb.update('adding file',
1575
num + len(self._removed_id), total_entries)
1576
file_id = new_path_file_ids[trans_id]
1580
kind = self.final_kind(trans_id)
1582
kind = self._tree.stored_kind(file_id)
1583
parent_trans_id = self.final_parent(trans_id)
1584
parent_file_id = new_path_file_ids.get(parent_trans_id)
1585
if parent_file_id is None:
1586
parent_file_id = self.final_file_id(parent_trans_id)
1587
if trans_id in self._new_reference_revision:
1588
new_entry = inventory.TreeReference(
1590
self._new_name[trans_id],
1591
self.final_file_id(self._new_parent[trans_id]),
1592
None, self._new_reference_revision[trans_id])
1594
new_entry = inventory.make_entry(kind,
1595
self.final_name(trans_id),
1596
parent_file_id, file_id)
1597
old_path = old_paths.get(new_entry.file_id)
1598
new_executability = self._new_executability.get(trans_id)
1599
if new_executability is not None:
1600
new_entry.executable = new_executability
1601
inventory_delta.append(
1602
(old_path, path, new_entry.file_id, new_entry))
1605
return inventory_delta
1607
def _apply_removals(self, mover):
1608
"""Perform tree operations that remove directory/inventory names.
1610
That is, delete files that are to be deleted, and put any files that
1611
need renaming into limbo. This must be done in strict child-to-parent
1614
If inventory_delta is None, no inventory delta generation is performed.
1616
tree_paths = list(self._tree_path_ids.iteritems())
1617
tree_paths.sort(reverse=True)
1618
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1620
for num, data in enumerate(tree_paths):
1621
path, trans_id = data
1622
child_pb.update('removing file', num, len(tree_paths))
1623
full_path = self._tree.abspath(path)
1624
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):
1630
mover.rename(full_path, self._limbo_name(trans_id))
1631
except errors.TransformRenameFailed, e:
1632
if e.errno != errno.ENOENT:
1635
self.rename_count += 1
1639
def _apply_insertions(self, mover):
1640
"""Perform tree operations that insert directory/inventory names.
1642
That is, create any files that need to be created, and restore from
1643
limbo any files that needed renaming. This must be done in strict
1644
parent-to-child order.
1646
If inventory_delta is None, no inventory delta is calculated, and
1647
no list of modified paths is returned.
1649
new_paths = self.new_paths(filesystem_only=True)
1651
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1653
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1655
for num, (path, trans_id) in enumerate(new_paths):
1657
child_pb.update('adding file', num, len(new_paths))
1658
full_path = self._tree.abspath(path)
1659
if trans_id in self._needs_rename:
1661
mover.rename(self._limbo_name(trans_id), full_path)
1662
except errors.TransformRenameFailed, e:
1663
# We may be renaming a dangling inventory id
1664
if e.errno != errno.ENOENT:
1667
self.rename_count += 1
1668
if (trans_id in self._new_contents or
1669
self.path_changed(trans_id)):
1670
if trans_id in self._new_contents:
1671
modified_paths.append(full_path)
1672
if trans_id in self._new_executability:
1673
self._set_executability(path, trans_id)
1676
self._new_contents.clear()
1677
return modified_paths
1680
class TransformPreview(DiskTreeTransform):
1681
"""A TreeTransform for generating preview trees.
1683
Unlike TreeTransform, this version works when the input tree is a
1684
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1685
unversioned files in the input tree.
1688
def __init__(self, tree, pb=None, case_sensitive=True):
1690
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1691
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1693
def canonical_path(self, path):
1696
def tree_kind(self, trans_id):
1697
path = self._tree_id_paths.get(trans_id)
1700
file_id = self._tree.path2id(path)
1702
return self._tree.kind(file_id)
1703
except errors.NoSuchFile:
1706
def _set_mode(self, trans_id, mode_id, typefunc):
1707
"""Set the mode of new file contents.
1708
The mode_id is the existing file to get the mode from (often the same
1709
as trans_id). The operation is only performed if there's a mode match
1710
according to typefunc.
1712
# is it ok to ignore this? probably
1715
def iter_tree_children(self, parent_id):
1716
"""Iterate through the entry's tree children, if any"""
1718
path = self._tree_id_paths[parent_id]
1721
file_id = self.tree_file_id(parent_id)
1724
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1725
children = getattr(entry, 'children', {})
1726
for child in children:
1727
childpath = joinpath(path, child)
1728
yield self.trans_id_tree_path(childpath)
1731
class _PreviewTree(tree.Tree):
1732
"""Partial implementation of Tree to support show_diff_trees"""
1734
def __init__(self, transform):
1735
self._transform = transform
1736
self._final_paths = FinalPaths(transform)
1737
self.__by_parent = None
1738
self._parent_ids = []
1739
self._all_children_cache = {}
1740
self._path2trans_id_cache = {}
1741
self._final_name_cache = {}
1742
self._iter_changes_cache = dict((c[0], c) for c in
1743
self._transform.iter_changes())
1745
def _content_change(self, file_id):
1746
"""Return True if the content of this file changed"""
1747
changes = self._iter_changes_cache.get(file_id)
1748
# changes[2] is true if the file content changed. See
1749
# InterTree.iter_changes.
1750
return (changes is not None and changes[2])
1752
def _get_repository(self):
1753
repo = getattr(self._transform._tree, '_repository', None)
1755
repo = self._transform._tree.branch.repository
1758
def _iter_parent_trees(self):
1759
for revision_id in self.get_parent_ids():
1761
yield self.revision_tree(revision_id)
1762
except errors.NoSuchRevisionInTree:
1763
yield self._get_repository().revision_tree(revision_id)
1765
def _get_file_revision(self, file_id, vf, tree_revision):
1766
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1767
self._iter_parent_trees()]
1768
vf.add_lines((file_id, tree_revision), parent_keys,
1769
self.get_file_lines(file_id))
1770
repo = self._get_repository()
1771
base_vf = repo.texts
1772
if base_vf not in vf.fallback_versionedfiles:
1773
vf.fallback_versionedfiles.append(base_vf)
1774
return tree_revision
1776
def _stat_limbo_file(self, file_id):
1777
trans_id = self._transform.trans_id_file_id(file_id)
1778
name = self._transform._limbo_name(trans_id)
1779
return os.lstat(name)
1782
def _by_parent(self):
1783
if self.__by_parent is None:
1784
self.__by_parent = self._transform.by_parent()
1785
return self.__by_parent
1787
def _comparison_data(self, entry, path):
1788
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1789
if kind == 'missing':
1793
file_id = self._transform.final_file_id(self._path2trans_id(path))
1794
executable = self.is_executable(file_id, path)
1795
return kind, executable, None
1797
def is_locked(self):
1800
def lock_read(self):
1801
# Perhaps in theory, this should lock the TreeTransform?
1808
def inventory(self):
1809
"""This Tree does not use inventory as its backing data."""
1810
raise NotImplementedError(_PreviewTree.inventory)
1812
def get_root_id(self):
1813
return self._transform.final_file_id(self._transform.root)
1815
def all_file_ids(self):
1816
tree_ids = set(self._transform._tree.all_file_ids())
1817
tree_ids.difference_update(self._transform.tree_file_id(t)
1818
for t in self._transform._removed_id)
1819
tree_ids.update(self._transform._new_id.values())
1823
return iter(self.all_file_ids())
1825
def _has_id(self, file_id, fallback_check):
1826
if file_id in self._transform._r_new_id:
1828
elif file_id in set([self._transform.tree_file_id(trans_id) for
1829
trans_id in self._transform._removed_id]):
1832
return fallback_check(file_id)
1834
def has_id(self, file_id):
1835
return self._has_id(file_id, self._transform._tree.has_id)
1837
def has_or_had_id(self, file_id):
1838
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1840
def _path2trans_id(self, path):
1841
# We must not use None here, because that is a valid value to store.
1842
trans_id = self._path2trans_id_cache.get(path, object)
1843
if trans_id is not object:
1845
segments = splitpath(path)
1846
cur_parent = self._transform.root
1847
for cur_segment in segments:
1848
for child in self._all_children(cur_parent):
1849
final_name = self._final_name_cache.get(child)
1850
if final_name is None:
1851
final_name = self._transform.final_name(child)
1852
self._final_name_cache[child] = final_name
1853
if final_name == cur_segment:
1857
self._path2trans_id_cache[path] = None
1859
self._path2trans_id_cache[path] = cur_parent
1862
def path2id(self, path):
1863
return self._transform.final_file_id(self._path2trans_id(path))
1865
def id2path(self, file_id):
1866
trans_id = self._transform.trans_id_file_id(file_id)
1868
return self._final_paths._determine_path(trans_id)
1870
raise errors.NoSuchId(self, file_id)
1872
def _all_children(self, trans_id):
1873
children = self._all_children_cache.get(trans_id)
1874
if children is not None:
1876
children = set(self._transform.iter_tree_children(trans_id))
1877
# children in the _new_parent set are provided by _by_parent.
1878
children.difference_update(self._transform._new_parent.keys())
1879
children.update(self._by_parent.get(trans_id, []))
1880
self._all_children_cache[trans_id] = children
1883
def iter_children(self, file_id):
1884
trans_id = self._transform.trans_id_file_id(file_id)
1885
for child_trans_id in self._all_children(trans_id):
1886
yield self._transform.final_file_id(child_trans_id)
1889
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1890
in self._transform._tree.extras())
1891
possible_extras.update(self._transform._new_contents)
1892
possible_extras.update(self._transform._removed_id)
1893
for trans_id in possible_extras:
1894
if self._transform.final_file_id(trans_id) is None:
1895
yield self._final_paths._determine_path(trans_id)
1897
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1898
yield_parents=False):
1899
for trans_id, parent_file_id in ordered_entries:
1900
file_id = self._transform.final_file_id(trans_id)
1903
if (specific_file_ids is not None
1904
and file_id not in specific_file_ids):
1906
kind = self._transform.final_kind(trans_id)
1908
kind = self._transform._tree.stored_kind(file_id)
1909
new_entry = inventory.make_entry(
1911
self._transform.final_name(trans_id),
1912
parent_file_id, file_id)
1913
yield new_entry, trans_id
1915
def _list_files_by_dir(self):
1916
todo = [ROOT_PARENT]
1918
while len(todo) > 0:
1920
parent_file_id = self._transform.final_file_id(parent)
1921
children = list(self._all_children(parent))
1922
paths = dict(zip(children, self._final_paths.get_paths(children)))
1923
children.sort(key=paths.get)
1924
todo.extend(reversed(children))
1925
for trans_id in children:
1926
ordered_ids.append((trans_id, parent_file_id))
1929
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1930
# This may not be a maximally efficient implementation, but it is
1931
# reasonably straightforward. An implementation that grafts the
1932
# TreeTransform changes onto the tree's iter_entries_by_dir results
1933
# might be more efficient, but requires tricky inferences about stack
1935
ordered_ids = self._list_files_by_dir()
1936
for entry, trans_id in self._make_inv_entries(ordered_ids,
1937
specific_file_ids, yield_parents=yield_parents):
1938
yield unicode(self._final_paths.get_path(trans_id)), entry
1940
def _iter_entries_for_dir(self, dir_path):
1941
"""Return path, entry for items in a directory without recursing down."""
1942
dir_file_id = self.path2id(dir_path)
1944
for file_id in self.iter_children(dir_file_id):
1945
trans_id = self._transform.trans_id_file_id(file_id)
1946
ordered_ids.append((trans_id, file_id))
1947
for entry, trans_id in self._make_inv_entries(ordered_ids):
1948
yield unicode(self._final_paths.get_path(trans_id)), entry
1950
def list_files(self, include_root=False, from_dir=None, recursive=True):
1951
"""See WorkingTree.list_files."""
1952
# XXX This should behave like WorkingTree.list_files, but is really
1953
# more like RevisionTree.list_files.
1957
prefix = from_dir + '/'
1958
entries = self.iter_entries_by_dir()
1959
for path, entry in entries:
1960
if entry.name == '' and not include_root:
1963
if not path.startswith(prefix):
1965
path = path[len(prefix):]
1966
yield path, 'V', entry.kind, entry.file_id, entry
1968
if from_dir is None and include_root is True:
1969
root_entry = inventory.make_entry('directory', '',
1970
ROOT_PARENT, self.get_root_id())
1971
yield '', 'V', 'directory', root_entry.file_id, root_entry
1972
entries = self._iter_entries_for_dir(from_dir or '')
1973
for path, entry in entries:
1974
yield path, 'V', entry.kind, entry.file_id, entry
1976
def kind(self, file_id):
1977
trans_id = self._transform.trans_id_file_id(file_id)
1978
return self._transform.final_kind(trans_id)
1980
def stored_kind(self, file_id):
1981
trans_id = self._transform.trans_id_file_id(file_id)
1983
return self._transform._new_contents[trans_id]
1985
return self._transform._tree.stored_kind(file_id)
1987
def get_file_mtime(self, file_id, path=None):
1988
"""See Tree.get_file_mtime"""
1989
if not self._content_change(file_id):
1990
return self._transform._tree.get_file_mtime(file_id)
1991
return self._stat_limbo_file(file_id).st_mtime
1993
def _file_size(self, entry, stat_value):
1994
return self.get_file_size(entry.file_id)
1996
def get_file_size(self, file_id):
1997
"""See Tree.get_file_size"""
1998
if self.kind(file_id) == 'file':
1999
return self._transform._tree.get_file_size(file_id)
2003
def get_file_sha1(self, file_id, path=None, stat_value=None):
2004
trans_id = self._transform.trans_id_file_id(file_id)
2005
kind = self._transform._new_contents.get(trans_id)
2007
return self._transform._tree.get_file_sha1(file_id)
2009
fileobj = self.get_file(file_id)
2011
return sha_file(fileobj)
2015
def is_executable(self, file_id, path=None):
2018
trans_id = self._transform.trans_id_file_id(file_id)
2020
return self._transform._new_executability[trans_id]
2023
return self._transform._tree.is_executable(file_id, path)
2025
if e.errno == errno.ENOENT:
2028
except errors.NoSuchId:
2031
def path_content_summary(self, path):
2032
trans_id = self._path2trans_id(path)
2033
tt = self._transform
2034
tree_path = tt._tree_id_paths.get(trans_id)
2035
kind = tt._new_contents.get(trans_id)
2037
if tree_path is None or trans_id in tt._removed_contents:
2038
return 'missing', None, None, None
2039
summary = tt._tree.path_content_summary(tree_path)
2040
kind, size, executable, link_or_sha1 = summary
2043
limbo_name = tt._limbo_name(trans_id)
2044
if trans_id in tt._new_reference_revision:
2045
kind = 'tree-reference'
2047
statval = os.lstat(limbo_name)
2048
size = statval.st_size
2049
if not supports_executable():
2052
executable = statval.st_mode & S_IEXEC
2056
if kind == 'symlink':
2057
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2058
executable = tt._new_executability.get(trans_id, executable)
2059
return kind, size, executable, link_or_sha1
2061
def iter_changes(self, from_tree, include_unchanged=False,
2062
specific_files=None, pb=None, extra_trees=None,
2063
require_versioned=True, want_unversioned=False):
2064
"""See InterTree.iter_changes.
2066
This has a fast path that is only used when the from_tree matches
2067
the transform tree, and no fancy options are supplied.
2069
if (from_tree is not self._transform._tree or include_unchanged or
2070
specific_files or want_unversioned):
2071
return tree.InterTree(from_tree, self).iter_changes(
2072
include_unchanged=include_unchanged,
2073
specific_files=specific_files,
2075
extra_trees=extra_trees,
2076
require_versioned=require_versioned,
2077
want_unversioned=want_unversioned)
2078
if want_unversioned:
2079
raise ValueError('want_unversioned is not supported')
2080
return self._transform.iter_changes()
2082
def get_file(self, file_id, path=None):
2083
"""See Tree.get_file"""
2084
if not self._content_change(file_id):
2085
return self._transform._tree.get_file(file_id, path)
2086
trans_id = self._transform.trans_id_file_id(file_id)
2087
name = self._transform._limbo_name(trans_id)
2088
return open(name, 'rb')
2090
def get_file_with_stat(self, file_id, path=None):
2091
return self.get_file(file_id, path), None
2093
def annotate_iter(self, file_id,
2094
default_revision=_mod_revision.CURRENT_REVISION):
2095
changes = self._iter_changes_cache.get(file_id)
2099
changed_content, versioned, kind = (changes[2], changes[3],
2103
get_old = (kind[0] == 'file' and versioned[0])
2105
old_annotation = self._transform._tree.annotate_iter(file_id,
2106
default_revision=default_revision)
2110
return old_annotation
2111
if not changed_content:
2112
return old_annotation
2113
# TODO: This is doing something similar to what WT.annotate_iter is
2114
# doing, however it fails slightly because it doesn't know what
2115
# the *other* revision_id is, so it doesn't know how to give the
2116
# other as the origin for some lines, they all get
2117
# 'default_revision'
2118
# It would be nice to be able to use the new Annotator based
2119
# approach, as well.
2120
return annotate.reannotate([old_annotation],
2121
self.get_file(file_id).readlines(),
2124
def get_symlink_target(self, file_id):
2125
"""See Tree.get_symlink_target"""
2126
if not self._content_change(file_id):
2127
return self._transform._tree.get_symlink_target(file_id)
2128
trans_id = self._transform.trans_id_file_id(file_id)
2129
name = self._transform._limbo_name(trans_id)
2130
return osutils.readlink(name)
2132
def walkdirs(self, prefix=''):
2133
pending = [self._transform.root]
2134
while len(pending) > 0:
2135
parent_id = pending.pop()
2138
prefix = prefix.rstrip('/')
2139
parent_path = self._final_paths.get_path(parent_id)
2140
parent_file_id = self._transform.final_file_id(parent_id)
2141
for child_id in self._all_children(parent_id):
2142
path_from_root = self._final_paths.get_path(child_id)
2143
basename = self._transform.final_name(child_id)
2144
file_id = self._transform.final_file_id(child_id)
2145
kind = self._transform.final_kind(child_id)
2146
if kind is not None:
2147
versioned_kind = kind
2150
versioned_kind = self._transform._tree.stored_kind(file_id)
2151
if versioned_kind == 'directory':
2152
subdirs.append(child_id)
2153
children.append((path_from_root, basename, kind, None,
2154
file_id, versioned_kind))
2156
if parent_path.startswith(prefix):
2157
yield (parent_path, parent_file_id), children
2158
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2161
def get_parent_ids(self):
2162
return self._parent_ids
2164
def set_parent_ids(self, parent_ids):
2165
self._parent_ids = parent_ids
2167
def get_revision_tree(self, revision_id):
2168
return self._transform._tree.get_revision_tree(revision_id)
2171
def joinpath(parent, child):
2172
"""Join tree-relative paths, handling the tree root specially"""
2173
if parent is None or parent == "":
2176
return pathjoin(parent, child)
2179
class FinalPaths(object):
2180
"""Make path calculation cheap by memoizing paths.
2182
The underlying tree must not be manipulated between calls, or else
2183
the results will likely be incorrect.
2185
def __init__(self, transform):
2186
object.__init__(self)
2187
self._known_paths = {}
2188
self.transform = transform
2190
def _determine_path(self, trans_id):
2191
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2193
name = self.transform.final_name(trans_id)
2194
parent_id = self.transform.final_parent(trans_id)
2195
if parent_id == self.transform.root:
2198
return pathjoin(self.get_path(parent_id), name)
2200
def get_path(self, trans_id):
2201
"""Find the final path associated with a trans_id"""
2202
if trans_id not in self._known_paths:
2203
self._known_paths[trans_id] = self._determine_path(trans_id)
2204
return self._known_paths[trans_id]
2206
def get_paths(self, trans_ids):
2207
return [(self.get_path(t), t) for t in trans_ids]
2211
def topology_sorted_ids(tree):
2212
"""Determine the topological order of the ids in a tree"""
2213
file_ids = list(tree)
2214
file_ids.sort(key=tree.id2path)
2218
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2219
delta_from_tree=False):
2220
"""Create working tree for a branch, using a TreeTransform.
2222
This function should be used on empty trees, having a tree root at most.
2223
(see merge and revert functionality for working with existing trees)
2225
Existing files are handled like so:
2227
- Existing bzrdirs take precedence over creating new items. They are
2228
created as '%s.diverted' % name.
2229
- Otherwise, if the content on disk matches the content we are building,
2230
it is silently replaced.
2231
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2233
:param tree: The tree to convert wt into a copy of
2234
:param wt: The working tree that files will be placed into
2235
:param accelerator_tree: A tree which can be used for retrieving file
2236
contents more quickly than tree itself, i.e. a workingtree. tree
2237
will be used for cases where accelerator_tree's content is different.
2238
:param hardlink: If true, hard-link files to accelerator_tree, where
2239
possible. accelerator_tree must implement abspath, i.e. be a
2241
:param delta_from_tree: If true, build_tree may use the input Tree to
2242
generate the inventory delta.
2244
wt.lock_tree_write()
2248
if accelerator_tree is not None:
2249
accelerator_tree.lock_read()
2251
return _build_tree(tree, wt, accelerator_tree, hardlink,
2254
if accelerator_tree is not None:
2255
accelerator_tree.unlock()
2262
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2263
"""See build_tree."""
2264
for num, _unused in enumerate(wt.all_file_ids()):
2265
if num > 0: # more than just a root
2266
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2268
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2269
pp = ProgressPhase("Build phase", 2, top_pb)
2270
if tree.inventory.root is not None:
2271
# This is kind of a hack: we should be altering the root
2272
# as part of the regular tree shape diff logic.
2273
# The conditional test here is to avoid doing an
2274
# expensive operation (flush) every time the root id
2275
# is set within the tree, nor setting the root and thus
2276
# marking the tree as dirty, because we use two different
2277
# idioms here: tree interfaces and inventory interfaces.
2278
if wt.get_root_id() != tree.get_root_id():
2279
wt.set_root_id(tree.get_root_id())
2281
tt = TreeTransform(wt)
2285
file_trans_id[wt.get_root_id()] = \
2286
tt.trans_id_tree_file_id(wt.get_root_id())
2287
pb = bzrlib.ui.ui_factory.nested_progress_bar()
2289
deferred_contents = []
2291
total = len(tree.inventory)
2293
precomputed_delta = []
2295
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
for num, (tree_path, entry) in \
2306
enumerate(tree.inventory.iter_entries_by_dir()):
2307
pb.update("Building tree", num - len(deferred_contents), total)
2308
if entry.parent_id is None:
2311
file_id = entry.file_id
2313
precomputed_delta.append((None, tree_path, file_id, entry))
2314
if tree_path in existing_files:
2315
target_path = wt.abspath(tree_path)
2316
kind = file_kind(target_path)
2317
if kind == "directory":
2319
bzrdir.BzrDir.open(target_path)
2320
except errors.NotBranchError:
2324
if (file_id not in divert and
2325
_content_match(tree, entry, file_id, kind,
2327
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2328
if kind == 'directory':
2330
parent_id = file_trans_id[entry.parent_id]
2331
if entry.kind == 'file':
2332
# We *almost* replicate new_by_entry, so that we can defer
2333
# getting the file text, and get them all at once.
2334
trans_id = tt.create_path(entry.name, parent_id)
2335
file_trans_id[file_id] = trans_id
2336
tt.version_file(file_id, trans_id)
2337
executable = tree.is_executable(file_id, tree_path)
2339
tt.set_executability(executable, trans_id)
2340
trans_data = (trans_id, tree_path)
2341
deferred_contents.append((file_id, trans_data))
2343
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2346
new_trans_id = file_trans_id[file_id]
2347
old_parent = tt.trans_id_tree_path(tree_path)
2348
_reparent_children(tt, old_parent, new_trans_id)
2349
offset = num + 1 - len(deferred_contents)
2350
_create_files(tt, tree, deferred_contents, pb, offset,
2351
accelerator_tree, hardlink)
2355
divert_trans = set(file_trans_id[f] for f in divert)
2356
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2357
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2358
if len(raw_conflicts) > 0:
2359
precomputed_delta = None
2360
conflicts = cook_conflicts(raw_conflicts, tt)
2361
for conflict in conflicts:
2364
wt.add_conflicts(conflicts)
2365
except errors.UnsupportedOperation:
2367
result = tt.apply(no_conflicts=True,
2368
precomputed_delta=precomputed_delta)
2375
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2377
total = len(desired_files) + offset
2379
if accelerator_tree is None:
2380
new_desired_files = desired_files
2382
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)
2389
new_desired_files = []
2391
for file_id, (trans_id, tree_path) in desired_files:
2392
accelerator_path = unchanged.get(file_id)
2393
if accelerator_path is None:
2394
new_desired_files.append((file_id, (trans_id, tree_path)))
2396
pb.update('Adding file contents', count + offset, total)
2398
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2401
contents = accelerator_tree.get_file(file_id, accelerator_path)
2402
if wt.supports_content_filtering():
2403
filters = wt._content_filter_stack(tree_path)
2404
contents = filtered_output_bytes(contents, filters,
2405
ContentFilterContext(tree_path, tree))
2407
tt.create_file(contents, trans_id)
2411
except AttributeError:
2412
# after filtering, contents may no longer be file-like
2416
for count, ((trans_id, tree_path), contents) in enumerate(
2417
tree.iter_files_bytes(new_desired_files)):
2418
if wt.supports_content_filtering():
2419
filters = wt._content_filter_stack(tree_path)
2420
contents = filtered_output_bytes(contents, filters,
2421
ContentFilterContext(tree_path, tree))
2422
tt.create_file(contents, trans_id)
2423
pb.update('Adding file contents', count + offset, total)
2426
def _reparent_children(tt, old_parent, new_parent):
2427
for child in tt.iter_tree_children(old_parent):
2428
tt.adjust_path(tt.final_name(child), new_parent, child)
2430
def _reparent_transform_children(tt, old_parent, new_parent):
2431
by_parent = tt.by_parent()
2432
for child in by_parent[old_parent]:
2433
tt.adjust_path(tt.final_name(child), new_parent, child)
2434
return by_parent[old_parent]
2436
def _content_match(tree, entry, file_id, kind, target_path):
2437
if entry.kind != kind:
2439
if entry.kind == "directory":
2441
if entry.kind == "file":
2442
f = file(target_path, 'rb')
2444
if tree.get_file_text(file_id) == f.read():
2448
elif entry.kind == "symlink":
2449
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2454
def resolve_checkout(tt, conflicts, divert):
2455
new_conflicts = set()
2456
for c_type, conflict in ((c[0], c) for c in conflicts):
2457
# Anything but a 'duplicate' would indicate programmer error
2458
if c_type != 'duplicate':
2459
raise AssertionError(c_type)
2460
# Now figure out which is new and which is old
2461
if tt.new_contents(conflict[1]):
2462
new_file = conflict[1]
2463
old_file = conflict[2]
2465
new_file = conflict[2]
2466
old_file = conflict[1]
2468
# We should only get here if the conflict wasn't completely
2470
final_parent = tt.final_parent(old_file)
2471
if new_file in divert:
2472
new_name = tt.final_name(old_file)+'.diverted'
2473
tt.adjust_path(new_name, final_parent, new_file)
2474
new_conflicts.add((c_type, 'Diverted to',
2475
new_file, old_file))
2477
new_name = tt.final_name(old_file)+'.moved'
2478
tt.adjust_path(new_name, final_parent, old_file)
2479
new_conflicts.add((c_type, 'Moved existing file to',
2480
old_file, new_file))
2481
return new_conflicts
2484
def new_by_entry(tt, entry, parent_id, tree):
2485
"""Create a new file according to its inventory entry"""
2489
contents = tree.get_file(entry.file_id).readlines()
2490
executable = tree.is_executable(entry.file_id)
2491
return tt.new_file(name, parent_id, contents, entry.file_id,
2493
elif kind in ('directory', 'tree-reference'):
2494
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2495
if kind == 'tree-reference':
2496
tt.set_tree_reference(entry.reference_revision, trans_id)
2498
elif kind == 'symlink':
2499
target = tree.get_symlink_target(entry.file_id)
2500
return tt.new_symlink(name, parent_id, target, entry.file_id)
2502
raise errors.BadFileKindError(name, kind)
2505
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2506
filter_tree_path=None):
2507
"""Create new file contents according to tree contents.
2509
:param filter_tree_path: the tree path to use to lookup
2510
content filters to apply to the bytes output in the working tree.
2511
This only applies if the working tree supports content filtering.
2513
kind = tree.kind(file_id)
2514
if kind == 'directory':
2515
tt.create_directory(trans_id)
2516
elif kind == "file":
2518
tree_file = tree.get_file(file_id)
2520
bytes = tree_file.readlines()
2524
if wt.supports_content_filtering() and filter_tree_path is not None:
2525
filters = wt._content_filter_stack(filter_tree_path)
2526
bytes = filtered_output_bytes(bytes, filters,
2527
ContentFilterContext(filter_tree_path, tree))
2528
tt.create_file(bytes, trans_id)
2529
elif kind == "symlink":
2530
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2532
raise AssertionError('Unknown kind %r' % kind)
2535
def create_entry_executability(tt, entry, trans_id):
2536
"""Set the executability of a trans_id according to an inventory entry"""
2537
if entry.kind == "file":
2538
tt.set_executability(entry.executable, trans_id)
2541
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2542
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2545
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2546
"""Produce a backup-style name that appears to be available"""
2550
yield "%s.~%d~" % (name, counter)
2552
for new_name in name_gen():
2553
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2557
def _entry_changes(file_id, entry, working_tree):
2558
"""Determine in which ways the inventory entry has changed.
2560
Returns booleans: has_contents, content_mod, meta_mod
2561
has_contents means there are currently contents, but they differ
2562
contents_mod means contents need to be modified
2563
meta_mod means the metadata needs to be modified
2565
cur_entry = working_tree.inventory[file_id]
2567
working_kind = working_tree.kind(file_id)
2570
has_contents = False
2573
if has_contents is True:
2574
if entry.kind != working_kind:
2575
contents_mod, meta_mod = True, False
2577
cur_entry._read_tree_state(working_tree.id2path(file_id),
2579
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2580
cur_entry._forget_tree_state()
2581
return has_contents, contents_mod, meta_mod
2584
def revert(working_tree, target_tree, filenames, backups=False,
2585
pb=None, change_reporter=None):
2586
"""Revert a working tree's contents to those of a target tree."""
2587
target_tree.lock_read()
2588
pb = ui.ui_factory.nested_progress_bar()
2589
tt = TreeTransform(working_tree, pb)
2591
pp = ProgressPhase("Revert phase", 3, pb)
2592
conflicts, merge_modified = _prepare_revert_transform(
2593
working_tree, target_tree, tt, filenames, backups, pp)
2595
change_reporter = delta._ChangeReporter(
2596
unversioned_filter=working_tree.is_ignored)
2597
delta.report_changes(tt.iter_changes(), change_reporter)
2598
for conflict in conflicts:
2602
working_tree.set_merge_modified(merge_modified)
2604
target_tree.unlock()
2610
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2611
backups, pp, basis_tree=None,
2612
merge_modified=None):
2613
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2615
if merge_modified is None:
2616
merge_modified = working_tree.merge_modified()
2617
merge_modified = _alter_files(working_tree, target_tree, tt,
2618
child_pb, filenames, backups,
2619
merge_modified, basis_tree)
2622
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2624
raw_conflicts = resolve_conflicts(tt, child_pb,
2625
lambda t, c: conflict_pass(t, c, target_tree))
2628
conflicts = cook_conflicts(raw_conflicts, tt)
2629
return conflicts, merge_modified
2632
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2633
backups, merge_modified, basis_tree=None):
2634
if basis_tree is not None:
2635
basis_tree.lock_read()
2636
change_list = target_tree.iter_changes(working_tree,
2637
specific_files=specific_files, pb=pb)
2638
if target_tree.get_root_id() is None:
2644
for id_num, (file_id, path, changed_content, versioned, parent, name,
2645
kind, executable) in enumerate(change_list):
2646
if skip_root and file_id[0] is not None and parent[0] is None:
2648
trans_id = tt.trans_id_file_id(file_id)
2651
keep_content = False
2652
if kind[0] == 'file' and (backups or kind[1] is None):
2653
wt_sha1 = working_tree.get_file_sha1(file_id)
2654
if merge_modified.get(file_id) != wt_sha1:
2655
# acquire the basis tree lazily to prevent the
2656
# expense of accessing it when it's not needed ?
2657
# (Guessing, RBC, 200702)
2658
if basis_tree is None:
2659
basis_tree = working_tree.basis_tree()
2660
basis_tree.lock_read()
2661
if file_id in basis_tree:
2662
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2664
elif kind[1] is None and not versioned[1]:
2666
if kind[0] is not None:
2667
if not keep_content:
2668
tt.delete_contents(trans_id)
2669
elif kind[1] is not None:
2670
parent_trans_id = tt.trans_id_file_id(parent[0])
2671
by_parent = tt.by_parent()
2672
backup_name = _get_backup_name(name[0], by_parent,
2673
parent_trans_id, tt)
2674
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2675
new_trans_id = tt.create_path(name[0], parent_trans_id)
2676
if versioned == (True, True):
2677
tt.unversion_file(trans_id)
2678
tt.version_file(file_id, new_trans_id)
2679
# New contents should have the same unix perms as old
2682
trans_id = new_trans_id
2683
if kind[1] in ('directory', 'tree-reference'):
2684
tt.create_directory(trans_id)
2685
if kind[1] == 'tree-reference':
2686
revision = target_tree.get_reference_revision(file_id,
2688
tt.set_tree_reference(revision, trans_id)
2689
elif kind[1] == 'symlink':
2690
tt.create_symlink(target_tree.get_symlink_target(file_id),
2692
elif kind[1] == 'file':
2693
deferred_files.append((file_id, (trans_id, mode_id)))
2694
if basis_tree is None:
2695
basis_tree = working_tree.basis_tree()
2696
basis_tree.lock_read()
2697
new_sha1 = target_tree.get_file_sha1(file_id)
2698
if (file_id in basis_tree and new_sha1 ==
2699
basis_tree.get_file_sha1(file_id)):
2700
if file_id in merge_modified:
2701
del merge_modified[file_id]
2703
merge_modified[file_id] = new_sha1
2705
# preserve the execute bit when backing up
2706
if keep_content and executable[0] == executable[1]:
2707
tt.set_executability(executable[1], trans_id)
2708
elif kind[1] is not None:
2709
raise AssertionError(kind[1])
2710
if versioned == (False, True):
2711
tt.version_file(file_id, trans_id)
2712
if versioned == (True, False):
2713
tt.unversion_file(trans_id)
2714
if (name[1] is not None and
2715
(name[0] != name[1] or parent[0] != parent[1])):
2716
if name[1] == '' and parent[1] is None:
2717
parent_trans = ROOT_PARENT
2719
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)
2724
if executable[0] != executable[1] and kind[1] == "file":
2725
tt.set_executability(executable[1], trans_id)
2726
if working_tree.supports_content_filtering():
2727
for index, ((trans_id, mode_id), bytes) in enumerate(
2728
target_tree.iter_files_bytes(deferred_files)):
2729
file_id = deferred_files[index][0]
2730
# We're reverting a tree to the target tree so using the
2731
# target tree to find the file path seems the best choice
2732
# here IMO - Ian C 27/Oct/2009
2733
filter_tree_path = target_tree.id2path(file_id)
2734
filters = working_tree._content_filter_stack(filter_tree_path)
2735
bytes = filtered_output_bytes(bytes, filters,
2736
ContentFilterContext(filter_tree_path, working_tree))
2737
tt.create_file(bytes, trans_id, mode_id)
2739
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2741
tt.create_file(bytes, trans_id, mode_id)
2742
tt.fixup_new_roots()
2744
if basis_tree is not None:
2746
return merge_modified
2749
def resolve_conflicts(tt, pb=None, pass_func=None):
2750
"""Make many conflict-resolution attempts, but die if they fail"""
2751
if pass_func is None:
2752
pass_func = conflict_pass
2753
new_conflicts = set()
2754
pb = ui.ui_factory.nested_progress_bar()
2757
pb.update('Resolution pass', n+1, 10)
2758
conflicts = tt.find_conflicts()
2759
if len(conflicts) == 0:
2760
return new_conflicts
2761
new_conflicts.update(pass_func(tt, conflicts))
2762
raise MalformedTransform(conflicts=conflicts)
2767
def conflict_pass(tt, conflicts, path_tree=None):
2768
"""Resolve some classes of conflicts.
2770
:param tt: The transform to resolve conflicts in
2771
:param conflicts: The conflicts to resolve
2772
:param path_tree: A Tree to get supplemental paths from
2774
new_conflicts = set()
2775
for c_type, conflict in ((c[0], c) for c in conflicts):
2776
if c_type == 'duplicate id':
2777
tt.unversion_file(conflict[1])
2778
new_conflicts.add((c_type, 'Unversioned existing file',
2779
conflict[1], conflict[2], ))
2780
elif c_type == 'duplicate':
2781
# files that were renamed take precedence
2782
final_parent = tt.final_parent(conflict[1])
2783
if tt.path_changed(conflict[1]):
2784
existing_file, new_file = conflict[2], conflict[1]
2786
existing_file, new_file = conflict[1], conflict[2]
2787
new_name = tt.final_name(existing_file)+'.moved'
2788
tt.adjust_path(new_name, final_parent, existing_file)
2789
new_conflicts.add((c_type, 'Moved existing file to',
2790
existing_file, new_file))
2791
elif c_type == 'parent loop':
2792
# break the loop by undoing one of the ops that caused the loop
2794
while not tt.path_changed(cur):
2795
cur = tt.final_parent(cur)
2796
new_conflicts.add((c_type, 'Cancelled move', cur,
2797
tt.final_parent(cur),))
2798
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2800
elif c_type == 'missing parent':
2801
trans_id = conflict[1]
2803
tt.cancel_deletion(trans_id)
2804
new_conflicts.add(('deleting parent', 'Not deleting',
2809
tt.final_name(trans_id)
2811
if path_tree is not None:
2812
file_id = tt.final_file_id(trans_id)
2814
file_id = tt.inactive_file_id(trans_id)
2815
entry = path_tree.inventory[file_id]
2816
# special-case the other tree root (move its
2817
# children to current root)
2818
if entry.parent_id is None:
2820
moved = _reparent_transform_children(
2821
tt, trans_id, tt.root)
2823
new_conflicts.add((c_type, 'Moved to root',
2826
parent_trans_id = tt.trans_id_file_id(
2828
tt.adjust_path(entry.name, parent_trans_id,
2831
tt.create_directory(trans_id)
2832
new_conflicts.add((c_type, 'Created directory', trans_id))
2833
elif c_type == 'unversioned parent':
2834
file_id = tt.inactive_file_id(conflict[1])
2835
# special-case the other tree root (move its children instead)
2836
if path_tree and file_id in path_tree:
2837
if path_tree.inventory[file_id].parent_id is None:
2839
tt.version_file(file_id, conflict[1])
2840
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2841
elif c_type == 'non-directory parent':
2842
parent_id = conflict[1]
2843
parent_parent = tt.final_parent(parent_id)
2844
parent_name = tt.final_name(parent_id)
2845
parent_file_id = tt.final_file_id(parent_id)
2846
new_parent_id = tt.new_directory(parent_name + '.new',
2847
parent_parent, parent_file_id)
2848
_reparent_transform_children(tt, parent_id, new_parent_id)
2849
if parent_file_id is not None:
2850
tt.unversion_file(parent_id)
2851
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2852
elif c_type == 'versioning no contents':
2853
tt.cancel_versioning(conflict[1])
2854
return new_conflicts
2857
def cook_conflicts(raw_conflicts, tt):
2858
"""Generate a list of cooked conflicts, sorted by file path"""
2859
from bzrlib.conflicts import Conflict
2860
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2861
return sorted(conflict_iter, key=Conflict.sort_key)
2864
def iter_cook_conflicts(raw_conflicts, tt):
2865
from bzrlib.conflicts import Conflict
2867
for conflict in raw_conflicts:
2868
c_type = conflict[0]
2869
action = conflict[1]
2870
modified_path = fp.get_path(conflict[2])
2871
modified_id = tt.final_file_id(conflict[2])
2872
if len(conflict) == 3:
2873
yield Conflict.factory(c_type, action=action, path=modified_path,
2874
file_id=modified_id)
2877
conflicting_path = fp.get_path(conflict[3])
2878
conflicting_id = tt.final_file_id(conflict[3])
2879
yield Conflict.factory(c_type, action=action, path=modified_path,
2880
file_id=modified_id,
2881
conflict_path=conflicting_path,
2882
conflict_file_id=conflicting_id)
2885
class _FileMover(object):
2886
"""Moves and deletes files for TreeTransform, tracking operations"""
2889
self.past_renames = []
2890
self.pending_deletions = []
2892
def rename(self, from_, to):
2893
"""Rename a file from one path to another."""
2895
os.rename(from_, to)
2897
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2898
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
self.past_renames.append((from_, to))
2904
def pre_delete(self, from_, to):
2905
"""Rename a file out of the way and mark it for deletion.
2907
Unlike os.unlink, this works equally well for files and directories.
2908
:param from_: The current file path
2909
:param to: A temporary path for the file
2911
self.rename(from_, to)
2912
self.pending_deletions.append(to)
2915
"""Reverse all renames that have been performed"""
2916
for from_, to in reversed(self.past_renames):
2918
os.rename(to, from_)
2920
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
2921
# after rollback, don't reuse _FileMover
2923
pending_deletions = None
2925
def apply_deletions(self):
2926
"""Apply all marked deletions"""
2927
for path in self.pending_deletions:
2929
# after apply_deletions, don't reuse _FileMover
2931
pending_deletions = None