1
# Copyright (C) 2006, 2007, 2008, 2009 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(), """
33
revision as _mod_revision,
36
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
37
ReusingTransform, NotVersionedError, CantMoveRoot,
38
ExistingLimbo, ImmortalLimbo, NoFinalPath,
40
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
41
from bzrlib.inventory import InventoryEntry
42
from bzrlib.osutils import (
52
from bzrlib.progress import DummyProgress, ProgressPhase
53
from bzrlib.symbol_versioning import (
57
from bzrlib.trace import mutter, warning
58
from bzrlib import tree
60
import bzrlib.urlutils as urlutils
63
ROOT_PARENT = "root-parent"
66
def unique_add(map, key, value):
68
raise DuplicateKey(key=key)
72
class _TransformResults(object):
73
def __init__(self, modified_paths, rename_count):
75
self.modified_paths = modified_paths
76
self.rename_count = rename_count
79
class TreeTransformBase(object):
80
"""The base class for TreeTransform and its kin."""
82
def __init__(self, tree, pb=DummyProgress(),
86
:param tree: The tree that will be transformed, but not necessarily
88
:param pb: A ProgressTask indicating how much progress is being made
89
:param case_sensitive: If True, the target of the transform is
90
case sensitive, not just case preserving.
95
# mapping of trans_id -> new basename
97
# mapping of trans_id -> new parent trans_id
99
# mapping of trans_id with new contents -> new file_kind
100
self._new_contents = {}
101
# Set of trans_ids whose contents will be removed
102
self._removed_contents = set()
103
# Mapping of trans_id -> new execute-bit value
104
self._new_executability = {}
105
# Mapping of trans_id -> new tree-reference value
106
self._new_reference_revision = {}
107
# Mapping of trans_id -> new file_id
109
# Mapping of old file-id -> trans_id
110
self._non_present_ids = {}
111
# Mapping of new file_id -> trans_id
113
# Set of trans_ids that will be removed
114
self._removed_id = set()
115
# Mapping of path in old tree -> trans_id
116
self._tree_path_ids = {}
117
# Mapping trans_id -> path in old tree
118
self._tree_id_paths = {}
119
# The trans_id that will be used as the tree root
120
root_id = tree.get_root_id()
121
if root_id is not None:
122
self._new_root = self.trans_id_tree_file_id(root_id)
124
self._new_root = None
125
# Indictor of whether the transform has been applied
129
# Whether the target is case sensitive
130
self._case_sensitive_target = case_sensitive
131
# A counter of how many files have been renamed
132
self.rename_count = 0
135
"""Release the working tree lock, if held.
137
This is required if apply has not been invoked, but can be invoked
140
if self._tree is None:
145
def __get_root(self):
146
return self._new_root
148
root = property(__get_root)
150
def _assign_id(self):
151
"""Produce a new tranform id"""
152
new_id = "new-%s" % self._id_number
156
def create_path(self, name, parent):
157
"""Assign a transaction id to a new path"""
158
trans_id = self._assign_id()
159
unique_add(self._new_name, trans_id, name)
160
unique_add(self._new_parent, trans_id, parent)
163
def adjust_path(self, name, parent, trans_id):
164
"""Change the path that is assigned to a transaction id."""
165
if trans_id == self._new_root:
167
self._new_name[trans_id] = name
168
self._new_parent[trans_id] = parent
169
if parent == ROOT_PARENT:
170
if self._new_root is not None:
171
raise ValueError("Cannot have multiple roots.")
172
self._new_root = trans_id
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 trans_id_tree_file_id(self, inventory_id):
207
"""Determine the transaction id of a working tree file.
209
This reflects only files that already exist, not ones that will be
210
added by transactions.
212
if inventory_id is None:
213
raise ValueError('None is not a valid file id')
214
path = self._tree.id2path(inventory_id)
215
return self.trans_id_tree_path(path)
217
def trans_id_file_id(self, file_id):
218
"""Determine or set the transaction id associated with a file ID.
219
A new id is only created for file_ids that were never present. If
220
a transaction has been unversioned, it is deliberately still returned.
221
(this will likely lead to an unversioned parent conflict.)
224
raise ValueError('None is not a valid file id')
225
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
226
return self._r_new_id[file_id]
229
self._tree.iter_entries_by_dir([file_id]).next()
230
except StopIteration:
231
if file_id in self._non_present_ids:
232
return self._non_present_ids[file_id]
234
trans_id = self._assign_id()
235
self._non_present_ids[file_id] = trans_id
238
return self.trans_id_tree_file_id(file_id)
240
def trans_id_tree_path(self, path):
241
"""Determine (and maybe set) the transaction ID for a tree path."""
242
path = self.canonical_path(path)
243
if path not in self._tree_path_ids:
244
self._tree_path_ids[path] = self._assign_id()
245
self._tree_id_paths[self._tree_path_ids[path]] = path
246
return self._tree_path_ids[path]
248
def get_tree_parent(self, trans_id):
249
"""Determine id of the parent in the tree."""
250
path = self._tree_id_paths[trans_id]
253
return self.trans_id_tree_path(os.path.dirname(path))
255
def delete_contents(self, trans_id):
256
"""Schedule the contents of a path entry for deletion"""
257
self.tree_kind(trans_id)
258
self._removed_contents.add(trans_id)
260
def cancel_deletion(self, trans_id):
261
"""Cancel a scheduled deletion"""
262
self._removed_contents.remove(trans_id)
264
def unversion_file(self, trans_id):
265
"""Schedule a path entry to become unversioned"""
266
self._removed_id.add(trans_id)
268
def delete_versioned(self, trans_id):
269
"""Delete and unversion a versioned file"""
270
self.delete_contents(trans_id)
271
self.unversion_file(trans_id)
273
def set_executability(self, executability, trans_id):
274
"""Schedule setting of the 'execute' bit
275
To unschedule, set to None
277
if executability is None:
278
del self._new_executability[trans_id]
280
unique_add(self._new_executability, trans_id, executability)
282
def set_tree_reference(self, revision_id, trans_id):
283
"""Set the reference associated with a directory"""
284
unique_add(self._new_reference_revision, trans_id, revision_id)
286
def version_file(self, file_id, trans_id):
287
"""Schedule a file to become versioned."""
290
unique_add(self._new_id, trans_id, file_id)
291
unique_add(self._r_new_id, file_id, trans_id)
293
def cancel_versioning(self, trans_id):
294
"""Undo a previous versioning of a file"""
295
file_id = self._new_id[trans_id]
296
del self._new_id[trans_id]
297
del self._r_new_id[file_id]
299
def new_paths(self, filesystem_only=False):
300
"""Determine the paths of all new and changed files.
302
:param filesystem_only: if True, only calculate values for files
303
that require renames or execute bit changes.
307
stale_ids = self._needs_rename.difference(self._new_name)
308
stale_ids.difference_update(self._new_parent)
309
stale_ids.difference_update(self._new_contents)
310
stale_ids.difference_update(self._new_id)
311
needs_rename = self._needs_rename.difference(stale_ids)
312
id_sets = (needs_rename, self._new_executability)
314
id_sets = (self._new_name, self._new_parent, self._new_contents,
315
self._new_id, self._new_executability)
316
for id_set in id_sets:
317
new_ids.update(id_set)
318
return sorted(FinalPaths(self).get_paths(new_ids))
320
def _inventory_altered(self):
321
"""Get the trans_ids and paths of files needing new inv entries."""
323
for id_set in [self._new_name, self._new_parent, self._new_id,
324
self._new_executability]:
325
new_ids.update(id_set)
326
changed_kind = set(self._removed_contents)
327
changed_kind.intersection_update(self._new_contents)
328
changed_kind.difference_update(new_ids)
329
changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
331
new_ids.update(changed_kind)
332
return sorted(FinalPaths(self).get_paths(new_ids))
334
def final_kind(self, trans_id):
335
"""Determine the final file kind, after any changes applied.
337
Raises NoSuchFile if the file does not exist/has no contents.
338
(It is conceivable that a path would be created without the
339
corresponding contents insertion command)
341
if trans_id in self._new_contents:
342
return self._new_contents[trans_id]
343
elif trans_id in self._removed_contents:
344
raise NoSuchFile(None)
346
return self.tree_kind(trans_id)
348
def tree_file_id(self, trans_id):
349
"""Determine the file id associated with the trans_id in the tree"""
351
path = self._tree_id_paths[trans_id]
353
# the file is a new, unversioned file, or invalid trans_id
355
# the file is old; the old id is still valid
356
if self._new_root == trans_id:
357
return self._tree.get_root_id()
358
return self._tree.path2id(path)
360
def final_file_id(self, trans_id):
361
"""Determine the file id after any changes are applied, or None.
363
None indicates that the file will not be versioned after changes are
367
return self._new_id[trans_id]
369
if trans_id in self._removed_id:
371
return self.tree_file_id(trans_id)
373
def inactive_file_id(self, trans_id):
374
"""Return the inactive file_id associated with a transaction id.
375
That is, the one in the tree or in non_present_ids.
376
The file_id may actually be active, too.
378
file_id = self.tree_file_id(trans_id)
379
if file_id is not None:
381
for key, value in self._non_present_ids.iteritems():
382
if value == trans_id:
385
def final_parent(self, trans_id):
386
"""Determine the parent file_id, after any changes are applied.
388
ROOT_PARENT is returned for the tree root.
391
return self._new_parent[trans_id]
393
return self.get_tree_parent(trans_id)
395
def final_name(self, trans_id):
396
"""Determine the final filename, after all changes are applied."""
398
return self._new_name[trans_id]
401
return os.path.basename(self._tree_id_paths[trans_id])
403
raise NoFinalPath(trans_id, self)
406
"""Return a map of parent: children for known parents.
408
Only new paths and parents of tree files with assigned ids are used.
411
items = list(self._new_parent.iteritems())
412
items.extend((t, self.final_parent(t)) for t in
413
self._tree_id_paths.keys())
414
for trans_id, parent_id in items:
415
if parent_id not in by_parent:
416
by_parent[parent_id] = set()
417
by_parent[parent_id].add(trans_id)
420
def path_changed(self, trans_id):
421
"""Return True if a trans_id's path has changed."""
422
return (trans_id in self._new_name) or (trans_id in self._new_parent)
424
def new_contents(self, trans_id):
425
return (trans_id in self._new_contents)
427
def find_conflicts(self):
428
"""Find any violations of inventory or filesystem invariants"""
429
if self._done is True:
430
raise ReusingTransform()
432
# ensure all children of all existent parents are known
433
# all children of non-existent parents are known, by definition.
434
self._add_tree_children()
435
by_parent = self.by_parent()
436
conflicts.extend(self._unversioned_parents(by_parent))
437
conflicts.extend(self._parent_loops())
438
conflicts.extend(self._duplicate_entries(by_parent))
439
conflicts.extend(self._duplicate_ids())
440
conflicts.extend(self._parent_type_conflicts(by_parent))
441
conflicts.extend(self._improper_versioning())
442
conflicts.extend(self._executability_conflicts())
443
conflicts.extend(self._overwrite_conflicts())
446
def _check_malformed(self):
447
conflicts = self.find_conflicts()
448
if len(conflicts) != 0:
449
raise MalformedTransform(conflicts=conflicts)
451
def _add_tree_children(self):
452
"""Add all the children of all active parents to the known paths.
454
Active parents are those which gain children, and those which are
455
removed. This is a necessary first step in detecting conflicts.
457
parents = self.by_parent().keys()
458
parents.extend([t for t in self._removed_contents if
459
self.tree_kind(t) == 'directory'])
460
for trans_id in self._removed_id:
461
file_id = self.tree_file_id(trans_id)
462
if file_id is not None:
463
if self._tree.inventory[file_id].kind == 'directory':
464
parents.append(trans_id)
465
elif self.tree_kind(trans_id) == 'directory':
466
parents.append(trans_id)
468
for parent_id in parents:
469
# ensure that all children are registered with the transaction
470
list(self.iter_tree_children(parent_id))
472
def has_named_child(self, by_parent, parent_id, name):
474
children = by_parent[parent_id]
477
for child in children:
478
if self.final_name(child) == name:
481
path = self._tree_id_paths[parent_id]
484
childpath = joinpath(path, name)
485
child_id = self._tree_path_ids.get(childpath)
487
return lexists(self._tree.abspath(childpath))
489
if self.final_parent(child_id) != parent_id:
491
if child_id in self._removed_contents:
492
# XXX What about dangling file-ids?
497
def _parent_loops(self):
498
"""No entry should be its own ancestor"""
500
for trans_id in self._new_parent:
503
while parent_id is not ROOT_PARENT:
506
parent_id = self.final_parent(parent_id)
509
if parent_id == trans_id:
510
conflicts.append(('parent loop', trans_id))
511
if parent_id in seen:
515
def _unversioned_parents(self, by_parent):
516
"""If parent directories are versioned, children must be versioned."""
518
for parent_id, children in by_parent.iteritems():
519
if parent_id is ROOT_PARENT:
521
if self.final_file_id(parent_id) is not None:
523
for child_id in children:
524
if self.final_file_id(child_id) is not None:
525
conflicts.append(('unversioned parent', parent_id))
529
def _improper_versioning(self):
530
"""Cannot version a file with no contents, or a bad type.
532
However, existing entries with no contents are okay.
535
for trans_id in self._new_id.iterkeys():
537
kind = self.final_kind(trans_id)
539
conflicts.append(('versioning no contents', trans_id))
541
if not InventoryEntry.versionable_kind(kind):
542
conflicts.append(('versioning bad kind', trans_id, kind))
545
def _executability_conflicts(self):
546
"""Check for bad executability changes.
548
Only versioned files may have their executability set, because
549
1. only versioned entries can have executability under windows
550
2. only files can be executable. (The execute bit on a directory
551
does not indicate searchability)
554
for trans_id in self._new_executability:
555
if self.final_file_id(trans_id) is None:
556
conflicts.append(('unversioned executability', trans_id))
559
non_file = self.final_kind(trans_id) != "file"
563
conflicts.append(('non-file executability', trans_id))
566
def _overwrite_conflicts(self):
567
"""Check for overwrites (not permitted on Win32)"""
569
for trans_id in self._new_contents:
571
self.tree_kind(trans_id)
574
if trans_id not in self._removed_contents:
575
conflicts.append(('overwrite', trans_id,
576
self.final_name(trans_id)))
579
def _duplicate_entries(self, by_parent):
580
"""No directory may have two entries with the same name."""
582
if (self._new_name, self._new_parent) == ({}, {}):
584
for children in by_parent.itervalues():
585
name_ids = [(self.final_name(t), t) for t in children]
586
if not self._case_sensitive_target:
587
name_ids = [(n.lower(), t) for n, t in name_ids]
591
for name, trans_id in name_ids:
593
kind = self.final_kind(trans_id)
596
file_id = self.final_file_id(trans_id)
597
if kind is None and file_id is None:
599
if name == last_name:
600
conflicts.append(('duplicate', last_trans_id, trans_id,
603
last_trans_id = trans_id
606
def _duplicate_ids(self):
607
"""Each inventory id may only be used once"""
609
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
611
all_ids = self._tree.all_file_ids()
612
active_tree_ids = all_ids.difference(removed_tree_ids)
613
for trans_id, file_id in self._new_id.iteritems():
614
if file_id in active_tree_ids:
615
old_trans_id = self.trans_id_tree_file_id(file_id)
616
conflicts.append(('duplicate id', old_trans_id, trans_id))
619
def _parent_type_conflicts(self, by_parent):
620
"""parents must have directory 'contents'."""
622
for parent_id, children in by_parent.iteritems():
623
if parent_id is ROOT_PARENT:
625
if not self._any_contents(children):
627
for child in children:
629
self.final_kind(child)
633
kind = self.final_kind(parent_id)
637
conflicts.append(('missing parent', parent_id))
638
elif kind != "directory":
639
conflicts.append(('non-directory parent', parent_id))
642
def _any_contents(self, trans_ids):
643
"""Return true if any of the trans_ids, will have contents."""
644
for trans_id in trans_ids:
646
kind = self.final_kind(trans_id)
652
def _set_executability(self, path, trans_id):
653
"""Set the executability of versioned files """
654
if supports_executable():
655
new_executability = self._new_executability[trans_id]
656
abspath = self._tree.abspath(path)
657
current_mode = os.stat(abspath).st_mode
658
if new_executability:
661
to_mode = current_mode | (0100 & ~umask)
662
# Enable x-bit for others only if they can read it.
663
if current_mode & 0004:
664
to_mode |= 0001 & ~umask
665
if current_mode & 0040:
666
to_mode |= 0010 & ~umask
668
to_mode = current_mode & ~0111
669
os.chmod(abspath, to_mode)
671
def _new_entry(self, name, parent_id, file_id):
672
"""Helper function to create a new filesystem entry."""
673
trans_id = self.create_path(name, parent_id)
674
if file_id is not None:
675
self.version_file(file_id, trans_id)
678
def new_file(self, name, parent_id, contents, file_id=None,
680
"""Convenience method to create files.
682
name is the name of the file to create.
683
parent_id is the transaction id of the parent directory of the file.
684
contents is an iterator of bytestrings, which will be used to produce
686
:param file_id: The inventory ID of the file, if it is to be versioned.
687
:param executable: Only valid when a file_id has been supplied.
689
trans_id = self._new_entry(name, parent_id, file_id)
690
# TODO: rather than scheduling a set_executable call,
691
# have create_file create the file with the right mode.
692
self.create_file(contents, trans_id)
693
if executable is not None:
694
self.set_executability(executable, trans_id)
697
def new_directory(self, name, parent_id, file_id=None):
698
"""Convenience method to create directories.
700
name is the name of the directory to create.
701
parent_id is the transaction id of the parent directory of the
703
file_id is the inventory ID of the directory, if it is to be versioned.
705
trans_id = self._new_entry(name, parent_id, file_id)
706
self.create_directory(trans_id)
709
def new_symlink(self, name, parent_id, target, file_id=None):
710
"""Convenience method to create symbolic link.
712
name is the name of the symlink to create.
713
parent_id is the transaction id of the parent directory of the symlink.
714
target is a bytestring of the target of the symlink.
715
file_id is the inventory ID of the file, if it is to be versioned.
717
trans_id = self._new_entry(name, parent_id, file_id)
718
self.create_symlink(target, trans_id)
721
def _affected_ids(self):
722
"""Return the set of transform ids affected by the transform"""
723
trans_ids = set(self._removed_id)
724
trans_ids.update(self._new_id.keys())
725
trans_ids.update(self._removed_contents)
726
trans_ids.update(self._new_contents.keys())
727
trans_ids.update(self._new_executability.keys())
728
trans_ids.update(self._new_name.keys())
729
trans_ids.update(self._new_parent.keys())
732
def _get_file_id_maps(self):
733
"""Return mapping of file_ids to trans_ids in the to and from states"""
734
trans_ids = self._affected_ids()
737
# Build up two dicts: trans_ids associated with file ids in the
738
# FROM state, vs the TO state.
739
for trans_id in trans_ids:
740
from_file_id = self.tree_file_id(trans_id)
741
if from_file_id is not None:
742
from_trans_ids[from_file_id] = trans_id
743
to_file_id = self.final_file_id(trans_id)
744
if to_file_id is not None:
745
to_trans_ids[to_file_id] = trans_id
746
return from_trans_ids, to_trans_ids
748
def _from_file_data(self, from_trans_id, from_versioned, file_id):
749
"""Get data about a file in the from (tree) state
751
Return a (name, parent, kind, executable) tuple
753
from_path = self._tree_id_paths.get(from_trans_id)
755
# get data from working tree if versioned
756
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
757
from_name = from_entry.name
758
from_parent = from_entry.parent_id
761
if from_path is None:
762
# File does not exist in FROM state
766
# File exists, but is not versioned. Have to use path-
768
from_name = os.path.basename(from_path)
769
tree_parent = self.get_tree_parent(from_trans_id)
770
from_parent = self.tree_file_id(tree_parent)
771
if from_path is not None:
772
from_kind, from_executable, from_stats = \
773
self._tree._comparison_data(from_entry, from_path)
776
from_executable = False
777
return from_name, from_parent, from_kind, from_executable
779
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
780
"""Get data about a file in the to (target) state
782
Return a (name, parent, kind, executable) tuple
784
to_name = self.final_name(to_trans_id)
786
to_kind = self.final_kind(to_trans_id)
789
to_parent = self.final_file_id(self.final_parent(to_trans_id))
790
if to_trans_id in self._new_executability:
791
to_executable = self._new_executability[to_trans_id]
792
elif to_trans_id == from_trans_id:
793
to_executable = from_executable
795
to_executable = False
796
return to_name, to_parent, to_kind, to_executable
798
def iter_changes(self):
799
"""Produce output in the same format as Tree.iter_changes.
801
Will produce nonsensical results if invoked while inventory/filesystem
802
conflicts (as reported by TreeTransform.find_conflicts()) are present.
804
This reads the Transform, but only reproduces changes involving a
805
file_id. Files that are not versioned in either of the FROM or TO
806
states are not reflected.
808
final_paths = FinalPaths(self)
809
from_trans_ids, to_trans_ids = self._get_file_id_maps()
811
# Now iterate through all active file_ids
812
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
814
from_trans_id = from_trans_ids.get(file_id)
815
# find file ids, and determine versioning state
816
if from_trans_id is None:
817
from_versioned = False
818
from_trans_id = to_trans_ids[file_id]
820
from_versioned = True
821
to_trans_id = to_trans_ids.get(file_id)
822
if to_trans_id is None:
824
to_trans_id = from_trans_id
828
from_name, from_parent, from_kind, from_executable = \
829
self._from_file_data(from_trans_id, from_versioned, file_id)
831
to_name, to_parent, to_kind, to_executable = \
832
self._to_file_data(to_trans_id, from_trans_id, from_executable)
834
if not from_versioned:
837
from_path = self._tree_id_paths.get(from_trans_id)
841
to_path = final_paths.get_path(to_trans_id)
842
if from_kind != to_kind:
844
elif to_kind in ('file', 'symlink') and (
845
to_trans_id != from_trans_id or
846
to_trans_id in self._new_contents):
848
if (not modified and from_versioned == to_versioned and
849
from_parent==to_parent and from_name == to_name and
850
from_executable == to_executable):
852
results.append((file_id, (from_path, to_path), modified,
853
(from_versioned, to_versioned),
854
(from_parent, to_parent),
855
(from_name, to_name),
856
(from_kind, to_kind),
857
(from_executable, to_executable)))
858
return iter(sorted(results, key=lambda x:x[1]))
860
def get_preview_tree(self):
861
"""Return a tree representing the result of the transform.
863
The tree is a snapshot, and altering the TreeTransform will invalidate
866
return _PreviewTree(self)
868
def commit(self, branch, message, merge_parents=None, strict=False):
869
"""Commit the result of this TreeTransform to a branch.
871
:param branch: The branch to commit to.
872
:param message: The message to attach to the commit.
873
:param merge_parents: Additional parents specified by pending merges.
874
:return: The revision_id of the revision committed.
876
self._check_malformed()
878
unversioned = set(self._new_contents).difference(set(self._new_id))
879
for trans_id in unversioned:
880
if self.final_file_id(trans_id) is None:
881
raise errors.StrictCommitFailed()
883
revno, last_rev_id = branch.last_revision_info()
884
if last_rev_id == _mod_revision.NULL_REVISION:
885
if merge_parents is not None:
886
raise ValueError('Cannot supply merge parents for first'
890
parent_ids = [last_rev_id]
891
if merge_parents is not None:
892
parent_ids.extend(merge_parents)
893
if self._tree.get_revision_id() != last_rev_id:
894
raise ValueError('TreeTransform not based on branch basis: %s' %
895
self._tree.get_revision_id())
896
builder = branch.get_commit_builder(parent_ids)
897
preview = self.get_preview_tree()
898
list(builder.record_iter_changes(preview, last_rev_id,
899
self.iter_changes()))
900
builder.finish_inventory()
901
revision_id = builder.commit(message)
902
branch.set_last_revision_info(revno + 1, revision_id)
905
def _text_parent(self, trans_id):
906
file_id = self.tree_file_id(trans_id)
908
if file_id is None or self._tree.kind(file_id) != 'file':
910
except errors.NoSuchFile:
914
def _get_parents_texts(self, trans_id):
915
"""Get texts for compression parents of this file."""
916
file_id = self._text_parent(trans_id)
919
return (self._tree.get_file_text(file_id),)
921
def _get_parents_lines(self, trans_id):
922
"""Get lines for compression parents of this file."""
923
file_id = self._text_parent(trans_id)
926
return (self._tree.get_file_lines(file_id),)
928
def serialize(self, serializer):
929
"""Serialize this TreeTransform.
931
:param serializer: A Serialiser like pack.ContainerSerializer.
933
new_name = dict((k, v.encode('utf-8')) for k, v in
934
self._new_name.items())
935
new_executability = dict((k, int(v)) for k, v in
936
self._new_executability.items())
937
tree_path_ids = dict((k.encode('utf-8'), v)
938
for k, v in self._tree_path_ids.items())
940
'_id_number': self._id_number,
941
'_new_name': new_name,
942
'_new_parent': self._new_parent,
943
'_new_executability': new_executability,
944
'_new_id': self._new_id,
945
'_tree_path_ids': tree_path_ids,
946
'_removed_id': list(self._removed_id),
947
'_removed_contents': list(self._removed_contents),
948
'_non_present_ids': self._non_present_ids,
950
yield serializer.bytes_record(bencode.bencode(attribs),
952
for trans_id, kind in self._new_contents.items():
954
lines = osutils.chunks_to_lines(
955
self._read_file_chunks(trans_id))
956
parents = self._get_parents_lines(trans_id)
957
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
958
content = ''.join(mpdiff.to_patch())
959
if kind == 'directory':
961
if kind == 'symlink':
962
content = self._read_symlink_target(trans_id)
963
yield serializer.bytes_record(content, ((trans_id, kind),))
965
def deserialize(self, records):
966
"""Deserialize a stored TreeTransform.
968
:param records: An iterable of (names, content) tuples, as per
969
pack.ContainerPushParser.
971
names, content = records.next()
972
attribs = bencode.bdecode(content)
973
self._id_number = attribs['_id_number']
974
self._new_name = dict((k, v.decode('utf-8'))
975
for k, v in attribs['_new_name'].items())
976
self._new_parent = attribs['_new_parent']
977
self._new_executability = dict((k, bool(v)) for k, v in
978
attribs['_new_executability'].items())
979
self._new_id = attribs['_new_id']
980
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
981
self._tree_path_ids = {}
982
self._tree_id_paths = {}
983
for bytepath, trans_id in attribs['_tree_path_ids'].items():
984
path = bytepath.decode('utf-8')
985
self._tree_path_ids[path] = trans_id
986
self._tree_id_paths[trans_id] = path
987
self._removed_id = set(attribs['_removed_id'])
988
self._removed_contents = set(attribs['_removed_contents'])
989
self._non_present_ids = attribs['_non_present_ids']
990
for ((trans_id, kind),), content in records:
992
mpdiff = multiparent.MultiParent.from_patch(content)
993
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
994
self.create_file(lines, trans_id)
995
if kind == 'directory':
996
self.create_directory(trans_id)
997
if kind == 'symlink':
998
self.create_symlink(content.decode('utf-8'), trans_id)
1001
class DiskTreeTransform(TreeTransformBase):
1002
"""Tree transform storing its contents on disk."""
1004
def __init__(self, tree, limbodir, pb=DummyProgress(),
1005
case_sensitive=True):
1007
:param tree: The tree that will be transformed, but not necessarily
1009
:param limbodir: A directory where new files can be stored until
1010
they are installed in their proper places
1011
:param pb: A ProgressBar indicating how much progress is being made
1012
:param case_sensitive: If True, the target of the transform is
1013
case sensitive, not just case preserving.
1015
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1016
self._limbodir = limbodir
1017
self._deletiondir = None
1018
# A mapping of transform ids to their limbo filename
1019
self._limbo_files = {}
1020
# A mapping of transform ids to a set of the transform ids of children
1021
# that their limbo directory has
1022
self._limbo_children = {}
1023
# Map transform ids to maps of child filename to child transform id
1024
self._limbo_children_names = {}
1025
# List of transform ids that need to be renamed from limbo into place
1026
self._needs_rename = set()
1027
self._creation_mtime = None
1030
"""Release the working tree lock, if held, clean up limbo dir.
1032
This is required if apply has not been invoked, but can be invoked
1035
if self._tree is None:
1038
entries = [(self._limbo_name(t), t, k) for t, k in
1039
self._new_contents.iteritems()]
1040
entries.sort(reverse=True)
1041
for path, trans_id, kind in entries:
1044
delete_any(self._limbodir)
1046
# We don't especially care *why* the dir is immortal.
1047
raise ImmortalLimbo(self._limbodir)
1049
if self._deletiondir is not None:
1050
delete_any(self._deletiondir)
1052
raise errors.ImmortalPendingDeletion(self._deletiondir)
1054
TreeTransformBase.finalize(self)
1056
def _limbo_name(self, trans_id):
1057
"""Generate the limbo name of a file"""
1058
limbo_name = self._limbo_files.get(trans_id)
1059
if limbo_name is None:
1060
limbo_name = self._generate_limbo_path(trans_id)
1061
self._limbo_files[trans_id] = limbo_name
1064
def _generate_limbo_path(self, trans_id):
1065
"""Generate a limbo path using the trans_id as the relative path.
1067
This is suitable as a fallback, and when the transform should not be
1068
sensitive to the path encoding of the limbo directory.
1070
self._needs_rename.add(trans_id)
1071
return pathjoin(self._limbodir, trans_id)
1073
def adjust_path(self, name, parent, trans_id):
1074
previous_parent = self._new_parent.get(trans_id)
1075
previous_name = self._new_name.get(trans_id)
1076
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1077
if (trans_id in self._limbo_files and
1078
trans_id not in self._needs_rename):
1079
self._rename_in_limbo([trans_id])
1080
self._limbo_children[previous_parent].remove(trans_id)
1081
del self._limbo_children_names[previous_parent][previous_name]
1083
def _rename_in_limbo(self, trans_ids):
1084
"""Fix limbo names so that the right final path is produced.
1086
This means we outsmarted ourselves-- we tried to avoid renaming
1087
these files later by creating them with their final names in their
1088
final parents. But now the previous name or parent is no longer
1089
suitable, so we have to rename them.
1091
Even for trans_ids that have no new contents, we must remove their
1092
entries from _limbo_files, because they are now stale.
1094
for trans_id in trans_ids:
1095
old_path = self._limbo_files.pop(trans_id)
1096
if trans_id not in self._new_contents:
1098
new_path = self._limbo_name(trans_id)
1099
os.rename(old_path, new_path)
1100
for descendant in self._limbo_descendants(trans_id):
1101
desc_path = self._limbo_files[descendant]
1102
desc_path = new_path + desc_path[len(old_path):]
1103
self._limbo_files[descendant] = desc_path
1105
def _limbo_descendants(self, trans_id):
1106
"""Return the set of trans_ids whose limbo paths descend from this."""
1107
descendants = set(self._limbo_children.get(trans_id, []))
1108
for descendant in list(descendants):
1109
descendants.update(self._limbo_descendants(descendant))
1112
def create_file(self, contents, trans_id, mode_id=None):
1113
"""Schedule creation of a new file.
1117
Contents is an iterator of strings, all of which will be written
1118
to the target destination.
1120
New file takes the permissions of any existing file with that id,
1121
unless mode_id is specified.
1123
name = self._limbo_name(trans_id)
1124
f = open(name, 'wb')
1127
unique_add(self._new_contents, trans_id, 'file')
1129
# Clean up the file, it never got registered so
1130
# TreeTransform.finalize() won't clean it up.
1135
f.writelines(contents)
1138
self._set_mtime(name)
1139
self._set_mode(trans_id, mode_id, S_ISREG)
1141
def _read_file_chunks(self, trans_id):
1142
cur_file = open(self._limbo_name(trans_id), 'rb')
1144
return cur_file.readlines()
1148
def _read_symlink_target(self, trans_id):
1149
return os.readlink(self._limbo_name(trans_id))
1151
def _set_mtime(self, path):
1152
"""All files that are created get the same mtime.
1154
This time is set by the first object to be created.
1156
if self._creation_mtime is None:
1157
self._creation_mtime = time.time()
1158
os.utime(path, (self._creation_mtime, self._creation_mtime))
1160
def create_hardlink(self, path, trans_id):
1161
"""Schedule creation of a hard link"""
1162
name = self._limbo_name(trans_id)
1166
if e.errno != errno.EPERM:
1168
raise errors.HardLinkNotSupported(path)
1170
unique_add(self._new_contents, trans_id, 'file')
1172
# Clean up the file, it never got registered so
1173
# TreeTransform.finalize() won't clean it up.
1177
def create_directory(self, trans_id):
1178
"""Schedule creation of a new directory.
1180
See also new_directory.
1182
os.mkdir(self._limbo_name(trans_id))
1183
unique_add(self._new_contents, trans_id, 'directory')
1185
def create_symlink(self, target, trans_id):
1186
"""Schedule creation of a new symbolic link.
1188
target is a bytestring.
1189
See also new_symlink.
1192
os.symlink(target, self._limbo_name(trans_id))
1193
unique_add(self._new_contents, trans_id, 'symlink')
1196
path = FinalPaths(self).get_path(trans_id)
1199
raise UnableCreateSymlink(path=path)
1201
def cancel_creation(self, trans_id):
1202
"""Cancel the creation of new file contents."""
1203
del self._new_contents[trans_id]
1204
children = self._limbo_children.get(trans_id)
1205
# if this is a limbo directory with children, move them before removing
1207
if children is not None:
1208
self._rename_in_limbo(children)
1209
del self._limbo_children[trans_id]
1210
del self._limbo_children_names[trans_id]
1211
delete_any(self._limbo_name(trans_id))
1214
class TreeTransform(DiskTreeTransform):
1215
"""Represent a tree transformation.
1217
This object is designed to support incremental generation of the transform,
1220
However, it gives optimum performance when parent directories are created
1221
before their contents. The transform is then able to put child files
1222
directly in their parent directory, avoiding later renames.
1224
It is easy to produce malformed transforms, but they are generally
1225
harmless. Attempting to apply a malformed transform will cause an
1226
exception to be raised before any modifications are made to the tree.
1228
Many kinds of malformed transforms can be corrected with the
1229
resolve_conflicts function. The remaining ones indicate programming error,
1230
such as trying to create a file with no path.
1232
Two sets of file creation methods are supplied. Convenience methods are:
1237
These are composed of the low-level methods:
1239
* create_file or create_directory or create_symlink
1243
Transform/Transaction ids
1244
-------------------------
1245
trans_ids are temporary ids assigned to all files involved in a transform.
1246
It's possible, even common, that not all files in the Tree have trans_ids.
1248
trans_ids are used because filenames and file_ids are not good enough
1249
identifiers; filenames change, and not all files have file_ids. File-ids
1250
are also associated with trans-ids, so that moving a file moves its
1253
trans_ids are only valid for the TreeTransform that generated them.
1257
Limbo is a temporary directory use to hold new versions of files.
1258
Files are added to limbo by create_file, create_directory, create_symlink,
1259
and their convenience variants (new_*). Files may be removed from limbo
1260
using cancel_creation. Files are renamed from limbo into their final
1261
location as part of TreeTransform.apply
1263
Limbo must be cleaned up, by either calling TreeTransform.apply or
1264
calling TreeTransform.finalize.
1266
Files are placed into limbo inside their parent directories, where
1267
possible. This reduces subsequent renames, and makes operations involving
1268
lots of files faster. This optimization is only possible if the parent
1269
directory is created *before* creating any of its children, so avoid
1270
creating children before parents, where possible.
1274
This temporary directory is used by _FileMover for storing files that are
1275
about to be deleted. In case of rollback, the files will be restored.
1276
FileMover does not delete files until it is sure that a rollback will not
1279
def __init__(self, tree, pb=DummyProgress()):
1280
"""Note: a tree_write lock is taken on the tree.
1282
Use TreeTransform.finalize() to release the lock (can be omitted if
1283
TreeTransform.apply() called).
1285
tree.lock_tree_write()
1288
limbodir = urlutils.local_path_from_url(
1289
tree._transport.abspath('limbo'))
1293
if e.errno == errno.EEXIST:
1294
raise ExistingLimbo(limbodir)
1295
deletiondir = urlutils.local_path_from_url(
1296
tree._transport.abspath('pending-deletion'))
1298
os.mkdir(deletiondir)
1300
if e.errno == errno.EEXIST:
1301
raise errors.ExistingPendingDeletion(deletiondir)
1306
# Cache of realpath results, to speed up canonical_path
1307
self._realpaths = {}
1308
# Cache of relpath results, to speed up canonical_path
1310
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1311
tree.case_sensitive)
1312
self._deletiondir = deletiondir
1314
def canonical_path(self, path):
1315
"""Get the canonical tree-relative path"""
1316
# don't follow final symlinks
1317
abs = self._tree.abspath(path)
1318
if abs in self._relpaths:
1319
return self._relpaths[abs]
1320
dirname, basename = os.path.split(abs)
1321
if dirname not in self._realpaths:
1322
self._realpaths[dirname] = os.path.realpath(dirname)
1323
dirname = self._realpaths[dirname]
1324
abs = pathjoin(dirname, basename)
1325
if dirname in self._relpaths:
1326
relpath = pathjoin(self._relpaths[dirname], basename)
1327
relpath = relpath.rstrip('/\\')
1329
relpath = self._tree.relpath(abs)
1330
self._relpaths[abs] = relpath
1333
def tree_kind(self, trans_id):
1334
"""Determine the file kind in the working tree.
1336
Raises NoSuchFile if the file does not exist
1338
path = self._tree_id_paths.get(trans_id)
1340
raise NoSuchFile(None)
1342
return file_kind(self._tree.abspath(path))
1344
if e.errno != errno.ENOENT:
1347
raise NoSuchFile(path)
1349
def _set_mode(self, trans_id, mode_id, typefunc):
1350
"""Set the mode of new file contents.
1351
The mode_id is the existing file to get the mode from (often the same
1352
as trans_id). The operation is only performed if there's a mode match
1353
according to typefunc.
1358
old_path = self._tree_id_paths[mode_id]
1362
mode = os.stat(self._tree.abspath(old_path)).st_mode
1364
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1365
# Either old_path doesn't exist, or the parent of the
1366
# target is not a directory (but will be one eventually)
1367
# Either way, we know it doesn't exist *right now*
1368
# See also bug #248448
1373
os.chmod(self._limbo_name(trans_id), mode)
1375
def iter_tree_children(self, parent_id):
1376
"""Iterate through the entry's tree children, if any"""
1378
path = self._tree_id_paths[parent_id]
1382
children = os.listdir(self._tree.abspath(path))
1384
if not (osutils._is_error_enotdir(e)
1385
or e.errno in (errno.ENOENT, errno.ESRCH)):
1389
for child in children:
1390
childpath = joinpath(path, child)
1391
if self._tree.is_control_filename(childpath):
1393
yield self.trans_id_tree_path(childpath)
1395
def _generate_limbo_path(self, trans_id):
1396
"""Generate a limbo path using the final path if possible.
1398
This optimizes the performance of applying the tree transform by
1399
avoiding renames. These renames can be avoided only when the parent
1400
directory is already scheduled for creation.
1402
If the final path cannot be used, falls back to using the trans_id as
1405
parent = self._new_parent.get(trans_id)
1406
# if the parent directory is already in limbo (e.g. when building a
1407
# tree), choose a limbo name inside the parent, to reduce further
1409
use_direct_path = False
1410
if self._new_contents.get(parent) == 'directory':
1411
filename = self._new_name.get(trans_id)
1412
if filename is not None:
1413
if parent not in self._limbo_children:
1414
self._limbo_children[parent] = set()
1415
self._limbo_children_names[parent] = {}
1416
use_direct_path = True
1417
# the direct path can only be used if no other file has
1418
# already taken this pathname, i.e. if the name is unused, or
1419
# if it is already associated with this trans_id.
1420
elif self._case_sensitive_target:
1421
if (self._limbo_children_names[parent].get(filename)
1422
in (trans_id, None)):
1423
use_direct_path = True
1425
for l_filename, l_trans_id in\
1426
self._limbo_children_names[parent].iteritems():
1427
if l_trans_id == trans_id:
1429
if l_filename.lower() == filename.lower():
1432
use_direct_path = True
1434
if not use_direct_path:
1435
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1437
limbo_name = pathjoin(self._limbo_files[parent], filename)
1438
self._limbo_children[parent].add(trans_id)
1439
self._limbo_children_names[parent][filename] = trans_id
1443
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1444
"""Apply all changes to the inventory and filesystem.
1446
If filesystem or inventory conflicts are present, MalformedTransform
1449
If apply succeeds, finalize is not necessary.
1451
:param no_conflicts: if True, the caller guarantees there are no
1452
conflicts, so no check is made.
1453
:param precomputed_delta: An inventory delta to use instead of
1455
:param _mover: Supply an alternate FileMover, for testing
1457
if not no_conflicts:
1458
self._check_malformed()
1459
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1461
if precomputed_delta is None:
1462
child_pb.update('Apply phase', 0, 2)
1463
inventory_delta = self._generate_inventory_delta()
1466
inventory_delta = precomputed_delta
1469
mover = _FileMover()
1473
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1474
self._apply_removals(mover)
1475
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1476
modified_paths = self._apply_insertions(mover)
1481
mover.apply_deletions()
1484
self._tree.apply_inventory_delta(inventory_delta)
1487
return _TransformResults(modified_paths, self.rename_count)
1489
def _generate_inventory_delta(self):
1490
"""Generate an inventory delta for the current transform."""
1491
inventory_delta = []
1492
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1493
new_paths = self._inventory_altered()
1494
total_entries = len(new_paths) + len(self._removed_id)
1496
for num, trans_id in enumerate(self._removed_id):
1498
child_pb.update('removing file', num, total_entries)
1499
if trans_id == self._new_root:
1500
file_id = self._tree.get_root_id()
1502
file_id = self.tree_file_id(trans_id)
1503
# File-id isn't really being deleted, just moved
1504
if file_id in self._r_new_id:
1506
path = self._tree_id_paths[trans_id]
1507
inventory_delta.append((path, None, file_id, None))
1508
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1510
entries = self._tree.iter_entries_by_dir(
1511
new_path_file_ids.values())
1512
old_paths = dict((e.file_id, p) for p, e in entries)
1514
for num, (path, trans_id) in enumerate(new_paths):
1516
child_pb.update('adding file',
1517
num + len(self._removed_id), total_entries)
1518
file_id = new_path_file_ids[trans_id]
1523
kind = self.final_kind(trans_id)
1525
kind = self._tree.stored_kind(file_id)
1526
parent_trans_id = self.final_parent(trans_id)
1527
parent_file_id = new_path_file_ids.get(parent_trans_id)
1528
if parent_file_id is None:
1529
parent_file_id = self.final_file_id(parent_trans_id)
1530
if trans_id in self._new_reference_revision:
1531
new_entry = inventory.TreeReference(
1533
self._new_name[trans_id],
1534
self.final_file_id(self._new_parent[trans_id]),
1535
None, self._new_reference_revision[trans_id])
1537
new_entry = inventory.make_entry(kind,
1538
self.final_name(trans_id),
1539
parent_file_id, file_id)
1540
old_path = old_paths.get(new_entry.file_id)
1541
new_executability = self._new_executability.get(trans_id)
1542
if new_executability is not None:
1543
new_entry.executable = new_executability
1544
inventory_delta.append(
1545
(old_path, path, new_entry.file_id, new_entry))
1548
return inventory_delta
1550
def _apply_removals(self, mover):
1551
"""Perform tree operations that remove directory/inventory names.
1553
That is, delete files that are to be deleted, and put any files that
1554
need renaming into limbo. This must be done in strict child-to-parent
1557
If inventory_delta is None, no inventory delta generation is performed.
1559
tree_paths = list(self._tree_path_ids.iteritems())
1560
tree_paths.sort(reverse=True)
1561
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1563
for num, data in enumerate(tree_paths):
1564
path, trans_id = data
1565
child_pb.update('removing file', num, len(tree_paths))
1566
full_path = self._tree.abspath(path)
1567
if trans_id in self._removed_contents:
1568
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1570
elif trans_id in self._new_name or trans_id in \
1573
mover.rename(full_path, self._limbo_name(trans_id))
1575
if e.errno != errno.ENOENT:
1578
self.rename_count += 1
1582
def _apply_insertions(self, mover):
1583
"""Perform tree operations that insert directory/inventory names.
1585
That is, create any files that need to be created, and restore from
1586
limbo any files that needed renaming. This must be done in strict
1587
parent-to-child order.
1589
If inventory_delta is None, no inventory delta is calculated, and
1590
no list of modified paths is returned.
1592
new_paths = self.new_paths(filesystem_only=True)
1594
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1596
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1598
for num, (path, trans_id) in enumerate(new_paths):
1600
child_pb.update('adding file', num, len(new_paths))
1601
full_path = self._tree.abspath(path)
1602
if trans_id in self._needs_rename:
1604
mover.rename(self._limbo_name(trans_id), full_path)
1606
# We may be renaming a dangling inventory id
1607
if e.errno != errno.ENOENT:
1610
self.rename_count += 1
1611
if (trans_id in self._new_contents or
1612
self.path_changed(trans_id)):
1613
if trans_id in self._new_contents:
1614
modified_paths.append(full_path)
1615
if trans_id in self._new_executability:
1616
self._set_executability(path, trans_id)
1619
self._new_contents.clear()
1620
return modified_paths
1623
class TransformPreview(DiskTreeTransform):
1624
"""A TreeTransform for generating preview trees.
1626
Unlike TreeTransform, this version works when the input tree is a
1627
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1628
unversioned files in the input tree.
1631
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1633
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1634
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1636
def canonical_path(self, path):
1639
def tree_kind(self, trans_id):
1640
path = self._tree_id_paths.get(trans_id)
1642
raise NoSuchFile(None)
1643
file_id = self._tree.path2id(path)
1644
return self._tree.kind(file_id)
1646
def _set_mode(self, trans_id, mode_id, typefunc):
1647
"""Set the mode of new file contents.
1648
The mode_id is the existing file to get the mode from (often the same
1649
as trans_id). The operation is only performed if there's a mode match
1650
according to typefunc.
1652
# is it ok to ignore this? probably
1655
def iter_tree_children(self, parent_id):
1656
"""Iterate through the entry's tree children, if any"""
1658
path = self._tree_id_paths[parent_id]
1661
file_id = self.tree_file_id(parent_id)
1664
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1665
children = getattr(entry, 'children', {})
1666
for child in children:
1667
childpath = joinpath(path, child)
1668
yield self.trans_id_tree_path(childpath)
1671
class _PreviewTree(tree.Tree):
1672
"""Partial implementation of Tree to support show_diff_trees"""
1674
def __init__(self, transform):
1675
self._transform = transform
1676
self._final_paths = FinalPaths(transform)
1677
self.__by_parent = None
1678
self._parent_ids = []
1679
self._all_children_cache = {}
1680
self._path2trans_id_cache = {}
1681
self._final_name_cache = {}
1682
self._iter_changes_cache = dict((c[0], c) for c in
1683
self._transform.iter_changes())
1685
def _content_change(self, file_id):
1686
"""Return True if the content of this file changed"""
1687
changes = self._iter_changes_cache.get(file_id)
1688
# changes[2] is true if the file content changed. See
1689
# InterTree.iter_changes.
1690
return (changes is not None and changes[2])
1692
def _get_repository(self):
1693
repo = getattr(self._transform._tree, '_repository', None)
1695
repo = self._transform._tree.branch.repository
1698
def _iter_parent_trees(self):
1699
for revision_id in self.get_parent_ids():
1701
yield self.revision_tree(revision_id)
1702
except errors.NoSuchRevisionInTree:
1703
yield self._get_repository().revision_tree(revision_id)
1705
def _get_file_revision(self, file_id, vf, tree_revision):
1706
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1707
self._iter_parent_trees()]
1708
vf.add_lines((file_id, tree_revision), parent_keys,
1709
self.get_file(file_id).readlines())
1710
repo = self._get_repository()
1711
base_vf = repo.texts
1712
if base_vf not in vf.fallback_versionedfiles:
1713
vf.fallback_versionedfiles.append(base_vf)
1714
return tree_revision
1716
def _stat_limbo_file(self, file_id):
1717
trans_id = self._transform.trans_id_file_id(file_id)
1718
name = self._transform._limbo_name(trans_id)
1719
return os.lstat(name)
1722
def _by_parent(self):
1723
if self.__by_parent is None:
1724
self.__by_parent = self._transform.by_parent()
1725
return self.__by_parent
1727
def _comparison_data(self, entry, path):
1728
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1729
if kind == 'missing':
1733
file_id = self._transform.final_file_id(self._path2trans_id(path))
1734
executable = self.is_executable(file_id, path)
1735
return kind, executable, None
1737
def lock_read(self):
1738
# Perhaps in theory, this should lock the TreeTransform?
1745
def inventory(self):
1746
"""This Tree does not use inventory as its backing data."""
1747
raise NotImplementedError(_PreviewTree.inventory)
1749
def get_root_id(self):
1750
return self._transform.final_file_id(self._transform.root)
1752
def all_file_ids(self):
1753
tree_ids = set(self._transform._tree.all_file_ids())
1754
tree_ids.difference_update(self._transform.tree_file_id(t)
1755
for t in self._transform._removed_id)
1756
tree_ids.update(self._transform._new_id.values())
1760
return iter(self.all_file_ids())
1762
def _has_id(self, file_id, fallback_check):
1763
if file_id in self._transform._r_new_id:
1765
elif file_id in set([self._transform.tree_file_id(trans_id) for
1766
trans_id in self._transform._removed_id]):
1769
return fallback_check(file_id)
1771
def has_id(self, file_id):
1772
return self._has_id(file_id, self._transform._tree.has_id)
1774
def has_or_had_id(self, file_id):
1775
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1777
def _path2trans_id(self, path):
1778
# We must not use None here, because that is a valid value to store.
1779
trans_id = self._path2trans_id_cache.get(path, object)
1780
if trans_id is not object:
1782
segments = splitpath(path)
1783
cur_parent = self._transform.root
1784
for cur_segment in segments:
1785
for child in self._all_children(cur_parent):
1786
final_name = self._final_name_cache.get(child)
1787
if final_name is None:
1788
final_name = self._transform.final_name(child)
1789
self._final_name_cache[child] = final_name
1790
if final_name == cur_segment:
1794
self._path2trans_id_cache[path] = None
1796
self._path2trans_id_cache[path] = cur_parent
1799
def path2id(self, path):
1800
return self._transform.final_file_id(self._path2trans_id(path))
1802
def id2path(self, file_id):
1803
trans_id = self._transform.trans_id_file_id(file_id)
1805
return self._final_paths._determine_path(trans_id)
1807
raise errors.NoSuchId(self, file_id)
1809
def _all_children(self, trans_id):
1810
children = self._all_children_cache.get(trans_id)
1811
if children is not None:
1813
children = set(self._transform.iter_tree_children(trans_id))
1814
# children in the _new_parent set are provided by _by_parent.
1815
children.difference_update(self._transform._new_parent.keys())
1816
children.update(self._by_parent.get(trans_id, []))
1817
self._all_children_cache[trans_id] = children
1820
def iter_children(self, file_id):
1821
trans_id = self._transform.trans_id_file_id(file_id)
1822
for child_trans_id in self._all_children(trans_id):
1823
yield self._transform.final_file_id(child_trans_id)
1826
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1827
in self._transform._tree.extras())
1828
possible_extras.update(self._transform._new_contents)
1829
possible_extras.update(self._transform._removed_id)
1830
for trans_id in possible_extras:
1831
if self._transform.final_file_id(trans_id) is None:
1832
yield self._final_paths._determine_path(trans_id)
1834
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1835
yield_parents=False):
1836
for trans_id, parent_file_id in ordered_entries:
1837
file_id = self._transform.final_file_id(trans_id)
1840
if (specific_file_ids is not None
1841
and file_id not in specific_file_ids):
1844
kind = self._transform.final_kind(trans_id)
1846
kind = self._transform._tree.stored_kind(file_id)
1847
new_entry = inventory.make_entry(
1849
self._transform.final_name(trans_id),
1850
parent_file_id, file_id)
1851
yield new_entry, trans_id
1853
def _list_files_by_dir(self):
1854
todo = [ROOT_PARENT]
1856
while len(todo) > 0:
1858
parent_file_id = self._transform.final_file_id(parent)
1859
children = list(self._all_children(parent))
1860
paths = dict(zip(children, self._final_paths.get_paths(children)))
1861
children.sort(key=paths.get)
1862
todo.extend(reversed(children))
1863
for trans_id in children:
1864
ordered_ids.append((trans_id, parent_file_id))
1867
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1868
# This may not be a maximally efficient implementation, but it is
1869
# reasonably straightforward. An implementation that grafts the
1870
# TreeTransform changes onto the tree's iter_entries_by_dir results
1871
# might be more efficient, but requires tricky inferences about stack
1873
ordered_ids = self._list_files_by_dir()
1874
for entry, trans_id in self._make_inv_entries(ordered_ids,
1875
specific_file_ids, yield_parents=yield_parents):
1876
yield unicode(self._final_paths.get_path(trans_id)), entry
1878
def _iter_entries_for_dir(self, dir_path):
1879
"""Return path, entry for items in a directory without recursing down."""
1880
dir_file_id = self.path2id(dir_path)
1882
for file_id in self.iter_children(dir_file_id):
1883
trans_id = self._transform.trans_id_file_id(file_id)
1884
ordered_ids.append((trans_id, file_id))
1885
for entry, trans_id in self._make_inv_entries(ordered_ids):
1886
yield unicode(self._final_paths.get_path(trans_id)), entry
1888
def list_files(self, include_root=False, from_dir=None, recursive=True):
1889
"""See WorkingTree.list_files."""
1890
# XXX This should behave like WorkingTree.list_files, but is really
1891
# more like RevisionTree.list_files.
1895
prefix = from_dir + '/'
1896
entries = self.iter_entries_by_dir()
1897
for path, entry in entries:
1898
if entry.name == '' and not include_root:
1901
if not path.startswith(prefix):
1903
path = path[len(prefix):]
1904
yield path, 'V', entry.kind, entry.file_id, entry
1906
if from_dir is None and include_root is True:
1907
root_entry = inventory.make_entry('directory', '',
1908
ROOT_PARENT, self.get_root_id())
1909
yield '', 'V', 'directory', root_entry.file_id, root_entry
1910
entries = self._iter_entries_for_dir(from_dir or '')
1911
for path, entry in entries:
1912
yield path, 'V', entry.kind, entry.file_id, entry
1914
def kind(self, file_id):
1915
trans_id = self._transform.trans_id_file_id(file_id)
1916
return self._transform.final_kind(trans_id)
1918
def stored_kind(self, file_id):
1919
trans_id = self._transform.trans_id_file_id(file_id)
1921
return self._transform._new_contents[trans_id]
1923
return self._transform._tree.stored_kind(file_id)
1925
def get_file_mtime(self, file_id, path=None):
1926
"""See Tree.get_file_mtime"""
1927
if not self._content_change(file_id):
1928
return self._transform._tree.get_file_mtime(file_id)
1929
return self._stat_limbo_file(file_id).st_mtime
1931
def _file_size(self, entry, stat_value):
1932
return self.get_file_size(entry.file_id)
1934
def get_file_size(self, file_id):
1935
"""See Tree.get_file_size"""
1936
if self.kind(file_id) == 'file':
1937
return self._transform._tree.get_file_size(file_id)
1941
def get_file_sha1(self, file_id, path=None, stat_value=None):
1942
trans_id = self._transform.trans_id_file_id(file_id)
1943
kind = self._transform._new_contents.get(trans_id)
1945
return self._transform._tree.get_file_sha1(file_id)
1947
fileobj = self.get_file(file_id)
1949
return sha_file(fileobj)
1953
def is_executable(self, file_id, path=None):
1956
trans_id = self._transform.trans_id_file_id(file_id)
1958
return self._transform._new_executability[trans_id]
1961
return self._transform._tree.is_executable(file_id, path)
1963
if e.errno == errno.ENOENT:
1966
except errors.NoSuchId:
1969
def path_content_summary(self, path):
1970
trans_id = self._path2trans_id(path)
1971
tt = self._transform
1972
tree_path = tt._tree_id_paths.get(trans_id)
1973
kind = tt._new_contents.get(trans_id)
1975
if tree_path is None or trans_id in tt._removed_contents:
1976
return 'missing', None, None, None
1977
summary = tt._tree.path_content_summary(tree_path)
1978
kind, size, executable, link_or_sha1 = summary
1981
limbo_name = tt._limbo_name(trans_id)
1982
if trans_id in tt._new_reference_revision:
1983
kind = 'tree-reference'
1985
statval = os.lstat(limbo_name)
1986
size = statval.st_size
1987
if not supports_executable():
1990
executable = statval.st_mode & S_IEXEC
1994
if kind == 'symlink':
1995
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
1996
executable = tt._new_executability.get(trans_id, executable)
1997
return kind, size, executable, link_or_sha1
1999
def iter_changes(self, from_tree, include_unchanged=False,
2000
specific_files=None, pb=None, extra_trees=None,
2001
require_versioned=True, want_unversioned=False):
2002
"""See InterTree.iter_changes.
2004
This has a fast path that is only used when the from_tree matches
2005
the transform tree, and no fancy options are supplied.
2007
if (from_tree is not self._transform._tree or include_unchanged or
2008
specific_files or want_unversioned):
2009
return tree.InterTree(from_tree, self).iter_changes(
2010
include_unchanged=include_unchanged,
2011
specific_files=specific_files,
2013
extra_trees=extra_trees,
2014
require_versioned=require_versioned,
2015
want_unversioned=want_unversioned)
2016
if want_unversioned:
2017
raise ValueError('want_unversioned is not supported')
2018
return self._transform.iter_changes()
2020
def get_file(self, file_id, path=None):
2021
"""See Tree.get_file"""
2022
if not self._content_change(file_id):
2023
return self._transform._tree.get_file(file_id, path)
2024
trans_id = self._transform.trans_id_file_id(file_id)
2025
name = self._transform._limbo_name(trans_id)
2026
return open(name, 'rb')
2028
def get_file_with_stat(self, file_id, path=None):
2029
return self.get_file(file_id, path), None
2031
def annotate_iter(self, file_id,
2032
default_revision=_mod_revision.CURRENT_REVISION):
2033
changes = self._iter_changes_cache.get(file_id)
2037
changed_content, versioned, kind = (changes[2], changes[3],
2041
get_old = (kind[0] == 'file' and versioned[0])
2043
old_annotation = self._transform._tree.annotate_iter(file_id,
2044
default_revision=default_revision)
2048
return old_annotation
2049
if not changed_content:
2050
return old_annotation
2051
# TODO: This is doing something similar to what WT.annotate_iter is
2052
# doing, however it fails slightly because it doesn't know what
2053
# the *other* revision_id is, so it doesn't know how to give the
2054
# other as the origin for some lines, they all get
2055
# 'default_revision'
2056
# It would be nice to be able to use the new Annotator based
2057
# approach, as well.
2058
return annotate.reannotate([old_annotation],
2059
self.get_file(file_id).readlines(),
2062
def get_symlink_target(self, file_id):
2063
"""See Tree.get_symlink_target"""
2064
if not self._content_change(file_id):
2065
return self._transform._tree.get_symlink_target(file_id)
2066
trans_id = self._transform.trans_id_file_id(file_id)
2067
name = self._transform._limbo_name(trans_id)
2068
return osutils.readlink(name)
2070
def walkdirs(self, prefix=''):
2071
pending = [self._transform.root]
2072
while len(pending) > 0:
2073
parent_id = pending.pop()
2076
prefix = prefix.rstrip('/')
2077
parent_path = self._final_paths.get_path(parent_id)
2078
parent_file_id = self._transform.final_file_id(parent_id)
2079
for child_id in self._all_children(parent_id):
2080
path_from_root = self._final_paths.get_path(child_id)
2081
basename = self._transform.final_name(child_id)
2082
file_id = self._transform.final_file_id(child_id)
2084
kind = self._transform.final_kind(child_id)
2085
versioned_kind = kind
2088
versioned_kind = self._transform._tree.stored_kind(file_id)
2089
if versioned_kind == 'directory':
2090
subdirs.append(child_id)
2091
children.append((path_from_root, basename, kind, None,
2092
file_id, versioned_kind))
2094
if parent_path.startswith(prefix):
2095
yield (parent_path, parent_file_id), children
2096
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2099
def get_parent_ids(self):
2100
return self._parent_ids
2102
def set_parent_ids(self, parent_ids):
2103
self._parent_ids = parent_ids
2105
def get_revision_tree(self, revision_id):
2106
return self._transform._tree.get_revision_tree(revision_id)
2109
def joinpath(parent, child):
2110
"""Join tree-relative paths, handling the tree root specially"""
2111
if parent is None or parent == "":
2114
return pathjoin(parent, child)
2117
class FinalPaths(object):
2118
"""Make path calculation cheap by memoizing paths.
2120
The underlying tree must not be manipulated between calls, or else
2121
the results will likely be incorrect.
2123
def __init__(self, transform):
2124
object.__init__(self)
2125
self._known_paths = {}
2126
self.transform = transform
2128
def _determine_path(self, trans_id):
2129
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2131
name = self.transform.final_name(trans_id)
2132
parent_id = self.transform.final_parent(trans_id)
2133
if parent_id == self.transform.root:
2136
return pathjoin(self.get_path(parent_id), name)
2138
def get_path(self, trans_id):
2139
"""Find the final path associated with a trans_id"""
2140
if trans_id not in self._known_paths:
2141
self._known_paths[trans_id] = self._determine_path(trans_id)
2142
return self._known_paths[trans_id]
2144
def get_paths(self, trans_ids):
2145
return [(self.get_path(t), t) for t in trans_ids]
2149
def topology_sorted_ids(tree):
2150
"""Determine the topological order of the ids in a tree"""
2151
file_ids = list(tree)
2152
file_ids.sort(key=tree.id2path)
2156
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2157
delta_from_tree=False):
2158
"""Create working tree for a branch, using a TreeTransform.
2160
This function should be used on empty trees, having a tree root at most.
2161
(see merge and revert functionality for working with existing trees)
2163
Existing files are handled like so:
2165
- Existing bzrdirs take precedence over creating new items. They are
2166
created as '%s.diverted' % name.
2167
- Otherwise, if the content on disk matches the content we are building,
2168
it is silently replaced.
2169
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2171
:param tree: The tree to convert wt into a copy of
2172
:param wt: The working tree that files will be placed into
2173
:param accelerator_tree: A tree which can be used for retrieving file
2174
contents more quickly than tree itself, i.e. a workingtree. tree
2175
will be used for cases where accelerator_tree's content is different.
2176
:param hardlink: If true, hard-link files to accelerator_tree, where
2177
possible. accelerator_tree must implement abspath, i.e. be a
2179
:param delta_from_tree: If true, build_tree may use the input Tree to
2180
generate the inventory delta.
2182
wt.lock_tree_write()
2186
if accelerator_tree is not None:
2187
accelerator_tree.lock_read()
2189
return _build_tree(tree, wt, accelerator_tree, hardlink,
2192
if accelerator_tree is not None:
2193
accelerator_tree.unlock()
2200
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2201
"""See build_tree."""
2202
for num, _unused in enumerate(wt.all_file_ids()):
2203
if num > 0: # more than just a root
2204
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2205
existing_files = set()
2206
for dir, files in wt.walkdirs():
2207
existing_files.update(f[0] for f in files)
2209
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2210
pp = ProgressPhase("Build phase", 2, top_pb)
2211
if tree.inventory.root is not None:
2212
# This is kind of a hack: we should be altering the root
2213
# as part of the regular tree shape diff logic.
2214
# The conditional test here is to avoid doing an
2215
# expensive operation (flush) every time the root id
2216
# is set within the tree, nor setting the root and thus
2217
# marking the tree as dirty, because we use two different
2218
# idioms here: tree interfaces and inventory interfaces.
2219
if wt.get_root_id() != tree.get_root_id():
2220
wt.set_root_id(tree.get_root_id())
2222
tt = TreeTransform(wt)
2226
file_trans_id[wt.get_root_id()] = \
2227
tt.trans_id_tree_file_id(wt.get_root_id())
2228
pb = bzrlib.ui.ui_factory.nested_progress_bar()
2230
deferred_contents = []
2232
total = len(tree.inventory)
2234
precomputed_delta = []
2236
precomputed_delta = None
2237
for num, (tree_path, entry) in \
2238
enumerate(tree.inventory.iter_entries_by_dir()):
2239
pb.update("Building tree", num - len(deferred_contents), total)
2240
if entry.parent_id is None:
2243
file_id = entry.file_id
2245
precomputed_delta.append((None, tree_path, file_id, entry))
2246
if tree_path in existing_files:
2247
target_path = wt.abspath(tree_path)
2248
kind = file_kind(target_path)
2249
if kind == "directory":
2251
bzrdir.BzrDir.open(target_path)
2252
except errors.NotBranchError:
2256
if (file_id not in divert and
2257
_content_match(tree, entry, file_id, kind,
2259
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2260
if kind == 'directory':
2262
parent_id = file_trans_id[entry.parent_id]
2263
if entry.kind == 'file':
2264
# We *almost* replicate new_by_entry, so that we can defer
2265
# getting the file text, and get them all at once.
2266
trans_id = tt.create_path(entry.name, parent_id)
2267
file_trans_id[file_id] = trans_id
2268
tt.version_file(file_id, trans_id)
2269
executable = tree.is_executable(file_id, tree_path)
2271
tt.set_executability(executable, trans_id)
2272
trans_data = (trans_id, tree_path)
2273
deferred_contents.append((file_id, trans_data))
2275
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2278
new_trans_id = file_trans_id[file_id]
2279
old_parent = tt.trans_id_tree_path(tree_path)
2280
_reparent_children(tt, old_parent, new_trans_id)
2281
offset = num + 1 - len(deferred_contents)
2282
_create_files(tt, tree, deferred_contents, pb, offset,
2283
accelerator_tree, hardlink)
2287
divert_trans = set(file_trans_id[f] for f in divert)
2288
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2289
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2290
if len(raw_conflicts) > 0:
2291
precomputed_delta = None
2292
conflicts = cook_conflicts(raw_conflicts, tt)
2293
for conflict in conflicts:
2296
wt.add_conflicts(conflicts)
2297
except errors.UnsupportedOperation:
2299
result = tt.apply(no_conflicts=True,
2300
precomputed_delta=precomputed_delta)
2307
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2309
total = len(desired_files) + offset
2311
if accelerator_tree is None:
2312
new_desired_files = desired_files
2314
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2315
unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
2316
in iter if not (c or e[0] != e[1])]
2317
if accelerator_tree.supports_content_filtering():
2318
unchanged = [(f, p) for (f, p) in unchanged
2319
if not accelerator_tree.iter_search_rules([p]).next()]
2320
unchanged = dict(unchanged)
2321
new_desired_files = []
2323
for file_id, (trans_id, tree_path) in desired_files:
2324
accelerator_path = unchanged.get(file_id)
2325
if accelerator_path is None:
2326
new_desired_files.append((file_id, (trans_id, tree_path)))
2328
pb.update('Adding file contents', count + offset, total)
2330
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2333
contents = accelerator_tree.get_file(file_id, accelerator_path)
2334
if wt.supports_content_filtering():
2335
filters = wt._content_filter_stack(tree_path)
2336
contents = filtered_output_bytes(contents, filters,
2337
ContentFilterContext(tree_path, tree))
2339
tt.create_file(contents, trans_id)
2343
except AttributeError:
2344
# after filtering, contents may no longer be file-like
2348
for count, ((trans_id, tree_path), contents) in enumerate(
2349
tree.iter_files_bytes(new_desired_files)):
2350
if wt.supports_content_filtering():
2351
filters = wt._content_filter_stack(tree_path)
2352
contents = filtered_output_bytes(contents, filters,
2353
ContentFilterContext(tree_path, tree))
2354
tt.create_file(contents, trans_id)
2355
pb.update('Adding file contents', count + offset, total)
2358
def _reparent_children(tt, old_parent, new_parent):
2359
for child in tt.iter_tree_children(old_parent):
2360
tt.adjust_path(tt.final_name(child), new_parent, child)
2362
def _reparent_transform_children(tt, old_parent, new_parent):
2363
by_parent = tt.by_parent()
2364
for child in by_parent[old_parent]:
2365
tt.adjust_path(tt.final_name(child), new_parent, child)
2366
return by_parent[old_parent]
2368
def _content_match(tree, entry, file_id, kind, target_path):
2369
if entry.kind != kind:
2371
if entry.kind == "directory":
2373
if entry.kind == "file":
2374
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2376
elif entry.kind == "symlink":
2377
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2382
def resolve_checkout(tt, conflicts, divert):
2383
new_conflicts = set()
2384
for c_type, conflict in ((c[0], c) for c in conflicts):
2385
# Anything but a 'duplicate' would indicate programmer error
2386
if c_type != 'duplicate':
2387
raise AssertionError(c_type)
2388
# Now figure out which is new and which is old
2389
if tt.new_contents(conflict[1]):
2390
new_file = conflict[1]
2391
old_file = conflict[2]
2393
new_file = conflict[2]
2394
old_file = conflict[1]
2396
# We should only get here if the conflict wasn't completely
2398
final_parent = tt.final_parent(old_file)
2399
if new_file in divert:
2400
new_name = tt.final_name(old_file)+'.diverted'
2401
tt.adjust_path(new_name, final_parent, new_file)
2402
new_conflicts.add((c_type, 'Diverted to',
2403
new_file, old_file))
2405
new_name = tt.final_name(old_file)+'.moved'
2406
tt.adjust_path(new_name, final_parent, old_file)
2407
new_conflicts.add((c_type, 'Moved existing file to',
2408
old_file, new_file))
2409
return new_conflicts
2412
def new_by_entry(tt, entry, parent_id, tree):
2413
"""Create a new file according to its inventory entry"""
2417
contents = tree.get_file(entry.file_id).readlines()
2418
executable = tree.is_executable(entry.file_id)
2419
return tt.new_file(name, parent_id, contents, entry.file_id,
2421
elif kind in ('directory', 'tree-reference'):
2422
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2423
if kind == 'tree-reference':
2424
tt.set_tree_reference(entry.reference_revision, trans_id)
2426
elif kind == 'symlink':
2427
target = tree.get_symlink_target(entry.file_id)
2428
return tt.new_symlink(name, parent_id, target, entry.file_id)
2430
raise errors.BadFileKindError(name, kind)
2433
@deprecated_function(deprecated_in((1, 9, 0)))
2434
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2435
"""Create new file contents according to an inventory entry.
2437
DEPRECATED. Use create_from_tree instead.
2439
if entry.kind == "file":
2441
lines = tree.get_file(entry.file_id).readlines()
2442
tt.create_file(lines, trans_id, mode_id=mode_id)
2443
elif entry.kind == "symlink":
2444
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2445
elif entry.kind == "directory":
2446
tt.create_directory(trans_id)
2449
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2450
filter_tree_path=None):
2451
"""Create new file contents according to tree contents.
2453
:param filter_tree_path: the tree path to use to lookup
2454
content filters to apply to the bytes output in the working tree.
2455
This only applies if the working tree supports content filtering.
2457
kind = tree.kind(file_id)
2458
if kind == 'directory':
2459
tt.create_directory(trans_id)
2460
elif kind == "file":
2462
tree_file = tree.get_file(file_id)
2464
bytes = tree_file.readlines()
2468
if wt.supports_content_filtering() and filter_tree_path is not None:
2469
filters = wt._content_filter_stack(filter_tree_path)
2470
bytes = filtered_output_bytes(bytes, filters,
2471
ContentFilterContext(filter_tree_path, tree))
2472
tt.create_file(bytes, trans_id)
2473
elif kind == "symlink":
2474
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2476
raise AssertionError('Unknown kind %r' % kind)
2479
def create_entry_executability(tt, entry, trans_id):
2480
"""Set the executability of a trans_id according to an inventory entry"""
2481
if entry.kind == "file":
2482
tt.set_executability(entry.executable, trans_id)
2485
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2486
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2489
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2490
"""Produce a backup-style name that appears to be available"""
2494
yield "%s.~%d~" % (name, counter)
2496
for new_name in name_gen():
2497
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2501
def _entry_changes(file_id, entry, working_tree):
2502
"""Determine in which ways the inventory entry has changed.
2504
Returns booleans: has_contents, content_mod, meta_mod
2505
has_contents means there are currently contents, but they differ
2506
contents_mod means contents need to be modified
2507
meta_mod means the metadata needs to be modified
2509
cur_entry = working_tree.inventory[file_id]
2511
working_kind = working_tree.kind(file_id)
2514
has_contents = False
2517
if has_contents is True:
2518
if entry.kind != working_kind:
2519
contents_mod, meta_mod = True, False
2521
cur_entry._read_tree_state(working_tree.id2path(file_id),
2523
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2524
cur_entry._forget_tree_state()
2525
return has_contents, contents_mod, meta_mod
2528
def revert(working_tree, target_tree, filenames, backups=False,
2529
pb=DummyProgress(), change_reporter=None):
2530
"""Revert a working tree's contents to those of a target tree."""
2531
target_tree.lock_read()
2532
tt = TreeTransform(working_tree, pb)
2534
pp = ProgressPhase("Revert phase", 3, pb)
2535
conflicts, merge_modified = _prepare_revert_transform(
2536
working_tree, target_tree, tt, filenames, backups, pp)
2538
change_reporter = delta._ChangeReporter(
2539
unversioned_filter=working_tree.is_ignored)
2540
delta.report_changes(tt.iter_changes(), change_reporter)
2541
for conflict in conflicts:
2545
working_tree.set_merge_modified(merge_modified)
2547
target_tree.unlock()
2553
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2554
backups, pp, basis_tree=None,
2555
merge_modified=None):
2557
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2559
if merge_modified is None:
2560
merge_modified = working_tree.merge_modified()
2561
merge_modified = _alter_files(working_tree, target_tree, tt,
2562
child_pb, filenames, backups,
2563
merge_modified, basis_tree)
2567
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2569
raw_conflicts = resolve_conflicts(tt, child_pb,
2570
lambda t, c: conflict_pass(t, c, target_tree))
2573
conflicts = cook_conflicts(raw_conflicts, tt)
2574
return conflicts, merge_modified
2577
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2578
backups, merge_modified, basis_tree=None):
2579
if basis_tree is not None:
2580
basis_tree.lock_read()
2581
change_list = target_tree.iter_changes(working_tree,
2582
specific_files=specific_files, pb=pb)
2583
if target_tree.get_root_id() is None:
2589
for id_num, (file_id, path, changed_content, versioned, parent, name,
2590
kind, executable) in enumerate(change_list):
2591
if skip_root and file_id[0] is not None and parent[0] is None:
2593
trans_id = tt.trans_id_file_id(file_id)
2596
keep_content = False
2597
if kind[0] == 'file' and (backups or kind[1] is None):
2598
wt_sha1 = working_tree.get_file_sha1(file_id)
2599
if merge_modified.get(file_id) != wt_sha1:
2600
# acquire the basis tree lazily to prevent the
2601
# expense of accessing it when it's not needed ?
2602
# (Guessing, RBC, 200702)
2603
if basis_tree is None:
2604
basis_tree = working_tree.basis_tree()
2605
basis_tree.lock_read()
2606
if file_id in basis_tree:
2607
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2609
elif kind[1] is None and not versioned[1]:
2611
if kind[0] is not None:
2612
if not keep_content:
2613
tt.delete_contents(trans_id)
2614
elif kind[1] is not None:
2615
parent_trans_id = tt.trans_id_file_id(parent[0])
2616
by_parent = tt.by_parent()
2617
backup_name = _get_backup_name(name[0], by_parent,
2618
parent_trans_id, tt)
2619
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2620
new_trans_id = tt.create_path(name[0], parent_trans_id)
2621
if versioned == (True, True):
2622
tt.unversion_file(trans_id)
2623
tt.version_file(file_id, new_trans_id)
2624
# New contents should have the same unix perms as old
2627
trans_id = new_trans_id
2628
if kind[1] in ('directory', 'tree-reference'):
2629
tt.create_directory(trans_id)
2630
if kind[1] == 'tree-reference':
2631
revision = target_tree.get_reference_revision(file_id,
2633
tt.set_tree_reference(revision, trans_id)
2634
elif kind[1] == 'symlink':
2635
tt.create_symlink(target_tree.get_symlink_target(file_id),
2637
elif kind[1] == 'file':
2638
deferred_files.append((file_id, (trans_id, mode_id)))
2639
if basis_tree is None:
2640
basis_tree = working_tree.basis_tree()
2641
basis_tree.lock_read()
2642
new_sha1 = target_tree.get_file_sha1(file_id)
2643
if (file_id in basis_tree and new_sha1 ==
2644
basis_tree.get_file_sha1(file_id)):
2645
if file_id in merge_modified:
2646
del merge_modified[file_id]
2648
merge_modified[file_id] = new_sha1
2650
# preserve the execute bit when backing up
2651
if keep_content and executable[0] == executable[1]:
2652
tt.set_executability(executable[1], trans_id)
2653
elif kind[1] is not None:
2654
raise AssertionError(kind[1])
2655
if versioned == (False, True):
2656
tt.version_file(file_id, trans_id)
2657
if versioned == (True, False):
2658
tt.unversion_file(trans_id)
2659
if (name[1] is not None and
2660
(name[0] != name[1] or parent[0] != parent[1])):
2661
if name[1] == '' and parent[1] is None:
2662
parent_trans = ROOT_PARENT
2664
parent_trans = tt.trans_id_file_id(parent[1])
2665
tt.adjust_path(name[1], parent_trans, trans_id)
2666
if executable[0] != executable[1] and kind[1] == "file":
2667
tt.set_executability(executable[1], trans_id)
2668
if working_tree.supports_content_filtering():
2669
for index, ((trans_id, mode_id), bytes) in enumerate(
2670
target_tree.iter_files_bytes(deferred_files)):
2671
file_id = deferred_files[index][0]
2672
# We're reverting a tree to the target tree so using the
2673
# target tree to find the file path seems the best choice
2674
# here IMO - Ian C 27/Oct/2009
2675
filter_tree_path = target_tree.id2path(file_id)
2676
filters = working_tree._content_filter_stack(filter_tree_path)
2677
bytes = filtered_output_bytes(bytes, filters,
2678
ContentFilterContext(filter_tree_path, working_tree))
2679
tt.create_file(bytes, trans_id, mode_id)
2681
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2683
tt.create_file(bytes, trans_id, mode_id)
2685
if basis_tree is not None:
2687
return merge_modified
2690
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2691
"""Make many conflict-resolution attempts, but die if they fail"""
2692
if pass_func is None:
2693
pass_func = conflict_pass
2694
new_conflicts = set()
2697
pb.update('Resolution pass', n+1, 10)
2698
conflicts = tt.find_conflicts()
2699
if len(conflicts) == 0:
2700
return new_conflicts
2701
new_conflicts.update(pass_func(tt, conflicts))
2702
raise MalformedTransform(conflicts=conflicts)
2707
def conflict_pass(tt, conflicts, path_tree=None):
2708
"""Resolve some classes of conflicts.
2710
:param tt: The transform to resolve conflicts in
2711
:param conflicts: The conflicts to resolve
2712
:param path_tree: A Tree to get supplemental paths from
2714
new_conflicts = set()
2715
for c_type, conflict in ((c[0], c) for c in conflicts):
2716
if c_type == 'duplicate id':
2717
tt.unversion_file(conflict[1])
2718
new_conflicts.add((c_type, 'Unversioned existing file',
2719
conflict[1], conflict[2], ))
2720
elif c_type == 'duplicate':
2721
# files that were renamed take precedence
2722
final_parent = tt.final_parent(conflict[1])
2723
if tt.path_changed(conflict[1]):
2724
existing_file, new_file = conflict[2], conflict[1]
2726
existing_file, new_file = conflict[1], conflict[2]
2727
new_name = tt.final_name(existing_file)+'.moved'
2728
tt.adjust_path(new_name, final_parent, existing_file)
2729
new_conflicts.add((c_type, 'Moved existing file to',
2730
existing_file, new_file))
2731
elif c_type == 'parent loop':
2732
# break the loop by undoing one of the ops that caused the loop
2734
while not tt.path_changed(cur):
2735
cur = tt.final_parent(cur)
2736
new_conflicts.add((c_type, 'Cancelled move', cur,
2737
tt.final_parent(cur),))
2738
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2740
elif c_type == 'missing parent':
2741
trans_id = conflict[1]
2743
tt.cancel_deletion(trans_id)
2744
new_conflicts.add(('deleting parent', 'Not deleting',
2749
tt.final_name(trans_id)
2751
if path_tree is not None:
2752
file_id = tt.final_file_id(trans_id)
2754
file_id = tt.inactive_file_id(trans_id)
2755
entry = path_tree.inventory[file_id]
2756
# special-case the other tree root (move its
2757
# children to current root)
2758
if entry.parent_id is None:
2760
moved = _reparent_transform_children(
2761
tt, trans_id, tt.root)
2763
new_conflicts.add((c_type, 'Moved to root',
2766
parent_trans_id = tt.trans_id_file_id(
2768
tt.adjust_path(entry.name, parent_trans_id,
2771
tt.create_directory(trans_id)
2772
new_conflicts.add((c_type, 'Created directory', trans_id))
2773
elif c_type == 'unversioned parent':
2774
file_id = tt.inactive_file_id(conflict[1])
2775
# special-case the other tree root (move its children instead)
2776
if path_tree and file_id in path_tree:
2777
if path_tree.inventory[file_id].parent_id is None:
2779
tt.version_file(file_id, conflict[1])
2780
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2781
elif c_type == 'non-directory parent':
2782
parent_id = conflict[1]
2783
parent_parent = tt.final_parent(parent_id)
2784
parent_name = tt.final_name(parent_id)
2785
parent_file_id = tt.final_file_id(parent_id)
2786
new_parent_id = tt.new_directory(parent_name + '.new',
2787
parent_parent, parent_file_id)
2788
_reparent_transform_children(tt, parent_id, new_parent_id)
2789
if parent_file_id is not None:
2790
tt.unversion_file(parent_id)
2791
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2792
elif c_type == 'versioning no contents':
2793
tt.cancel_versioning(conflict[1])
2794
return new_conflicts
2797
def cook_conflicts(raw_conflicts, tt):
2798
"""Generate a list of cooked conflicts, sorted by file path"""
2799
from bzrlib.conflicts import Conflict
2800
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2801
return sorted(conflict_iter, key=Conflict.sort_key)
2804
def iter_cook_conflicts(raw_conflicts, tt):
2805
from bzrlib.conflicts import Conflict
2807
for conflict in raw_conflicts:
2808
c_type = conflict[0]
2809
action = conflict[1]
2810
modified_path = fp.get_path(conflict[2])
2811
modified_id = tt.final_file_id(conflict[2])
2812
if len(conflict) == 3:
2813
yield Conflict.factory(c_type, action=action, path=modified_path,
2814
file_id=modified_id)
2817
conflicting_path = fp.get_path(conflict[3])
2818
conflicting_id = tt.final_file_id(conflict[3])
2819
yield Conflict.factory(c_type, action=action, path=modified_path,
2820
file_id=modified_id,
2821
conflict_path=conflicting_path,
2822
conflict_file_id=conflicting_id)
2825
class _FileMover(object):
2826
"""Moves and deletes files for TreeTransform, tracking operations"""
2829
self.past_renames = []
2830
self.pending_deletions = []
2832
def rename(self, from_, to):
2833
"""Rename a file from one path to another. Functions like os.rename"""
2835
os.rename(from_, to)
2837
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2838
raise errors.FileExists(to, str(e))
2840
self.past_renames.append((from_, to))
2842
def pre_delete(self, from_, to):
2843
"""Rename a file out of the way and mark it for deletion.
2845
Unlike os.unlink, this works equally well for files and directories.
2846
:param from_: The current file path
2847
:param to: A temporary path for the file
2849
self.rename(from_, to)
2850
self.pending_deletions.append(to)
2853
"""Reverse all renames that have been performed"""
2854
for from_, to in reversed(self.past_renames):
2855
os.rename(to, from_)
2856
# after rollback, don't reuse _FileMover
2858
pending_deletions = None
2860
def apply_deletions(self):
2861
"""Apply all marked deletions"""
2862
for path in self.pending_deletions:
2864
# after apply_deletions, don't reuse _FileMover
2866
pending_deletions = None