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