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
77
ROOT_PARENT = "root-parent"
79
def unique_add(map, key, value):
81
raise DuplicateKey(key=key)
86
class _TransformResults(object):
87
def __init__(self, modified_paths, rename_count):
89
self.modified_paths = modified_paths
90
self.rename_count = rename_count
93
class TreeTransformBase(object):
94
"""The base class for TreeTransform and its kin."""
96
def __init__(self, tree, pb=None, case_sensitive=True):
99
:param tree: The tree that will be transformed, but not necessarily
102
:param case_sensitive: If True, the target of the transform is
103
case sensitive, not just case preserving.
105
object.__init__(self)
108
# mapping of trans_id -> new basename
110
# mapping of trans_id -> new parent trans_id
111
self._new_parent = {}
112
# mapping of trans_id with new contents -> new file_kind
113
self._new_contents = {}
114
# mapping of trans_id => (sha1 of content, stat_value)
115
self._observed_sha1s = {}
116
# Set of trans_ids whose contents will be removed
117
self._removed_contents = set()
118
# Mapping of trans_id -> new execute-bit value
119
self._new_executability = {}
120
# Mapping of trans_id -> new tree-reference value
121
self._new_reference_revision = {}
122
# Mapping of trans_id -> new file_id
124
# Mapping of old file-id -> trans_id
125
self._non_present_ids = {}
126
# Mapping of new file_id -> trans_id
128
# Set of trans_ids that will be removed
129
self._removed_id = set()
130
# Mapping of path in old tree -> trans_id
131
self._tree_path_ids = {}
132
# Mapping trans_id -> path in old tree
133
self._tree_id_paths = {}
134
# The trans_id that will be used as the tree root
135
root_id = tree.get_root_id()
136
if root_id is not None:
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
'_id_number': self._id_number,
1110
'_new_name': new_name,
1111
'_new_parent': self._new_parent,
1112
'_new_executability': new_executability,
1113
'_new_id': self._new_id,
1114
'_tree_path_ids': tree_path_ids,
1115
'_removed_id': list(self._removed_id),
1116
'_removed_contents': list(self._removed_contents),
1117
'_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
lines = osutils.chunks_to_lines(
1124
self._read_file_chunks(trans_id))
1125
parents = self._get_parents_lines(trans_id)
1126
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1127
content = ''.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['_id_number']
1143
self._new_name = dict((k, v.decode('utf-8'))
1144
for k, v in viewitems(attribs['_new_name']))
1145
self._new_parent = attribs['_new_parent']
1146
self._new_executability = dict((k, bool(v))
1147
for k, v in viewitems(attribs['_new_executability']))
1148
self._new_id = attribs['_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['_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['_removed_id'])
1157
self._removed_contents = set(attribs['_removed_contents'])
1158
self._non_present_ids = attribs['_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
f = open(name, 'wb')
1316
unique_add(self._new_contents, trans_id, 'file')
1317
f.writelines(contents)
1320
self._set_mtime(name)
1321
self._set_mode(trans_id, mode_id, S_ISREG)
1322
# It is unfortunate we have to use lstat instead of fstat, but we just
1323
# used utime and chmod on the file, so we need the accurate final
1325
if sha1 is not None:
1326
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1328
def _read_file_chunks(self, trans_id):
1329
cur_file = open(self._limbo_name(trans_id), 'rb')
1331
return cur_file.readlines()
1335
def _read_symlink_target(self, trans_id):
1336
return os.readlink(self._limbo_name(trans_id))
1338
def _set_mtime(self, path):
1339
"""All files that are created get the same mtime.
1341
This time is set by the first object to be created.
1343
if self._creation_mtime is None:
1344
self._creation_mtime = time.time()
1345
os.utime(path, (self._creation_mtime, self._creation_mtime))
1347
def create_hardlink(self, path, trans_id):
1348
"""Schedule creation of a hard link"""
1349
name = self._limbo_name(trans_id)
1352
except OSError as e:
1353
if e.errno != errno.EPERM:
1355
raise errors.HardLinkNotSupported(path)
1357
unique_add(self._new_contents, trans_id, 'file')
1359
# Clean up the file, it never got registered so
1360
# TreeTransform.finalize() won't clean it up.
1364
def create_directory(self, trans_id):
1365
"""Schedule creation of a new directory.
1367
See also new_directory.
1369
os.mkdir(self._limbo_name(trans_id))
1370
unique_add(self._new_contents, trans_id, 'directory')
1372
def create_symlink(self, target, trans_id):
1373
"""Schedule creation of a new symbolic link.
1375
target is a bytestring.
1376
See also new_symlink.
1379
os.symlink(target, self._limbo_name(trans_id))
1380
unique_add(self._new_contents, trans_id, 'symlink')
1383
path = FinalPaths(self).get_path(trans_id)
1386
raise UnableCreateSymlink(path=path)
1388
def cancel_creation(self, trans_id):
1389
"""Cancel the creation of new file contents."""
1390
del self._new_contents[trans_id]
1391
if trans_id in self._observed_sha1s:
1392
del self._observed_sha1s[trans_id]
1393
children = self._limbo_children.get(trans_id)
1394
# if this is a limbo directory with children, move them before removing
1396
if children is not None:
1397
self._rename_in_limbo(children)
1398
del self._limbo_children[trans_id]
1399
del self._limbo_children_names[trans_id]
1400
delete_any(self._limbo_name(trans_id))
1402
def new_orphan(self, trans_id, parent_id):
1403
conf = self._tree.get_config_stack()
1404
handle_orphan = conf.get('transform.orphan_policy')
1405
handle_orphan(self, trans_id, parent_id)
1408
class OrphaningError(errors.BzrError):
1410
# Only bugs could lead to such exception being seen by the user
1411
internal_error = True
1412
_fmt = "Error while orphaning %s in %s directory"
1414
def __init__(self, orphan, parent):
1415
errors.BzrError.__init__(self)
1416
self.orphan = orphan
1417
self.parent = parent
1420
class OrphaningForbidden(OrphaningError):
1422
_fmt = "Policy: %s doesn't allow creating orphans."
1424
def __init__(self, policy):
1425
errors.BzrError.__init__(self)
1426
self.policy = policy
1429
def move_orphan(tt, orphan_id, parent_id):
1430
"""See TreeTransformBase.new_orphan.
1432
This creates a new orphan in the `brz-orphans` dir at the root of the
1435
:param tt: The TreeTransform orphaning `trans_id`.
1437
:param orphan_id: The trans id that should be orphaned.
1439
:param parent_id: The orphan parent trans id.
1441
# Add the orphan dir if it doesn't exist
1442
orphan_dir_basename = 'brz-orphans'
1443
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1444
if tt.final_kind(od_id) is None:
1445
tt.create_directory(od_id)
1446
parent_path = tt._tree_id_paths[parent_id]
1447
# Find a name that doesn't exist yet in the orphan dir
1448
actual_name = tt.final_name(orphan_id)
1449
new_name = tt._available_backup_name(actual_name, od_id)
1450
tt.adjust_path(new_name, od_id, orphan_id)
1451
trace.warning('%s has been orphaned in %s'
1452
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1455
def refuse_orphan(tt, orphan_id, parent_id):
1456
"""See TreeTransformBase.new_orphan.
1458
This refuses to create orphan, letting the caller handle the conflict.
1460
raise OrphaningForbidden('never')
1463
orphaning_registry = registry.Registry()
1464
orphaning_registry.register(
1465
'conflict', refuse_orphan,
1466
'Leave orphans in place and create a conflict on the directory.')
1467
orphaning_registry.register(
1468
'move', move_orphan,
1469
'Move orphans into the brz-orphans directory.')
1470
orphaning_registry._set_default_key('conflict')
1473
opt_transform_orphan = _mod_config.RegistryOption(
1474
'transform.orphan_policy', orphaning_registry,
1475
help='Policy for orphaned files during transform operations.',
1479
class TreeTransform(DiskTreeTransform):
1480
"""Represent a tree transformation.
1482
This object is designed to support incremental generation of the transform,
1485
However, it gives optimum performance when parent directories are created
1486
before their contents. The transform is then able to put child files
1487
directly in their parent directory, avoiding later renames.
1489
It is easy to produce malformed transforms, but they are generally
1490
harmless. Attempting to apply a malformed transform will cause an
1491
exception to be raised before any modifications are made to the tree.
1493
Many kinds of malformed transforms can be corrected with the
1494
resolve_conflicts function. The remaining ones indicate programming error,
1495
such as trying to create a file with no path.
1497
Two sets of file creation methods are supplied. Convenience methods are:
1502
These are composed of the low-level methods:
1504
* create_file or create_directory or create_symlink
1508
Transform/Transaction ids
1509
-------------------------
1510
trans_ids are temporary ids assigned to all files involved in a transform.
1511
It's possible, even common, that not all files in the Tree have trans_ids.
1513
trans_ids are used because filenames and file_ids are not good enough
1514
identifiers; filenames change, and not all files have file_ids. File-ids
1515
are also associated with trans-ids, so that moving a file moves its
1518
trans_ids are only valid for the TreeTransform that generated them.
1522
Limbo is a temporary directory use to hold new versions of files.
1523
Files are added to limbo by create_file, create_directory, create_symlink,
1524
and their convenience variants (new_*). Files may be removed from limbo
1525
using cancel_creation. Files are renamed from limbo into their final
1526
location as part of TreeTransform.apply
1528
Limbo must be cleaned up, by either calling TreeTransform.apply or
1529
calling TreeTransform.finalize.
1531
Files are placed into limbo inside their parent directories, where
1532
possible. This reduces subsequent renames, and makes operations involving
1533
lots of files faster. This optimization is only possible if the parent
1534
directory is created *before* creating any of its children, so avoid
1535
creating children before parents, where possible.
1539
This temporary directory is used by _FileMover for storing files that are
1540
about to be deleted. In case of rollback, the files will be restored.
1541
FileMover does not delete files until it is sure that a rollback will not
1544
def __init__(self, tree, pb=None):
1545
"""Note: a tree_write lock is taken on the tree.
1547
Use TreeTransform.finalize() to release the lock (can be omitted if
1548
TreeTransform.apply() called).
1550
tree.lock_tree_write()
1552
limbodir = urlutils.local_path_from_url(
1553
tree._transport.abspath('limbo'))
1554
osutils.ensure_empty_directory_exists(
1556
errors.ExistingLimbo)
1557
deletiondir = urlutils.local_path_from_url(
1558
tree._transport.abspath('pending-deletion'))
1559
osutils.ensure_empty_directory_exists(
1561
errors.ExistingPendingDeletion)
1566
# Cache of realpath results, to speed up canonical_path
1567
self._realpaths = {}
1568
# Cache of relpath results, to speed up canonical_path
1570
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1571
tree.case_sensitive)
1572
self._deletiondir = deletiondir
1574
def canonical_path(self, path):
1575
"""Get the canonical tree-relative path"""
1576
# don't follow final symlinks
1577
abs = self._tree.abspath(path)
1578
if abs in self._relpaths:
1579
return self._relpaths[abs]
1580
dirname, basename = os.path.split(abs)
1581
if dirname not in self._realpaths:
1582
self._realpaths[dirname] = os.path.realpath(dirname)
1583
dirname = self._realpaths[dirname]
1584
abs = pathjoin(dirname, basename)
1585
if dirname in self._relpaths:
1586
relpath = pathjoin(self._relpaths[dirname], basename)
1587
relpath = relpath.rstrip('/\\')
1589
relpath = self._tree.relpath(abs)
1590
self._relpaths[abs] = relpath
1593
def tree_kind(self, trans_id):
1594
"""Determine the file kind in the working tree.
1596
:returns: The file kind or None if the file does not exist
1598
path = self._tree_id_paths.get(trans_id)
1602
return file_kind(self._tree.abspath(path))
1603
except errors.NoSuchFile:
1606
def _set_mode(self, trans_id, mode_id, typefunc):
1607
"""Set the mode of new file contents.
1608
The mode_id is the existing file to get the mode from (often the same
1609
as trans_id). The operation is only performed if there's a mode match
1610
according to typefunc.
1615
old_path = self._tree_id_paths[mode_id]
1619
mode = os.stat(self._tree.abspath(old_path)).st_mode
1620
except OSError as e:
1621
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1622
# Either old_path doesn't exist, or the parent of the
1623
# target is not a directory (but will be one eventually)
1624
# Either way, we know it doesn't exist *right now*
1625
# See also bug #248448
1630
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1632
def iter_tree_children(self, parent_id):
1633
"""Iterate through the entry's tree children, if any"""
1635
path = self._tree_id_paths[parent_id]
1639
children = os.listdir(self._tree.abspath(path))
1640
except OSError as e:
1641
if not (osutils._is_error_enotdir(e)
1642
or e.errno in (errno.ENOENT, errno.ESRCH)):
1646
for child in children:
1647
childpath = joinpath(path, child)
1648
if self._tree.is_control_filename(childpath):
1650
yield self.trans_id_tree_path(childpath)
1652
def _generate_limbo_path(self, trans_id):
1653
"""Generate a limbo path using the final path if possible.
1655
This optimizes the performance of applying the tree transform by
1656
avoiding renames. These renames can be avoided only when the parent
1657
directory is already scheduled for creation.
1659
If the final path cannot be used, falls back to using the trans_id as
1662
parent = self._new_parent.get(trans_id)
1663
# if the parent directory is already in limbo (e.g. when building a
1664
# tree), choose a limbo name inside the parent, to reduce further
1666
use_direct_path = False
1667
if self._new_contents.get(parent) == 'directory':
1668
filename = self._new_name.get(trans_id)
1669
if filename is not None:
1670
if parent not in self._limbo_children:
1671
self._limbo_children[parent] = set()
1672
self._limbo_children_names[parent] = {}
1673
use_direct_path = True
1674
# the direct path can only be used if no other file has
1675
# already taken this pathname, i.e. if the name is unused, or
1676
# if it is already associated with this trans_id.
1677
elif self._case_sensitive_target:
1678
if (self._limbo_children_names[parent].get(filename)
1679
in (trans_id, None)):
1680
use_direct_path = True
1682
for l_filename, l_trans_id in viewitems(
1683
self._limbo_children_names[parent]):
1684
if l_trans_id == trans_id:
1686
if l_filename.lower() == filename.lower():
1689
use_direct_path = True
1691
if not use_direct_path:
1692
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1694
limbo_name = pathjoin(self._limbo_files[parent], filename)
1695
self._limbo_children[parent].add(trans_id)
1696
self._limbo_children_names[parent][filename] = trans_id
1700
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1701
"""Apply all changes to the inventory and filesystem.
1703
If filesystem or inventory conflicts are present, MalformedTransform
1706
If apply succeeds, finalize is not necessary.
1708
:param no_conflicts: if True, the caller guarantees there are no
1709
conflicts, so no check is made.
1710
:param precomputed_delta: An inventory delta to use instead of
1712
:param _mover: Supply an alternate FileMover, for testing
1714
for hook in MutableTree.hooks['pre_transform']:
1715
hook(self._tree, self)
1716
if not no_conflicts:
1717
self._check_malformed()
1718
with ui.ui_factory.nested_progress_bar() as child_pb:
1719
if precomputed_delta is None:
1720
child_pb.update(gettext('Apply phase'), 0, 2)
1721
inventory_delta = self._generate_inventory_delta()
1724
inventory_delta = precomputed_delta
1727
mover = _FileMover()
1731
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1732
self._apply_removals(mover)
1733
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1734
modified_paths = self._apply_insertions(mover)
1739
mover.apply_deletions()
1740
if self.final_file_id(self.root) is None:
1741
inventory_delta = [e for e in inventory_delta if e[0] != '']
1742
self._tree.apply_inventory_delta(inventory_delta)
1743
self._apply_observed_sha1s()
1746
return _TransformResults(modified_paths, self.rename_count)
1748
def _generate_inventory_delta(self):
1749
"""Generate an inventory delta for the current transform."""
1750
inventory_delta = []
1751
new_paths = self._inventory_altered()
1752
total_entries = len(new_paths) + len(self._removed_id)
1753
with ui.ui_factory.nested_progress_bar() as child_pb:
1754
for num, trans_id in enumerate(self._removed_id):
1756
child_pb.update(gettext('removing file'), num, total_entries)
1757
if trans_id == self._new_root:
1758
file_id = self._tree.get_root_id()
1760
file_id = self.tree_file_id(trans_id)
1761
# File-id isn't really being deleted, just moved
1762
if file_id in self._r_new_id:
1764
path = self._tree_id_paths[trans_id]
1765
inventory_delta.append((path, None, file_id, None))
1766
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1769
for num, (path, trans_id) in enumerate(new_paths):
1771
child_pb.update(gettext('adding file'),
1772
num + len(self._removed_id), total_entries)
1773
file_id = new_path_file_ids[trans_id]
1777
kind = self.final_kind(trans_id)
1779
kind = self._tree.stored_kind(
1780
self._tree.id2path(file_id), file_id)
1781
parent_trans_id = self.final_parent(trans_id)
1782
parent_file_id = new_path_file_ids.get(parent_trans_id)
1783
if parent_file_id is None:
1784
parent_file_id = self.final_file_id(parent_trans_id)
1785
if trans_id in self._new_reference_revision:
1786
new_entry = inventory.TreeReference(
1788
self._new_name[trans_id],
1789
self.final_file_id(self._new_parent[trans_id]),
1790
None, self._new_reference_revision[trans_id])
1792
new_entry = inventory.make_entry(kind,
1793
self.final_name(trans_id),
1794
parent_file_id, file_id)
1796
old_path = self._tree.id2path(new_entry.file_id)
1797
except errors.NoSuchId:
1799
new_executability = self._new_executability.get(trans_id)
1800
if new_executability is not None:
1801
new_entry.executable = new_executability
1802
inventory_delta.append(
1803
(old_path, path, new_entry.file_id, new_entry))
1804
return inventory_delta
1806
def _apply_removals(self, mover):
1807
"""Perform tree operations that remove directory/inventory names.
1809
That is, delete files that are to be deleted, and put any files that
1810
need renaming into limbo. This must be done in strict child-to-parent
1813
If inventory_delta is None, no inventory delta generation is performed.
1815
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1816
with ui.ui_factory.nested_progress_bar() as child_pb:
1817
for num, (path, trans_id) in enumerate(tree_paths):
1818
# do not attempt to move root into a subdirectory of itself.
1821
child_pb.update(gettext('removing file'), num, len(tree_paths))
1822
full_path = self._tree.abspath(path)
1823
if trans_id in self._removed_contents:
1824
delete_path = os.path.join(self._deletiondir, trans_id)
1825
mover.pre_delete(full_path, delete_path)
1826
elif (trans_id in self._new_name
1827
or trans_id in self._new_parent):
1829
mover.rename(full_path, self._limbo_name(trans_id))
1830
except errors.TransformRenameFailed as e:
1831
if e.errno != errno.ENOENT:
1834
self.rename_count += 1
1836
def _apply_insertions(self, mover):
1837
"""Perform tree operations that insert directory/inventory names.
1839
That is, create any files that need to be created, and restore from
1840
limbo any files that needed renaming. This must be done in strict
1841
parent-to-child order.
1843
If inventory_delta is None, no inventory delta is calculated, and
1844
no list of modified paths is returned.
1846
new_paths = self.new_paths(filesystem_only=True)
1848
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1850
with ui.ui_factory.nested_progress_bar() as child_pb:
1851
for num, (path, trans_id) in enumerate(new_paths):
1853
child_pb.update(gettext('adding file'), num, len(new_paths))
1854
full_path = self._tree.abspath(path)
1855
if trans_id in self._needs_rename:
1857
mover.rename(self._limbo_name(trans_id), full_path)
1858
except errors.TransformRenameFailed as e:
1859
# We may be renaming a dangling inventory id
1860
if e.errno != errno.ENOENT:
1863
self.rename_count += 1
1864
# TODO: if trans_id in self._observed_sha1s, we should
1865
# re-stat the final target, since ctime will be
1866
# updated by the change.
1867
if (trans_id in self._new_contents or
1868
self.path_changed(trans_id)):
1869
if trans_id in self._new_contents:
1870
modified_paths.append(full_path)
1871
if trans_id in self._new_executability:
1872
self._set_executability(path, trans_id)
1873
if trans_id in self._observed_sha1s:
1874
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1875
st = osutils.lstat(full_path)
1876
self._observed_sha1s[trans_id] = (o_sha1, st)
1877
for path, trans_id in new_paths:
1878
# new_paths includes stuff like workingtree conflicts. Only the
1879
# stuff in new_contents actually comes from limbo.
1880
if trans_id in self._limbo_files:
1881
del self._limbo_files[trans_id]
1882
self._new_contents.clear()
1883
return modified_paths
1885
def _apply_observed_sha1s(self):
1886
"""After we have finished renaming everything, update observed sha1s
1888
This has to be done after self._tree.apply_inventory_delta, otherwise
1889
it doesn't know anything about the files we are updating. Also, we want
1890
to do this as late as possible, so that most entries end up cached.
1892
# TODO: this doesn't update the stat information for directories. So
1893
# the first 'bzr status' will still need to rewrite
1894
# .bzr/checkout/dirstate. However, we at least don't need to
1895
# re-read all of the files.
1896
# TODO: If the operation took a while, we could do a time.sleep(3) here
1897
# to allow the clock to tick over and ensure we won't have any
1898
# problems. (we could observe start time, and finish time, and if
1899
# it is less than eg 10% overhead, add a sleep call.)
1900
paths = FinalPaths(self)
1901
for trans_id, observed in viewitems(self._observed_sha1s):
1902
path = paths.get_path(trans_id)
1903
# We could get the file_id, but dirstate prefers to use the path
1904
# anyway, and it is 'cheaper' to determine.
1905
# file_id = self._new_id[trans_id]
1906
self._tree._observed_sha1(None, path, observed)
1909
class TransformPreview(DiskTreeTransform):
1910
"""A TreeTransform for generating preview trees.
1912
Unlike TreeTransform, this version works when the input tree is a
1913
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1914
unversioned files in the input tree.
1917
def __init__(self, tree, pb=None, case_sensitive=True):
1919
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1920
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1922
def canonical_path(self, path):
1925
def tree_kind(self, trans_id):
1926
path = self._tree_id_paths.get(trans_id)
1929
kind = self._tree.path_content_summary(path)[0]
1930
if kind == 'missing':
1934
def _set_mode(self, trans_id, mode_id, typefunc):
1935
"""Set the mode of new file contents.
1936
The mode_id is the existing file to get the mode from (often the same
1937
as trans_id). The operation is only performed if there's a mode match
1938
according to typefunc.
1940
# is it ok to ignore this? probably
1943
def iter_tree_children(self, parent_id):
1944
"""Iterate through the entry's tree children, if any"""
1946
path = self._tree_id_paths[parent_id]
1949
entry = next(self._tree.iter_entries_by_dir(
1950
specific_files=[path]))[1]
1951
children = getattr(entry, 'children', {})
1952
for child in children:
1953
childpath = joinpath(path, child)
1954
yield self.trans_id_tree_path(childpath)
1956
def new_orphan(self, trans_id, parent_id):
1957
raise NotImplementedError(self.new_orphan)
1960
class _PreviewTree(inventorytree.InventoryTree):
1961
"""Partial implementation of Tree to support show_diff_trees"""
1963
def __init__(self, transform):
1964
self._transform = transform
1965
self._final_paths = FinalPaths(transform)
1966
self.__by_parent = None
1967
self._parent_ids = []
1968
self._all_children_cache = {}
1969
self._path2trans_id_cache = {}
1970
self._final_name_cache = {}
1971
self._iter_changes_cache = dict((c[0], c) for c in
1972
self._transform.iter_changes())
1974
def _content_change(self, file_id):
1975
"""Return True if the content of this file changed"""
1976
changes = self._iter_changes_cache.get(file_id)
1977
# changes[2] is true if the file content changed. See
1978
# InterTree.iter_changes.
1979
return (changes is not None and changes[2])
1981
def _get_repository(self):
1982
repo = getattr(self._transform._tree, '_repository', None)
1984
repo = self._transform._tree.branch.repository
1987
def _iter_parent_trees(self):
1988
for revision_id in self.get_parent_ids():
1990
yield self.revision_tree(revision_id)
1991
except errors.NoSuchRevisionInTree:
1992
yield self._get_repository().revision_tree(revision_id)
1994
def _get_file_revision(self, path, file_id, vf, tree_revision):
1996
(file_id, t.get_file_revision(t.id2path(file_id), file_id))
1997
for t in self._iter_parent_trees()]
1998
vf.add_lines((file_id, tree_revision), parent_keys,
1999
self.get_file_lines(path, file_id))
2000
repo = self._get_repository()
2001
base_vf = repo.texts
2002
if base_vf not in vf.fallback_versionedfiles:
2003
vf.fallback_versionedfiles.append(base_vf)
2004
return tree_revision
2006
def _stat_limbo_file(self, trans_id):
2007
name = self._transform._limbo_name(trans_id)
2008
return os.lstat(name)
2011
def _by_parent(self):
2012
if self.__by_parent is None:
2013
self.__by_parent = self._transform.by_parent()
2014
return self.__by_parent
2016
def _comparison_data(self, entry, path):
2017
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2018
if kind == 'missing':
2022
file_id = self._transform.final_file_id(self._path2trans_id(path))
2023
executable = self.is_executable(path, file_id)
2024
return kind, executable, None
2026
def is_locked(self):
2029
def lock_read(self):
2030
# Perhaps in theory, this should lock the TreeTransform?
2031
return lock.LogicalLockResult(self.unlock)
2037
def root_inventory(self):
2038
"""This Tree does not use inventory as its backing data."""
2039
raise NotImplementedError(_PreviewTree.root_inventory)
2041
def get_root_id(self):
2042
return self._transform.final_file_id(self._transform.root)
2044
def all_file_ids(self):
2045
tree_ids = set(self._transform._tree.all_file_ids())
2046
tree_ids.difference_update(self._transform.tree_file_id(t)
2047
for t in self._transform._removed_id)
2048
tree_ids.update(viewvalues(self._transform._new_id))
2051
def all_versioned_paths(self):
2052
return {self.id2path(fid) for fid in self.all_file_ids()}
2054
def _has_id(self, file_id, fallback_check):
2055
if file_id in self._transform._r_new_id:
2057
elif file_id in {self._transform.tree_file_id(trans_id) for
2058
trans_id in self._transform._removed_id}:
2061
return fallback_check(file_id)
2063
def has_id(self, file_id):
2064
return self._has_id(file_id, self._transform._tree.has_id)
2066
def has_or_had_id(self, file_id):
2067
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2069
def _path2trans_id(self, path):
2070
# We must not use None here, because that is a valid value to store.
2071
trans_id = self._path2trans_id_cache.get(path, object)
2072
if trans_id is not object:
2074
segments = splitpath(path)
2075
cur_parent = self._transform.root
2076
for cur_segment in segments:
2077
for child in self._all_children(cur_parent):
2078
final_name = self._final_name_cache.get(child)
2079
if final_name is None:
2080
final_name = self._transform.final_name(child)
2081
self._final_name_cache[child] = final_name
2082
if final_name == cur_segment:
2086
self._path2trans_id_cache[path] = None
2088
self._path2trans_id_cache[path] = cur_parent
2091
def path2id(self, path):
2092
if isinstance(path, list):
2095
path = osutils.pathjoin(*path)
2096
return self._transform.final_file_id(self._path2trans_id(path))
2098
def id2path(self, file_id):
2099
trans_id = self._transform.trans_id_file_id(file_id)
2101
return self._final_paths._determine_path(trans_id)
2103
raise errors.NoSuchId(self, file_id)
2105
def _all_children(self, trans_id):
2106
children = self._all_children_cache.get(trans_id)
2107
if children is not None:
2109
children = set(self._transform.iter_tree_children(trans_id))
2110
# children in the _new_parent set are provided by _by_parent.
2111
children.difference_update(self._transform._new_parent)
2112
children.update(self._by_parent.get(trans_id, []))
2113
self._all_children_cache[trans_id] = children
2116
def _iter_children(self, file_id):
2117
trans_id = self._transform.trans_id_file_id(file_id)
2118
for child_trans_id in self._all_children(trans_id):
2119
yield self._transform.final_file_id(child_trans_id)
2122
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2123
in self._transform._tree.extras())
2124
possible_extras.update(self._transform._new_contents)
2125
possible_extras.update(self._transform._removed_id)
2126
for trans_id in possible_extras:
2127
if self._transform.final_file_id(trans_id) is None:
2128
yield self._final_paths._determine_path(trans_id)
2130
def _make_inv_entries(self, ordered_entries, specific_files=None):
2131
for trans_id, parent_file_id in ordered_entries:
2132
file_id = self._transform.final_file_id(trans_id)
2135
if (specific_files is not None and
2136
unicode(self._final_paths.get_path(trans_id)) not in specific_files):
2138
kind = self._transform.final_kind(trans_id)
2140
kind = self._transform._tree.stored_kind(
2141
self._transform._tree.id2path(file_id),
2143
new_entry = inventory.make_entry(
2145
self._transform.final_name(trans_id),
2146
parent_file_id, file_id)
2147
yield new_entry, trans_id
2149
def _list_files_by_dir(self):
2150
todo = [ROOT_PARENT]
2152
while len(todo) > 0:
2154
parent_file_id = self._transform.final_file_id(parent)
2155
children = list(self._all_children(parent))
2156
paths = dict(zip(children, self._final_paths.get_paths(children)))
2157
children.sort(key=paths.get)
2158
todo.extend(reversed(children))
2159
for trans_id in children:
2160
ordered_ids.append((trans_id, parent_file_id))
2163
def iter_child_entries(self, path, file_id=None):
2164
trans_id = self._path2trans_id(path)
2165
if trans_id is None:
2166
raise errors.NoSuchFile(path)
2167
todo = [(child_trans_id, trans_id) for child_trans_id in
2168
self._all_children(trans_id)]
2169
for entry, trans_id in self._make_inv_entries(todo):
2172
def iter_entries_by_dir(self, specific_files=None):
2173
# This may not be a maximally efficient implementation, but it is
2174
# reasonably straightforward. An implementation that grafts the
2175
# TreeTransform changes onto the tree's iter_entries_by_dir results
2176
# might be more efficient, but requires tricky inferences about stack
2178
ordered_ids = self._list_files_by_dir()
2179
for entry, trans_id in self._make_inv_entries(ordered_ids,
2181
yield unicode(self._final_paths.get_path(trans_id)), entry
2183
def _iter_entries_for_dir(self, dir_path):
2184
"""Return path, entry for items in a directory without recursing down."""
2186
dir_trans_id = self._path2trans_id(dir_path)
2187
dir_id = self._transform.final_file_id(dir_trans_id)
2188
for child_trans_id in self._all_children(dir_trans_id):
2189
ordered_ids.append((child_trans_id, dir_id))
2190
for entry, trans_id in self._make_inv_entries(ordered_ids):
2191
yield unicode(self._final_paths.get_path(trans_id)), entry
2193
def list_files(self, include_root=False, from_dir=None, recursive=True):
2194
"""See WorkingTree.list_files."""
2195
# XXX This should behave like WorkingTree.list_files, but is really
2196
# more like RevisionTree.list_files.
2200
prefix = from_dir + '/'
2201
entries = self.iter_entries_by_dir()
2202
for path, entry in entries:
2203
if entry.name == '' and not include_root:
2206
if not path.startswith(prefix):
2208
path = path[len(prefix):]
2209
yield path, 'V', entry.kind, entry.file_id, entry
2211
if from_dir is None and include_root is True:
2212
root_entry = inventory.make_entry('directory', '',
2213
ROOT_PARENT, self.get_root_id())
2214
yield '', 'V', 'directory', root_entry.file_id, root_entry
2215
entries = self._iter_entries_for_dir(from_dir or '')
2216
for path, entry in entries:
2217
yield path, 'V', entry.kind, entry.file_id, entry
2219
def kind(self, path, file_id=None):
2220
trans_id = self._path2trans_id(path)
2221
if trans_id is None:
2222
raise errors.NoSuchFile(path)
2223
return self._transform.final_kind(trans_id)
2225
def stored_kind(self, path, file_id=None):
2226
trans_id = self._path2trans_id(path)
2227
if trans_id is None:
2228
raise errors.NoSuchFile(path)
2230
return self._transform._new_contents[trans_id]
2232
return self._transform._tree.stored_kind(path, file_id)
2234
def get_file_mtime(self, path, file_id=None):
2235
"""See Tree.get_file_mtime"""
2237
file_id = self.path2id(path)
2239
raise errors.NoSuchFile(path)
2240
if not self._content_change(file_id):
2241
return self._transform._tree.get_file_mtime(
2242
self._transform._tree.id2path(file_id), file_id)
2243
trans_id = self._path2trans_id(path)
2244
return self._stat_limbo_file(trans_id).st_mtime
2246
def get_file_size(self, path, file_id=None):
2247
"""See Tree.get_file_size"""
2248
trans_id = self._path2trans_id(path)
2249
if trans_id is None:
2250
raise errors.NoSuchFile(path)
2251
kind = self._transform.final_kind(trans_id)
2254
if trans_id in self._transform._new_contents:
2255
return self._stat_limbo_file(trans_id).st_size
2256
if self.kind(path, file_id) == 'file':
2257
return self._transform._tree.get_file_size(path, file_id)
2261
def get_file_verifier(self, path, file_id=None, stat_value=None):
2262
trans_id = self._path2trans_id(path)
2263
if trans_id is None:
2264
raise errors.NoSuchFile(path)
2265
kind = self._transform._new_contents.get(trans_id)
2267
return self._transform._tree.get_file_verifier(path, file_id)
2269
fileobj = self.get_file(path, file_id)
2271
return ("SHA1", sha_file(fileobj))
2275
def get_file_sha1(self, path, file_id=None, stat_value=None):
2276
trans_id = self._path2trans_id(path)
2277
if trans_id is None:
2278
raise errors.NoSuchFile(path)
2279
kind = self._transform._new_contents.get(trans_id)
2281
return self._transform._tree.get_file_sha1(path, file_id)
2283
fileobj = self.get_file(path, file_id)
2285
return sha_file(fileobj)
2289
def is_executable(self, path, file_id=None):
2290
trans_id = self._path2trans_id(path)
2291
if trans_id is None:
2294
return self._transform._new_executability[trans_id]
2297
return self._transform._tree.is_executable(path, file_id)
2298
except OSError as e:
2299
if e.errno == errno.ENOENT:
2302
except errors.NoSuchFile:
2305
def has_filename(self, path):
2306
trans_id = self._path2trans_id(path)
2307
if trans_id in self._transform._new_contents:
2309
elif trans_id in self._transform._removed_contents:
2312
return self._transform._tree.has_filename(path)
2314
def path_content_summary(self, path):
2315
trans_id = self._path2trans_id(path)
2316
tt = self._transform
2317
tree_path = tt._tree_id_paths.get(trans_id)
2318
kind = tt._new_contents.get(trans_id)
2320
if tree_path is None or trans_id in tt._removed_contents:
2321
return 'missing', None, None, None
2322
summary = tt._tree.path_content_summary(tree_path)
2323
kind, size, executable, link_or_sha1 = summary
2326
limbo_name = tt._limbo_name(trans_id)
2327
if trans_id in tt._new_reference_revision:
2328
kind = 'tree-reference'
2330
statval = os.lstat(limbo_name)
2331
size = statval.st_size
2332
if not tt._limbo_supports_executable():
2335
executable = statval.st_mode & S_IEXEC
2339
if kind == 'symlink':
2340
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2341
executable = tt._new_executability.get(trans_id, executable)
2342
return kind, size, executable, link_or_sha1
2344
def iter_changes(self, from_tree, include_unchanged=False,
2345
specific_files=None, pb=None, extra_trees=None,
2346
require_versioned=True, want_unversioned=False):
2347
"""See InterTree.iter_changes.
2349
This has a fast path that is only used when the from_tree matches
2350
the transform tree, and no fancy options are supplied.
2352
if (from_tree is not self._transform._tree or include_unchanged or
2353
specific_files or want_unversioned):
2354
return tree.InterTree(from_tree, self).iter_changes(
2355
include_unchanged=include_unchanged,
2356
specific_files=specific_files,
2358
extra_trees=extra_trees,
2359
require_versioned=require_versioned,
2360
want_unversioned=want_unversioned)
2361
if want_unversioned:
2362
raise ValueError('want_unversioned is not supported')
2363
return self._transform.iter_changes()
2365
def get_file(self, path, file_id=None):
2366
"""See Tree.get_file"""
2368
file_id = self.path2id(path)
2369
if not self._content_change(file_id):
2370
return self._transform._tree.get_file(path, file_id)
2371
trans_id = self._path2trans_id(path)
2372
name = self._transform._limbo_name(trans_id)
2373
return open(name, 'rb')
2375
def get_file_with_stat(self, path, file_id=None):
2376
return self.get_file(path, file_id), None
2378
def annotate_iter(self, path, file_id=None,
2379
default_revision=_mod_revision.CURRENT_REVISION):
2381
file_id = self.path2id(path)
2382
changes = self._iter_changes_cache.get(file_id)
2386
changed_content, versioned, kind = (changes[2], changes[3],
2390
get_old = (kind[0] == 'file' and versioned[0])
2392
old_annotation = self._transform._tree.annotate_iter(
2393
path, file_id=file_id, default_revision=default_revision)
2397
return old_annotation
2398
if not changed_content:
2399
return old_annotation
2400
# TODO: This is doing something similar to what WT.annotate_iter is
2401
# doing, however it fails slightly because it doesn't know what
2402
# the *other* revision_id is, so it doesn't know how to give the
2403
# other as the origin for some lines, they all get
2404
# 'default_revision'
2405
# It would be nice to be able to use the new Annotator based
2406
# approach, as well.
2407
return annotate.reannotate([old_annotation],
2408
self.get_file(path, file_id).readlines(),
2411
def get_symlink_target(self, path, file_id=None):
2412
"""See Tree.get_symlink_target"""
2414
file_id = self.path2id(path)
2415
if not self._content_change(file_id):
2416
return self._transform._tree.get_symlink_target(path)
2417
trans_id = self._path2trans_id(path)
2418
name = self._transform._limbo_name(trans_id)
2419
return osutils.readlink(name)
2421
def walkdirs(self, prefix=''):
2422
pending = [self._transform.root]
2423
while len(pending) > 0:
2424
parent_id = pending.pop()
2427
prefix = prefix.rstrip('/')
2428
parent_path = self._final_paths.get_path(parent_id)
2429
parent_file_id = self._transform.final_file_id(parent_id)
2430
for child_id in self._all_children(parent_id):
2431
path_from_root = self._final_paths.get_path(child_id)
2432
basename = self._transform.final_name(child_id)
2433
file_id = self._transform.final_file_id(child_id)
2434
kind = self._transform.final_kind(child_id)
2435
if kind is not None:
2436
versioned_kind = kind
2439
versioned_kind = self._transform._tree.stored_kind(
2440
self._transform._tree.id2path(file_id),
2442
if versioned_kind == 'directory':
2443
subdirs.append(child_id)
2444
children.append((path_from_root, basename, kind, None,
2445
file_id, versioned_kind))
2447
if parent_path.startswith(prefix):
2448
yield (parent_path, parent_file_id), children
2449
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2452
def get_parent_ids(self):
2453
return self._parent_ids
2455
def set_parent_ids(self, parent_ids):
2456
self._parent_ids = parent_ids
2458
def get_revision_tree(self, revision_id):
2459
return self._transform._tree.get_revision_tree(revision_id)
2462
def joinpath(parent, child):
2463
"""Join tree-relative paths, handling the tree root specially"""
2464
if parent is None or parent == "":
2467
return pathjoin(parent, child)
2470
class FinalPaths(object):
2471
"""Make path calculation cheap by memoizing paths.
2473
The underlying tree must not be manipulated between calls, or else
2474
the results will likely be incorrect.
2476
def __init__(self, transform):
2477
object.__init__(self)
2478
self._known_paths = {}
2479
self.transform = transform
2481
def _determine_path(self, trans_id):
2482
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2484
name = self.transform.final_name(trans_id)
2485
parent_id = self.transform.final_parent(trans_id)
2486
if parent_id == self.transform.root:
2489
return pathjoin(self.get_path(parent_id), name)
2491
def get_path(self, trans_id):
2492
"""Find the final path associated with a trans_id"""
2493
if trans_id not in self._known_paths:
2494
self._known_paths[trans_id] = self._determine_path(trans_id)
2495
return self._known_paths[trans_id]
2497
def get_paths(self, trans_ids):
2498
return [(self.get_path(t), t) for t in trans_ids]
2502
def topology_sorted_ids(tree):
2503
"""Determine the topological order of the ids in a tree"""
2504
file_ids = list(tree)
2505
file_ids.sort(key=tree.id2path)
2509
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2510
delta_from_tree=False):
2511
"""Create working tree for a branch, using a TreeTransform.
2513
This function should be used on empty trees, having a tree root at most.
2514
(see merge and revert functionality for working with existing trees)
2516
Existing files are handled like so:
2518
- Existing bzrdirs take precedence over creating new items. They are
2519
created as '%s.diverted' % name.
2520
- Otherwise, if the content on disk matches the content we are building,
2521
it is silently replaced.
2522
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2524
:param tree: The tree to convert wt into a copy of
2525
:param wt: The working tree that files will be placed into
2526
:param accelerator_tree: A tree which can be used for retrieving file
2527
contents more quickly than tree itself, i.e. a workingtree. tree
2528
will be used for cases where accelerator_tree's content is different.
2529
:param hardlink: If true, hard-link files to accelerator_tree, where
2530
possible. accelerator_tree must implement abspath, i.e. be a
2532
:param delta_from_tree: If true, build_tree may use the input Tree to
2533
generate the inventory delta.
2535
with wt.lock_tree_write(), tree.lock_read():
2536
if accelerator_tree is not None:
2537
accelerator_tree.lock_read()
2539
return _build_tree(tree, wt, accelerator_tree, hardlink,
2542
if accelerator_tree is not None:
2543
accelerator_tree.unlock()
2546
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2547
"""See build_tree."""
2548
for num, _unused in enumerate(wt.all_versioned_paths()):
2549
if num > 0: # more than just a root
2550
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2552
top_pb = ui.ui_factory.nested_progress_bar()
2553
pp = ProgressPhase("Build phase", 2, top_pb)
2554
if tree.get_root_id() is not None:
2555
# This is kind of a hack: we should be altering the root
2556
# as part of the regular tree shape diff logic.
2557
# The conditional test here is to avoid doing an
2558
# expensive operation (flush) every time the root id
2559
# is set within the tree, nor setting the root and thus
2560
# marking the tree as dirty, because we use two different
2561
# idioms here: tree interfaces and inventory interfaces.
2562
if wt.get_root_id() != tree.get_root_id():
2563
wt.set_root_id(tree.get_root_id())
2565
tt = TreeTransform(wt)
2569
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_path('')
2570
with ui.ui_factory.nested_progress_bar() as pb:
2571
deferred_contents = []
2573
total = len(tree.all_versioned_paths())
2575
precomputed_delta = []
2577
precomputed_delta = None
2578
# Check if tree inventory has content. If so, we populate
2579
# existing_files with the directory content. If there are no
2580
# entries we skip populating existing_files as its not used.
2581
# This improves performance and unncessary work on large
2582
# directory trees. (#501307)
2584
existing_files = set()
2585
for dir, files in wt.walkdirs():
2586
existing_files.update(f[0] for f in files)
2587
for num, (tree_path, entry) in \
2588
enumerate(tree.iter_entries_by_dir()):
2589
pb.update(gettext("Building tree"), num - len(deferred_contents), total)
2590
if entry.parent_id is None:
2593
file_id = entry.file_id
2595
precomputed_delta.append((None, tree_path, file_id, entry))
2596
if tree_path in existing_files:
2597
target_path = wt.abspath(tree_path)
2598
kind = file_kind(target_path)
2599
if kind == "directory":
2601
controldir.ControlDir.open(target_path)
2602
except errors.NotBranchError:
2606
if (file_id not in divert and
2607
_content_match(tree, entry, tree_path, file_id, kind,
2609
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2610
if kind == 'directory':
2612
parent_id = file_trans_id[entry.parent_id]
2613
if entry.kind == 'file':
2614
# We *almost* replicate new_by_entry, so that we can defer
2615
# getting the file text, and get them all at once.
2616
trans_id = tt.create_path(entry.name, parent_id)
2617
file_trans_id[file_id] = trans_id
2618
tt.version_file(file_id, trans_id)
2619
executable = tree.is_executable(tree_path, file_id)
2621
tt.set_executability(executable, trans_id)
2622
trans_data = (trans_id, file_id, tree_path, entry.text_sha1)
2623
deferred_contents.append((tree_path, trans_data))
2625
file_trans_id[file_id] = new_by_entry(
2626
tree_path, tt, entry, parent_id, tree)
2628
new_trans_id = file_trans_id[file_id]
2629
old_parent = tt.trans_id_tree_path(tree_path)
2630
_reparent_children(tt, old_parent, new_trans_id)
2631
offset = num + 1 - len(deferred_contents)
2632
_create_files(tt, tree, deferred_contents, pb, offset,
2633
accelerator_tree, hardlink)
2635
divert_trans = set(file_trans_id[f] for f in divert)
2636
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2637
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2638
if len(raw_conflicts) > 0:
2639
precomputed_delta = None
2640
conflicts = cook_conflicts(raw_conflicts, tt)
2641
for conflict in conflicts:
2642
trace.warning(unicode(conflict))
2644
wt.add_conflicts(conflicts)
2645
except errors.UnsupportedOperation:
2647
result = tt.apply(no_conflicts=True,
2648
precomputed_delta=precomputed_delta)
2655
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2657
total = len(desired_files) + offset
2659
if accelerator_tree is None:
2660
new_desired_files = desired_files
2662
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2663
unchanged = [(p[0], p[1]) for (f, p, c, v, d, n, k, e)
2664
in iter if not (c or e[0] != e[1])]
2665
if accelerator_tree.supports_content_filtering():
2666
unchanged = [(tp, ap) for (tp, ap) in unchanged
2667
if not next(accelerator_tree.iter_search_rules([ap]))]
2668
unchanged = dict(unchanged)
2669
new_desired_files = []
2671
for unused_tree_path, (trans_id, file_id, tree_path, text_sha1) in desired_files:
2672
accelerator_path = unchanged.get(tree_path)
2673
if accelerator_path is None:
2674
new_desired_files.append((tree_path,
2675
(trans_id, file_id, tree_path, text_sha1)))
2677
pb.update(gettext('Adding file contents'), count + offset, total)
2679
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2682
contents = accelerator_tree.get_file(accelerator_path, file_id)
2683
if wt.supports_content_filtering():
2684
filters = wt._content_filter_stack(tree_path)
2685
contents = filtered_output_bytes(contents, filters,
2686
ContentFilterContext(tree_path, tree))
2688
tt.create_file(contents, trans_id, sha1=text_sha1)
2692
except AttributeError:
2693
# after filtering, contents may no longer be file-like
2697
for count, ((trans_id, file_id, tree_path, text_sha1), contents) in enumerate(
2698
tree.iter_files_bytes(new_desired_files)):
2699
if wt.supports_content_filtering():
2700
filters = wt._content_filter_stack(tree_path)
2701
contents = filtered_output_bytes(contents, filters,
2702
ContentFilterContext(tree_path, tree))
2703
tt.create_file(contents, trans_id, sha1=text_sha1)
2704
pb.update(gettext('Adding file contents'), count + offset, total)
2707
def _reparent_children(tt, old_parent, new_parent):
2708
for child in tt.iter_tree_children(old_parent):
2709
tt.adjust_path(tt.final_name(child), new_parent, child)
2712
def _reparent_transform_children(tt, old_parent, new_parent):
2713
by_parent = tt.by_parent()
2714
for child in by_parent[old_parent]:
2715
tt.adjust_path(tt.final_name(child), new_parent, child)
2716
return by_parent[old_parent]
2719
def _content_match(tree, entry, tree_path, file_id, kind, target_path):
2720
if entry.kind != kind:
2722
if entry.kind == "directory":
2724
if entry.kind == "file":
2725
f = file(target_path, 'rb')
2727
if tree.get_file_text(tree_path, file_id) == f.read():
2731
elif entry.kind == "symlink":
2732
if tree.get_symlink_target(tree_path, file_id) == os.readlink(target_path):
2737
def resolve_checkout(tt, conflicts, divert):
2738
new_conflicts = set()
2739
for c_type, conflict in ((c[0], c) for c in conflicts):
2740
# Anything but a 'duplicate' would indicate programmer error
2741
if c_type != 'duplicate':
2742
raise AssertionError(c_type)
2743
# Now figure out which is new and which is old
2744
if tt.new_contents(conflict[1]):
2745
new_file = conflict[1]
2746
old_file = conflict[2]
2748
new_file = conflict[2]
2749
old_file = conflict[1]
2751
# We should only get here if the conflict wasn't completely
2753
final_parent = tt.final_parent(old_file)
2754
if new_file in divert:
2755
new_name = tt.final_name(old_file)+'.diverted'
2756
tt.adjust_path(new_name, final_parent, new_file)
2757
new_conflicts.add((c_type, 'Diverted to',
2758
new_file, old_file))
2760
new_name = tt.final_name(old_file)+'.moved'
2761
tt.adjust_path(new_name, final_parent, old_file)
2762
new_conflicts.add((c_type, 'Moved existing file to',
2763
old_file, new_file))
2764
return new_conflicts
2767
def new_by_entry(path, tt, entry, parent_id, tree):
2768
"""Create a new file according to its inventory entry"""
2772
contents = tree.get_file(path, entry.file_id).readlines()
2773
executable = tree.is_executable(path, entry.file_id)
2774
return tt.new_file(name, parent_id, contents, entry.file_id,
2776
elif kind in ('directory', 'tree-reference'):
2777
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2778
if kind == 'tree-reference':
2779
tt.set_tree_reference(entry.reference_revision, trans_id)
2781
elif kind == 'symlink':
2782
target = tree.get_symlink_target(path, entry.file_id)
2783
return tt.new_symlink(name, parent_id, target, entry.file_id)
2785
raise errors.BadFileKindError(name, kind)
2788
def create_from_tree(tt, trans_id, tree, path, file_id=None, bytes=None,
2789
filter_tree_path=None):
2790
"""Create new file contents according to tree contents.
2792
:param filter_tree_path: the tree path to use to lookup
2793
content filters to apply to the bytes output in the working tree.
2794
This only applies if the working tree supports content filtering.
2796
kind = tree.kind(path, file_id)
2797
if kind == 'directory':
2798
tt.create_directory(trans_id)
2799
elif kind == "file":
2801
tree_file = tree.get_file(path, file_id)
2803
bytes = tree_file.readlines()
2807
if wt.supports_content_filtering() and filter_tree_path is not None:
2808
filters = wt._content_filter_stack(filter_tree_path)
2809
bytes = filtered_output_bytes(bytes, filters,
2810
ContentFilterContext(filter_tree_path, tree))
2811
tt.create_file(bytes, trans_id)
2812
elif kind == "symlink":
2813
tt.create_symlink(tree.get_symlink_target(path, file_id), trans_id)
2815
raise AssertionError('Unknown kind %r' % kind)
2818
def create_entry_executability(tt, entry, trans_id):
2819
"""Set the executability of a trans_id according to an inventory entry"""
2820
if entry.kind == "file":
2821
tt.set_executability(entry.executable, trans_id)
2824
def revert(working_tree, target_tree, filenames, backups=False,
2825
pb=None, change_reporter=None):
2826
"""Revert a working tree's contents to those of a target tree."""
2827
target_tree.lock_read()
2828
pb = ui.ui_factory.nested_progress_bar()
2829
tt = TreeTransform(working_tree, pb)
2831
pp = ProgressPhase("Revert phase", 3, pb)
2832
conflicts, merge_modified = _prepare_revert_transform(
2833
working_tree, target_tree, tt, filenames, backups, pp)
2835
change_reporter = delta._ChangeReporter(
2836
unversioned_filter=working_tree.is_ignored)
2837
delta.report_changes(tt.iter_changes(), change_reporter)
2838
for conflict in conflicts:
2839
trace.warning(unicode(conflict))
2842
if working_tree.supports_merge_modified():
2843
working_tree.set_merge_modified(merge_modified)
2845
target_tree.unlock()
2851
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2852
backups, pp, basis_tree=None,
2853
merge_modified=None):
2854
with ui.ui_factory.nested_progress_bar() as child_pb:
2855
if merge_modified is None:
2856
merge_modified = working_tree.merge_modified()
2857
merge_modified = _alter_files(working_tree, target_tree, tt,
2858
child_pb, filenames, backups,
2859
merge_modified, basis_tree)
2860
with ui.ui_factory.nested_progress_bar() as child_pb:
2861
raw_conflicts = resolve_conflicts(tt, child_pb,
2862
lambda t, c: conflict_pass(t, c, target_tree))
2863
conflicts = cook_conflicts(raw_conflicts, tt)
2864
return conflicts, merge_modified
2867
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2868
backups, merge_modified, basis_tree=None):
2869
if basis_tree is not None:
2870
basis_tree.lock_read()
2871
# We ask the working_tree for its changes relative to the target, rather
2872
# than the target changes relative to the working tree. Because WT4 has an
2873
# optimizer to compare itself to a target, but no optimizer for the
2875
change_list = working_tree.iter_changes(target_tree,
2876
specific_files=specific_files, pb=pb)
2877
if target_tree.get_root_id() is None:
2883
for id_num, (file_id, path, changed_content, versioned, parent, name,
2884
kind, executable) in enumerate(change_list):
2885
target_path, wt_path = path
2886
target_versioned, wt_versioned = versioned
2887
target_parent, wt_parent = parent
2888
target_name, wt_name = name
2889
target_kind, wt_kind = kind
2890
target_executable, wt_executable = executable
2891
if skip_root and wt_parent is None:
2893
trans_id = tt.trans_id_file_id(file_id)
2896
keep_content = False
2897
if wt_kind == 'file' and (backups or target_kind is None):
2898
wt_sha1 = working_tree.get_file_sha1(wt_path, file_id)
2899
if merge_modified.get(file_id) != wt_sha1:
2900
# acquire the basis tree lazily to prevent the
2901
# expense of accessing it when it's not needed ?
2902
# (Guessing, RBC, 200702)
2903
if basis_tree is None:
2904
basis_tree = working_tree.basis_tree()
2905
basis_tree.lock_read()
2906
basis_path = find_previous_path(working_tree, basis_tree, wt_path)
2907
if basis_path is None:
2908
if target_kind is None and not target_versioned:
2911
if wt_sha1 != basis_tree.get_file_sha1(basis_path, file_id):
2913
if wt_kind is not None:
2914
if not keep_content:
2915
tt.delete_contents(trans_id)
2916
elif target_kind is not None:
2917
parent_trans_id = tt.trans_id_file_id(wt_parent)
2918
backup_name = tt._available_backup_name(
2919
wt_name, parent_trans_id)
2920
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2921
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2922
if wt_versioned and target_versioned:
2923
tt.unversion_file(trans_id)
2924
tt.version_file(file_id, new_trans_id)
2925
# New contents should have the same unix perms as old
2928
trans_id = new_trans_id
2929
if target_kind in ('directory', 'tree-reference'):
2930
tt.create_directory(trans_id)
2931
if target_kind == 'tree-reference':
2932
revision = target_tree.get_reference_revision(
2933
target_path, file_id)
2934
tt.set_tree_reference(revision, trans_id)
2935
elif target_kind == 'symlink':
2936
tt.create_symlink(target_tree.get_symlink_target(
2937
target_path, file_id), trans_id)
2938
elif target_kind == 'file':
2939
deferred_files.append((target_path, (trans_id, mode_id, file_id)))
2940
if basis_tree is None:
2941
basis_tree = working_tree.basis_tree()
2942
basis_tree.lock_read()
2943
new_sha1 = target_tree.get_file_sha1(target_path, file_id)
2944
basis_path = find_previous_path(target_tree, basis_tree, target_path)
2945
if (basis_path is not None and
2946
new_sha1 == basis_tree.get_file_sha1(basis_path, file_id)):
2947
if file_id in merge_modified:
2948
del merge_modified[file_id]
2950
merge_modified[file_id] = new_sha1
2952
# preserve the execute bit when backing up
2953
if keep_content and wt_executable == target_executable:
2954
tt.set_executability(target_executable, trans_id)
2955
elif target_kind is not None:
2956
raise AssertionError(target_kind)
2957
if not wt_versioned and target_versioned:
2958
tt.version_file(file_id, trans_id)
2959
if wt_versioned and not target_versioned:
2960
tt.unversion_file(trans_id)
2961
if (target_name is not None and
2962
(wt_name != target_name or wt_parent != target_parent)):
2963
if target_name == '' and target_parent is None:
2964
parent_trans = ROOT_PARENT
2966
parent_trans = tt.trans_id_file_id(target_parent)
2967
if wt_parent is None and wt_versioned:
2968
tt.adjust_root_path(target_name, parent_trans)
2970
tt.adjust_path(target_name, parent_trans, trans_id)
2971
if wt_executable != target_executable and target_kind == "file":
2972
tt.set_executability(target_executable, trans_id)
2973
if working_tree.supports_content_filtering():
2974
for (trans_id, mode_id, file_id), bytes in (
2975
target_tree.iter_files_bytes(deferred_files)):
2976
# We're reverting a tree to the target tree so using the
2977
# target tree to find the file path seems the best choice
2978
# here IMO - Ian C 27/Oct/2009
2979
filter_tree_path = target_tree.id2path(file_id)
2980
filters = working_tree._content_filter_stack(filter_tree_path)
2981
bytes = filtered_output_bytes(bytes, filters,
2982
ContentFilterContext(filter_tree_path, working_tree))
2983
tt.create_file(bytes, trans_id, mode_id)
2985
for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
2987
tt.create_file(bytes, trans_id, mode_id)
2988
tt.fixup_new_roots()
2990
if basis_tree is not None:
2992
return merge_modified
2995
def resolve_conflicts(tt, pb=None, pass_func=None):
2996
"""Make many conflict-resolution attempts, but die if they fail"""
2997
if pass_func is None:
2998
pass_func = conflict_pass
2999
new_conflicts = set()
3000
with ui.ui_factory.nested_progress_bar() as pb:
3002
pb.update(gettext('Resolution pass'), n+1, 10)
3003
conflicts = tt.find_conflicts()
3004
if len(conflicts) == 0:
3005
return new_conflicts
3006
new_conflicts.update(pass_func(tt, conflicts))
3007
raise MalformedTransform(conflicts=conflicts)
3010
def conflict_pass(tt, conflicts, path_tree=None):
3011
"""Resolve some classes of conflicts.
3013
:param tt: The transform to resolve conflicts in
3014
:param conflicts: The conflicts to resolve
3015
:param path_tree: A Tree to get supplemental paths from
3017
new_conflicts = set()
3018
for c_type, conflict in ((c[0], c) for c in conflicts):
3019
if c_type == 'duplicate id':
3020
tt.unversion_file(conflict[1])
3021
new_conflicts.add((c_type, 'Unversioned existing file',
3022
conflict[1], conflict[2], ))
3023
elif c_type == 'duplicate':
3024
# files that were renamed take precedence
3025
final_parent = tt.final_parent(conflict[1])
3026
if tt.path_changed(conflict[1]):
3027
existing_file, new_file = conflict[2], conflict[1]
3029
existing_file, new_file = conflict[1], conflict[2]
3030
new_name = tt.final_name(existing_file) + '.moved'
3031
tt.adjust_path(new_name, final_parent, existing_file)
3032
new_conflicts.add((c_type, 'Moved existing file to',
3033
existing_file, new_file))
3034
elif c_type == 'parent loop':
3035
# break the loop by undoing one of the ops that caused the loop
3037
while not tt.path_changed(cur):
3038
cur = tt.final_parent(cur)
3039
new_conflicts.add((c_type, 'Cancelled move', cur,
3040
tt.final_parent(cur),))
3041
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3043
elif c_type == 'missing parent':
3044
trans_id = conflict[1]
3045
if trans_id in tt._removed_contents:
3046
cancel_deletion = True
3047
orphans = tt._get_potential_orphans(trans_id)
3049
cancel_deletion = False
3050
# All children are orphans
3053
tt.new_orphan(o, trans_id)
3054
except OrphaningError:
3055
# Something bad happened so we cancel the directory
3056
# deletion which will leave it in place with a
3057
# conflict. The user can deal with it from there.
3058
# Note that this also catch the case where we don't
3059
# want to create orphans and leave the directory in
3061
cancel_deletion = True
3064
# Cancel the directory deletion
3065
tt.cancel_deletion(trans_id)
3066
new_conflicts.add(('deleting parent', 'Not deleting',
3071
tt.final_name(trans_id)
3073
if path_tree is not None:
3074
file_id = tt.final_file_id(trans_id)
3076
file_id = tt.inactive_file_id(trans_id)
3077
_, entry = next(path_tree.iter_entries_by_dir(
3078
specific_files=[path_tree.id2path(file_id)]))
3079
# special-case the other tree root (move its
3080
# children to current root)
3081
if entry.parent_id is None:
3083
moved = _reparent_transform_children(
3084
tt, trans_id, tt.root)
3086
new_conflicts.add((c_type, 'Moved to root',
3089
parent_trans_id = tt.trans_id_file_id(
3091
tt.adjust_path(entry.name, parent_trans_id,
3094
tt.create_directory(trans_id)
3095
new_conflicts.add((c_type, 'Created directory', trans_id))
3096
elif c_type == 'unversioned parent':
3097
file_id = tt.inactive_file_id(conflict[1])
3098
# special-case the other tree root (move its children instead)
3099
if path_tree and path_tree.path2id('') == file_id:
3100
# This is the root entry, skip it
3102
tt.version_file(file_id, conflict[1])
3103
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3104
elif c_type == 'non-directory parent':
3105
parent_id = conflict[1]
3106
parent_parent = tt.final_parent(parent_id)
3107
parent_name = tt.final_name(parent_id)
3108
parent_file_id = tt.final_file_id(parent_id)
3109
new_parent_id = tt.new_directory(parent_name + '.new',
3110
parent_parent, parent_file_id)
3111
_reparent_transform_children(tt, parent_id, new_parent_id)
3112
if parent_file_id is not None:
3113
tt.unversion_file(parent_id)
3114
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3115
elif c_type == 'versioning no contents':
3116
tt.cancel_versioning(conflict[1])
3117
return new_conflicts
3120
def cook_conflicts(raw_conflicts, tt):
3121
"""Generate a list of cooked conflicts, sorted by file path"""
3122
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3123
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3126
def iter_cook_conflicts(raw_conflicts, tt):
3128
for conflict in raw_conflicts:
3129
c_type = conflict[0]
3130
action = conflict[1]
3131
modified_path = fp.get_path(conflict[2])
3132
modified_id = tt.final_file_id(conflict[2])
3133
if len(conflict) == 3:
3134
yield conflicts.Conflict.factory(
3135
c_type, action=action, path=modified_path, file_id=modified_id)
3138
conflicting_path = fp.get_path(conflict[3])
3139
conflicting_id = tt.final_file_id(conflict[3])
3140
yield conflicts.Conflict.factory(
3141
c_type, action=action, path=modified_path,
3142
file_id=modified_id,
3143
conflict_path=conflicting_path,
3144
conflict_file_id=conflicting_id)
3147
class _FileMover(object):
3148
"""Moves and deletes files for TreeTransform, tracking operations"""
3151
self.past_renames = []
3152
self.pending_deletions = []
3154
def rename(self, from_, to):
3155
"""Rename a file from one path to another."""
3157
os.rename(from_, to)
3158
except OSError as e:
3159
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3160
raise errors.FileExists(to, str(e))
3161
# normal OSError doesn't include filenames so it's hard to see where
3162
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3163
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3164
self.past_renames.append((from_, to))
3166
def pre_delete(self, from_, to):
3167
"""Rename a file out of the way and mark it for deletion.
3169
Unlike os.unlink, this works equally well for files and directories.
3170
:param from_: The current file path
3171
:param to: A temporary path for the file
3173
self.rename(from_, to)
3174
self.pending_deletions.append(to)
3177
"""Reverse all renames that have been performed"""
3178
for from_, to in reversed(self.past_renames):
3180
os.rename(to, from_)
3181
except OSError as e:
3182
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3183
# after rollback, don't reuse _FileMover
3185
pending_deletions = None
3187
def apply_deletions(self):
3188
"""Apply all marked deletions"""
3189
for path in self.pending_deletions:
3191
# after apply_deletions, don't reuse _FileMover
3193
pending_deletions = None
3196
def link_tree(target_tree, source_tree):
3197
"""Where possible, hard-link files in a tree to those in another tree.
3199
:param target_tree: Tree to change
3200
:param source_tree: Tree to hard-link from
3202
tt = TreeTransform(target_tree)
3204
for (file_id, paths, changed_content, versioned, parent, name, kind,
3205
executable) in target_tree.iter_changes(source_tree,
3206
include_unchanged=True):
3209
if kind != ('file', 'file'):
3211
if executable[0] != executable[1]:
3213
trans_id = tt.trans_id_tree_path(paths[1])
3214
tt.delete_contents(trans_id)
3215
tt.create_hardlink(source_tree.abspath(paths[0]), trans_id)