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)))
1009
return (paths[0] or '', paths[1] or '')
1010
return iter(sorted(results, key=path_key))
1012
def get_preview_tree(self):
1013
"""Return a tree representing the result of the transform.
1015
The tree is a snapshot, and altering the TreeTransform will invalidate
1018
return _PreviewTree(self)
1020
def commit(self, branch, message, merge_parents=None, strict=False,
1021
timestamp=None, timezone=None, committer=None, authors=None,
1022
revprops=None, revision_id=None):
1023
"""Commit the result of this TreeTransform to a branch.
1025
:param branch: The branch to commit to.
1026
:param message: The message to attach to the commit.
1027
:param merge_parents: Additional parent revision-ids specified by
1029
:param strict: If True, abort the commit if there are unversioned
1031
:param timestamp: if not None, seconds-since-epoch for the time and
1032
date. (May be a float.)
1033
:param timezone: Optional timezone for timestamp, as an offset in
1035
:param committer: Optional committer in email-id format.
1036
(e.g. "J Random Hacker <jrandom@example.com>")
1037
:param authors: Optional list of authors in email-id format.
1038
:param revprops: Optional dictionary of revision properties.
1039
:param revision_id: Optional revision id. (Specifying a revision-id
1040
may reduce performance for some non-native formats.)
1041
:return: The revision_id of the revision committed.
1043
self._check_malformed()
1045
unversioned = set(self._new_contents).difference(set(self._new_id))
1046
for trans_id in unversioned:
1047
if self.final_file_id(trans_id) is None:
1048
raise errors.StrictCommitFailed()
1050
revno, last_rev_id = branch.last_revision_info()
1051
if last_rev_id == _mod_revision.NULL_REVISION:
1052
if merge_parents is not None:
1053
raise ValueError('Cannot supply merge parents for first'
1057
parent_ids = [last_rev_id]
1058
if merge_parents is not None:
1059
parent_ids.extend(merge_parents)
1060
if self._tree.get_revision_id() != last_rev_id:
1061
raise ValueError('TreeTransform not based on branch basis: %s' %
1062
self._tree.get_revision_id().decode('utf-8'))
1063
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1064
builder = branch.get_commit_builder(parent_ids,
1065
timestamp=timestamp,
1067
committer=committer,
1069
revision_id=revision_id)
1070
preview = self.get_preview_tree()
1071
list(builder.record_iter_changes(preview, last_rev_id,
1072
self.iter_changes()))
1073
builder.finish_inventory()
1074
revision_id = builder.commit(message)
1075
branch.set_last_revision_info(revno + 1, revision_id)
1078
def _text_parent(self, trans_id):
1079
path = self.tree_path(trans_id)
1081
if path is None or self._tree.kind(path) != 'file':
1083
except errors.NoSuchFile:
1087
def _get_parents_texts(self, trans_id):
1088
"""Get texts for compression parents of this file."""
1089
path = self._text_parent(trans_id)
1092
return (self._tree.get_file_text(path),)
1094
def _get_parents_lines(self, trans_id):
1095
"""Get lines for compression parents of this file."""
1096
path = self._text_parent(trans_id)
1099
return (self._tree.get_file_lines(path),)
1101
def serialize(self, serializer):
1102
"""Serialize this TreeTransform.
1104
:param serializer: A Serialiser like pack.ContainerSerializer.
1106
new_name = {k.encode('utf-8'): v.encode('utf-8')
1107
for k, v in viewitems(self._new_name)}
1108
new_parent = {k.encode('utf-8'): v.encode('utf-8')
1109
for k, v in viewitems(self._new_parent)}
1110
new_id = {k.encode('utf-8'): v
1111
for k, v in viewitems(self._new_id)}
1112
new_executability = {k.encode('utf-8'): int(v)
1113
for k, v in viewitems(self._new_executability)}
1114
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
1115
for k, v in viewitems(self._tree_path_ids)}
1116
non_present_ids = {k: v.encode('utf-8')
1117
for k, v in viewitems(self._non_present_ids)}
1118
removed_contents = [trans_id.encode('utf-8')
1119
for trans_id in self._removed_contents]
1120
removed_id = [trans_id.encode('utf-8')
1121
for trans_id in self._removed_id]
1123
b'_id_number': self._id_number,
1124
b'_new_name': new_name,
1125
b'_new_parent': new_parent,
1126
b'_new_executability': new_executability,
1128
b'_tree_path_ids': tree_path_ids,
1129
b'_removed_id': removed_id,
1130
b'_removed_contents': removed_contents,
1131
b'_non_present_ids': non_present_ids,
1133
yield serializer.bytes_record(bencode.bencode(attribs),
1135
for trans_id, kind in sorted(viewitems(self._new_contents)):
1137
with open(self._limbo_name(trans_id), 'rb') as cur_file:
1138
lines = cur_file.readlines()
1139
parents = self._get_parents_lines(trans_id)
1140
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1141
content = b''.join(mpdiff.to_patch())
1142
if kind == 'directory':
1144
if kind == 'symlink':
1145
content = self._read_symlink_target(trans_id)
1146
if not isinstance(content, bytes):
1147
content = content.encode('utf-8')
1148
yield serializer.bytes_record(
1149
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
1151
def deserialize(self, records):
1152
"""Deserialize a stored TreeTransform.
1154
:param records: An iterable of (names, content) tuples, as per
1155
pack.ContainerPushParser.
1157
names, content = next(records)
1158
attribs = bencode.bdecode(content)
1159
self._id_number = attribs[b'_id_number']
1160
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
1161
for k, v in viewitems(attribs[b'_new_name'])}
1162
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
1163
for k, v in viewitems(attribs[b'_new_parent'])}
1164
self._new_executability = {k.decode('utf-8'): bool(v)
1165
for k, v in viewitems(attribs[b'_new_executability'])}
1166
self._new_id = {k.decode('utf-8'): v
1167
for k, v in viewitems(attribs[b'_new_id'])}
1168
self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
1169
self._tree_path_ids = {}
1170
self._tree_id_paths = {}
1171
for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
1172
path = bytepath.decode('utf-8')
1173
trans_id = trans_id.decode('utf-8')
1174
self._tree_path_ids[path] = trans_id
1175
self._tree_id_paths[trans_id] = path
1176
self._removed_id = {trans_id.decode('utf-8')
1177
for trans_id in attribs[b'_removed_id']}
1178
self._removed_contents = set(trans_id.decode('utf-8')
1179
for trans_id in attribs[b'_removed_contents'])
1180
self._non_present_ids = {k: v.decode('utf-8')
1181
for k, v in viewitems(attribs[b'_non_present_ids'])}
1182
for ((trans_id, kind),), content in records:
1183
trans_id = trans_id.decode('utf-8')
1184
kind = kind.decode('ascii')
1186
mpdiff = multiparent.MultiParent.from_patch(content)
1187
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1188
self.create_file(lines, trans_id)
1189
if kind == 'directory':
1190
self.create_directory(trans_id)
1191
if kind == 'symlink':
1192
self.create_symlink(content.decode('utf-8'), trans_id)
1195
class DiskTreeTransform(TreeTransformBase):
1196
"""Tree transform storing its contents on disk."""
1198
def __init__(self, tree, limbodir, pb=None,
1199
case_sensitive=True):
1201
:param tree: The tree that will be transformed, but not necessarily
1203
:param limbodir: A directory where new files can be stored until
1204
they are installed in their proper places
1206
:param case_sensitive: If True, the target of the transform is
1207
case sensitive, not just case preserving.
1209
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1210
self._limbodir = limbodir
1211
self._deletiondir = None
1212
# A mapping of transform ids to their limbo filename
1213
self._limbo_files = {}
1214
self._possibly_stale_limbo_files = set()
1215
# A mapping of transform ids to a set of the transform ids of children
1216
# that their limbo directory has
1217
self._limbo_children = {}
1218
# Map transform ids to maps of child filename to child transform id
1219
self._limbo_children_names = {}
1220
# List of transform ids that need to be renamed from limbo into place
1221
self._needs_rename = set()
1222
self._creation_mtime = None
1225
"""Release the working tree lock, if held, clean up limbo dir.
1227
This is required if apply has not been invoked, but can be invoked
1230
if self._tree is None:
1233
limbo_paths = list(viewvalues(self._limbo_files))
1234
limbo_paths.extend(self._possibly_stale_limbo_files)
1235
limbo_paths.sort(reverse=True)
1236
for path in limbo_paths:
1239
except OSError as e:
1240
if e.errno != errno.ENOENT:
1242
# XXX: warn? perhaps we just got interrupted at an
1243
# inconvenient moment, but perhaps files are disappearing
1246
delete_any(self._limbodir)
1248
# We don't especially care *why* the dir is immortal.
1249
raise ImmortalLimbo(self._limbodir)
1251
if self._deletiondir is not None:
1252
delete_any(self._deletiondir)
1254
raise errors.ImmortalPendingDeletion(self._deletiondir)
1256
TreeTransformBase.finalize(self)
1258
def _limbo_supports_executable(self):
1259
"""Check if the limbo path supports the executable bit."""
1260
# FIXME: Check actual file system capabilities of limbodir
1261
return osutils.supports_executable()
1263
def _limbo_name(self, trans_id):
1264
"""Generate the limbo name of a file"""
1265
limbo_name = self._limbo_files.get(trans_id)
1266
if limbo_name is None:
1267
limbo_name = self._generate_limbo_path(trans_id)
1268
self._limbo_files[trans_id] = limbo_name
1271
def _generate_limbo_path(self, trans_id):
1272
"""Generate a limbo path using the trans_id as the relative path.
1274
This is suitable as a fallback, and when the transform should not be
1275
sensitive to the path encoding of the limbo directory.
1277
self._needs_rename.add(trans_id)
1278
return pathjoin(self._limbodir, trans_id)
1280
def adjust_path(self, name, parent, trans_id):
1281
previous_parent = self._new_parent.get(trans_id)
1282
previous_name = self._new_name.get(trans_id)
1283
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1284
if (trans_id in self._limbo_files and
1285
trans_id not in self._needs_rename):
1286
self._rename_in_limbo([trans_id])
1287
if previous_parent != parent:
1288
self._limbo_children[previous_parent].remove(trans_id)
1289
if previous_parent != parent or previous_name != name:
1290
del self._limbo_children_names[previous_parent][previous_name]
1292
def _rename_in_limbo(self, trans_ids):
1293
"""Fix limbo names so that the right final path is produced.
1295
This means we outsmarted ourselves-- we tried to avoid renaming
1296
these files later by creating them with their final names in their
1297
final parents. But now the previous name or parent is no longer
1298
suitable, so we have to rename them.
1300
Even for trans_ids that have no new contents, we must remove their
1301
entries from _limbo_files, because they are now stale.
1303
for trans_id in trans_ids:
1304
old_path = self._limbo_files[trans_id]
1305
self._possibly_stale_limbo_files.add(old_path)
1306
del self._limbo_files[trans_id]
1307
if trans_id not in self._new_contents:
1309
new_path = self._limbo_name(trans_id)
1310
os.rename(old_path, new_path)
1311
self._possibly_stale_limbo_files.remove(old_path)
1312
for descendant in self._limbo_descendants(trans_id):
1313
desc_path = self._limbo_files[descendant]
1314
desc_path = new_path + desc_path[len(old_path):]
1315
self._limbo_files[descendant] = desc_path
1317
def _limbo_descendants(self, trans_id):
1318
"""Return the set of trans_ids whose limbo paths descend from this."""
1319
descendants = set(self._limbo_children.get(trans_id, []))
1320
for descendant in list(descendants):
1321
descendants.update(self._limbo_descendants(descendant))
1324
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1325
"""Schedule creation of a new file.
1329
:param contents: an iterator of strings, all of which will be written
1330
to the target destination.
1331
:param trans_id: TreeTransform handle
1332
:param mode_id: If not None, force the mode of the target file to match
1333
the mode of the object referenced by mode_id.
1334
Otherwise, we will try to preserve mode bits of an existing file.
1335
:param sha1: If the sha1 of this content is already known, pass it in.
1336
We can use it to prevent future sha1 computations.
1338
name = self._limbo_name(trans_id)
1339
with open(name, 'wb') as f:
1340
unique_add(self._new_contents, trans_id, 'file')
1341
f.writelines(contents)
1342
self._set_mtime(name)
1343
self._set_mode(trans_id, mode_id, S_ISREG)
1344
# It is unfortunate we have to use lstat instead of fstat, but we just
1345
# used utime and chmod on the file, so we need the accurate final
1347
if sha1 is not None:
1348
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1350
def _read_symlink_target(self, trans_id):
1351
return os.readlink(self._limbo_name(trans_id))
1353
def _set_mtime(self, path):
1354
"""All files that are created get the same mtime.
1356
This time is set by the first object to be created.
1358
if self._creation_mtime is None:
1359
self._creation_mtime = time.time()
1360
os.utime(path, (self._creation_mtime, self._creation_mtime))
1362
def create_hardlink(self, path, trans_id):
1363
"""Schedule creation of a hard link"""
1364
name = self._limbo_name(trans_id)
1367
except OSError as e:
1368
if e.errno != errno.EPERM:
1370
raise errors.HardLinkNotSupported(path)
1372
unique_add(self._new_contents, trans_id, 'file')
1374
# Clean up the file, it never got registered so
1375
# TreeTransform.finalize() won't clean it up.
1379
def create_directory(self, trans_id):
1380
"""Schedule creation of a new directory.
1382
See also new_directory.
1384
os.mkdir(self._limbo_name(trans_id))
1385
unique_add(self._new_contents, trans_id, 'directory')
1387
def create_symlink(self, target, trans_id):
1388
"""Schedule creation of a new symbolic link.
1390
target is a bytestring.
1391
See also new_symlink.
1394
os.symlink(target, self._limbo_name(trans_id))
1395
unique_add(self._new_contents, trans_id, 'symlink')
1398
path = FinalPaths(self).get_path(trans_id)
1401
raise UnableCreateSymlink(path=path)
1403
def cancel_creation(self, trans_id):
1404
"""Cancel the creation of new file contents."""
1405
del self._new_contents[trans_id]
1406
if trans_id in self._observed_sha1s:
1407
del self._observed_sha1s[trans_id]
1408
children = self._limbo_children.get(trans_id)
1409
# if this is a limbo directory with children, move them before removing
1411
if children is not None:
1412
self._rename_in_limbo(children)
1413
del self._limbo_children[trans_id]
1414
del self._limbo_children_names[trans_id]
1415
delete_any(self._limbo_name(trans_id))
1417
def new_orphan(self, trans_id, parent_id):
1418
conf = self._tree.get_config_stack()
1419
handle_orphan = conf.get('transform.orphan_policy')
1420
handle_orphan(self, trans_id, parent_id)
1423
class OrphaningError(errors.BzrError):
1425
# Only bugs could lead to such exception being seen by the user
1426
internal_error = True
1427
_fmt = "Error while orphaning %s in %s directory"
1429
def __init__(self, orphan, parent):
1430
errors.BzrError.__init__(self)
1431
self.orphan = orphan
1432
self.parent = parent
1435
class OrphaningForbidden(OrphaningError):
1437
_fmt = "Policy: %s doesn't allow creating orphans."
1439
def __init__(self, policy):
1440
errors.BzrError.__init__(self)
1441
self.policy = policy
1444
def move_orphan(tt, orphan_id, parent_id):
1445
"""See TreeTransformBase.new_orphan.
1447
This creates a new orphan in the `brz-orphans` dir at the root of the
1450
:param tt: The TreeTransform orphaning `trans_id`.
1452
:param orphan_id: The trans id that should be orphaned.
1454
:param parent_id: The orphan parent trans id.
1456
# Add the orphan dir if it doesn't exist
1457
orphan_dir_basename = 'brz-orphans'
1458
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1459
if tt.final_kind(od_id) is None:
1460
tt.create_directory(od_id)
1461
parent_path = tt._tree_id_paths[parent_id]
1462
# Find a name that doesn't exist yet in the orphan dir
1463
actual_name = tt.final_name(orphan_id)
1464
new_name = tt._available_backup_name(actual_name, od_id)
1465
tt.adjust_path(new_name, od_id, orphan_id)
1466
trace.warning('%s has been orphaned in %s'
1467
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1470
def refuse_orphan(tt, orphan_id, parent_id):
1471
"""See TreeTransformBase.new_orphan.
1473
This refuses to create orphan, letting the caller handle the conflict.
1475
raise OrphaningForbidden('never')
1478
orphaning_registry = registry.Registry()
1479
orphaning_registry.register(
1480
u'conflict', refuse_orphan,
1481
'Leave orphans in place and create a conflict on the directory.')
1482
orphaning_registry.register(
1483
u'move', move_orphan,
1484
'Move orphans into the brz-orphans directory.')
1485
orphaning_registry._set_default_key(u'conflict')
1488
opt_transform_orphan = _mod_config.RegistryOption(
1489
'transform.orphan_policy', orphaning_registry,
1490
help='Policy for orphaned files during transform operations.',
1494
class TreeTransform(DiskTreeTransform):
1495
"""Represent a tree transformation.
1497
This object is designed to support incremental generation of the transform,
1500
However, it gives optimum performance when parent directories are created
1501
before their contents. The transform is then able to put child files
1502
directly in their parent directory, avoiding later renames.
1504
It is easy to produce malformed transforms, but they are generally
1505
harmless. Attempting to apply a malformed transform will cause an
1506
exception to be raised before any modifications are made to the tree.
1508
Many kinds of malformed transforms can be corrected with the
1509
resolve_conflicts function. The remaining ones indicate programming error,
1510
such as trying to create a file with no path.
1512
Two sets of file creation methods are supplied. Convenience methods are:
1517
These are composed of the low-level methods:
1519
* create_file or create_directory or create_symlink
1523
Transform/Transaction ids
1524
-------------------------
1525
trans_ids are temporary ids assigned to all files involved in a transform.
1526
It's possible, even common, that not all files in the Tree have trans_ids.
1528
trans_ids are used because filenames and file_ids are not good enough
1529
identifiers; filenames change, and not all files have file_ids. File-ids
1530
are also associated with trans-ids, so that moving a file moves its
1533
trans_ids are only valid for the TreeTransform that generated them.
1537
Limbo is a temporary directory use to hold new versions of files.
1538
Files are added to limbo by create_file, create_directory, create_symlink,
1539
and their convenience variants (new_*). Files may be removed from limbo
1540
using cancel_creation. Files are renamed from limbo into their final
1541
location as part of TreeTransform.apply
1543
Limbo must be cleaned up, by either calling TreeTransform.apply or
1544
calling TreeTransform.finalize.
1546
Files are placed into limbo inside their parent directories, where
1547
possible. This reduces subsequent renames, and makes operations involving
1548
lots of files faster. This optimization is only possible if the parent
1549
directory is created *before* creating any of its children, so avoid
1550
creating children before parents, where possible.
1554
This temporary directory is used by _FileMover for storing files that are
1555
about to be deleted. In case of rollback, the files will be restored.
1556
FileMover does not delete files until it is sure that a rollback will not
1559
def __init__(self, tree, pb=None):
1560
"""Note: a tree_write lock is taken on the tree.
1562
Use TreeTransform.finalize() to release the lock (can be omitted if
1563
TreeTransform.apply() called).
1565
tree.lock_tree_write()
1567
limbodir = urlutils.local_path_from_url(
1568
tree._transport.abspath('limbo'))
1569
osutils.ensure_empty_directory_exists(
1571
errors.ExistingLimbo)
1572
deletiondir = urlutils.local_path_from_url(
1573
tree._transport.abspath('pending-deletion'))
1574
osutils.ensure_empty_directory_exists(
1576
errors.ExistingPendingDeletion)
1581
# Cache of realpath results, to speed up canonical_path
1582
self._realpaths = {}
1583
# Cache of relpath results, to speed up canonical_path
1585
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1586
tree.case_sensitive)
1587
self._deletiondir = deletiondir
1589
def canonical_path(self, path):
1590
"""Get the canonical tree-relative path"""
1591
# don't follow final symlinks
1592
abs = self._tree.abspath(path)
1593
if abs in self._relpaths:
1594
return self._relpaths[abs]
1595
dirname, basename = os.path.split(abs)
1596
if dirname not in self._realpaths:
1597
self._realpaths[dirname] = os.path.realpath(dirname)
1598
dirname = self._realpaths[dirname]
1599
abs = pathjoin(dirname, basename)
1600
if dirname in self._relpaths:
1601
relpath = pathjoin(self._relpaths[dirname], basename)
1602
relpath = relpath.rstrip('/\\')
1604
relpath = self._tree.relpath(abs)
1605
self._relpaths[abs] = relpath
1608
def tree_kind(self, trans_id):
1609
"""Determine the file kind in the working tree.
1611
:returns: The file kind or None if the file does not exist
1613
path = self._tree_id_paths.get(trans_id)
1617
return file_kind(self._tree.abspath(path))
1618
except errors.NoSuchFile:
1621
def _set_mode(self, trans_id, mode_id, typefunc):
1622
"""Set the mode of new file contents.
1623
The mode_id is the existing file to get the mode from (often the same
1624
as trans_id). The operation is only performed if there's a mode match
1625
according to typefunc.
1630
old_path = self._tree_id_paths[mode_id]
1634
mode = os.stat(self._tree.abspath(old_path)).st_mode
1635
except OSError as e:
1636
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1637
# Either old_path doesn't exist, or the parent of the
1638
# target is not a directory (but will be one eventually)
1639
# Either way, we know it doesn't exist *right now*
1640
# See also bug #248448
1645
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1647
def iter_tree_children(self, parent_id):
1648
"""Iterate through the entry's tree children, if any"""
1650
path = self._tree_id_paths[parent_id]
1654
children = os.listdir(self._tree.abspath(path))
1655
except OSError as e:
1656
if not (osutils._is_error_enotdir(e)
1657
or e.errno in (errno.ENOENT, errno.ESRCH)):
1661
for child in children:
1662
childpath = joinpath(path, child)
1663
if self._tree.is_control_filename(childpath):
1665
yield self.trans_id_tree_path(childpath)
1667
def _generate_limbo_path(self, trans_id):
1668
"""Generate a limbo path using the final path if possible.
1670
This optimizes the performance of applying the tree transform by
1671
avoiding renames. These renames can be avoided only when the parent
1672
directory is already scheduled for creation.
1674
If the final path cannot be used, falls back to using the trans_id as
1677
parent = self._new_parent.get(trans_id)
1678
# if the parent directory is already in limbo (e.g. when building a
1679
# tree), choose a limbo name inside the parent, to reduce further
1681
use_direct_path = False
1682
if self._new_contents.get(parent) == 'directory':
1683
filename = self._new_name.get(trans_id)
1684
if filename is not None:
1685
if parent not in self._limbo_children:
1686
self._limbo_children[parent] = set()
1687
self._limbo_children_names[parent] = {}
1688
use_direct_path = True
1689
# the direct path can only be used if no other file has
1690
# already taken this pathname, i.e. if the name is unused, or
1691
# if it is already associated with this trans_id.
1692
elif self._case_sensitive_target:
1693
if (self._limbo_children_names[parent].get(filename)
1694
in (trans_id, None)):
1695
use_direct_path = True
1697
for l_filename, l_trans_id in viewitems(
1698
self._limbo_children_names[parent]):
1699
if l_trans_id == trans_id:
1701
if l_filename.lower() == filename.lower():
1704
use_direct_path = True
1706
if not use_direct_path:
1707
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1709
limbo_name = pathjoin(self._limbo_files[parent], filename)
1710
self._limbo_children[parent].add(trans_id)
1711
self._limbo_children_names[parent][filename] = trans_id
1714
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1715
"""Apply all changes to the inventory and filesystem.
1717
If filesystem or inventory conflicts are present, MalformedTransform
1720
If apply succeeds, finalize is not necessary.
1722
:param no_conflicts: if True, the caller guarantees there are no
1723
conflicts, so no check is made.
1724
:param precomputed_delta: An inventory delta to use instead of
1726
:param _mover: Supply an alternate FileMover, for testing
1728
for hook in MutableTree.hooks['pre_transform']:
1729
hook(self._tree, self)
1730
if not no_conflicts:
1731
self._check_malformed()
1732
with ui.ui_factory.nested_progress_bar() as child_pb:
1733
if precomputed_delta is None:
1734
child_pb.update(gettext('Apply phase'), 0, 2)
1735
inventory_delta = self._generate_inventory_delta()
1738
inventory_delta = precomputed_delta
1741
mover = _FileMover()
1745
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1746
self._apply_removals(mover)
1747
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1748
modified_paths = self._apply_insertions(mover)
1753
mover.apply_deletions()
1754
if self.final_file_id(self.root) is None:
1755
inventory_delta = [e for e in inventory_delta if e[0] != '']
1756
self._tree.apply_inventory_delta(inventory_delta)
1757
self._apply_observed_sha1s()
1760
return _TransformResults(modified_paths, self.rename_count)
1762
def _generate_inventory_delta(self):
1763
"""Generate an inventory delta for the current transform."""
1764
inventory_delta = []
1765
new_paths = self._inventory_altered()
1766
total_entries = len(new_paths) + len(self._removed_id)
1767
with ui.ui_factory.nested_progress_bar() as child_pb:
1768
for num, trans_id in enumerate(self._removed_id):
1770
child_pb.update(gettext('removing file'), num, total_entries)
1771
if trans_id == self._new_root:
1772
file_id = self._tree.get_root_id()
1774
file_id = self.tree_file_id(trans_id)
1775
# File-id isn't really being deleted, just moved
1776
if file_id in self._r_new_id:
1778
path = self._tree_id_paths[trans_id]
1779
inventory_delta.append((path, None, file_id, None))
1780
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1783
for num, (path, trans_id) in enumerate(new_paths):
1785
child_pb.update(gettext('adding file'),
1786
num + len(self._removed_id), total_entries)
1787
file_id = new_path_file_ids[trans_id]
1791
kind = self.final_kind(trans_id)
1793
kind = self._tree.stored_kind(
1794
self._tree.id2path(file_id), file_id)
1795
parent_trans_id = self.final_parent(trans_id)
1796
parent_file_id = new_path_file_ids.get(parent_trans_id)
1797
if parent_file_id is None:
1798
parent_file_id = self.final_file_id(parent_trans_id)
1799
if trans_id in self._new_reference_revision:
1800
new_entry = inventory.TreeReference(
1802
self._new_name[trans_id],
1803
self.final_file_id(self._new_parent[trans_id]),
1804
None, self._new_reference_revision[trans_id])
1806
new_entry = inventory.make_entry(kind,
1807
self.final_name(trans_id),
1808
parent_file_id, file_id)
1810
old_path = self._tree.id2path(new_entry.file_id)
1811
except errors.NoSuchId:
1813
new_executability = self._new_executability.get(trans_id)
1814
if new_executability is not None:
1815
new_entry.executable = new_executability
1816
inventory_delta.append(
1817
(old_path, path, new_entry.file_id, new_entry))
1818
return inventory_delta
1820
def _apply_removals(self, mover):
1821
"""Perform tree operations that remove directory/inventory names.
1823
That is, delete files that are to be deleted, and put any files that
1824
need renaming into limbo. This must be done in strict child-to-parent
1827
If inventory_delta is None, no inventory delta generation is performed.
1829
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1830
with ui.ui_factory.nested_progress_bar() as child_pb:
1831
for num, (path, trans_id) in enumerate(tree_paths):
1832
# do not attempt to move root into a subdirectory of itself.
1835
child_pb.update(gettext('removing file'), num, len(tree_paths))
1836
full_path = self._tree.abspath(path)
1837
if trans_id in self._removed_contents:
1838
delete_path = os.path.join(self._deletiondir, trans_id)
1839
mover.pre_delete(full_path, delete_path)
1840
elif (trans_id in self._new_name
1841
or trans_id in self._new_parent):
1843
mover.rename(full_path, self._limbo_name(trans_id))
1844
except errors.TransformRenameFailed as e:
1845
if e.errno != errno.ENOENT:
1848
self.rename_count += 1
1850
def _apply_insertions(self, mover):
1851
"""Perform tree operations that insert directory/inventory names.
1853
That is, create any files that need to be created, and restore from
1854
limbo any files that needed renaming. This must be done in strict
1855
parent-to-child order.
1857
If inventory_delta is None, no inventory delta is calculated, and
1858
no list of modified paths is returned.
1860
new_paths = self.new_paths(filesystem_only=True)
1862
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1864
with ui.ui_factory.nested_progress_bar() as child_pb:
1865
for num, (path, trans_id) in enumerate(new_paths):
1867
child_pb.update(gettext('adding file'), num, len(new_paths))
1868
full_path = self._tree.abspath(path)
1869
if trans_id in self._needs_rename:
1871
mover.rename(self._limbo_name(trans_id), full_path)
1872
except errors.TransformRenameFailed as e:
1873
# We may be renaming a dangling inventory id
1874
if e.errno != errno.ENOENT:
1877
self.rename_count += 1
1878
# TODO: if trans_id in self._observed_sha1s, we should
1879
# re-stat the final target, since ctime will be
1880
# updated by the change.
1881
if (trans_id in self._new_contents or
1882
self.path_changed(trans_id)):
1883
if trans_id in self._new_contents:
1884
modified_paths.append(full_path)
1885
if trans_id in self._new_executability:
1886
self._set_executability(path, trans_id)
1887
if trans_id in self._observed_sha1s:
1888
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1889
st = osutils.lstat(full_path)
1890
self._observed_sha1s[trans_id] = (o_sha1, st)
1891
for path, trans_id in new_paths:
1892
# new_paths includes stuff like workingtree conflicts. Only the
1893
# stuff in new_contents actually comes from limbo.
1894
if trans_id in self._limbo_files:
1895
del self._limbo_files[trans_id]
1896
self._new_contents.clear()
1897
return modified_paths
1899
def _apply_observed_sha1s(self):
1900
"""After we have finished renaming everything, update observed sha1s
1902
This has to be done after self._tree.apply_inventory_delta, otherwise
1903
it doesn't know anything about the files we are updating. Also, we want
1904
to do this as late as possible, so that most entries end up cached.
1906
# TODO: this doesn't update the stat information for directories. So
1907
# the first 'bzr status' will still need to rewrite
1908
# .bzr/checkout/dirstate. However, we at least don't need to
1909
# re-read all of the files.
1910
# TODO: If the operation took a while, we could do a time.sleep(3) here
1911
# to allow the clock to tick over and ensure we won't have any
1912
# problems. (we could observe start time, and finish time, and if
1913
# it is less than eg 10% overhead, add a sleep call.)
1914
paths = FinalPaths(self)
1915
for trans_id, observed in viewitems(self._observed_sha1s):
1916
path = paths.get_path(trans_id)
1917
# We could get the file_id, but dirstate prefers to use the path
1918
# anyway, and it is 'cheaper' to determine.
1919
# file_id = self._new_id[trans_id]
1920
self._tree._observed_sha1(None, path, observed)
1923
class TransformPreview(DiskTreeTransform):
1924
"""A TreeTransform for generating preview trees.
1926
Unlike TreeTransform, this version works when the input tree is a
1927
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1928
unversioned files in the input tree.
1931
def __init__(self, tree, pb=None, case_sensitive=True):
1933
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1934
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1936
def canonical_path(self, path):
1939
def tree_kind(self, trans_id):
1940
path = self._tree_id_paths.get(trans_id)
1943
kind = self._tree.path_content_summary(path)[0]
1944
if kind == 'missing':
1948
def _set_mode(self, trans_id, mode_id, typefunc):
1949
"""Set the mode of new file contents.
1950
The mode_id is the existing file to get the mode from (often the same
1951
as trans_id). The operation is only performed if there's a mode match
1952
according to typefunc.
1954
# is it ok to ignore this? probably
1957
def iter_tree_children(self, parent_id):
1958
"""Iterate through the entry's tree children, if any"""
1960
path = self._tree_id_paths[parent_id]
1964
entry = next(self._tree.iter_entries_by_dir(
1965
specific_files=[path]))[1]
1966
except StopIteration:
1968
children = getattr(entry, 'children', {})
1969
for child in children:
1970
childpath = joinpath(path, child)
1971
yield self.trans_id_tree_path(childpath)
1973
def new_orphan(self, trans_id, parent_id):
1974
raise NotImplementedError(self.new_orphan)
1977
class _PreviewTree(inventorytree.InventoryTree):
1978
"""Partial implementation of Tree to support show_diff_trees"""
1980
def __init__(self, transform):
1981
self._transform = transform
1982
self._final_paths = FinalPaths(transform)
1983
self.__by_parent = None
1984
self._parent_ids = []
1985
self._all_children_cache = {}
1986
self._path2trans_id_cache = {}
1987
self._final_name_cache = {}
1988
self._iter_changes_cache = dict((c[0], c) for c in
1989
self._transform.iter_changes())
1991
def _content_change(self, file_id):
1992
"""Return True if the content of this file changed"""
1993
changes = self._iter_changes_cache.get(file_id)
1994
# changes[2] is true if the file content changed. See
1995
# InterTree.iter_changes.
1996
return (changes is not None and changes[2])
1998
def _get_repository(self):
1999
repo = getattr(self._transform._tree, '_repository', None)
2001
repo = self._transform._tree.branch.repository
2004
def _iter_parent_trees(self):
2005
for revision_id in self.get_parent_ids():
2007
yield self.revision_tree(revision_id)
2008
except errors.NoSuchRevisionInTree:
2009
yield self._get_repository().revision_tree(revision_id)
2011
def _get_file_revision(self, path, file_id, vf, tree_revision):
2013
(file_id, t.get_file_revision(t.id2path(file_id), file_id))
2014
for t in self._iter_parent_trees()]
2015
vf.add_lines((file_id, tree_revision), parent_keys,
2016
self.get_file_lines(path, file_id))
2017
repo = self._get_repository()
2018
base_vf = repo.texts
2019
if base_vf not in vf.fallback_versionedfiles:
2020
vf.fallback_versionedfiles.append(base_vf)
2021
return tree_revision
2023
def _stat_limbo_file(self, trans_id):
2024
name = self._transform._limbo_name(trans_id)
2025
return os.lstat(name)
2028
def _by_parent(self):
2029
if self.__by_parent is None:
2030
self.__by_parent = self._transform.by_parent()
2031
return self.__by_parent
2033
def _comparison_data(self, entry, path):
2034
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2035
if kind == 'missing':
2039
file_id = self._transform.final_file_id(self._path2trans_id(path))
2040
executable = self.is_executable(path, file_id)
2041
return kind, executable, None
2043
def is_locked(self):
2046
def lock_read(self):
2047
# Perhaps in theory, this should lock the TreeTransform?
2048
return lock.LogicalLockResult(self.unlock)
2054
def root_inventory(self):
2055
"""This Tree does not use inventory as its backing data."""
2056
raise NotImplementedError(_PreviewTree.root_inventory)
2058
def get_root_id(self):
2059
return self._transform.final_file_id(self._transform.root)
2061
def all_file_ids(self):
2062
tree_ids = set(self._transform._tree.all_file_ids())
2063
tree_ids.difference_update(self._transform.tree_file_id(t)
2064
for t in self._transform._removed_id)
2065
tree_ids.update(viewvalues(self._transform._new_id))
2068
def all_versioned_paths(self):
2069
return {self.id2path(fid) for fid in self.all_file_ids()}
2071
def _has_id(self, file_id, fallback_check):
2072
if file_id in self._transform._r_new_id:
2074
elif file_id in {self._transform.tree_file_id(trans_id) for
2075
trans_id in self._transform._removed_id}:
2078
return fallback_check(file_id)
2080
def has_id(self, file_id):
2081
return self._has_id(file_id, self._transform._tree.has_id)
2083
def has_or_had_id(self, file_id):
2084
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2086
def _path2trans_id(self, path):
2087
# We must not use None here, because that is a valid value to store.
2088
trans_id = self._path2trans_id_cache.get(path, object)
2089
if trans_id is not object:
2091
segments = splitpath(path)
2092
cur_parent = self._transform.root
2093
for cur_segment in segments:
2094
for child in self._all_children(cur_parent):
2095
final_name = self._final_name_cache.get(child)
2096
if final_name is None:
2097
final_name = self._transform.final_name(child)
2098
self._final_name_cache[child] = final_name
2099
if final_name == cur_segment:
2103
self._path2trans_id_cache[path] = None
2105
self._path2trans_id_cache[path] = cur_parent
2108
def path2id(self, path):
2109
if isinstance(path, list):
2112
path = osutils.pathjoin(*path)
2113
return self._transform.final_file_id(self._path2trans_id(path))
2115
def id2path(self, file_id):
2116
trans_id = self._transform.trans_id_file_id(file_id)
2118
return self._final_paths._determine_path(trans_id)
2120
raise errors.NoSuchId(self, file_id)
2122
def _all_children(self, trans_id):
2123
children = self._all_children_cache.get(trans_id)
2124
if children is not None:
2126
children = set(self._transform.iter_tree_children(trans_id))
2127
# children in the _new_parent set are provided by _by_parent.
2128
children.difference_update(self._transform._new_parent)
2129
children.update(self._by_parent.get(trans_id, []))
2130
self._all_children_cache[trans_id] = children
2133
def _iter_children(self, file_id):
2134
trans_id = self._transform.trans_id_file_id(file_id)
2135
for child_trans_id in self._all_children(trans_id):
2136
yield self._transform.final_file_id(child_trans_id)
2139
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2140
in self._transform._tree.extras())
2141
possible_extras.update(self._transform._new_contents)
2142
possible_extras.update(self._transform._removed_id)
2143
for trans_id in possible_extras:
2144
if self._transform.final_file_id(trans_id) is None:
2145
yield self._final_paths._determine_path(trans_id)
2147
def _make_inv_entries(self, ordered_entries, specific_files=None):
2148
for trans_id, parent_file_id in ordered_entries:
2149
file_id = self._transform.final_file_id(trans_id)
2152
if (specific_files is not None and
2153
self._final_paths.get_path(trans_id) not in specific_files):
2155
kind = self._transform.final_kind(trans_id)
2157
kind = self._transform._tree.stored_kind(
2158
self._transform._tree.id2path(file_id),
2160
new_entry = inventory.make_entry(
2162
self._transform.final_name(trans_id),
2163
parent_file_id, file_id)
2164
yield new_entry, trans_id
2166
def _list_files_by_dir(self):
2167
todo = [ROOT_PARENT]
2169
while len(todo) > 0:
2171
parent_file_id = self._transform.final_file_id(parent)
2172
children = list(self._all_children(parent))
2173
paths = dict(zip(children, self._final_paths.get_paths(children)))
2174
children.sort(key=paths.get)
2175
todo.extend(reversed(children))
2176
for trans_id in children:
2177
ordered_ids.append((trans_id, parent_file_id))
2180
def iter_child_entries(self, path, file_id=None):
2181
trans_id = self._path2trans_id(path)
2182
if trans_id is None:
2183
raise errors.NoSuchFile(path)
2184
todo = [(child_trans_id, trans_id) for child_trans_id in
2185
self._all_children(trans_id)]
2186
for entry, trans_id in self._make_inv_entries(todo):
2189
def iter_entries_by_dir(self, specific_files=None):
2190
# This may not be a maximally efficient implementation, but it is
2191
# reasonably straightforward. An implementation that grafts the
2192
# TreeTransform changes onto the tree's iter_entries_by_dir results
2193
# might be more efficient, but requires tricky inferences about stack
2195
ordered_ids = self._list_files_by_dir()
2196
for entry, trans_id in self._make_inv_entries(ordered_ids,
2198
yield self._final_paths.get_path(trans_id), entry
2200
def _iter_entries_for_dir(self, dir_path):
2201
"""Return path, entry for items in a directory without recursing down."""
2203
dir_trans_id = self._path2trans_id(dir_path)
2204
dir_id = self._transform.final_file_id(dir_trans_id)
2205
for child_trans_id in self._all_children(dir_trans_id):
2206
ordered_ids.append((child_trans_id, dir_id))
2208
for entry, trans_id in self._make_inv_entries(ordered_ids):
2209
path_entries.append((self._final_paths.get_path(trans_id), entry))
2213
def list_files(self, include_root=False, from_dir=None, recursive=True):
2214
"""See WorkingTree.list_files."""
2215
# XXX This should behave like WorkingTree.list_files, but is really
2216
# more like RevisionTree.list_files.
2220
prefix = from_dir + '/'
2221
entries = self.iter_entries_by_dir()
2222
for path, entry in entries:
2223
if entry.name == '' and not include_root:
2226
if not path.startswith(prefix):
2228
path = path[len(prefix):]
2229
yield path, 'V', entry.kind, entry.file_id, entry
2231
if from_dir is None and include_root is True:
2232
root_entry = inventory.make_entry('directory', '',
2233
ROOT_PARENT, self.get_root_id())
2234
yield '', 'V', 'directory', root_entry.file_id, root_entry
2235
entries = self._iter_entries_for_dir(from_dir or '')
2236
for path, entry in entries:
2237
yield path, 'V', entry.kind, entry.file_id, entry
2239
def kind(self, path, file_id=None):
2240
trans_id = self._path2trans_id(path)
2241
if trans_id is None:
2242
raise errors.NoSuchFile(path)
2243
return self._transform.final_kind(trans_id)
2245
def stored_kind(self, path, file_id=None):
2246
trans_id = self._path2trans_id(path)
2247
if trans_id is None:
2248
raise errors.NoSuchFile(path)
2250
return self._transform._new_contents[trans_id]
2252
return self._transform._tree.stored_kind(path, file_id)
2254
def get_file_mtime(self, path, file_id=None):
2255
"""See Tree.get_file_mtime"""
2257
file_id = self.path2id(path)
2259
raise errors.NoSuchFile(path)
2260
if not self._content_change(file_id):
2261
return self._transform._tree.get_file_mtime(
2262
self._transform._tree.id2path(file_id), file_id)
2263
trans_id = self._path2trans_id(path)
2264
return self._stat_limbo_file(trans_id).st_mtime
2266
def get_file_size(self, path, file_id=None):
2267
"""See Tree.get_file_size"""
2268
trans_id = self._path2trans_id(path)
2269
if trans_id is None:
2270
raise errors.NoSuchFile(path)
2271
kind = self._transform.final_kind(trans_id)
2274
if trans_id in self._transform._new_contents:
2275
return self._stat_limbo_file(trans_id).st_size
2276
if self.kind(path, file_id) == 'file':
2277
return self._transform._tree.get_file_size(path, file_id)
2281
def get_file_verifier(self, path, file_id=None, stat_value=None):
2282
trans_id = self._path2trans_id(path)
2283
if trans_id is None:
2284
raise errors.NoSuchFile(path)
2285
kind = self._transform._new_contents.get(trans_id)
2287
return self._transform._tree.get_file_verifier(path, file_id)
2289
with self.get_file(path, file_id) as fileobj:
2290
return ("SHA1", sha_file(fileobj))
2292
def get_file_sha1(self, path, file_id=None, stat_value=None):
2293
trans_id = self._path2trans_id(path)
2294
if trans_id is None:
2295
raise errors.NoSuchFile(path)
2296
kind = self._transform._new_contents.get(trans_id)
2298
return self._transform._tree.get_file_sha1(path, file_id)
2300
with self.get_file(path, file_id) as fileobj:
2301
return sha_file(fileobj)
2303
def is_executable(self, path, file_id=None):
2304
trans_id = self._path2trans_id(path)
2305
if trans_id is None:
2308
return self._transform._new_executability[trans_id]
2311
return self._transform._tree.is_executable(path, file_id)
2312
except OSError as e:
2313
if e.errno == errno.ENOENT:
2316
except errors.NoSuchFile:
2319
def has_filename(self, path):
2320
trans_id = self._path2trans_id(path)
2321
if trans_id in self._transform._new_contents:
2323
elif trans_id in self._transform._removed_contents:
2326
return self._transform._tree.has_filename(path)
2328
def path_content_summary(self, path):
2329
trans_id = self._path2trans_id(path)
2330
tt = self._transform
2331
tree_path = tt._tree_id_paths.get(trans_id)
2332
kind = tt._new_contents.get(trans_id)
2334
if tree_path is None or trans_id in tt._removed_contents:
2335
return 'missing', None, None, None
2336
summary = tt._tree.path_content_summary(tree_path)
2337
kind, size, executable, link_or_sha1 = summary
2340
limbo_name = tt._limbo_name(trans_id)
2341
if trans_id in tt._new_reference_revision:
2342
kind = 'tree-reference'
2344
statval = os.lstat(limbo_name)
2345
size = statval.st_size
2346
if not tt._limbo_supports_executable():
2349
executable = statval.st_mode & S_IEXEC
2353
if kind == 'symlink':
2354
link_or_sha1 = os.readlink(limbo_name)
2355
if not isinstance(link_or_sha1, text_type):
2356
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2357
executable = tt._new_executability.get(trans_id, executable)
2358
return kind, size, executable, link_or_sha1
2360
def iter_changes(self, from_tree, include_unchanged=False,
2361
specific_files=None, pb=None, extra_trees=None,
2362
require_versioned=True, want_unversioned=False):
2363
"""See InterTree.iter_changes.
2365
This has a fast path that is only used when the from_tree matches
2366
the transform tree, and no fancy options are supplied.
2368
if (from_tree is not self._transform._tree or include_unchanged or
2369
specific_files or want_unversioned):
2370
return tree.InterTree(from_tree, self).iter_changes(
2371
include_unchanged=include_unchanged,
2372
specific_files=specific_files,
2374
extra_trees=extra_trees,
2375
require_versioned=require_versioned,
2376
want_unversioned=want_unversioned)
2377
if want_unversioned:
2378
raise ValueError('want_unversioned is not supported')
2379
return self._transform.iter_changes()
2381
def get_file(self, path, file_id=None):
2382
"""See Tree.get_file"""
2384
file_id = self.path2id(path)
2385
if not self._content_change(file_id):
2386
return self._transform._tree.get_file(path, file_id)
2387
trans_id = self._path2trans_id(path)
2388
name = self._transform._limbo_name(trans_id)
2389
return open(name, 'rb')
2391
def get_file_with_stat(self, path, file_id=None):
2392
return self.get_file(path, file_id), None
2394
def annotate_iter(self, path, file_id=None,
2395
default_revision=_mod_revision.CURRENT_REVISION):
2397
file_id = self.path2id(path)
2398
changes = self._iter_changes_cache.get(file_id)
2402
changed_content, versioned, kind = (changes[2], changes[3],
2406
get_old = (kind[0] == 'file' and versioned[0])
2408
old_annotation = self._transform._tree.annotate_iter(
2409
path, file_id=file_id, default_revision=default_revision)
2413
return old_annotation
2414
if not changed_content:
2415
return old_annotation
2416
# TODO: This is doing something similar to what WT.annotate_iter is
2417
# doing, however it fails slightly because it doesn't know what
2418
# the *other* revision_id is, so it doesn't know how to give the
2419
# other as the origin for some lines, they all get
2420
# 'default_revision'
2421
# It would be nice to be able to use the new Annotator based
2422
# approach, as well.
2423
return annotate.reannotate([old_annotation],
2424
self.get_file(path, file_id).readlines(),
2427
def get_symlink_target(self, path, file_id=None):
2428
"""See Tree.get_symlink_target"""
2430
file_id = self.path2id(path)
2431
if not self._content_change(file_id):
2432
return self._transform._tree.get_symlink_target(path)
2433
trans_id = self._path2trans_id(path)
2434
name = self._transform._limbo_name(trans_id)
2435
return osutils.readlink(name)
2437
def walkdirs(self, prefix=''):
2438
pending = [self._transform.root]
2439
while len(pending) > 0:
2440
parent_id = pending.pop()
2443
prefix = prefix.rstrip('/')
2444
parent_path = self._final_paths.get_path(parent_id)
2445
parent_file_id = self._transform.final_file_id(parent_id)
2446
for child_id in self._all_children(parent_id):
2447
path_from_root = self._final_paths.get_path(child_id)
2448
basename = self._transform.final_name(child_id)
2449
file_id = self._transform.final_file_id(child_id)
2450
kind = self._transform.final_kind(child_id)
2451
if kind is not None:
2452
versioned_kind = kind
2455
versioned_kind = self._transform._tree.stored_kind(
2456
self._transform._tree.id2path(file_id),
2458
if versioned_kind == 'directory':
2459
subdirs.append(child_id)
2460
children.append((path_from_root, basename, kind, None,
2461
file_id, versioned_kind))
2463
if parent_path.startswith(prefix):
2464
yield (parent_path, parent_file_id), children
2465
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2468
def get_parent_ids(self):
2469
return self._parent_ids
2471
def set_parent_ids(self, parent_ids):
2472
self._parent_ids = parent_ids
2474
def get_revision_tree(self, revision_id):
2475
return self._transform._tree.get_revision_tree(revision_id)
2478
def joinpath(parent, child):
2479
"""Join tree-relative paths, handling the tree root specially"""
2480
if parent is None or parent == "":
2483
return pathjoin(parent, child)
2486
class FinalPaths(object):
2487
"""Make path calculation cheap by memoizing paths.
2489
The underlying tree must not be manipulated between calls, or else
2490
the results will likely be incorrect.
2492
def __init__(self, transform):
2493
object.__init__(self)
2494
self._known_paths = {}
2495
self.transform = transform
2497
def _determine_path(self, trans_id):
2498
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2500
name = self.transform.final_name(trans_id)
2501
parent_id = self.transform.final_parent(trans_id)
2502
if parent_id == self.transform.root:
2505
return pathjoin(self.get_path(parent_id), name)
2507
def get_path(self, trans_id):
2508
"""Find the final path associated with a trans_id"""
2509
if trans_id not in self._known_paths:
2510
self._known_paths[trans_id] = self._determine_path(trans_id)
2511
return self._known_paths[trans_id]
2513
def get_paths(self, trans_ids):
2514
return [(self.get_path(t), t) for t in trans_ids]
2518
def topology_sorted_ids(tree):
2519
"""Determine the topological order of the ids in a tree"""
2520
file_ids = list(tree)
2521
file_ids.sort(key=tree.id2path)
2525
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2526
delta_from_tree=False):
2527
"""Create working tree for a branch, using a TreeTransform.
2529
This function should be used on empty trees, having a tree root at most.
2530
(see merge and revert functionality for working with existing trees)
2532
Existing files are handled like so:
2534
- Existing bzrdirs take precedence over creating new items. They are
2535
created as '%s.diverted' % name.
2536
- Otherwise, if the content on disk matches the content we are building,
2537
it is silently replaced.
2538
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2540
:param tree: The tree to convert wt into a copy of
2541
:param wt: The working tree that files will be placed into
2542
:param accelerator_tree: A tree which can be used for retrieving file
2543
contents more quickly than tree itself, i.e. a workingtree. tree
2544
will be used for cases where accelerator_tree's content is different.
2545
:param hardlink: If true, hard-link files to accelerator_tree, where
2546
possible. accelerator_tree must implement abspath, i.e. be a
2548
:param delta_from_tree: If true, build_tree may use the input Tree to
2549
generate the inventory delta.
2551
with wt.lock_tree_write(), tree.lock_read():
2552
if accelerator_tree is not None:
2553
accelerator_tree.lock_read()
2555
return _build_tree(tree, wt, accelerator_tree, hardlink,
2558
if accelerator_tree is not None:
2559
accelerator_tree.unlock()
2562
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2563
"""See build_tree."""
2564
for num, _unused in enumerate(wt.all_versioned_paths()):
2565
if num > 0: # more than just a root
2566
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2568
top_pb = ui.ui_factory.nested_progress_bar()
2569
pp = ProgressPhase("Build phase", 2, top_pb)
2570
if tree.get_root_id() is not None:
2571
# This is kind of a hack: we should be altering the root
2572
# as part of the regular tree shape diff logic.
2573
# The conditional test here is to avoid doing an
2574
# expensive operation (flush) every time the root id
2575
# is set within the tree, nor setting the root and thus
2576
# marking the tree as dirty, because we use two different
2577
# idioms here: tree interfaces and inventory interfaces.
2578
if wt.get_root_id() != tree.get_root_id():
2579
wt.set_root_id(tree.get_root_id())
2581
tt = TreeTransform(wt)
2585
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_path('')
2586
with ui.ui_factory.nested_progress_bar() as pb:
2587
deferred_contents = []
2589
total = len(tree.all_versioned_paths())
2591
precomputed_delta = []
2593
precomputed_delta = None
2594
# Check if tree inventory has content. If so, we populate
2595
# existing_files with the directory content. If there are no
2596
# entries we skip populating existing_files as its not used.
2597
# This improves performance and unncessary work on large
2598
# directory trees. (#501307)
2600
existing_files = set()
2601
for dir, files in wt.walkdirs():
2602
existing_files.update(f[0] for f in files)
2603
for num, (tree_path, entry) in \
2604
enumerate(tree.iter_entries_by_dir()):
2605
pb.update(gettext("Building tree"), num - len(deferred_contents), total)
2606
if entry.parent_id is None:
2609
file_id = entry.file_id
2611
precomputed_delta.append((None, tree_path, file_id, entry))
2612
if tree_path in existing_files:
2613
target_path = wt.abspath(tree_path)
2614
kind = file_kind(target_path)
2615
if kind == "directory":
2617
controldir.ControlDir.open(target_path)
2618
except errors.NotBranchError:
2622
if (file_id not in divert and
2623
_content_match(tree, entry, tree_path, file_id, kind,
2625
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2626
if kind == 'directory':
2628
parent_id = file_trans_id[entry.parent_id]
2629
if entry.kind == 'file':
2630
# We *almost* replicate new_by_entry, so that we can defer
2631
# getting the file text, and get them all at once.
2632
trans_id = tt.create_path(entry.name, parent_id)
2633
file_trans_id[file_id] = trans_id
2634
tt.version_file(file_id, trans_id)
2635
executable = tree.is_executable(tree_path, file_id)
2637
tt.set_executability(executable, trans_id)
2638
trans_data = (trans_id, file_id, tree_path, entry.text_sha1)
2639
deferred_contents.append((tree_path, trans_data))
2641
file_trans_id[file_id] = new_by_entry(
2642
tree_path, tt, entry, parent_id, tree)
2644
new_trans_id = file_trans_id[file_id]
2645
old_parent = tt.trans_id_tree_path(tree_path)
2646
_reparent_children(tt, old_parent, new_trans_id)
2647
offset = num + 1 - len(deferred_contents)
2648
_create_files(tt, tree, deferred_contents, pb, offset,
2649
accelerator_tree, hardlink)
2651
divert_trans = set(file_trans_id[f] for f in divert)
2652
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2653
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2654
if len(raw_conflicts) > 0:
2655
precomputed_delta = None
2656
conflicts = cook_conflicts(raw_conflicts, tt)
2657
for conflict in conflicts:
2658
trace.warning(text_type(conflict))
2660
wt.add_conflicts(conflicts)
2661
except errors.UnsupportedOperation:
2663
result = tt.apply(no_conflicts=True,
2664
precomputed_delta=precomputed_delta)
2671
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2673
total = len(desired_files) + offset
2675
if accelerator_tree is None:
2676
new_desired_files = desired_files
2678
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2679
unchanged = [(p[0], p[1]) for (f, p, c, v, d, n, k, e)
2680
in iter if not (c or e[0] != e[1])]
2681
if accelerator_tree.supports_content_filtering():
2682
unchanged = [(tp, ap) for (tp, ap) in unchanged
2683
if not next(accelerator_tree.iter_search_rules([ap]))]
2684
unchanged = dict(unchanged)
2685
new_desired_files = []
2687
for unused_tree_path, (trans_id, file_id, tree_path, text_sha1) in desired_files:
2688
accelerator_path = unchanged.get(tree_path)
2689
if accelerator_path is None:
2690
new_desired_files.append((tree_path,
2691
(trans_id, file_id, tree_path, text_sha1)))
2693
pb.update(gettext('Adding file contents'), count + offset, total)
2695
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2698
with accelerator_tree.get_file(accelerator_path, file_id) as f:
2699
chunks = osutils.file_iterator(f)
2700
if wt.supports_content_filtering():
2701
filters = wt._content_filter_stack(tree_path)
2702
chunks = filtered_output_bytes(chunks, filters,
2703
ContentFilterContext(tree_path, tree))
2704
tt.create_file(chunks, trans_id, sha1=text_sha1)
2707
for count, ((trans_id, file_id, tree_path, text_sha1), contents) in enumerate(
2708
tree.iter_files_bytes(new_desired_files)):
2709
if wt.supports_content_filtering():
2710
filters = wt._content_filter_stack(tree_path)
2711
contents = filtered_output_bytes(contents, filters,
2712
ContentFilterContext(tree_path, tree))
2713
tt.create_file(contents, trans_id, sha1=text_sha1)
2714
pb.update(gettext('Adding file contents'), count + offset, total)
2717
def _reparent_children(tt, old_parent, new_parent):
2718
for child in tt.iter_tree_children(old_parent):
2719
tt.adjust_path(tt.final_name(child), new_parent, child)
2722
def _reparent_transform_children(tt, old_parent, new_parent):
2723
by_parent = tt.by_parent()
2724
for child in by_parent[old_parent]:
2725
tt.adjust_path(tt.final_name(child), new_parent, child)
2726
return by_parent[old_parent]
2729
def _content_match(tree, entry, tree_path, file_id, kind, target_path):
2730
if entry.kind != kind:
2732
if entry.kind == "directory":
2734
if entry.kind == "file":
2735
with open(target_path, 'rb') as f1, \
2736
tree.get_file(tree_path, file_id) as f2:
2737
if osutils.compare_files(f1, f2):
2739
elif entry.kind == "symlink":
2740
if tree.get_symlink_target(tree_path, file_id) == os.readlink(target_path):
2745
def resolve_checkout(tt, conflicts, divert):
2746
new_conflicts = set()
2747
for c_type, conflict in ((c[0], c) for c in conflicts):
2748
# Anything but a 'duplicate' would indicate programmer error
2749
if c_type != 'duplicate':
2750
raise AssertionError(c_type)
2751
# Now figure out which is new and which is old
2752
if tt.new_contents(conflict[1]):
2753
new_file = conflict[1]
2754
old_file = conflict[2]
2756
new_file = conflict[2]
2757
old_file = conflict[1]
2759
# We should only get here if the conflict wasn't completely
2761
final_parent = tt.final_parent(old_file)
2762
if new_file in divert:
2763
new_name = tt.final_name(old_file)+'.diverted'
2764
tt.adjust_path(new_name, final_parent, new_file)
2765
new_conflicts.add((c_type, 'Diverted to',
2766
new_file, old_file))
2768
new_name = tt.final_name(old_file)+'.moved'
2769
tt.adjust_path(new_name, final_parent, old_file)
2770
new_conflicts.add((c_type, 'Moved existing file to',
2771
old_file, new_file))
2772
return new_conflicts
2775
def new_by_entry(path, tt, entry, parent_id, tree):
2776
"""Create a new file according to its inventory entry"""
2780
with tree.get_file(path, entry.file_id) as f:
2781
executable = tree.is_executable(path, entry.file_id)
2783
name, parent_id, osutils.file_iterator(f), entry.file_id,
2785
elif kind in ('directory', 'tree-reference'):
2786
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2787
if kind == 'tree-reference':
2788
tt.set_tree_reference(entry.reference_revision, trans_id)
2790
elif kind == 'symlink':
2791
target = tree.get_symlink_target(path, entry.file_id)
2792
return tt.new_symlink(name, parent_id, target, entry.file_id)
2794
raise errors.BadFileKindError(name, kind)
2797
def create_from_tree(tt, trans_id, tree, path, file_id=None, chunks=None,
2798
filter_tree_path=None):
2799
"""Create new file contents according to tree contents.
2801
:param filter_tree_path: the tree path to use to lookup
2802
content filters to apply to the bytes output in the working tree.
2803
This only applies if the working tree supports content filtering.
2805
kind = tree.kind(path, file_id)
2806
if kind == 'directory':
2807
tt.create_directory(trans_id)
2808
elif kind == "file":
2810
f = tree.get_file(path, file_id)
2811
chunks = osutils.file_iterator(f)
2816
if wt.supports_content_filtering() and filter_tree_path is not None:
2817
filters = wt._content_filter_stack(filter_tree_path)
2818
chunks = filtered_output_bytes(chunks, filters,
2819
ContentFilterContext(filter_tree_path, tree))
2820
tt.create_file(chunks, trans_id)
2824
elif kind == "symlink":
2825
tt.create_symlink(tree.get_symlink_target(path, file_id), trans_id)
2827
raise AssertionError('Unknown kind %r' % kind)
2830
def create_entry_executability(tt, entry, trans_id):
2831
"""Set the executability of a trans_id according to an inventory entry"""
2832
if entry.kind == "file":
2833
tt.set_executability(entry.executable, trans_id)
2836
def revert(working_tree, target_tree, filenames, backups=False,
2837
pb=None, change_reporter=None):
2838
"""Revert a working tree's contents to those of a target tree."""
2839
pb = ui.ui_factory.nested_progress_bar()
2841
with target_tree.lock_read(), TreeTransform(working_tree, pb) as tt:
2842
pp = ProgressPhase("Revert phase", 3, pb)
2843
conflicts, merge_modified = _prepare_revert_transform(
2844
working_tree, target_tree, tt, filenames, backups, pp)
2846
change_reporter = delta._ChangeReporter(
2847
unversioned_filter=working_tree.is_ignored)
2848
delta.report_changes(tt.iter_changes(), change_reporter)
2849
for conflict in conflicts:
2850
trace.warning(text_type(conflict))
2853
if working_tree.supports_merge_modified():
2854
working_tree.set_merge_modified(merge_modified)
2860
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2861
backups, pp, basis_tree=None,
2862
merge_modified=None):
2863
with ui.ui_factory.nested_progress_bar() as child_pb:
2864
if merge_modified is None:
2865
merge_modified = working_tree.merge_modified()
2866
merge_modified = _alter_files(working_tree, target_tree, tt,
2867
child_pb, filenames, backups,
2868
merge_modified, basis_tree)
2869
with ui.ui_factory.nested_progress_bar() as child_pb:
2870
raw_conflicts = resolve_conflicts(tt, child_pb,
2871
lambda t, c: conflict_pass(t, c, target_tree))
2872
conflicts = cook_conflicts(raw_conflicts, tt)
2873
return conflicts, merge_modified
2876
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2877
backups, merge_modified, basis_tree=None):
2878
if basis_tree is not None:
2879
basis_tree.lock_read()
2880
# We ask the working_tree for its changes relative to the target, rather
2881
# than the target changes relative to the working tree. Because WT4 has an
2882
# optimizer to compare itself to a target, but no optimizer for the
2884
change_list = working_tree.iter_changes(target_tree,
2885
specific_files=specific_files, pb=pb)
2886
if not target_tree.is_versioned(u''):
2892
for id_num, (file_id, path, changed_content, versioned, parent, name,
2893
kind, executable) in enumerate(change_list):
2894
target_path, wt_path = path
2895
target_versioned, wt_versioned = versioned
2896
target_parent, wt_parent = parent
2897
target_name, wt_name = name
2898
target_kind, wt_kind = kind
2899
target_executable, wt_executable = executable
2900
if skip_root and wt_parent is None:
2902
trans_id = tt.trans_id_file_id(file_id)
2905
keep_content = False
2906
if wt_kind == 'file' and (backups or target_kind is None):
2907
wt_sha1 = working_tree.get_file_sha1(wt_path, file_id)
2908
if merge_modified.get(file_id) != wt_sha1:
2909
# acquire the basis tree lazily to prevent the
2910
# expense of accessing it when it's not needed ?
2911
# (Guessing, RBC, 200702)
2912
if basis_tree is None:
2913
basis_tree = working_tree.basis_tree()
2914
basis_tree.lock_read()
2915
basis_path = find_previous_path(working_tree, basis_tree, wt_path)
2916
if basis_path is None:
2917
if target_kind is None and not target_versioned:
2920
if wt_sha1 != basis_tree.get_file_sha1(basis_path, file_id):
2922
if wt_kind is not None:
2923
if not keep_content:
2924
tt.delete_contents(trans_id)
2925
elif target_kind is not None:
2926
parent_trans_id = tt.trans_id_file_id(wt_parent)
2927
backup_name = tt._available_backup_name(
2928
wt_name, parent_trans_id)
2929
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2930
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2931
if wt_versioned and target_versioned:
2932
tt.unversion_file(trans_id)
2933
tt.version_file(file_id, new_trans_id)
2934
# New contents should have the same unix perms as old
2937
trans_id = new_trans_id
2938
if target_kind in ('directory', 'tree-reference'):
2939
tt.create_directory(trans_id)
2940
if target_kind == 'tree-reference':
2941
revision = target_tree.get_reference_revision(
2942
target_path, file_id)
2943
tt.set_tree_reference(revision, trans_id)
2944
elif target_kind == 'symlink':
2945
tt.create_symlink(target_tree.get_symlink_target(
2946
target_path, file_id), trans_id)
2947
elif target_kind == 'file':
2948
deferred_files.append((target_path, (trans_id, mode_id, file_id)))
2949
if basis_tree is None:
2950
basis_tree = working_tree.basis_tree()
2951
basis_tree.lock_read()
2952
new_sha1 = target_tree.get_file_sha1(target_path, file_id)
2953
basis_path = find_previous_path(target_tree, basis_tree, target_path)
2954
if (basis_path is not None and
2955
new_sha1 == basis_tree.get_file_sha1(basis_path, file_id)):
2956
if file_id in merge_modified:
2957
del merge_modified[file_id]
2959
merge_modified[file_id] = new_sha1
2961
# preserve the execute bit when backing up
2962
if keep_content and wt_executable == target_executable:
2963
tt.set_executability(target_executable, trans_id)
2964
elif target_kind is not None:
2965
raise AssertionError(target_kind)
2966
if not wt_versioned and target_versioned:
2967
tt.version_file(file_id, trans_id)
2968
if wt_versioned and not target_versioned:
2969
tt.unversion_file(trans_id)
2970
if (target_name is not None and
2971
(wt_name != target_name or wt_parent != target_parent)):
2972
if target_name == '' and target_parent is None:
2973
parent_trans = ROOT_PARENT
2975
parent_trans = tt.trans_id_file_id(target_parent)
2976
if wt_parent is None and wt_versioned:
2977
tt.adjust_root_path(target_name, parent_trans)
2979
tt.adjust_path(target_name, parent_trans, trans_id)
2980
if wt_executable != target_executable and target_kind == "file":
2981
tt.set_executability(target_executable, trans_id)
2982
if working_tree.supports_content_filtering():
2983
for (trans_id, mode_id, file_id), bytes in (
2984
target_tree.iter_files_bytes(deferred_files)):
2985
# We're reverting a tree to the target tree so using the
2986
# target tree to find the file path seems the best choice
2987
# here IMO - Ian C 27/Oct/2009
2988
filter_tree_path = target_tree.id2path(file_id)
2989
filters = working_tree._content_filter_stack(filter_tree_path)
2990
bytes = filtered_output_bytes(bytes, filters,
2991
ContentFilterContext(filter_tree_path, working_tree))
2992
tt.create_file(bytes, trans_id, mode_id)
2994
for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
2996
tt.create_file(bytes, trans_id, mode_id)
2997
tt.fixup_new_roots()
2999
if basis_tree is not None:
3001
return merge_modified
3004
def resolve_conflicts(tt, pb=None, pass_func=None):
3005
"""Make many conflict-resolution attempts, but die if they fail"""
3006
if pass_func is None:
3007
pass_func = conflict_pass
3008
new_conflicts = set()
3009
with ui.ui_factory.nested_progress_bar() as pb:
3011
pb.update(gettext('Resolution pass'), n+1, 10)
3012
conflicts = tt.find_conflicts()
3013
if len(conflicts) == 0:
3014
return new_conflicts
3015
new_conflicts.update(pass_func(tt, conflicts))
3016
raise MalformedTransform(conflicts=conflicts)
3019
def conflict_pass(tt, conflicts, path_tree=None):
3020
"""Resolve some classes of conflicts.
3022
:param tt: The transform to resolve conflicts in
3023
:param conflicts: The conflicts to resolve
3024
:param path_tree: A Tree to get supplemental paths from
3026
new_conflicts = set()
3027
for c_type, conflict in ((c[0], c) for c in conflicts):
3028
if c_type == 'duplicate id':
3029
tt.unversion_file(conflict[1])
3030
new_conflicts.add((c_type, 'Unversioned existing file',
3031
conflict[1], conflict[2], ))
3032
elif c_type == 'duplicate':
3033
# files that were renamed take precedence
3034
final_parent = tt.final_parent(conflict[1])
3035
if tt.path_changed(conflict[1]):
3036
existing_file, new_file = conflict[2], conflict[1]
3038
existing_file, new_file = conflict[1], conflict[2]
3039
new_name = tt.final_name(existing_file) + '.moved'
3040
tt.adjust_path(new_name, final_parent, existing_file)
3041
new_conflicts.add((c_type, 'Moved existing file to',
3042
existing_file, new_file))
3043
elif c_type == 'parent loop':
3044
# break the loop by undoing one of the ops that caused the loop
3046
while not tt.path_changed(cur):
3047
cur = tt.final_parent(cur)
3048
new_conflicts.add((c_type, 'Cancelled move', cur,
3049
tt.final_parent(cur),))
3050
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3052
elif c_type == 'missing parent':
3053
trans_id = conflict[1]
3054
if trans_id in tt._removed_contents:
3055
cancel_deletion = True
3056
orphans = tt._get_potential_orphans(trans_id)
3058
cancel_deletion = False
3059
# All children are orphans
3062
tt.new_orphan(o, trans_id)
3063
except OrphaningError:
3064
# Something bad happened so we cancel the directory
3065
# deletion which will leave it in place with a
3066
# conflict. The user can deal with it from there.
3067
# Note that this also catch the case where we don't
3068
# want to create orphans and leave the directory in
3070
cancel_deletion = True
3073
# Cancel the directory deletion
3074
tt.cancel_deletion(trans_id)
3075
new_conflicts.add(('deleting parent', 'Not deleting',
3080
tt.final_name(trans_id)
3082
if path_tree is not None:
3083
file_id = tt.final_file_id(trans_id)
3085
file_id = tt.inactive_file_id(trans_id)
3086
_, entry = next(path_tree.iter_entries_by_dir(
3087
specific_files=[path_tree.id2path(file_id)]))
3088
# special-case the other tree root (move its
3089
# children to current root)
3090
if entry.parent_id is None:
3092
moved = _reparent_transform_children(
3093
tt, trans_id, tt.root)
3095
new_conflicts.add((c_type, 'Moved to root',
3098
parent_trans_id = tt.trans_id_file_id(
3100
tt.adjust_path(entry.name, parent_trans_id,
3103
tt.create_directory(trans_id)
3104
new_conflicts.add((c_type, 'Created directory', trans_id))
3105
elif c_type == 'unversioned parent':
3106
file_id = tt.inactive_file_id(conflict[1])
3107
# special-case the other tree root (move its children instead)
3108
if path_tree and path_tree.path2id('') == file_id:
3109
# This is the root entry, skip it
3111
tt.version_file(file_id, conflict[1])
3112
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3113
elif c_type == 'non-directory parent':
3114
parent_id = conflict[1]
3115
parent_parent = tt.final_parent(parent_id)
3116
parent_name = tt.final_name(parent_id)
3117
parent_file_id = tt.final_file_id(parent_id)
3118
new_parent_id = tt.new_directory(parent_name + '.new',
3119
parent_parent, parent_file_id)
3120
_reparent_transform_children(tt, parent_id, new_parent_id)
3121
if parent_file_id is not None:
3122
tt.unversion_file(parent_id)
3123
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3124
elif c_type == 'versioning no contents':
3125
tt.cancel_versioning(conflict[1])
3126
return new_conflicts
3129
def cook_conflicts(raw_conflicts, tt):
3130
"""Generate a list of cooked conflicts, sorted by file path"""
3131
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3132
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3135
def iter_cook_conflicts(raw_conflicts, tt):
3137
for conflict in raw_conflicts:
3138
c_type = conflict[0]
3139
action = conflict[1]
3140
modified_path = fp.get_path(conflict[2])
3141
modified_id = tt.final_file_id(conflict[2])
3142
if len(conflict) == 3:
3143
yield conflicts.Conflict.factory(
3144
c_type, action=action, path=modified_path, file_id=modified_id)
3147
conflicting_path = fp.get_path(conflict[3])
3148
conflicting_id = tt.final_file_id(conflict[3])
3149
yield conflicts.Conflict.factory(
3150
c_type, action=action, path=modified_path,
3151
file_id=modified_id,
3152
conflict_path=conflicting_path,
3153
conflict_file_id=conflicting_id)
3156
class _FileMover(object):
3157
"""Moves and deletes files for TreeTransform, tracking operations"""
3160
self.past_renames = []
3161
self.pending_deletions = []
3163
def rename(self, from_, to):
3164
"""Rename a file from one path to another."""
3166
os.rename(from_, to)
3167
except OSError as e:
3168
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3169
raise errors.FileExists(to, str(e))
3170
# normal OSError doesn't include filenames so it's hard to see where
3171
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3172
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3173
self.past_renames.append((from_, to))
3175
def pre_delete(self, from_, to):
3176
"""Rename a file out of the way and mark it for deletion.
3178
Unlike os.unlink, this works equally well for files and directories.
3179
:param from_: The current file path
3180
:param to: A temporary path for the file
3182
self.rename(from_, to)
3183
self.pending_deletions.append(to)
3186
"""Reverse all renames that have been performed"""
3187
for from_, to in reversed(self.past_renames):
3189
os.rename(to, from_)
3190
except OSError as e:
3191
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3192
# after rollback, don't reuse _FileMover
3194
pending_deletions = None
3196
def apply_deletions(self):
3197
"""Apply all marked deletions"""
3198
for path in self.pending_deletions:
3200
# after apply_deletions, don't reuse _FileMover
3202
pending_deletions = None
3205
def link_tree(target_tree, source_tree):
3206
"""Where possible, hard-link files in a tree to those in another tree.
3208
:param target_tree: Tree to change
3209
:param source_tree: Tree to hard-link from
3211
tt = TreeTransform(target_tree)
3213
for (file_id, paths, changed_content, versioned, parent, name, kind,
3214
executable) in target_tree.iter_changes(source_tree,
3215
include_unchanged=True):
3218
if kind != ('file', 'file'):
3220
if executable[0] != executable[1]:
3222
trans_id = tt.trans_id_tree_path(paths[1])
3223
tt.delete_contents(trans_id)
3224
tt.create_hardlink(source_tree.abspath(paths[0]), trans_id)