1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
21
from stat import S_ISREG, S_IEXEC
25
config as _mod_config,
32
lazy_import.lazy_import(globals(), """
43
revision as _mod_revision,
47
from breezy.bzr import (
51
from breezy.i18n import gettext
53
from .errors import (DuplicateKey, MalformedTransform,
54
ReusingTransform, CantMoveRoot,
55
ImmortalLimbo, NoFinalPath,
57
from .filters import filtered_output_bytes, ContentFilterContext
58
from .mutabletree import MutableTree
59
from .osutils import (
67
from .progress import ProgressPhase
78
ROOT_PARENT = "root-parent"
80
def unique_add(map, key, value):
82
raise DuplicateKey(key=key)
87
class _TransformResults(object):
88
def __init__(self, modified_paths, rename_count):
90
self.modified_paths = modified_paths
91
self.rename_count = rename_count
94
class TreeTransformBase(object):
95
"""The base class for TreeTransform and its kin."""
97
def __init__(self, tree, pb=None, case_sensitive=True):
100
:param tree: The tree that will be transformed, but not necessarily
103
:param case_sensitive: If True, the target of the transform is
104
case sensitive, not just case preserving.
106
object.__init__(self)
109
# mapping of trans_id -> new basename
111
# mapping of trans_id -> new parent trans_id
112
self._new_parent = {}
113
# mapping of trans_id with new contents -> new file_kind
114
self._new_contents = {}
115
# mapping of trans_id => (sha1 of content, stat_value)
116
self._observed_sha1s = {}
117
# Set of trans_ids whose contents will be removed
118
self._removed_contents = set()
119
# Mapping of trans_id -> new execute-bit value
120
self._new_executability = {}
121
# Mapping of trans_id -> new tree-reference value
122
self._new_reference_revision = {}
123
# Mapping of trans_id -> new file_id
125
# Mapping of old file-id -> trans_id
126
self._non_present_ids = {}
127
# Mapping of new file_id -> trans_id
129
# Set of trans_ids that will be removed
130
self._removed_id = set()
131
# Mapping of path in old tree -> trans_id
132
self._tree_path_ids = {}
133
# Mapping trans_id -> path in old tree
134
self._tree_id_paths = {}
135
# The trans_id that will be used as the tree root
136
if tree.is_versioned(''):
137
self._new_root = self.trans_id_tree_path('')
139
self._new_root = None
140
# Indicator of whether the transform has been applied
144
# Whether the target is case sensitive
145
self._case_sensitive_target = case_sensitive
146
# A counter of how many files have been renamed
147
self.rename_count = 0
150
"""Support Context Manager API."""
153
def __exit__(self, exc_type, exc_val, exc_tb):
154
"""Support Context Manager API."""
158
"""Release the working tree lock, if held.
160
This is required if apply has not been invoked, but can be invoked
163
if self._tree is None:
165
for hook in MutableTree.hooks['post_transform']:
166
hook(self._tree, self)
170
def __get_root(self):
171
return self._new_root
173
root = property(__get_root)
175
def _assign_id(self):
176
"""Produce a new tranform id"""
177
new_id = "new-%s" % self._id_number
181
def create_path(self, name, parent):
182
"""Assign a transaction id to a new path"""
183
trans_id = self._assign_id()
184
unique_add(self._new_name, trans_id, name)
185
unique_add(self._new_parent, trans_id, parent)
188
def adjust_path(self, name, parent, trans_id):
189
"""Change the path that is assigned to a transaction id."""
191
raise ValueError("Parent trans-id may not be None")
192
if trans_id == self._new_root:
194
self._new_name[trans_id] = name
195
self._new_parent[trans_id] = parent
197
def adjust_root_path(self, name, parent):
198
"""Emulate moving the root by moving all children, instead.
200
We do this by undoing the association of root's transaction id with the
201
current tree. This allows us to create a new directory with that
202
transaction id. We unversion the root directory and version the
203
physically new directory, and hope someone versions the tree root
206
old_root = self._new_root
207
old_root_file_id = self.final_file_id(old_root)
208
# force moving all children of root
209
for child_id in self.iter_tree_children(old_root):
210
if child_id != parent:
211
self.adjust_path(self.final_name(child_id),
212
self.final_parent(child_id), child_id)
213
file_id = self.final_file_id(child_id)
214
if file_id is not None:
215
self.unversion_file(child_id)
216
self.version_file(file_id, child_id)
218
# the physical root needs a new transaction id
219
self._tree_path_ids.pop("")
220
self._tree_id_paths.pop(old_root)
221
self._new_root = self.trans_id_tree_path('')
222
if parent == old_root:
223
parent = self._new_root
224
self.adjust_path(name, parent, old_root)
225
self.create_directory(old_root)
226
self.version_file(old_root_file_id, old_root)
227
self.unversion_file(self._new_root)
229
def fixup_new_roots(self):
230
"""Reinterpret requests to change the root directory
232
Instead of creating a root directory, or moving an existing directory,
233
all the attributes and children of the new root are applied to the
234
existing root directory.
236
This means that the old root trans-id becomes obsolete, so it is
237
recommended only to invoke this after the root trans-id has become
241
new_roots = [k for k, v in viewitems(self._new_parent)
243
if len(new_roots) < 1:
245
if len(new_roots) != 1:
246
raise ValueError('A tree cannot have two roots!')
247
if self._new_root is None:
248
self._new_root = new_roots[0]
250
old_new_root = new_roots[0]
251
# unversion the new root's directory.
252
if self.final_kind(self._new_root) is None:
253
file_id = self.final_file_id(old_new_root)
255
file_id = self.final_file_id(self._new_root)
256
if old_new_root in self._new_id:
257
self.cancel_versioning(old_new_root)
259
self.unversion_file(old_new_root)
260
# if, at this stage, root still has an old file_id, zap it so we can
261
# stick a new one in.
262
if (self.tree_file_id(self._new_root) is not None and
263
self._new_root not in self._removed_id):
264
self.unversion_file(self._new_root)
265
if file_id is not None:
266
self.version_file(file_id, self._new_root)
268
# Now move children of new root into old root directory.
269
# Ensure all children are registered with the transaction, but don't
270
# use directly-- some tree children have new parents
271
list(self.iter_tree_children(old_new_root))
272
# Move all children of new root into old root directory.
273
for child in self.by_parent().get(old_new_root, []):
274
self.adjust_path(self.final_name(child), self._new_root, child)
276
# Ensure old_new_root has no directory.
277
if old_new_root in self._new_contents:
278
self.cancel_creation(old_new_root)
280
self.delete_contents(old_new_root)
282
# prevent deletion of root directory.
283
if self._new_root in self._removed_contents:
284
self.cancel_deletion(self._new_root)
286
# destroy path info for old_new_root.
287
del self._new_parent[old_new_root]
288
del self._new_name[old_new_root]
290
def trans_id_file_id(self, file_id):
291
"""Determine or set the transaction id associated with a file ID.
292
A new id is only created for file_ids that were never present. If
293
a transaction has been unversioned, it is deliberately still returned.
294
(this will likely lead to an unversioned parent conflict.)
297
raise ValueError('None is not a valid file id')
298
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
299
return self._r_new_id[file_id]
302
path = self._tree.id2path(file_id)
303
except errors.NoSuchId:
304
if file_id in self._non_present_ids:
305
return self._non_present_ids[file_id]
307
trans_id = self._assign_id()
308
self._non_present_ids[file_id] = trans_id
311
return self.trans_id_tree_path(path)
313
def trans_id_tree_path(self, path):
314
"""Determine (and maybe set) the transaction ID for a tree path."""
315
path = self.canonical_path(path)
316
if path not in self._tree_path_ids:
317
self._tree_path_ids[path] = self._assign_id()
318
self._tree_id_paths[self._tree_path_ids[path]] = path
319
return self._tree_path_ids[path]
321
def get_tree_parent(self, trans_id):
322
"""Determine id of the parent in the tree."""
323
path = self._tree_id_paths[trans_id]
326
return self.trans_id_tree_path(os.path.dirname(path))
328
def delete_contents(self, trans_id):
329
"""Schedule the contents of a path entry for deletion"""
330
kind = self.tree_kind(trans_id)
332
self._removed_contents.add(trans_id)
334
def cancel_deletion(self, trans_id):
335
"""Cancel a scheduled deletion"""
336
self._removed_contents.remove(trans_id)
338
def unversion_file(self, trans_id):
339
"""Schedule a path entry to become unversioned"""
340
self._removed_id.add(trans_id)
342
def delete_versioned(self, trans_id):
343
"""Delete and unversion a versioned file"""
344
self.delete_contents(trans_id)
345
self.unversion_file(trans_id)
347
def set_executability(self, executability, trans_id):
348
"""Schedule setting of the 'execute' bit
349
To unschedule, set to None
351
if executability is None:
352
del self._new_executability[trans_id]
354
unique_add(self._new_executability, trans_id, executability)
356
def set_tree_reference(self, revision_id, trans_id):
357
"""Set the reference associated with a directory"""
358
unique_add(self._new_reference_revision, trans_id, revision_id)
360
def version_file(self, file_id, trans_id):
361
"""Schedule a file to become versioned."""
364
unique_add(self._new_id, trans_id, file_id)
365
unique_add(self._r_new_id, file_id, trans_id)
367
def cancel_versioning(self, trans_id):
368
"""Undo a previous versioning of a file"""
369
file_id = self._new_id[trans_id]
370
del self._new_id[trans_id]
371
del self._r_new_id[file_id]
373
def new_paths(self, filesystem_only=False):
374
"""Determine the paths of all new and changed files.
376
:param filesystem_only: if True, only calculate values for files
377
that require renames or execute bit changes.
381
stale_ids = self._needs_rename.difference(self._new_name)
382
stale_ids.difference_update(self._new_parent)
383
stale_ids.difference_update(self._new_contents)
384
stale_ids.difference_update(self._new_id)
385
needs_rename = self._needs_rename.difference(stale_ids)
386
id_sets = (needs_rename, self._new_executability)
388
id_sets = (self._new_name, self._new_parent, self._new_contents,
389
self._new_id, self._new_executability)
390
for id_set in id_sets:
391
new_ids.update(id_set)
392
return sorted(FinalPaths(self).get_paths(new_ids))
394
def _inventory_altered(self):
395
"""Determine which trans_ids need new Inventory entries.
397
An new entry is needed when anything that would be reflected by an
398
inventory entry changes, including file name, file_id, parent file_id,
399
file kind, and the execute bit.
401
Some care is taken to return entries with real changes, not cases
402
where the value is deleted and then restored to its original value,
403
but some actually unchanged values may be returned.
405
:returns: A list of (path, trans_id) for all items requiring an
406
inventory change. Ordered by path.
409
# Find entries whose file_ids are new (or changed).
410
new_file_id = set(t for t in self._new_id
411
if self._new_id[t] != self.tree_file_id(t))
412
for id_set in [self._new_name, self._new_parent, new_file_id,
413
self._new_executability]:
414
changed_ids.update(id_set)
415
# removing implies a kind change
416
changed_kind = set(self._removed_contents)
418
changed_kind.intersection_update(self._new_contents)
419
# Ignore entries that are already known to have changed.
420
changed_kind.difference_update(changed_ids)
421
# to keep only the truly changed ones
422
changed_kind = (t for t in changed_kind
423
if self.tree_kind(t) != self.final_kind(t))
424
# all kind changes will alter the inventory
425
changed_ids.update(changed_kind)
426
# To find entries with changed parent_ids, find parents which existed,
427
# but changed file_id.
428
changed_file_id = set(t for t in new_file_id if t in self._removed_id)
429
# Now add all their children to the set.
430
for parent_trans_id in new_file_id:
431
changed_ids.update(self.iter_tree_children(parent_trans_id))
432
return sorted(FinalPaths(self).get_paths(changed_ids))
434
def final_kind(self, trans_id):
435
"""Determine the final file kind, after any changes applied.
437
:return: None if the file does not exist/has no contents. (It is
438
conceivable that a path would be created without the corresponding
439
contents insertion command)
441
if trans_id in self._new_contents:
442
return self._new_contents[trans_id]
443
elif trans_id in self._removed_contents:
446
return self.tree_kind(trans_id)
448
def tree_path(self, trans_id):
449
"""Determine the tree path associated with the trans_id."""
450
return self._tree_id_paths.get(trans_id)
452
def tree_file_id(self, trans_id):
453
"""Determine the file id associated with the trans_id in the tree"""
454
path = self.tree_path(trans_id)
457
# the file is old; the old id is still valid
458
if self._new_root == trans_id:
459
return self._tree.get_root_id()
460
return self._tree.path2id(path)
462
def final_file_id(self, trans_id):
463
"""Determine the file id after any changes are applied, or None.
465
None indicates that the file will not be versioned after changes are
469
return self._new_id[trans_id]
471
if trans_id in self._removed_id:
473
return self.tree_file_id(trans_id)
475
def inactive_file_id(self, trans_id):
476
"""Return the inactive file_id associated with a transaction id.
477
That is, the one in the tree or in non_present_ids.
478
The file_id may actually be active, too.
480
file_id = self.tree_file_id(trans_id)
481
if file_id is not None:
483
for key, value in viewitems(self._non_present_ids):
484
if value == trans_id:
487
def final_parent(self, trans_id):
488
"""Determine the parent file_id, after any changes are applied.
490
ROOT_PARENT is returned for the tree root.
493
return self._new_parent[trans_id]
495
return self.get_tree_parent(trans_id)
497
def final_name(self, trans_id):
498
"""Determine the final filename, after all changes are applied."""
500
return self._new_name[trans_id]
503
return os.path.basename(self._tree_id_paths[trans_id])
505
raise NoFinalPath(trans_id, self)
508
"""Return a map of parent: children for known parents.
510
Only new paths and parents of tree files with assigned ids are used.
513
items = list(viewitems(self._new_parent))
514
items.extend((t, self.final_parent(t))
515
for t in list(self._tree_id_paths))
516
for trans_id, parent_id in items:
517
if parent_id not in by_parent:
518
by_parent[parent_id] = set()
519
by_parent[parent_id].add(trans_id)
522
def path_changed(self, trans_id):
523
"""Return True if a trans_id's path has changed."""
524
return (trans_id in self._new_name) or (trans_id in self._new_parent)
526
def new_contents(self, trans_id):
527
return (trans_id in self._new_contents)
529
def find_conflicts(self):
530
"""Find any violations of inventory or filesystem invariants"""
531
if self._done is True:
532
raise ReusingTransform()
534
# ensure all children of all existent parents are known
535
# all children of non-existent parents are known, by definition.
536
self._add_tree_children()
537
by_parent = self.by_parent()
538
conflicts.extend(self._unversioned_parents(by_parent))
539
conflicts.extend(self._parent_loops())
540
conflicts.extend(self._duplicate_entries(by_parent))
541
conflicts.extend(self._duplicate_ids())
542
conflicts.extend(self._parent_type_conflicts(by_parent))
543
conflicts.extend(self._improper_versioning())
544
conflicts.extend(self._executability_conflicts())
545
conflicts.extend(self._overwrite_conflicts())
548
def _check_malformed(self):
549
conflicts = self.find_conflicts()
550
if len(conflicts) != 0:
551
raise MalformedTransform(conflicts=conflicts)
553
def _add_tree_children(self):
554
"""Add all the children of all active parents to the known paths.
556
Active parents are those which gain children, and those which are
557
removed. This is a necessary first step in detecting conflicts.
559
parents = list(self.by_parent())
560
parents.extend([t for t in self._removed_contents if
561
self.tree_kind(t) == 'directory'])
562
for trans_id in self._removed_id:
563
path = self.tree_path(trans_id)
565
if self._tree.stored_kind(path) == 'directory':
566
parents.append(trans_id)
567
elif self.tree_kind(trans_id) == 'directory':
568
parents.append(trans_id)
570
for parent_id in parents:
571
# ensure that all children are registered with the transaction
572
list(self.iter_tree_children(parent_id))
574
def _has_named_child(self, name, parent_id, known_children):
575
"""Does a parent already have a name child.
577
:param name: The searched for name.
579
:param parent_id: The parent for which the check is made.
581
:param known_children: The already known children. This should have
582
been recently obtained from `self.by_parent.get(parent_id)`
583
(or will be if None is passed).
585
if known_children is None:
586
known_children = self.by_parent().get(parent_id, [])
587
for child in known_children:
588
if self.final_name(child) == name:
590
parent_path = self._tree_id_paths.get(parent_id, None)
591
if parent_path is None:
592
# No parent... no children
594
child_path = joinpath(parent_path, name)
595
child_id = self._tree_path_ids.get(child_path, None)
597
# Not known by the tree transform yet, check the filesystem
598
return osutils.lexists(self._tree.abspath(child_path))
600
raise AssertionError('child_id is missing: %s, %s, %s'
601
% (name, parent_id, child_id))
603
def _available_backup_name(self, name, target_id):
604
"""Find an available backup name.
606
:param name: The basename of the file.
608
:param target_id: The directory trans_id where the backup should
611
known_children = self.by_parent().get(target_id, [])
612
return osutils.available_backup_name(
614
lambda base: self._has_named_child(
615
base, target_id, known_children))
617
def _parent_loops(self):
618
"""No entry should be its own ancestor"""
620
for trans_id in self._new_parent:
623
while parent_id != ROOT_PARENT:
626
parent_id = self.final_parent(parent_id)
629
if parent_id == trans_id:
630
conflicts.append(('parent loop', trans_id))
631
if parent_id in seen:
635
def _unversioned_parents(self, by_parent):
636
"""If parent directories are versioned, children must be versioned."""
638
for parent_id, children in viewitems(by_parent):
639
if parent_id == ROOT_PARENT:
641
if self.final_file_id(parent_id) is not None:
643
for child_id in children:
644
if self.final_file_id(child_id) is not None:
645
conflicts.append(('unversioned parent', parent_id))
649
def _improper_versioning(self):
650
"""Cannot version a file with no contents, or a bad type.
652
However, existing entries with no contents are okay.
655
for trans_id in self._new_id:
656
kind = self.final_kind(trans_id)
658
conflicts.append(('versioning no contents', trans_id))
660
if not self._tree.versionable_kind(kind):
661
conflicts.append(('versioning bad kind', trans_id, kind))
664
def _executability_conflicts(self):
665
"""Check for bad executability changes.
667
Only versioned files may have their executability set, because
668
1. only versioned entries can have executability under windows
669
2. only files can be executable. (The execute bit on a directory
670
does not indicate searchability)
673
for trans_id in self._new_executability:
674
if self.final_file_id(trans_id) is None:
675
conflicts.append(('unversioned executability', trans_id))
677
if self.final_kind(trans_id) != "file":
678
conflicts.append(('non-file executability', trans_id))
681
def _overwrite_conflicts(self):
682
"""Check for overwrites (not permitted on Win32)"""
684
for trans_id in self._new_contents:
685
if self.tree_kind(trans_id) is None:
687
if trans_id not in self._removed_contents:
688
conflicts.append(('overwrite', trans_id,
689
self.final_name(trans_id)))
692
def _duplicate_entries(self, by_parent):
693
"""No directory may have two entries with the same name."""
695
if (self._new_name, self._new_parent) == ({}, {}):
697
for children in viewvalues(by_parent):
699
for child_tid in children:
700
name = self.final_name(child_tid)
702
# Keep children only if they still exist in the end
703
if not self._case_sensitive_target:
705
name_ids.append((name, child_tid))
709
for name, trans_id in name_ids:
710
kind = self.final_kind(trans_id)
711
file_id = self.final_file_id(trans_id)
712
if kind is None and file_id is None:
714
if name == last_name:
715
conflicts.append(('duplicate', last_trans_id, trans_id,
718
last_trans_id = trans_id
721
def _duplicate_ids(self):
722
"""Each inventory id may only be used once"""
724
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
726
all_ids = self._tree.all_file_ids()
727
active_tree_ids = all_ids.difference(removed_tree_ids)
728
for trans_id, file_id in viewitems(self._new_id):
729
if file_id in active_tree_ids:
730
path = self._tree.id2path(file_id)
731
old_trans_id = self.trans_id_tree_path(path)
732
conflicts.append(('duplicate id', old_trans_id, trans_id))
735
def _parent_type_conflicts(self, by_parent):
736
"""Children must have a directory parent"""
738
for parent_id, children in viewitems(by_parent):
739
if parent_id == ROOT_PARENT:
742
for child_id in children:
743
if self.final_kind(child_id) is not None:
748
# There is at least a child, so we need an existing directory to
750
kind = self.final_kind(parent_id)
752
# The directory will be deleted
753
conflicts.append(('missing parent', parent_id))
754
elif kind != "directory":
755
# Meh, we need a *directory* to put something in it
756
conflicts.append(('non-directory parent', parent_id))
759
def _set_executability(self, path, trans_id):
760
"""Set the executability of versioned files """
761
if self._tree._supports_executable():
762
new_executability = self._new_executability[trans_id]
763
abspath = self._tree.abspath(path)
764
current_mode = os.stat(abspath).st_mode
765
if new_executability:
768
to_mode = current_mode | (0o100 & ~umask)
769
# Enable x-bit for others only if they can read it.
770
if current_mode & 0o004:
771
to_mode |= 0o001 & ~umask
772
if current_mode & 0o040:
773
to_mode |= 0o010 & ~umask
775
to_mode = current_mode & ~0o111
776
osutils.chmod_if_possible(abspath, to_mode)
778
def _new_entry(self, name, parent_id, file_id):
779
"""Helper function to create a new filesystem entry."""
780
trans_id = self.create_path(name, parent_id)
781
if file_id is not None:
782
self.version_file(file_id, trans_id)
785
def new_file(self, name, parent_id, contents, file_id=None,
786
executable=None, sha1=None):
787
"""Convenience method to create files.
789
name is the name of the file to create.
790
parent_id is the transaction id of the parent directory of the file.
791
contents is an iterator of bytestrings, which will be used to produce
793
:param file_id: The inventory ID of the file, if it is to be versioned.
794
:param executable: Only valid when a file_id has been supplied.
796
trans_id = self._new_entry(name, parent_id, file_id)
797
# TODO: rather than scheduling a set_executable call,
798
# have create_file create the file with the right mode.
799
self.create_file(contents, trans_id, sha1=sha1)
800
if executable is not None:
801
self.set_executability(executable, trans_id)
804
def new_directory(self, name, parent_id, file_id=None):
805
"""Convenience method to create directories.
807
name is the name of the directory to create.
808
parent_id is the transaction id of the parent directory of the
810
file_id is the inventory ID of the directory, if it is to be versioned.
812
trans_id = self._new_entry(name, parent_id, file_id)
813
self.create_directory(trans_id)
816
def new_symlink(self, name, parent_id, target, file_id=None):
817
"""Convenience method to create symbolic link.
819
name is the name of the symlink to create.
820
parent_id is the transaction id of the parent directory of the symlink.
821
target is a bytestring of the target of the symlink.
822
file_id is the inventory ID of the file, if it is to be versioned.
824
trans_id = self._new_entry(name, parent_id, file_id)
825
self.create_symlink(target, trans_id)
828
def new_orphan(self, trans_id, parent_id):
829
"""Schedule an item to be orphaned.
831
When a directory is about to be removed, its children, if they are not
832
versioned are moved out of the way: they don't have a parent anymore.
834
:param trans_id: The trans_id of the existing item.
835
:param parent_id: The parent trans_id of the item.
837
raise NotImplementedError(self.new_orphan)
839
def _get_potential_orphans(self, dir_id):
840
"""Find the potential orphans in a directory.
842
A directory can't be safely deleted if there are versioned files in it.
843
If all the contained files are unversioned then they can be orphaned.
845
The 'None' return value means that the directory contains at least one
846
versioned file and should not be deleted.
848
:param dir_id: The directory trans id.
850
:return: A list of the orphan trans ids or None if at least one
851
versioned file is present.
854
# Find the potential orphans, stop if one item should be kept
855
for child_tid in self.by_parent()[dir_id]:
856
if child_tid in self._removed_contents:
857
# The child is removed as part of the transform. Since it was
858
# versioned before, it's not an orphan
860
elif self.final_file_id(child_tid) is None:
861
# The child is not versioned
862
orphans.append(child_tid)
864
# We have a versioned file here, searching for orphans is
870
def _affected_ids(self):
871
"""Return the set of transform ids affected by the transform"""
872
trans_ids = set(self._removed_id)
873
trans_ids.update(self._new_id)
874
trans_ids.update(self._removed_contents)
875
trans_ids.update(self._new_contents)
876
trans_ids.update(self._new_executability)
877
trans_ids.update(self._new_name)
878
trans_ids.update(self._new_parent)
881
def _get_file_id_maps(self):
882
"""Return mapping of file_ids to trans_ids in the to and from states"""
883
trans_ids = self._affected_ids()
886
# Build up two dicts: trans_ids associated with file ids in the
887
# FROM state, vs the TO state.
888
for trans_id in trans_ids:
889
from_file_id = self.tree_file_id(trans_id)
890
if from_file_id is not None:
891
from_trans_ids[from_file_id] = trans_id
892
to_file_id = self.final_file_id(trans_id)
893
if to_file_id is not None:
894
to_trans_ids[to_file_id] = trans_id
895
return from_trans_ids, to_trans_ids
897
def _from_file_data(self, from_trans_id, from_versioned, from_path):
898
"""Get data about a file in the from (tree) state
900
Return a (name, parent, kind, executable) tuple
902
from_path = self._tree_id_paths.get(from_trans_id)
904
# get data from working tree if versioned
905
from_entry = next(self._tree.iter_entries_by_dir(
906
specific_files=[from_path]))[1]
907
from_name = from_entry.name
908
from_parent = from_entry.parent_id
911
if from_path is None:
912
# File does not exist in FROM state
916
# File exists, but is not versioned. Have to use path-
918
from_name = os.path.basename(from_path)
919
tree_parent = self.get_tree_parent(from_trans_id)
920
from_parent = self.tree_file_id(tree_parent)
921
if from_path is not None:
922
from_kind, from_executable, from_stats = \
923
self._tree._comparison_data(from_entry, from_path)
926
from_executable = False
927
return from_name, from_parent, from_kind, from_executable
929
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
930
"""Get data about a file in the to (target) state
932
Return a (name, parent, kind, executable) tuple
934
to_name = self.final_name(to_trans_id)
935
to_kind = self.final_kind(to_trans_id)
936
to_parent = self.final_file_id(self.final_parent(to_trans_id))
937
if to_trans_id in self._new_executability:
938
to_executable = self._new_executability[to_trans_id]
939
elif to_trans_id == from_trans_id:
940
to_executable = from_executable
942
to_executable = False
943
return to_name, to_parent, to_kind, to_executable
945
def iter_changes(self):
946
"""Produce output in the same format as Tree.iter_changes.
948
Will produce nonsensical results if invoked while inventory/filesystem
949
conflicts (as reported by TreeTransform.find_conflicts()) are present.
951
This reads the Transform, but only reproduces changes involving a
952
file_id. Files that are not versioned in either of the FROM or TO
953
states are not reflected.
955
final_paths = FinalPaths(self)
956
from_trans_ids, to_trans_ids = self._get_file_id_maps()
958
# Now iterate through all active file_ids
959
for file_id in set(from_trans_ids).union(to_trans_ids):
961
from_trans_id = from_trans_ids.get(file_id)
962
# find file ids, and determine versioning state
963
if from_trans_id is None:
964
from_versioned = False
965
from_trans_id = to_trans_ids[file_id]
967
from_versioned = True
968
to_trans_id = to_trans_ids.get(file_id)
969
if to_trans_id is None:
971
to_trans_id = from_trans_id
975
if not from_versioned:
978
from_path = self._tree_id_paths.get(from_trans_id)
982
to_path = final_paths.get_path(to_trans_id)
984
from_name, from_parent, from_kind, from_executable = \
985
self._from_file_data(from_trans_id, from_versioned, from_path)
987
to_name, to_parent, to_kind, to_executable = \
988
self._to_file_data(to_trans_id, from_trans_id, from_executable)
990
if from_kind != to_kind:
992
elif to_kind in ('file', 'symlink') and (
993
to_trans_id != from_trans_id or
994
to_trans_id in self._new_contents):
996
if (not modified and from_versioned == to_versioned and
997
from_parent==to_parent and from_name == to_name and
998
from_executable == to_executable):
1000
results.append((file_id, (from_path, to_path), modified,
1001
(from_versioned, to_versioned),
1002
(from_parent, to_parent),
1003
(from_name, to_name),
1004
(from_kind, to_kind),
1005
(from_executable, to_executable)))
1006
return iter(sorted(results, key=lambda x:x[1]))
1008
def get_preview_tree(self):
1009
"""Return a tree representing the result of the transform.
1011
The tree is a snapshot, and altering the TreeTransform will invalidate
1014
return _PreviewTree(self)
1016
def commit(self, branch, message, merge_parents=None, strict=False,
1017
timestamp=None, timezone=None, committer=None, authors=None,
1018
revprops=None, revision_id=None):
1019
"""Commit the result of this TreeTransform to a branch.
1021
:param branch: The branch to commit to.
1022
:param message: The message to attach to the commit.
1023
:param merge_parents: Additional parent revision-ids specified by
1025
:param strict: If True, abort the commit if there are unversioned
1027
:param timestamp: if not None, seconds-since-epoch for the time and
1028
date. (May be a float.)
1029
:param timezone: Optional timezone for timestamp, as an offset in
1031
:param committer: Optional committer in email-id format.
1032
(e.g. "J Random Hacker <jrandom@example.com>")
1033
:param authors: Optional list of authors in email-id format.
1034
:param revprops: Optional dictionary of revision properties.
1035
:param revision_id: Optional revision id. (Specifying a revision-id
1036
may reduce performance for some non-native formats.)
1037
:return: The revision_id of the revision committed.
1039
self._check_malformed()
1041
unversioned = set(self._new_contents).difference(set(self._new_id))
1042
for trans_id in unversioned:
1043
if self.final_file_id(trans_id) is None:
1044
raise errors.StrictCommitFailed()
1046
revno, last_rev_id = branch.last_revision_info()
1047
if last_rev_id == _mod_revision.NULL_REVISION:
1048
if merge_parents is not None:
1049
raise ValueError('Cannot supply merge parents for first'
1053
parent_ids = [last_rev_id]
1054
if merge_parents is not None:
1055
parent_ids.extend(merge_parents)
1056
if self._tree.get_revision_id() != last_rev_id:
1057
raise ValueError('TreeTransform not based on branch basis: %s' %
1058
self._tree.get_revision_id().decode('utf-8'))
1059
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1060
builder = branch.get_commit_builder(parent_ids,
1061
timestamp=timestamp,
1063
committer=committer,
1065
revision_id=revision_id)
1066
preview = self.get_preview_tree()
1067
list(builder.record_iter_changes(preview, last_rev_id,
1068
self.iter_changes()))
1069
builder.finish_inventory()
1070
revision_id = builder.commit(message)
1071
branch.set_last_revision_info(revno + 1, revision_id)
1074
def _text_parent(self, trans_id):
1075
path = self.tree_path(trans_id)
1077
if path is None or self._tree.kind(path) != 'file':
1079
except errors.NoSuchFile:
1083
def _get_parents_texts(self, trans_id):
1084
"""Get texts for compression parents of this file."""
1085
path = self._text_parent(trans_id)
1088
return (self._tree.get_file_text(path),)
1090
def _get_parents_lines(self, trans_id):
1091
"""Get lines for compression parents of this file."""
1092
path = self._text_parent(trans_id)
1095
return (self._tree.get_file_lines(path),)
1097
def serialize(self, serializer):
1098
"""Serialize this TreeTransform.
1100
:param serializer: A Serialiser like pack.ContainerSerializer.
1102
new_name = {k.encode('utf-8'): v.encode('utf-8')
1103
for k, v in viewitems(self._new_name)}
1104
new_parent = {k.encode('utf-8'): v.encode('utf-8')
1105
for k, v in viewitems(self._new_parent)}
1106
new_id = {k.encode('utf-8'): v
1107
for k, v in viewitems(self._new_id)}
1108
new_executability = {k.encode('utf-8'): int(v)
1109
for k, v in viewitems(self._new_executability)}
1110
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
1111
for k, v in viewitems(self._tree_path_ids)}
1112
non_present_ids = {k: v.encode('utf-8')
1113
for k, v in viewitems(self._non_present_ids)}
1114
removed_contents = [trans_id.encode('utf-8')
1115
for trans_id in self._removed_contents]
1116
removed_id = [trans_id.encode('utf-8')
1117
for trans_id in self._removed_id]
1119
b'_id_number': self._id_number,
1120
b'_new_name': new_name,
1121
b'_new_parent': new_parent,
1122
b'_new_executability': new_executability,
1124
b'_tree_path_ids': tree_path_ids,
1125
b'_removed_id': removed_id,
1126
b'_removed_contents': removed_contents,
1127
b'_non_present_ids': non_present_ids,
1129
yield serializer.bytes_record(bencode.bencode(attribs),
1131
for trans_id, kind in viewitems(self._new_contents):
1133
with open(self._limbo_name(trans_id), 'rb') as cur_file:
1134
lines = cur_file.readlines()
1135
parents = self._get_parents_lines(trans_id)
1136
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1137
content = b''.join(mpdiff.to_patch())
1138
if kind == 'directory':
1140
if kind == 'symlink':
1141
content = self._read_symlink_target(trans_id)
1142
if not isinstance(content, bytes):
1143
content = content.encode('utf-8')
1144
yield serializer.bytes_record(
1145
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
1147
def deserialize(self, records):
1148
"""Deserialize a stored TreeTransform.
1150
:param records: An iterable of (names, content) tuples, as per
1151
pack.ContainerPushParser.
1153
names, content = next(records)
1154
attribs = bencode.bdecode(content)
1155
self._id_number = attribs[b'_id_number']
1156
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
1157
for k, v in viewitems(attribs[b'_new_name'])}
1158
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
1159
for k, v in viewitems(attribs[b'_new_parent'])}
1160
self._new_executability = {k.decode('utf-8'): bool(v)
1161
for k, v in viewitems(attribs[b'_new_executability'])}
1162
self._new_id = {k.decode('utf-8'): v
1163
for k, v in viewitems(attribs[b'_new_id'])}
1164
self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
1165
self._tree_path_ids = {}
1166
self._tree_id_paths = {}
1167
for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
1168
path = bytepath.decode('utf-8')
1169
trans_id = trans_id.decode('utf-8')
1170
self._tree_path_ids[path] = trans_id
1171
self._tree_id_paths[trans_id] = path
1172
self._removed_id = {trans_id.decode('utf-8')
1173
for trans_id in attribs[b'_removed_id']}
1174
self._removed_contents = set(trans_id.decode('utf-8')
1175
for trans_id in attribs[b'_removed_contents'])
1176
self._non_present_ids = {k: v.decode('utf-8')
1177
for k, v in viewitems(attribs[b'_non_present_ids'])}
1178
for ((trans_id, kind),), content in records:
1179
trans_id = trans_id.decode('utf-8')
1180
kind = kind.decode('ascii')
1182
mpdiff = multiparent.MultiParent.from_patch(content)
1183
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1184
self.create_file(lines, trans_id)
1185
if kind == 'directory':
1186
self.create_directory(trans_id)
1187
if kind == 'symlink':
1188
self.create_symlink(content.decode('utf-8'), trans_id)
1191
class DiskTreeTransform(TreeTransformBase):
1192
"""Tree transform storing its contents on disk."""
1194
def __init__(self, tree, limbodir, pb=None,
1195
case_sensitive=True):
1197
:param tree: The tree that will be transformed, but not necessarily
1199
:param limbodir: A directory where new files can be stored until
1200
they are installed in their proper places
1202
:param case_sensitive: If True, the target of the transform is
1203
case sensitive, not just case preserving.
1205
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1206
self._limbodir = limbodir
1207
self._deletiondir = None
1208
# A mapping of transform ids to their limbo filename
1209
self._limbo_files = {}
1210
self._possibly_stale_limbo_files = set()
1211
# A mapping of transform ids to a set of the transform ids of children
1212
# that their limbo directory has
1213
self._limbo_children = {}
1214
# Map transform ids to maps of child filename to child transform id
1215
self._limbo_children_names = {}
1216
# List of transform ids that need to be renamed from limbo into place
1217
self._needs_rename = set()
1218
self._creation_mtime = None
1221
"""Release the working tree lock, if held, clean up limbo dir.
1223
This is required if apply has not been invoked, but can be invoked
1226
if self._tree is None:
1229
limbo_paths = list(viewvalues(self._limbo_files))
1230
limbo_paths.extend(self._possibly_stale_limbo_files)
1231
limbo_paths.sort(reverse=True)
1232
for path in limbo_paths:
1235
except OSError as e:
1236
if e.errno != errno.ENOENT:
1238
# XXX: warn? perhaps we just got interrupted at an
1239
# inconvenient moment, but perhaps files are disappearing
1242
delete_any(self._limbodir)
1244
# We don't especially care *why* the dir is immortal.
1245
raise ImmortalLimbo(self._limbodir)
1247
if self._deletiondir is not None:
1248
delete_any(self._deletiondir)
1250
raise errors.ImmortalPendingDeletion(self._deletiondir)
1252
TreeTransformBase.finalize(self)
1254
def _limbo_supports_executable(self):
1255
"""Check if the limbo path supports the executable bit."""
1256
# FIXME: Check actual file system capabilities of limbodir
1257
return osutils.supports_executable()
1259
def _limbo_name(self, trans_id):
1260
"""Generate the limbo name of a file"""
1261
limbo_name = self._limbo_files.get(trans_id)
1262
if limbo_name is None:
1263
limbo_name = self._generate_limbo_path(trans_id)
1264
self._limbo_files[trans_id] = limbo_name
1267
def _generate_limbo_path(self, trans_id):
1268
"""Generate a limbo path using the trans_id as the relative path.
1270
This is suitable as a fallback, and when the transform should not be
1271
sensitive to the path encoding of the limbo directory.
1273
self._needs_rename.add(trans_id)
1274
return pathjoin(self._limbodir, trans_id)
1276
def adjust_path(self, name, parent, trans_id):
1277
previous_parent = self._new_parent.get(trans_id)
1278
previous_name = self._new_name.get(trans_id)
1279
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1280
if (trans_id in self._limbo_files and
1281
trans_id not in self._needs_rename):
1282
self._rename_in_limbo([trans_id])
1283
if previous_parent != parent:
1284
self._limbo_children[previous_parent].remove(trans_id)
1285
if previous_parent != parent or previous_name != name:
1286
del self._limbo_children_names[previous_parent][previous_name]
1288
def _rename_in_limbo(self, trans_ids):
1289
"""Fix limbo names so that the right final path is produced.
1291
This means we outsmarted ourselves-- we tried to avoid renaming
1292
these files later by creating them with their final names in their
1293
final parents. But now the previous name or parent is no longer
1294
suitable, so we have to rename them.
1296
Even for trans_ids that have no new contents, we must remove their
1297
entries from _limbo_files, because they are now stale.
1299
for trans_id in trans_ids:
1300
old_path = self._limbo_files[trans_id]
1301
self._possibly_stale_limbo_files.add(old_path)
1302
del self._limbo_files[trans_id]
1303
if trans_id not in self._new_contents:
1305
new_path = self._limbo_name(trans_id)
1306
os.rename(old_path, new_path)
1307
self._possibly_stale_limbo_files.remove(old_path)
1308
for descendant in self._limbo_descendants(trans_id):
1309
desc_path = self._limbo_files[descendant]
1310
desc_path = new_path + desc_path[len(old_path):]
1311
self._limbo_files[descendant] = desc_path
1313
def _limbo_descendants(self, trans_id):
1314
"""Return the set of trans_ids whose limbo paths descend from this."""
1315
descendants = set(self._limbo_children.get(trans_id, []))
1316
for descendant in list(descendants):
1317
descendants.update(self._limbo_descendants(descendant))
1320
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1321
"""Schedule creation of a new file.
1325
:param contents: an iterator of strings, all of which will be written
1326
to the target destination.
1327
:param trans_id: TreeTransform handle
1328
:param mode_id: If not None, force the mode of the target file to match
1329
the mode of the object referenced by mode_id.
1330
Otherwise, we will try to preserve mode bits of an existing file.
1331
:param sha1: If the sha1 of this content is already known, pass it in.
1332
We can use it to prevent future sha1 computations.
1334
name = self._limbo_name(trans_id)
1335
with open(name, 'wb') as f:
1336
unique_add(self._new_contents, trans_id, 'file')
1337
f.writelines(contents)
1338
self._set_mtime(name)
1339
self._set_mode(trans_id, mode_id, S_ISREG)
1340
# It is unfortunate we have to use lstat instead of fstat, but we just
1341
# used utime and chmod on the file, so we need the accurate final
1343
if sha1 is not None:
1344
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1346
def _read_symlink_target(self, trans_id):
1347
return os.readlink(self._limbo_name(trans_id))
1349
def _set_mtime(self, path):
1350
"""All files that are created get the same mtime.
1352
This time is set by the first object to be created.
1354
if self._creation_mtime is None:
1355
self._creation_mtime = time.time()
1356
os.utime(path, (self._creation_mtime, self._creation_mtime))
1358
def create_hardlink(self, path, trans_id):
1359
"""Schedule creation of a hard link"""
1360
name = self._limbo_name(trans_id)
1363
except OSError as e:
1364
if e.errno != errno.EPERM:
1366
raise errors.HardLinkNotSupported(path)
1368
unique_add(self._new_contents, trans_id, 'file')
1370
# Clean up the file, it never got registered so
1371
# TreeTransform.finalize() won't clean it up.
1375
def create_directory(self, trans_id):
1376
"""Schedule creation of a new directory.
1378
See also new_directory.
1380
os.mkdir(self._limbo_name(trans_id))
1381
unique_add(self._new_contents, trans_id, 'directory')
1383
def create_symlink(self, target, trans_id):
1384
"""Schedule creation of a new symbolic link.
1386
target is a bytestring.
1387
See also new_symlink.
1390
os.symlink(target, self._limbo_name(trans_id))
1391
unique_add(self._new_contents, trans_id, 'symlink')
1394
path = FinalPaths(self).get_path(trans_id)
1397
raise UnableCreateSymlink(path=path)
1399
def cancel_creation(self, trans_id):
1400
"""Cancel the creation of new file contents."""
1401
del self._new_contents[trans_id]
1402
if trans_id in self._observed_sha1s:
1403
del self._observed_sha1s[trans_id]
1404
children = self._limbo_children.get(trans_id)
1405
# if this is a limbo directory with children, move them before removing
1407
if children is not None:
1408
self._rename_in_limbo(children)
1409
del self._limbo_children[trans_id]
1410
del self._limbo_children_names[trans_id]
1411
delete_any(self._limbo_name(trans_id))
1413
def new_orphan(self, trans_id, parent_id):
1414
conf = self._tree.get_config_stack()
1415
handle_orphan = conf.get('transform.orphan_policy')
1416
handle_orphan(self, trans_id, parent_id)
1419
class OrphaningError(errors.BzrError):
1421
# Only bugs could lead to such exception being seen by the user
1422
internal_error = True
1423
_fmt = "Error while orphaning %s in %s directory"
1425
def __init__(self, orphan, parent):
1426
errors.BzrError.__init__(self)
1427
self.orphan = orphan
1428
self.parent = parent
1431
class OrphaningForbidden(OrphaningError):
1433
_fmt = "Policy: %s doesn't allow creating orphans."
1435
def __init__(self, policy):
1436
errors.BzrError.__init__(self)
1437
self.policy = policy
1440
def move_orphan(tt, orphan_id, parent_id):
1441
"""See TreeTransformBase.new_orphan.
1443
This creates a new orphan in the `brz-orphans` dir at the root of the
1446
:param tt: The TreeTransform orphaning `trans_id`.
1448
:param orphan_id: The trans id that should be orphaned.
1450
:param parent_id: The orphan parent trans id.
1452
# Add the orphan dir if it doesn't exist
1453
orphan_dir_basename = 'brz-orphans'
1454
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1455
if tt.final_kind(od_id) is None:
1456
tt.create_directory(od_id)
1457
parent_path = tt._tree_id_paths[parent_id]
1458
# Find a name that doesn't exist yet in the orphan dir
1459
actual_name = tt.final_name(orphan_id)
1460
new_name = tt._available_backup_name(actual_name, od_id)
1461
tt.adjust_path(new_name, od_id, orphan_id)
1462
trace.warning('%s has been orphaned in %s'
1463
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1466
def refuse_orphan(tt, orphan_id, parent_id):
1467
"""See TreeTransformBase.new_orphan.
1469
This refuses to create orphan, letting the caller handle the conflict.
1471
raise OrphaningForbidden('never')
1474
orphaning_registry = registry.Registry()
1475
orphaning_registry.register(
1476
u'conflict', refuse_orphan,
1477
'Leave orphans in place and create a conflict on the directory.')
1478
orphaning_registry.register(
1479
u'move', move_orphan,
1480
'Move orphans into the brz-orphans directory.')
1481
orphaning_registry._set_default_key(u'conflict')
1484
opt_transform_orphan = _mod_config.RegistryOption(
1485
'transform.orphan_policy', orphaning_registry,
1486
help='Policy for orphaned files during transform operations.',
1490
class TreeTransform(DiskTreeTransform):
1491
"""Represent a tree transformation.
1493
This object is designed to support incremental generation of the transform,
1496
However, it gives optimum performance when parent directories are created
1497
before their contents. The transform is then able to put child files
1498
directly in their parent directory, avoiding later renames.
1500
It is easy to produce malformed transforms, but they are generally
1501
harmless. Attempting to apply a malformed transform will cause an
1502
exception to be raised before any modifications are made to the tree.
1504
Many kinds of malformed transforms can be corrected with the
1505
resolve_conflicts function. The remaining ones indicate programming error,
1506
such as trying to create a file with no path.
1508
Two sets of file creation methods are supplied. Convenience methods are:
1513
These are composed of the low-level methods:
1515
* create_file or create_directory or create_symlink
1519
Transform/Transaction ids
1520
-------------------------
1521
trans_ids are temporary ids assigned to all files involved in a transform.
1522
It's possible, even common, that not all files in the Tree have trans_ids.
1524
trans_ids are used because filenames and file_ids are not good enough
1525
identifiers; filenames change, and not all files have file_ids. File-ids
1526
are also associated with trans-ids, so that moving a file moves its
1529
trans_ids are only valid for the TreeTransform that generated them.
1533
Limbo is a temporary directory use to hold new versions of files.
1534
Files are added to limbo by create_file, create_directory, create_symlink,
1535
and their convenience variants (new_*). Files may be removed from limbo
1536
using cancel_creation. Files are renamed from limbo into their final
1537
location as part of TreeTransform.apply
1539
Limbo must be cleaned up, by either calling TreeTransform.apply or
1540
calling TreeTransform.finalize.
1542
Files are placed into limbo inside their parent directories, where
1543
possible. This reduces subsequent renames, and makes operations involving
1544
lots of files faster. This optimization is only possible if the parent
1545
directory is created *before* creating any of its children, so avoid
1546
creating children before parents, where possible.
1550
This temporary directory is used by _FileMover for storing files that are
1551
about to be deleted. In case of rollback, the files will be restored.
1552
FileMover does not delete files until it is sure that a rollback will not
1555
def __init__(self, tree, pb=None):
1556
"""Note: a tree_write lock is taken on the tree.
1558
Use TreeTransform.finalize() to release the lock (can be omitted if
1559
TreeTransform.apply() called).
1561
tree.lock_tree_write()
1563
limbodir = urlutils.local_path_from_url(
1564
tree._transport.abspath('limbo'))
1565
osutils.ensure_empty_directory_exists(
1567
errors.ExistingLimbo)
1568
deletiondir = urlutils.local_path_from_url(
1569
tree._transport.abspath('pending-deletion'))
1570
osutils.ensure_empty_directory_exists(
1572
errors.ExistingPendingDeletion)
1577
# Cache of realpath results, to speed up canonical_path
1578
self._realpaths = {}
1579
# Cache of relpath results, to speed up canonical_path
1581
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1582
tree.case_sensitive)
1583
self._deletiondir = deletiondir
1585
def canonical_path(self, path):
1586
"""Get the canonical tree-relative path"""
1587
# don't follow final symlinks
1588
abs = self._tree.abspath(path)
1589
if abs in self._relpaths:
1590
return self._relpaths[abs]
1591
dirname, basename = os.path.split(abs)
1592
if dirname not in self._realpaths:
1593
self._realpaths[dirname] = os.path.realpath(dirname)
1594
dirname = self._realpaths[dirname]
1595
abs = pathjoin(dirname, basename)
1596
if dirname in self._relpaths:
1597
relpath = pathjoin(self._relpaths[dirname], basename)
1598
relpath = relpath.rstrip('/\\')
1600
relpath = self._tree.relpath(abs)
1601
self._relpaths[abs] = relpath
1604
def tree_kind(self, trans_id):
1605
"""Determine the file kind in the working tree.
1607
:returns: The file kind or None if the file does not exist
1609
path = self._tree_id_paths.get(trans_id)
1613
return file_kind(self._tree.abspath(path))
1614
except errors.NoSuchFile:
1617
def _set_mode(self, trans_id, mode_id, typefunc):
1618
"""Set the mode of new file contents.
1619
The mode_id is the existing file to get the mode from (often the same
1620
as trans_id). The operation is only performed if there's a mode match
1621
according to typefunc.
1626
old_path = self._tree_id_paths[mode_id]
1630
mode = os.stat(self._tree.abspath(old_path)).st_mode
1631
except OSError as e:
1632
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1633
# Either old_path doesn't exist, or the parent of the
1634
# target is not a directory (but will be one eventually)
1635
# Either way, we know it doesn't exist *right now*
1636
# See also bug #248448
1641
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1643
def iter_tree_children(self, parent_id):
1644
"""Iterate through the entry's tree children, if any"""
1646
path = self._tree_id_paths[parent_id]
1650
children = os.listdir(self._tree.abspath(path))
1651
except OSError as e:
1652
if not (osutils._is_error_enotdir(e)
1653
or e.errno in (errno.ENOENT, errno.ESRCH)):
1657
for child in children:
1658
childpath = joinpath(path, child)
1659
if self._tree.is_control_filename(childpath):
1661
yield self.trans_id_tree_path(childpath)
1663
def _generate_limbo_path(self, trans_id):
1664
"""Generate a limbo path using the final path if possible.
1666
This optimizes the performance of applying the tree transform by
1667
avoiding renames. These renames can be avoided only when the parent
1668
directory is already scheduled for creation.
1670
If the final path cannot be used, falls back to using the trans_id as
1673
parent = self._new_parent.get(trans_id)
1674
# if the parent directory is already in limbo (e.g. when building a
1675
# tree), choose a limbo name inside the parent, to reduce further
1677
use_direct_path = False
1678
if self._new_contents.get(parent) == 'directory':
1679
filename = self._new_name.get(trans_id)
1680
if filename is not None:
1681
if parent not in self._limbo_children:
1682
self._limbo_children[parent] = set()
1683
self._limbo_children_names[parent] = {}
1684
use_direct_path = True
1685
# the direct path can only be used if no other file has
1686
# already taken this pathname, i.e. if the name is unused, or
1687
# if it is already associated with this trans_id.
1688
elif self._case_sensitive_target:
1689
if (self._limbo_children_names[parent].get(filename)
1690
in (trans_id, None)):
1691
use_direct_path = True
1693
for l_filename, l_trans_id in viewitems(
1694
self._limbo_children_names[parent]):
1695
if l_trans_id == trans_id:
1697
if l_filename.lower() == filename.lower():
1700
use_direct_path = True
1702
if not use_direct_path:
1703
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1705
limbo_name = pathjoin(self._limbo_files[parent], filename)
1706
self._limbo_children[parent].add(trans_id)
1707
self._limbo_children_names[parent][filename] = trans_id
1710
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1711
"""Apply all changes to the inventory and filesystem.
1713
If filesystem or inventory conflicts are present, MalformedTransform
1716
If apply succeeds, finalize is not necessary.
1718
:param no_conflicts: if True, the caller guarantees there are no
1719
conflicts, so no check is made.
1720
:param precomputed_delta: An inventory delta to use instead of
1722
:param _mover: Supply an alternate FileMover, for testing
1724
for hook in MutableTree.hooks['pre_transform']:
1725
hook(self._tree, self)
1726
if not no_conflicts:
1727
self._check_malformed()
1728
with ui.ui_factory.nested_progress_bar() as child_pb:
1729
if precomputed_delta is None:
1730
child_pb.update(gettext('Apply phase'), 0, 2)
1731
inventory_delta = self._generate_inventory_delta()
1734
inventory_delta = precomputed_delta
1737
mover = _FileMover()
1741
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1742
self._apply_removals(mover)
1743
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1744
modified_paths = self._apply_insertions(mover)
1749
mover.apply_deletions()
1750
if self.final_file_id(self.root) is None:
1751
inventory_delta = [e for e in inventory_delta if e[0] != '']
1752
self._tree.apply_inventory_delta(inventory_delta)
1753
self._apply_observed_sha1s()
1756
return _TransformResults(modified_paths, self.rename_count)
1758
def _generate_inventory_delta(self):
1759
"""Generate an inventory delta for the current transform."""
1760
inventory_delta = []
1761
new_paths = self._inventory_altered()
1762
total_entries = len(new_paths) + len(self._removed_id)
1763
with ui.ui_factory.nested_progress_bar() as child_pb:
1764
for num, trans_id in enumerate(self._removed_id):
1766
child_pb.update(gettext('removing file'), num, total_entries)
1767
if trans_id == self._new_root:
1768
file_id = self._tree.get_root_id()
1770
file_id = self.tree_file_id(trans_id)
1771
# File-id isn't really being deleted, just moved
1772
if file_id in self._r_new_id:
1774
path = self._tree_id_paths[trans_id]
1775
inventory_delta.append((path, None, file_id, None))
1776
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1779
for num, (path, trans_id) in enumerate(new_paths):
1781
child_pb.update(gettext('adding file'),
1782
num + len(self._removed_id), total_entries)
1783
file_id = new_path_file_ids[trans_id]
1787
kind = self.final_kind(trans_id)
1789
kind = self._tree.stored_kind(
1790
self._tree.id2path(file_id), file_id)
1791
parent_trans_id = self.final_parent(trans_id)
1792
parent_file_id = new_path_file_ids.get(parent_trans_id)
1793
if parent_file_id is None:
1794
parent_file_id = self.final_file_id(parent_trans_id)
1795
if trans_id in self._new_reference_revision:
1796
new_entry = inventory.TreeReference(
1798
self._new_name[trans_id],
1799
self.final_file_id(self._new_parent[trans_id]),
1800
None, self._new_reference_revision[trans_id])
1802
new_entry = inventory.make_entry(kind,
1803
self.final_name(trans_id),
1804
parent_file_id, file_id)
1806
old_path = self._tree.id2path(new_entry.file_id)
1807
except errors.NoSuchId:
1809
new_executability = self._new_executability.get(trans_id)
1810
if new_executability is not None:
1811
new_entry.executable = new_executability
1812
inventory_delta.append(
1813
(old_path, path, new_entry.file_id, new_entry))
1814
return inventory_delta
1816
def _apply_removals(self, mover):
1817
"""Perform tree operations that remove directory/inventory names.
1819
That is, delete files that are to be deleted, and put any files that
1820
need renaming into limbo. This must be done in strict child-to-parent
1823
If inventory_delta is None, no inventory delta generation is performed.
1825
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1826
with ui.ui_factory.nested_progress_bar() as child_pb:
1827
for num, (path, trans_id) in enumerate(tree_paths):
1828
# do not attempt to move root into a subdirectory of itself.
1831
child_pb.update(gettext('removing file'), num, len(tree_paths))
1832
full_path = self._tree.abspath(path)
1833
if trans_id in self._removed_contents:
1834
delete_path = os.path.join(self._deletiondir, trans_id)
1835
mover.pre_delete(full_path, delete_path)
1836
elif (trans_id in self._new_name
1837
or trans_id in self._new_parent):
1839
mover.rename(full_path, self._limbo_name(trans_id))
1840
except errors.TransformRenameFailed as e:
1841
if e.errno != errno.ENOENT:
1844
self.rename_count += 1
1846
def _apply_insertions(self, mover):
1847
"""Perform tree operations that insert directory/inventory names.
1849
That is, create any files that need to be created, and restore from
1850
limbo any files that needed renaming. This must be done in strict
1851
parent-to-child order.
1853
If inventory_delta is None, no inventory delta is calculated, and
1854
no list of modified paths is returned.
1856
new_paths = self.new_paths(filesystem_only=True)
1858
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1860
with ui.ui_factory.nested_progress_bar() as child_pb:
1861
for num, (path, trans_id) in enumerate(new_paths):
1863
child_pb.update(gettext('adding file'), num, len(new_paths))
1864
full_path = self._tree.abspath(path)
1865
if trans_id in self._needs_rename:
1867
mover.rename(self._limbo_name(trans_id), full_path)
1868
except errors.TransformRenameFailed as e:
1869
# We may be renaming a dangling inventory id
1870
if e.errno != errno.ENOENT:
1873
self.rename_count += 1
1874
# TODO: if trans_id in self._observed_sha1s, we should
1875
# re-stat the final target, since ctime will be
1876
# updated by the change.
1877
if (trans_id in self._new_contents or
1878
self.path_changed(trans_id)):
1879
if trans_id in self._new_contents:
1880
modified_paths.append(full_path)
1881
if trans_id in self._new_executability:
1882
self._set_executability(path, trans_id)
1883
if trans_id in self._observed_sha1s:
1884
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1885
st = osutils.lstat(full_path)
1886
self._observed_sha1s[trans_id] = (o_sha1, st)
1887
for path, trans_id in new_paths:
1888
# new_paths includes stuff like workingtree conflicts. Only the
1889
# stuff in new_contents actually comes from limbo.
1890
if trans_id in self._limbo_files:
1891
del self._limbo_files[trans_id]
1892
self._new_contents.clear()
1893
return modified_paths
1895
def _apply_observed_sha1s(self):
1896
"""After we have finished renaming everything, update observed sha1s
1898
This has to be done after self._tree.apply_inventory_delta, otherwise
1899
it doesn't know anything about the files we are updating. Also, we want
1900
to do this as late as possible, so that most entries end up cached.
1902
# TODO: this doesn't update the stat information for directories. So
1903
# the first 'bzr status' will still need to rewrite
1904
# .bzr/checkout/dirstate. However, we at least don't need to
1905
# re-read all of the files.
1906
# TODO: If the operation took a while, we could do a time.sleep(3) here
1907
# to allow the clock to tick over and ensure we won't have any
1908
# problems. (we could observe start time, and finish time, and if
1909
# it is less than eg 10% overhead, add a sleep call.)
1910
paths = FinalPaths(self)
1911
for trans_id, observed in viewitems(self._observed_sha1s):
1912
path = paths.get_path(trans_id)
1913
# We could get the file_id, but dirstate prefers to use the path
1914
# anyway, and it is 'cheaper' to determine.
1915
# file_id = self._new_id[trans_id]
1916
self._tree._observed_sha1(None, path, observed)
1919
class TransformPreview(DiskTreeTransform):
1920
"""A TreeTransform for generating preview trees.
1922
Unlike TreeTransform, this version works when the input tree is a
1923
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1924
unversioned files in the input tree.
1927
def __init__(self, tree, pb=None, case_sensitive=True):
1929
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1930
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1932
def canonical_path(self, path):
1935
def tree_kind(self, trans_id):
1936
path = self._tree_id_paths.get(trans_id)
1939
kind = self._tree.path_content_summary(path)[0]
1940
if kind == 'missing':
1944
def _set_mode(self, trans_id, mode_id, typefunc):
1945
"""Set the mode of new file contents.
1946
The mode_id is the existing file to get the mode from (often the same
1947
as trans_id). The operation is only performed if there's a mode match
1948
according to typefunc.
1950
# is it ok to ignore this? probably
1953
def iter_tree_children(self, parent_id):
1954
"""Iterate through the entry's tree children, if any"""
1956
path = self._tree_id_paths[parent_id]
1959
entry = next(self._tree.iter_entries_by_dir(
1960
specific_files=[path]))[1]
1961
children = getattr(entry, 'children', {})
1962
for child in children:
1963
childpath = joinpath(path, child)
1964
yield self.trans_id_tree_path(childpath)
1966
def new_orphan(self, trans_id, parent_id):
1967
raise NotImplementedError(self.new_orphan)
1970
class _PreviewTree(inventorytree.InventoryTree):
1971
"""Partial implementation of Tree to support show_diff_trees"""
1973
def __init__(self, transform):
1974
self._transform = transform
1975
self._final_paths = FinalPaths(transform)
1976
self.__by_parent = None
1977
self._parent_ids = []
1978
self._all_children_cache = {}
1979
self._path2trans_id_cache = {}
1980
self._final_name_cache = {}
1981
self._iter_changes_cache = dict((c[0], c) for c in
1982
self._transform.iter_changes())
1984
def _content_change(self, file_id):
1985
"""Return True if the content of this file changed"""
1986
changes = self._iter_changes_cache.get(file_id)
1987
# changes[2] is true if the file content changed. See
1988
# InterTree.iter_changes.
1989
return (changes is not None and changes[2])
1991
def _get_repository(self):
1992
repo = getattr(self._transform._tree, '_repository', None)
1994
repo = self._transform._tree.branch.repository
1997
def _iter_parent_trees(self):
1998
for revision_id in self.get_parent_ids():
2000
yield self.revision_tree(revision_id)
2001
except errors.NoSuchRevisionInTree:
2002
yield self._get_repository().revision_tree(revision_id)
2004
def _get_file_revision(self, path, file_id, vf, tree_revision):
2006
(file_id, t.get_file_revision(t.id2path(file_id), file_id))
2007
for t in self._iter_parent_trees()]
2008
vf.add_lines((file_id, tree_revision), parent_keys,
2009
self.get_file_lines(path, file_id))
2010
repo = self._get_repository()
2011
base_vf = repo.texts
2012
if base_vf not in vf.fallback_versionedfiles:
2013
vf.fallback_versionedfiles.append(base_vf)
2014
return tree_revision
2016
def _stat_limbo_file(self, trans_id):
2017
name = self._transform._limbo_name(trans_id)
2018
return os.lstat(name)
2021
def _by_parent(self):
2022
if self.__by_parent is None:
2023
self.__by_parent = self._transform.by_parent()
2024
return self.__by_parent
2026
def _comparison_data(self, entry, path):
2027
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2028
if kind == 'missing':
2032
file_id = self._transform.final_file_id(self._path2trans_id(path))
2033
executable = self.is_executable(path, file_id)
2034
return kind, executable, None
2036
def is_locked(self):
2039
def lock_read(self):
2040
# Perhaps in theory, this should lock the TreeTransform?
2041
return lock.LogicalLockResult(self.unlock)
2047
def root_inventory(self):
2048
"""This Tree does not use inventory as its backing data."""
2049
raise NotImplementedError(_PreviewTree.root_inventory)
2051
def get_root_id(self):
2052
return self._transform.final_file_id(self._transform.root)
2054
def all_file_ids(self):
2055
tree_ids = set(self._transform._tree.all_file_ids())
2056
tree_ids.difference_update(self._transform.tree_file_id(t)
2057
for t in self._transform._removed_id)
2058
tree_ids.update(viewvalues(self._transform._new_id))
2061
def all_versioned_paths(self):
2062
return {self.id2path(fid) for fid in self.all_file_ids()}
2064
def _has_id(self, file_id, fallback_check):
2065
if file_id in self._transform._r_new_id:
2067
elif file_id in {self._transform.tree_file_id(trans_id) for
2068
trans_id in self._transform._removed_id}:
2071
return fallback_check(file_id)
2073
def has_id(self, file_id):
2074
return self._has_id(file_id, self._transform._tree.has_id)
2076
def has_or_had_id(self, file_id):
2077
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2079
def _path2trans_id(self, path):
2080
# We must not use None here, because that is a valid value to store.
2081
trans_id = self._path2trans_id_cache.get(path, object)
2082
if trans_id is not object:
2084
segments = splitpath(path)
2085
cur_parent = self._transform.root
2086
for cur_segment in segments:
2087
for child in self._all_children(cur_parent):
2088
final_name = self._final_name_cache.get(child)
2089
if final_name is None:
2090
final_name = self._transform.final_name(child)
2091
self._final_name_cache[child] = final_name
2092
if final_name == cur_segment:
2096
self._path2trans_id_cache[path] = None
2098
self._path2trans_id_cache[path] = cur_parent
2101
def path2id(self, path):
2102
if isinstance(path, list):
2105
path = osutils.pathjoin(*path)
2106
return self._transform.final_file_id(self._path2trans_id(path))
2108
def id2path(self, file_id):
2109
trans_id = self._transform.trans_id_file_id(file_id)
2111
return self._final_paths._determine_path(trans_id)
2113
raise errors.NoSuchId(self, file_id)
2115
def _all_children(self, trans_id):
2116
children = self._all_children_cache.get(trans_id)
2117
if children is not None:
2119
children = set(self._transform.iter_tree_children(trans_id))
2120
# children in the _new_parent set are provided by _by_parent.
2121
children.difference_update(self._transform._new_parent)
2122
children.update(self._by_parent.get(trans_id, []))
2123
self._all_children_cache[trans_id] = children
2126
def _iter_children(self, file_id):
2127
trans_id = self._transform.trans_id_file_id(file_id)
2128
for child_trans_id in self._all_children(trans_id):
2129
yield self._transform.final_file_id(child_trans_id)
2132
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2133
in self._transform._tree.extras())
2134
possible_extras.update(self._transform._new_contents)
2135
possible_extras.update(self._transform._removed_id)
2136
for trans_id in possible_extras:
2137
if self._transform.final_file_id(trans_id) is None:
2138
yield self._final_paths._determine_path(trans_id)
2140
def _make_inv_entries(self, ordered_entries, specific_files=None):
2141
for trans_id, parent_file_id in ordered_entries:
2142
file_id = self._transform.final_file_id(trans_id)
2145
if (specific_files is not None and
2146
self._final_paths.get_path(trans_id) not in specific_files):
2148
kind = self._transform.final_kind(trans_id)
2150
kind = self._transform._tree.stored_kind(
2151
self._transform._tree.id2path(file_id),
2153
new_entry = inventory.make_entry(
2155
self._transform.final_name(trans_id),
2156
parent_file_id, file_id)
2157
yield new_entry, trans_id
2159
def _list_files_by_dir(self):
2160
todo = [ROOT_PARENT]
2162
while len(todo) > 0:
2164
parent_file_id = self._transform.final_file_id(parent)
2165
children = list(self._all_children(parent))
2166
paths = dict(zip(children, self._final_paths.get_paths(children)))
2167
children.sort(key=paths.get)
2168
todo.extend(reversed(children))
2169
for trans_id in children:
2170
ordered_ids.append((trans_id, parent_file_id))
2173
def iter_child_entries(self, path, file_id=None):
2174
trans_id = self._path2trans_id(path)
2175
if trans_id is None:
2176
raise errors.NoSuchFile(path)
2177
todo = [(child_trans_id, trans_id) for child_trans_id in
2178
self._all_children(trans_id)]
2179
for entry, trans_id in self._make_inv_entries(todo):
2182
def iter_entries_by_dir(self, specific_files=None):
2183
# This may not be a maximally efficient implementation, but it is
2184
# reasonably straightforward. An implementation that grafts the
2185
# TreeTransform changes onto the tree's iter_entries_by_dir results
2186
# might be more efficient, but requires tricky inferences about stack
2188
ordered_ids = self._list_files_by_dir()
2189
for entry, trans_id in self._make_inv_entries(ordered_ids,
2191
yield self._final_paths.get_path(trans_id), entry
2193
def _iter_entries_for_dir(self, dir_path):
2194
"""Return path, entry for items in a directory without recursing down."""
2196
dir_trans_id = self._path2trans_id(dir_path)
2197
dir_id = self._transform.final_file_id(dir_trans_id)
2198
for child_trans_id in self._all_children(dir_trans_id):
2199
ordered_ids.append((child_trans_id, dir_id))
2201
for entry, trans_id in self._make_inv_entries(ordered_ids):
2202
path_entries.append((self._final_paths.get_path(trans_id), entry))
2206
def list_files(self, include_root=False, from_dir=None, recursive=True):
2207
"""See WorkingTree.list_files."""
2208
# XXX This should behave like WorkingTree.list_files, but is really
2209
# more like RevisionTree.list_files.
2213
prefix = from_dir + '/'
2214
entries = self.iter_entries_by_dir()
2215
for path, entry in entries:
2216
if entry.name == '' and not include_root:
2219
if not path.startswith(prefix):
2221
path = path[len(prefix):]
2222
yield path, 'V', entry.kind, entry.file_id, entry
2224
if from_dir is None and include_root is True:
2225
root_entry = inventory.make_entry('directory', '',
2226
ROOT_PARENT, self.get_root_id())
2227
yield '', 'V', 'directory', root_entry.file_id, root_entry
2228
entries = self._iter_entries_for_dir(from_dir or '')
2229
for path, entry in entries:
2230
yield path, 'V', entry.kind, entry.file_id, entry
2232
def kind(self, path, file_id=None):
2233
trans_id = self._path2trans_id(path)
2234
if trans_id is None:
2235
raise errors.NoSuchFile(path)
2236
return self._transform.final_kind(trans_id)
2238
def stored_kind(self, path, file_id=None):
2239
trans_id = self._path2trans_id(path)
2240
if trans_id is None:
2241
raise errors.NoSuchFile(path)
2243
return self._transform._new_contents[trans_id]
2245
return self._transform._tree.stored_kind(path, file_id)
2247
def get_file_mtime(self, path, file_id=None):
2248
"""See Tree.get_file_mtime"""
2250
file_id = self.path2id(path)
2252
raise errors.NoSuchFile(path)
2253
if not self._content_change(file_id):
2254
return self._transform._tree.get_file_mtime(
2255
self._transform._tree.id2path(file_id), file_id)
2256
trans_id = self._path2trans_id(path)
2257
return self._stat_limbo_file(trans_id).st_mtime
2259
def get_file_size(self, path, file_id=None):
2260
"""See Tree.get_file_size"""
2261
trans_id = self._path2trans_id(path)
2262
if trans_id is None:
2263
raise errors.NoSuchFile(path)
2264
kind = self._transform.final_kind(trans_id)
2267
if trans_id in self._transform._new_contents:
2268
return self._stat_limbo_file(trans_id).st_size
2269
if self.kind(path, file_id) == 'file':
2270
return self._transform._tree.get_file_size(path, file_id)
2274
def get_file_verifier(self, path, file_id=None, stat_value=None):
2275
trans_id = self._path2trans_id(path)
2276
if trans_id is None:
2277
raise errors.NoSuchFile(path)
2278
kind = self._transform._new_contents.get(trans_id)
2280
return self._transform._tree.get_file_verifier(path, file_id)
2282
with self.get_file(path, file_id) as fileobj:
2283
return ("SHA1", sha_file(fileobj))
2285
def get_file_sha1(self, path, file_id=None, stat_value=None):
2286
trans_id = self._path2trans_id(path)
2287
if trans_id is None:
2288
raise errors.NoSuchFile(path)
2289
kind = self._transform._new_contents.get(trans_id)
2291
return self._transform._tree.get_file_sha1(path, file_id)
2293
with self.get_file(path, file_id) as fileobj:
2294
return sha_file(fileobj)
2296
def is_executable(self, path, file_id=None):
2297
trans_id = self._path2trans_id(path)
2298
if trans_id is None:
2301
return self._transform._new_executability[trans_id]
2304
return self._transform._tree.is_executable(path, file_id)
2305
except OSError as e:
2306
if e.errno == errno.ENOENT:
2309
except errors.NoSuchFile:
2312
def has_filename(self, path):
2313
trans_id = self._path2trans_id(path)
2314
if trans_id in self._transform._new_contents:
2316
elif trans_id in self._transform._removed_contents:
2319
return self._transform._tree.has_filename(path)
2321
def path_content_summary(self, path):
2322
trans_id = self._path2trans_id(path)
2323
tt = self._transform
2324
tree_path = tt._tree_id_paths.get(trans_id)
2325
kind = tt._new_contents.get(trans_id)
2327
if tree_path is None or trans_id in tt._removed_contents:
2328
return 'missing', None, None, None
2329
summary = tt._tree.path_content_summary(tree_path)
2330
kind, size, executable, link_or_sha1 = summary
2333
limbo_name = tt._limbo_name(trans_id)
2334
if trans_id in tt._new_reference_revision:
2335
kind = 'tree-reference'
2337
statval = os.lstat(limbo_name)
2338
size = statval.st_size
2339
if not tt._limbo_supports_executable():
2342
executable = statval.st_mode & S_IEXEC
2346
if kind == 'symlink':
2347
link_or_sha1 = os.readlink(limbo_name)
2348
if not isinstance(link_or_sha1, text_type):
2349
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2350
executable = tt._new_executability.get(trans_id, executable)
2351
return kind, size, executable, link_or_sha1
2353
def iter_changes(self, from_tree, include_unchanged=False,
2354
specific_files=None, pb=None, extra_trees=None,
2355
require_versioned=True, want_unversioned=False):
2356
"""See InterTree.iter_changes.
2358
This has a fast path that is only used when the from_tree matches
2359
the transform tree, and no fancy options are supplied.
2361
if (from_tree is not self._transform._tree or include_unchanged or
2362
specific_files or want_unversioned):
2363
return tree.InterTree(from_tree, self).iter_changes(
2364
include_unchanged=include_unchanged,
2365
specific_files=specific_files,
2367
extra_trees=extra_trees,
2368
require_versioned=require_versioned,
2369
want_unversioned=want_unversioned)
2370
if want_unversioned:
2371
raise ValueError('want_unversioned is not supported')
2372
return self._transform.iter_changes()
2374
def get_file(self, path, file_id=None):
2375
"""See Tree.get_file"""
2377
file_id = self.path2id(path)
2378
if not self._content_change(file_id):
2379
return self._transform._tree.get_file(path, file_id)
2380
trans_id = self._path2trans_id(path)
2381
name = self._transform._limbo_name(trans_id)
2382
return open(name, 'rb')
2384
def get_file_with_stat(self, path, file_id=None):
2385
return self.get_file(path, file_id), None
2387
def annotate_iter(self, path, file_id=None,
2388
default_revision=_mod_revision.CURRENT_REVISION):
2390
file_id = self.path2id(path)
2391
changes = self._iter_changes_cache.get(file_id)
2395
changed_content, versioned, kind = (changes[2], changes[3],
2399
get_old = (kind[0] == 'file' and versioned[0])
2401
old_annotation = self._transform._tree.annotate_iter(
2402
path, file_id=file_id, default_revision=default_revision)
2406
return old_annotation
2407
if not changed_content:
2408
return old_annotation
2409
# TODO: This is doing something similar to what WT.annotate_iter is
2410
# doing, however it fails slightly because it doesn't know what
2411
# the *other* revision_id is, so it doesn't know how to give the
2412
# other as the origin for some lines, they all get
2413
# 'default_revision'
2414
# It would be nice to be able to use the new Annotator based
2415
# approach, as well.
2416
return annotate.reannotate([old_annotation],
2417
self.get_file(path, file_id).readlines(),
2420
def get_symlink_target(self, path, file_id=None):
2421
"""See Tree.get_symlink_target"""
2423
file_id = self.path2id(path)
2424
if not self._content_change(file_id):
2425
return self._transform._tree.get_symlink_target(path)
2426
trans_id = self._path2trans_id(path)
2427
name = self._transform._limbo_name(trans_id)
2428
return osutils.readlink(name)
2430
def walkdirs(self, prefix=''):
2431
pending = [self._transform.root]
2432
while len(pending) > 0:
2433
parent_id = pending.pop()
2436
prefix = prefix.rstrip('/')
2437
parent_path = self._final_paths.get_path(parent_id)
2438
parent_file_id = self._transform.final_file_id(parent_id)
2439
for child_id in self._all_children(parent_id):
2440
path_from_root = self._final_paths.get_path(child_id)
2441
basename = self._transform.final_name(child_id)
2442
file_id = self._transform.final_file_id(child_id)
2443
kind = self._transform.final_kind(child_id)
2444
if kind is not None:
2445
versioned_kind = kind
2448
versioned_kind = self._transform._tree.stored_kind(
2449
self._transform._tree.id2path(file_id),
2451
if versioned_kind == 'directory':
2452
subdirs.append(child_id)
2453
children.append((path_from_root, basename, kind, None,
2454
file_id, versioned_kind))
2456
if parent_path.startswith(prefix):
2457
yield (parent_path, parent_file_id), children
2458
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2461
def get_parent_ids(self):
2462
return self._parent_ids
2464
def set_parent_ids(self, parent_ids):
2465
self._parent_ids = parent_ids
2467
def get_revision_tree(self, revision_id):
2468
return self._transform._tree.get_revision_tree(revision_id)
2471
def joinpath(parent, child):
2472
"""Join tree-relative paths, handling the tree root specially"""
2473
if parent is None or parent == "":
2476
return pathjoin(parent, child)
2479
class FinalPaths(object):
2480
"""Make path calculation cheap by memoizing paths.
2482
The underlying tree must not be manipulated between calls, or else
2483
the results will likely be incorrect.
2485
def __init__(self, transform):
2486
object.__init__(self)
2487
self._known_paths = {}
2488
self.transform = transform
2490
def _determine_path(self, trans_id):
2491
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2493
name = self.transform.final_name(trans_id)
2494
parent_id = self.transform.final_parent(trans_id)
2495
if parent_id == self.transform.root:
2498
return pathjoin(self.get_path(parent_id), name)
2500
def get_path(self, trans_id):
2501
"""Find the final path associated with a trans_id"""
2502
if trans_id not in self._known_paths:
2503
self._known_paths[trans_id] = self._determine_path(trans_id)
2504
return self._known_paths[trans_id]
2506
def get_paths(self, trans_ids):
2507
return [(self.get_path(t), t) for t in trans_ids]
2511
def topology_sorted_ids(tree):
2512
"""Determine the topological order of the ids in a tree"""
2513
file_ids = list(tree)
2514
file_ids.sort(key=tree.id2path)
2518
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2519
delta_from_tree=False):
2520
"""Create working tree for a branch, using a TreeTransform.
2522
This function should be used on empty trees, having a tree root at most.
2523
(see merge and revert functionality for working with existing trees)
2525
Existing files are handled like so:
2527
- Existing bzrdirs take precedence over creating new items. They are
2528
created as '%s.diverted' % name.
2529
- Otherwise, if the content on disk matches the content we are building,
2530
it is silently replaced.
2531
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2533
:param tree: The tree to convert wt into a copy of
2534
:param wt: The working tree that files will be placed into
2535
:param accelerator_tree: A tree which can be used for retrieving file
2536
contents more quickly than tree itself, i.e. a workingtree. tree
2537
will be used for cases where accelerator_tree's content is different.
2538
:param hardlink: If true, hard-link files to accelerator_tree, where
2539
possible. accelerator_tree must implement abspath, i.e. be a
2541
:param delta_from_tree: If true, build_tree may use the input Tree to
2542
generate the inventory delta.
2544
with wt.lock_tree_write(), tree.lock_read():
2545
if accelerator_tree is not None:
2546
accelerator_tree.lock_read()
2548
return _build_tree(tree, wt, accelerator_tree, hardlink,
2551
if accelerator_tree is not None:
2552
accelerator_tree.unlock()
2555
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2556
"""See build_tree."""
2557
for num, _unused in enumerate(wt.all_versioned_paths()):
2558
if num > 0: # more than just a root
2559
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2561
top_pb = ui.ui_factory.nested_progress_bar()
2562
pp = ProgressPhase("Build phase", 2, top_pb)
2563
if tree.get_root_id() is not None:
2564
# This is kind of a hack: we should be altering the root
2565
# as part of the regular tree shape diff logic.
2566
# The conditional test here is to avoid doing an
2567
# expensive operation (flush) every time the root id
2568
# is set within the tree, nor setting the root and thus
2569
# marking the tree as dirty, because we use two different
2570
# idioms here: tree interfaces and inventory interfaces.
2571
if wt.get_root_id() != tree.get_root_id():
2572
wt.set_root_id(tree.get_root_id())
2574
tt = TreeTransform(wt)
2578
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_path('')
2579
with ui.ui_factory.nested_progress_bar() as pb:
2580
deferred_contents = []
2582
total = len(tree.all_versioned_paths())
2584
precomputed_delta = []
2586
precomputed_delta = None
2587
# Check if tree inventory has content. If so, we populate
2588
# existing_files with the directory content. If there are no
2589
# entries we skip populating existing_files as its not used.
2590
# This improves performance and unncessary work on large
2591
# directory trees. (#501307)
2593
existing_files = set()
2594
for dir, files in wt.walkdirs():
2595
existing_files.update(f[0] for f in files)
2596
for num, (tree_path, entry) in \
2597
enumerate(tree.iter_entries_by_dir()):
2598
pb.update(gettext("Building tree"), num - len(deferred_contents), total)
2599
if entry.parent_id is None:
2602
file_id = entry.file_id
2604
precomputed_delta.append((None, tree_path, file_id, entry))
2605
if tree_path in existing_files:
2606
target_path = wt.abspath(tree_path)
2607
kind = file_kind(target_path)
2608
if kind == "directory":
2610
controldir.ControlDir.open(target_path)
2611
except errors.NotBranchError:
2615
if (file_id not in divert and
2616
_content_match(tree, entry, tree_path, file_id, kind,
2618
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2619
if kind == 'directory':
2621
parent_id = file_trans_id[entry.parent_id]
2622
if entry.kind == 'file':
2623
# We *almost* replicate new_by_entry, so that we can defer
2624
# getting the file text, and get them all at once.
2625
trans_id = tt.create_path(entry.name, parent_id)
2626
file_trans_id[file_id] = trans_id
2627
tt.version_file(file_id, trans_id)
2628
executable = tree.is_executable(tree_path, file_id)
2630
tt.set_executability(executable, trans_id)
2631
trans_data = (trans_id, file_id, tree_path, entry.text_sha1)
2632
deferred_contents.append((tree_path, trans_data))
2634
file_trans_id[file_id] = new_by_entry(
2635
tree_path, tt, entry, parent_id, tree)
2637
new_trans_id = file_trans_id[file_id]
2638
old_parent = tt.trans_id_tree_path(tree_path)
2639
_reparent_children(tt, old_parent, new_trans_id)
2640
offset = num + 1 - len(deferred_contents)
2641
_create_files(tt, tree, deferred_contents, pb, offset,
2642
accelerator_tree, hardlink)
2644
divert_trans = set(file_trans_id[f] for f in divert)
2645
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2646
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2647
if len(raw_conflicts) > 0:
2648
precomputed_delta = None
2649
conflicts = cook_conflicts(raw_conflicts, tt)
2650
for conflict in conflicts:
2651
trace.warning(text_type(conflict))
2653
wt.add_conflicts(conflicts)
2654
except errors.UnsupportedOperation:
2656
result = tt.apply(no_conflicts=True,
2657
precomputed_delta=precomputed_delta)
2664
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2666
total = len(desired_files) + offset
2668
if accelerator_tree is None:
2669
new_desired_files = desired_files
2671
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2672
unchanged = [(p[0], p[1]) for (f, p, c, v, d, n, k, e)
2673
in iter if not (c or e[0] != e[1])]
2674
if accelerator_tree.supports_content_filtering():
2675
unchanged = [(tp, ap) for (tp, ap) in unchanged
2676
if not next(accelerator_tree.iter_search_rules([ap]))]
2677
unchanged = dict(unchanged)
2678
new_desired_files = []
2680
for unused_tree_path, (trans_id, file_id, tree_path, text_sha1) in desired_files:
2681
accelerator_path = unchanged.get(tree_path)
2682
if accelerator_path is None:
2683
new_desired_files.append((tree_path,
2684
(trans_id, file_id, tree_path, text_sha1)))
2686
pb.update(gettext('Adding file contents'), count + offset, total)
2688
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2691
with accelerator_tree.get_file(accelerator_path, file_id) as f:
2692
chunks = osutils.file_iterator(f)
2693
if wt.supports_content_filtering():
2694
filters = wt._content_filter_stack(tree_path)
2695
chunks = filtered_output_bytes(chunks, filters,
2696
ContentFilterContext(tree_path, tree))
2697
tt.create_file(chunks, trans_id, sha1=text_sha1)
2700
for count, ((trans_id, file_id, tree_path, text_sha1), contents) in enumerate(
2701
tree.iter_files_bytes(new_desired_files)):
2702
if wt.supports_content_filtering():
2703
filters = wt._content_filter_stack(tree_path)
2704
contents = filtered_output_bytes(contents, filters,
2705
ContentFilterContext(tree_path, tree))
2706
tt.create_file(contents, trans_id, sha1=text_sha1)
2707
pb.update(gettext('Adding file contents'), count + offset, total)
2710
def _reparent_children(tt, old_parent, new_parent):
2711
for child in tt.iter_tree_children(old_parent):
2712
tt.adjust_path(tt.final_name(child), new_parent, child)
2715
def _reparent_transform_children(tt, old_parent, new_parent):
2716
by_parent = tt.by_parent()
2717
for child in by_parent[old_parent]:
2718
tt.adjust_path(tt.final_name(child), new_parent, child)
2719
return by_parent[old_parent]
2722
def _content_match(tree, entry, tree_path, file_id, kind, target_path):
2723
if entry.kind != kind:
2725
if entry.kind == "directory":
2727
if entry.kind == "file":
2728
with open(target_path, 'rb') as f1, \
2729
tree.get_file(tree_path, file_id) as f2:
2730
if osutils.compare_files(f1, f2):
2732
elif entry.kind == "symlink":
2733
if tree.get_symlink_target(tree_path, file_id) == os.readlink(target_path):
2738
def resolve_checkout(tt, conflicts, divert):
2739
new_conflicts = set()
2740
for c_type, conflict in ((c[0], c) for c in conflicts):
2741
# Anything but a 'duplicate' would indicate programmer error
2742
if c_type != 'duplicate':
2743
raise AssertionError(c_type)
2744
# Now figure out which is new and which is old
2745
if tt.new_contents(conflict[1]):
2746
new_file = conflict[1]
2747
old_file = conflict[2]
2749
new_file = conflict[2]
2750
old_file = conflict[1]
2752
# We should only get here if the conflict wasn't completely
2754
final_parent = tt.final_parent(old_file)
2755
if new_file in divert:
2756
new_name = tt.final_name(old_file)+'.diverted'
2757
tt.adjust_path(new_name, final_parent, new_file)
2758
new_conflicts.add((c_type, 'Diverted to',
2759
new_file, old_file))
2761
new_name = tt.final_name(old_file)+'.moved'
2762
tt.adjust_path(new_name, final_parent, old_file)
2763
new_conflicts.add((c_type, 'Moved existing file to',
2764
old_file, new_file))
2765
return new_conflicts
2768
def new_by_entry(path, tt, entry, parent_id, tree):
2769
"""Create a new file according to its inventory entry"""
2773
with tree.get_file(path, entry.file_id) as f:
2774
executable = tree.is_executable(path, entry.file_id)
2776
name, parent_id, osutils.file_iterator(f), entry.file_id,
2778
elif kind in ('directory', 'tree-reference'):
2779
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2780
if kind == 'tree-reference':
2781
tt.set_tree_reference(entry.reference_revision, trans_id)
2783
elif kind == 'symlink':
2784
target = tree.get_symlink_target(path, entry.file_id)
2785
return tt.new_symlink(name, parent_id, target, entry.file_id)
2787
raise errors.BadFileKindError(name, kind)
2790
def create_from_tree(tt, trans_id, tree, path, file_id=None, chunks=None,
2791
filter_tree_path=None):
2792
"""Create new file contents according to tree contents.
2794
:param filter_tree_path: the tree path to use to lookup
2795
content filters to apply to the bytes output in the working tree.
2796
This only applies if the working tree supports content filtering.
2798
kind = tree.kind(path, file_id)
2799
if kind == 'directory':
2800
tt.create_directory(trans_id)
2801
elif kind == "file":
2803
f = tree.get_file(path, file_id)
2804
chunks = osutils.file_iterator(f)
2809
if wt.supports_content_filtering() and filter_tree_path is not None:
2810
filters = wt._content_filter_stack(filter_tree_path)
2811
chunks = filtered_output_bytes(chunks, filters,
2812
ContentFilterContext(filter_tree_path, tree))
2813
tt.create_file(chunks, trans_id)
2817
elif kind == "symlink":
2818
tt.create_symlink(tree.get_symlink_target(path, file_id), trans_id)
2820
raise AssertionError('Unknown kind %r' % kind)
2823
def create_entry_executability(tt, entry, trans_id):
2824
"""Set the executability of a trans_id according to an inventory entry"""
2825
if entry.kind == "file":
2826
tt.set_executability(entry.executable, trans_id)
2829
def revert(working_tree, target_tree, filenames, backups=False,
2830
pb=None, change_reporter=None):
2831
"""Revert a working tree's contents to those of a target tree."""
2832
pb = ui.ui_factory.nested_progress_bar()
2834
with target_tree.lock_read(), TreeTransform(working_tree, pb) as tt:
2835
pp = ProgressPhase("Revert phase", 3, pb)
2836
conflicts, merge_modified = _prepare_revert_transform(
2837
working_tree, target_tree, tt, filenames, backups, pp)
2839
change_reporter = delta._ChangeReporter(
2840
unversioned_filter=working_tree.is_ignored)
2841
delta.report_changes(tt.iter_changes(), change_reporter)
2842
for conflict in conflicts:
2843
trace.warning(text_type(conflict))
2846
if working_tree.supports_merge_modified():
2847
working_tree.set_merge_modified(merge_modified)
2853
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2854
backups, pp, basis_tree=None,
2855
merge_modified=None):
2856
with ui.ui_factory.nested_progress_bar() as child_pb:
2857
if merge_modified is None:
2858
merge_modified = working_tree.merge_modified()
2859
merge_modified = _alter_files(working_tree, target_tree, tt,
2860
child_pb, filenames, backups,
2861
merge_modified, basis_tree)
2862
with ui.ui_factory.nested_progress_bar() as child_pb:
2863
raw_conflicts = resolve_conflicts(tt, child_pb,
2864
lambda t, c: conflict_pass(t, c, target_tree))
2865
conflicts = cook_conflicts(raw_conflicts, tt)
2866
return conflicts, merge_modified
2869
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2870
backups, merge_modified, basis_tree=None):
2871
if basis_tree is not None:
2872
basis_tree.lock_read()
2873
# We ask the working_tree for its changes relative to the target, rather
2874
# than the target changes relative to the working tree. Because WT4 has an
2875
# optimizer to compare itself to a target, but no optimizer for the
2877
change_list = working_tree.iter_changes(target_tree,
2878
specific_files=specific_files, pb=pb)
2879
if not target_tree.is_versioned(u''):
2885
for id_num, (file_id, path, changed_content, versioned, parent, name,
2886
kind, executable) in enumerate(change_list):
2887
target_path, wt_path = path
2888
target_versioned, wt_versioned = versioned
2889
target_parent, wt_parent = parent
2890
target_name, wt_name = name
2891
target_kind, wt_kind = kind
2892
target_executable, wt_executable = executable
2893
if skip_root and wt_parent is None:
2895
trans_id = tt.trans_id_file_id(file_id)
2898
keep_content = False
2899
if wt_kind == 'file' and (backups or target_kind is None):
2900
wt_sha1 = working_tree.get_file_sha1(wt_path, file_id)
2901
if merge_modified.get(file_id) != wt_sha1:
2902
# acquire the basis tree lazily to prevent the
2903
# expense of accessing it when it's not needed ?
2904
# (Guessing, RBC, 200702)
2905
if basis_tree is None:
2906
basis_tree = working_tree.basis_tree()
2907
basis_tree.lock_read()
2908
basis_path = find_previous_path(working_tree, basis_tree, wt_path)
2909
if basis_path is None:
2910
if target_kind is None and not target_versioned:
2913
if wt_sha1 != basis_tree.get_file_sha1(basis_path, file_id):
2915
if wt_kind is not None:
2916
if not keep_content:
2917
tt.delete_contents(trans_id)
2918
elif target_kind is not None:
2919
parent_trans_id = tt.trans_id_file_id(wt_parent)
2920
backup_name = tt._available_backup_name(
2921
wt_name, parent_trans_id)
2922
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2923
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2924
if wt_versioned and target_versioned:
2925
tt.unversion_file(trans_id)
2926
tt.version_file(file_id, new_trans_id)
2927
# New contents should have the same unix perms as old
2930
trans_id = new_trans_id
2931
if target_kind in ('directory', 'tree-reference'):
2932
tt.create_directory(trans_id)
2933
if target_kind == 'tree-reference':
2934
revision = target_tree.get_reference_revision(
2935
target_path, file_id)
2936
tt.set_tree_reference(revision, trans_id)
2937
elif target_kind == 'symlink':
2938
tt.create_symlink(target_tree.get_symlink_target(
2939
target_path, file_id), trans_id)
2940
elif target_kind == 'file':
2941
deferred_files.append((target_path, (trans_id, mode_id, file_id)))
2942
if basis_tree is None:
2943
basis_tree = working_tree.basis_tree()
2944
basis_tree.lock_read()
2945
new_sha1 = target_tree.get_file_sha1(target_path, file_id)
2946
basis_path = find_previous_path(target_tree, basis_tree, target_path)
2947
if (basis_path is not None and
2948
new_sha1 == basis_tree.get_file_sha1(basis_path, file_id)):
2949
if file_id in merge_modified:
2950
del merge_modified[file_id]
2952
merge_modified[file_id] = new_sha1
2954
# preserve the execute bit when backing up
2955
if keep_content and wt_executable == target_executable:
2956
tt.set_executability(target_executable, trans_id)
2957
elif target_kind is not None:
2958
raise AssertionError(target_kind)
2959
if not wt_versioned and target_versioned:
2960
tt.version_file(file_id, trans_id)
2961
if wt_versioned and not target_versioned:
2962
tt.unversion_file(trans_id)
2963
if (target_name is not None and
2964
(wt_name != target_name or wt_parent != target_parent)):
2965
if target_name == '' and target_parent is None:
2966
parent_trans = ROOT_PARENT
2968
parent_trans = tt.trans_id_file_id(target_parent)
2969
if wt_parent is None and wt_versioned:
2970
tt.adjust_root_path(target_name, parent_trans)
2972
tt.adjust_path(target_name, parent_trans, trans_id)
2973
if wt_executable != target_executable and target_kind == "file":
2974
tt.set_executability(target_executable, trans_id)
2975
if working_tree.supports_content_filtering():
2976
for (trans_id, mode_id, file_id), bytes in (
2977
target_tree.iter_files_bytes(deferred_files)):
2978
# We're reverting a tree to the target tree so using the
2979
# target tree to find the file path seems the best choice
2980
# here IMO - Ian C 27/Oct/2009
2981
filter_tree_path = target_tree.id2path(file_id)
2982
filters = working_tree._content_filter_stack(filter_tree_path)
2983
bytes = filtered_output_bytes(bytes, filters,
2984
ContentFilterContext(filter_tree_path, working_tree))
2985
tt.create_file(bytes, trans_id, mode_id)
2987
for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
2989
tt.create_file(bytes, trans_id, mode_id)
2990
tt.fixup_new_roots()
2992
if basis_tree is not None:
2994
return merge_modified
2997
def resolve_conflicts(tt, pb=None, pass_func=None):
2998
"""Make many conflict-resolution attempts, but die if they fail"""
2999
if pass_func is None:
3000
pass_func = conflict_pass
3001
new_conflicts = set()
3002
with ui.ui_factory.nested_progress_bar() as pb:
3004
pb.update(gettext('Resolution pass'), n+1, 10)
3005
conflicts = tt.find_conflicts()
3006
if len(conflicts) == 0:
3007
return new_conflicts
3008
new_conflicts.update(pass_func(tt, conflicts))
3009
raise MalformedTransform(conflicts=conflicts)
3012
def conflict_pass(tt, conflicts, path_tree=None):
3013
"""Resolve some classes of conflicts.
3015
:param tt: The transform to resolve conflicts in
3016
:param conflicts: The conflicts to resolve
3017
:param path_tree: A Tree to get supplemental paths from
3019
new_conflicts = set()
3020
for c_type, conflict in ((c[0], c) for c in conflicts):
3021
if c_type == 'duplicate id':
3022
tt.unversion_file(conflict[1])
3023
new_conflicts.add((c_type, 'Unversioned existing file',
3024
conflict[1], conflict[2], ))
3025
elif c_type == 'duplicate':
3026
# files that were renamed take precedence
3027
final_parent = tt.final_parent(conflict[1])
3028
if tt.path_changed(conflict[1]):
3029
existing_file, new_file = conflict[2], conflict[1]
3031
existing_file, new_file = conflict[1], conflict[2]
3032
new_name = tt.final_name(existing_file) + '.moved'
3033
tt.adjust_path(new_name, final_parent, existing_file)
3034
new_conflicts.add((c_type, 'Moved existing file to',
3035
existing_file, new_file))
3036
elif c_type == 'parent loop':
3037
# break the loop by undoing one of the ops that caused the loop
3039
while not tt.path_changed(cur):
3040
cur = tt.final_parent(cur)
3041
new_conflicts.add((c_type, 'Cancelled move', cur,
3042
tt.final_parent(cur),))
3043
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3045
elif c_type == 'missing parent':
3046
trans_id = conflict[1]
3047
if trans_id in tt._removed_contents:
3048
cancel_deletion = True
3049
orphans = tt._get_potential_orphans(trans_id)
3051
cancel_deletion = False
3052
# All children are orphans
3055
tt.new_orphan(o, trans_id)
3056
except OrphaningError:
3057
# Something bad happened so we cancel the directory
3058
# deletion which will leave it in place with a
3059
# conflict. The user can deal with it from there.
3060
# Note that this also catch the case where we don't
3061
# want to create orphans and leave the directory in
3063
cancel_deletion = True
3066
# Cancel the directory deletion
3067
tt.cancel_deletion(trans_id)
3068
new_conflicts.add(('deleting parent', 'Not deleting',
3073
tt.final_name(trans_id)
3075
if path_tree is not None:
3076
file_id = tt.final_file_id(trans_id)
3078
file_id = tt.inactive_file_id(trans_id)
3079
_, entry = next(path_tree.iter_entries_by_dir(
3080
specific_files=[path_tree.id2path(file_id)]))
3081
# special-case the other tree root (move its
3082
# children to current root)
3083
if entry.parent_id is None:
3085
moved = _reparent_transform_children(
3086
tt, trans_id, tt.root)
3088
new_conflicts.add((c_type, 'Moved to root',
3091
parent_trans_id = tt.trans_id_file_id(
3093
tt.adjust_path(entry.name, parent_trans_id,
3096
tt.create_directory(trans_id)
3097
new_conflicts.add((c_type, 'Created directory', trans_id))
3098
elif c_type == 'unversioned parent':
3099
file_id = tt.inactive_file_id(conflict[1])
3100
# special-case the other tree root (move its children instead)
3101
if path_tree and path_tree.path2id('') == file_id:
3102
# This is the root entry, skip it
3104
tt.version_file(file_id, conflict[1])
3105
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3106
elif c_type == 'non-directory parent':
3107
parent_id = conflict[1]
3108
parent_parent = tt.final_parent(parent_id)
3109
parent_name = tt.final_name(parent_id)
3110
parent_file_id = tt.final_file_id(parent_id)
3111
new_parent_id = tt.new_directory(parent_name + '.new',
3112
parent_parent, parent_file_id)
3113
_reparent_transform_children(tt, parent_id, new_parent_id)
3114
if parent_file_id is not None:
3115
tt.unversion_file(parent_id)
3116
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3117
elif c_type == 'versioning no contents':
3118
tt.cancel_versioning(conflict[1])
3119
return new_conflicts
3122
def cook_conflicts(raw_conflicts, tt):
3123
"""Generate a list of cooked conflicts, sorted by file path"""
3124
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3125
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3128
def iter_cook_conflicts(raw_conflicts, tt):
3130
for conflict in raw_conflicts:
3131
c_type = conflict[0]
3132
action = conflict[1]
3133
modified_path = fp.get_path(conflict[2])
3134
modified_id = tt.final_file_id(conflict[2])
3135
if len(conflict) == 3:
3136
yield conflicts.Conflict.factory(
3137
c_type, action=action, path=modified_path, file_id=modified_id)
3140
conflicting_path = fp.get_path(conflict[3])
3141
conflicting_id = tt.final_file_id(conflict[3])
3142
yield conflicts.Conflict.factory(
3143
c_type, action=action, path=modified_path,
3144
file_id=modified_id,
3145
conflict_path=conflicting_path,
3146
conflict_file_id=conflicting_id)
3149
class _FileMover(object):
3150
"""Moves and deletes files for TreeTransform, tracking operations"""
3153
self.past_renames = []
3154
self.pending_deletions = []
3156
def rename(self, from_, to):
3157
"""Rename a file from one path to another."""
3159
os.rename(from_, to)
3160
except OSError as e:
3161
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3162
raise errors.FileExists(to, str(e))
3163
# normal OSError doesn't include filenames so it's hard to see where
3164
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3165
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3166
self.past_renames.append((from_, to))
3168
def pre_delete(self, from_, to):
3169
"""Rename a file out of the way and mark it for deletion.
3171
Unlike os.unlink, this works equally well for files and directories.
3172
:param from_: The current file path
3173
:param to: A temporary path for the file
3175
self.rename(from_, to)
3176
self.pending_deletions.append(to)
3179
"""Reverse all renames that have been performed"""
3180
for from_, to in reversed(self.past_renames):
3182
os.rename(to, from_)
3183
except OSError as e:
3184
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3185
# after rollback, don't reuse _FileMover
3187
pending_deletions = None
3189
def apply_deletions(self):
3190
"""Apply all marked deletions"""
3191
for path in self.pending_deletions:
3193
# after apply_deletions, don't reuse _FileMover
3195
pending_deletions = None
3198
def link_tree(target_tree, source_tree):
3199
"""Where possible, hard-link files in a tree to those in another tree.
3201
:param target_tree: Tree to change
3202
:param source_tree: Tree to hard-link from
3204
tt = TreeTransform(target_tree)
3206
for (file_id, paths, changed_content, versioned, parent, name, kind,
3207
executable) in target_tree.iter_changes(source_tree,
3208
include_unchanged=True):
3211
if kind != ('file', 'file'):
3213
if executable[0] != executable[1]:
3215
trans_id = tt.trans_id_tree_path(paths[1])
3216
tt.delete_contents(trans_id)
3217
tt.create_hardlink(source_tree.abspath(paths[0]), trans_id)