1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
21
from stat import S_ISREG, S_IEXEC
25
config as _mod_config,
32
lazy_import.lazy_import(globals(), """
43
revision as _mod_revision,
47
from breezy.bzr import (
51
from breezy.i18n import gettext
53
from .errors import (DuplicateKey, MalformedTransform,
54
ReusingTransform, CantMoveRoot,
55
ImmortalLimbo, NoFinalPath,
57
from .filters import filtered_output_bytes, ContentFilterContext
58
from .mutabletree import MutableTree
59
from .osutils import (
67
from .progress import ProgressPhase
78
ROOT_PARENT = "root-parent"
81
def unique_add(map, key, value):
83
raise DuplicateKey(key=key)
87
class _TransformResults(object):
88
def __init__(self, modified_paths, rename_count):
90
self.modified_paths = modified_paths
91
self.rename_count = rename_count
94
class TreeTransformBase(object):
95
"""The base class for TreeTransform and its kin."""
97
def __init__(self, tree, pb=None, case_sensitive=True):
100
:param tree: The tree that will be transformed, but not necessarily
103
:param case_sensitive: If True, the target of the transform is
104
case sensitive, not just case preserving.
106
object.__init__(self)
109
# mapping of trans_id -> new basename
111
# mapping of trans_id -> new parent trans_id
112
self._new_parent = {}
113
# mapping of trans_id with new contents -> new file_kind
114
self._new_contents = {}
115
# mapping of trans_id => (sha1 of content, stat_value)
116
self._observed_sha1s = {}
117
# Set of trans_ids whose contents will be removed
118
self._removed_contents = set()
119
# Mapping of trans_id -> new execute-bit value
120
self._new_executability = {}
121
# Mapping of trans_id -> new tree-reference value
122
self._new_reference_revision = {}
123
# Mapping of trans_id -> new file_id
125
# Mapping of old file-id -> trans_id
126
self._non_present_ids = {}
127
# Mapping of new file_id -> trans_id
129
# Set of trans_ids that will be removed
130
self._removed_id = set()
131
# Mapping of path in old tree -> trans_id
132
self._tree_path_ids = {}
133
# Mapping trans_id -> path in old tree
134
self._tree_id_paths = {}
135
# The trans_id that will be used as the tree root
136
if tree.is_versioned(''):
137
self._new_root = self.trans_id_tree_path('')
139
self._new_root = None
140
# Indicator of whether the transform has been applied
144
# Whether the target is case sensitive
145
self._case_sensitive_target = case_sensitive
146
# A counter of how many files have been renamed
147
self.rename_count = 0
150
"""Support Context Manager API."""
153
def __exit__(self, exc_type, exc_val, exc_tb):
154
"""Support Context Manager API."""
158
"""Release the working tree lock, if held.
160
This is required if apply has not been invoked, but can be invoked
163
if self._tree is None:
165
for hook in MutableTree.hooks['post_transform']:
166
hook(self._tree, self)
170
def __get_root(self):
171
return self._new_root
173
root = property(__get_root)
175
def _assign_id(self):
176
"""Produce a new tranform id"""
177
new_id = "new-%s" % self._id_number
181
def create_path(self, name, parent):
182
"""Assign a transaction id to a new path"""
183
trans_id = self._assign_id()
184
unique_add(self._new_name, trans_id, name)
185
unique_add(self._new_parent, trans_id, parent)
188
def adjust_path(self, name, parent, trans_id):
189
"""Change the path that is assigned to a transaction id."""
191
raise ValueError("Parent trans-id may not be None")
192
if trans_id == self._new_root:
194
self._new_name[trans_id] = name
195
self._new_parent[trans_id] = parent
197
def adjust_root_path(self, name, parent):
198
"""Emulate moving the root by moving all children, instead.
200
We do this by undoing the association of root's transaction id with the
201
current tree. This allows us to create a new directory with that
202
transaction id. We unversion the root directory and version the
203
physically new directory, and hope someone versions the tree root
206
old_root = self._new_root
207
old_root_file_id = self.final_file_id(old_root)
208
# force moving all children of root
209
for child_id in self.iter_tree_children(old_root):
210
if child_id != parent:
211
self.adjust_path(self.final_name(child_id),
212
self.final_parent(child_id), child_id)
213
file_id = self.final_file_id(child_id)
214
if file_id is not None:
215
self.unversion_file(child_id)
216
self.version_file(file_id, child_id)
218
# the physical root needs a new transaction id
219
self._tree_path_ids.pop("")
220
self._tree_id_paths.pop(old_root)
221
self._new_root = self.trans_id_tree_path('')
222
if parent == old_root:
223
parent = self._new_root
224
self.adjust_path(name, parent, old_root)
225
self.create_directory(old_root)
226
self.version_file(old_root_file_id, old_root)
227
self.unversion_file(self._new_root)
229
def fixup_new_roots(self):
230
"""Reinterpret requests to change the root directory
232
Instead of creating a root directory, or moving an existing directory,
233
all the attributes and children of the new root are applied to the
234
existing root directory.
236
This means that the old root trans-id becomes obsolete, so it is
237
recommended only to invoke this after the root trans-id has become
241
new_roots = [k for k, v in viewitems(self._new_parent)
243
if len(new_roots) < 1:
245
if len(new_roots) != 1:
246
raise ValueError('A tree cannot have two roots!')
247
if self._new_root is None:
248
self._new_root = new_roots[0]
250
old_new_root = new_roots[0]
251
# unversion the new root's directory.
252
if self.final_kind(self._new_root) is None:
253
file_id = self.final_file_id(old_new_root)
255
file_id = self.final_file_id(self._new_root)
256
if old_new_root in self._new_id:
257
self.cancel_versioning(old_new_root)
259
self.unversion_file(old_new_root)
260
# if, at this stage, root still has an old file_id, zap it so we can
261
# stick a new one in.
262
if (self.tree_file_id(self._new_root) is not None
263
and 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
# Now add all their children to the set.
429
for parent_trans_id in new_file_id:
430
changed_ids.update(self.iter_tree_children(parent_trans_id))
431
return sorted(FinalPaths(self).get_paths(changed_ids))
433
def final_kind(self, trans_id):
434
"""Determine the final file kind, after any changes applied.
436
:return: None if the file does not exist/has no contents. (It is
437
conceivable that a path would be created without the corresponding
438
contents insertion command)
440
if trans_id in self._new_contents:
441
return self._new_contents[trans_id]
442
elif trans_id in self._removed_contents:
445
return self.tree_kind(trans_id)
447
def tree_path(self, trans_id):
448
"""Determine the tree path associated with the trans_id."""
449
return self._tree_id_paths.get(trans_id)
451
def tree_file_id(self, trans_id):
452
"""Determine the file id associated with the trans_id in the tree"""
453
path = self.tree_path(trans_id)
456
# the file is old; the old id is still valid
457
if self._new_root == trans_id:
458
return self._tree.get_root_id()
459
return self._tree.path2id(path)
461
def final_file_id(self, trans_id):
462
"""Determine the file id after any changes are applied, or None.
464
None indicates that the file will not be versioned after changes are
468
return self._new_id[trans_id]
470
if trans_id in self._removed_id:
472
return self.tree_file_id(trans_id)
474
def inactive_file_id(self, trans_id):
475
"""Return the inactive file_id associated with a transaction id.
476
That is, the one in the tree or in non_present_ids.
477
The file_id may actually be active, too.
479
file_id = self.tree_file_id(trans_id)
480
if file_id is not None:
482
for key, value in viewitems(self._non_present_ids):
483
if value == trans_id:
486
def final_parent(self, trans_id):
487
"""Determine the parent file_id, after any changes are applied.
489
ROOT_PARENT is returned for the tree root.
492
return self._new_parent[trans_id]
494
return self.get_tree_parent(trans_id)
496
def final_name(self, trans_id):
497
"""Determine the final filename, after all changes are applied."""
499
return self._new_name[trans_id]
502
return os.path.basename(self._tree_id_paths[trans_id])
504
raise NoFinalPath(trans_id, self)
507
"""Return a map of parent: children for known parents.
509
Only new paths and parents of tree files with assigned ids are used.
512
items = list(viewitems(self._new_parent))
513
items.extend((t, self.final_parent(t))
514
for t in list(self._tree_id_paths))
515
for trans_id, parent_id in items:
516
if parent_id not in by_parent:
517
by_parent[parent_id] = set()
518
by_parent[parent_id].add(trans_id)
521
def path_changed(self, trans_id):
522
"""Return True if a trans_id's path has changed."""
523
return (trans_id in self._new_name) or (trans_id in self._new_parent)
525
def new_contents(self, trans_id):
526
return (trans_id in self._new_contents)
528
def find_conflicts(self):
529
"""Find any violations of inventory or filesystem invariants"""
530
if self._done is True:
531
raise ReusingTransform()
533
# ensure all children of all existent parents are known
534
# all children of non-existent parents are known, by definition.
535
self._add_tree_children()
536
by_parent = self.by_parent()
537
conflicts.extend(self._unversioned_parents(by_parent))
538
conflicts.extend(self._parent_loops())
539
conflicts.extend(self._duplicate_entries(by_parent))
540
conflicts.extend(self._duplicate_ids())
541
conflicts.extend(self._parent_type_conflicts(by_parent))
542
conflicts.extend(self._improper_versioning())
543
conflicts.extend(self._executability_conflicts())
544
conflicts.extend(self._overwrite_conflicts())
547
def _check_malformed(self):
548
conflicts = self.find_conflicts()
549
if len(conflicts) != 0:
550
raise MalformedTransform(conflicts=conflicts)
552
def _add_tree_children(self):
553
"""Add all the children of all active parents to the known paths.
555
Active parents are those which gain children, and those which are
556
removed. This is a necessary first step in detecting conflicts.
558
parents = list(self.by_parent())
559
parents.extend([t for t in self._removed_contents if
560
self.tree_kind(t) == 'directory'])
561
for trans_id in self._removed_id:
562
path = self.tree_path(trans_id)
564
if self._tree.stored_kind(path) == 'directory':
565
parents.append(trans_id)
566
elif self.tree_kind(trans_id) == 'directory':
567
parents.append(trans_id)
569
for parent_id in parents:
570
# ensure that all children are registered with the transaction
571
list(self.iter_tree_children(parent_id))
573
def _has_named_child(self, name, parent_id, known_children):
574
"""Does a parent already have a name child.
576
:param name: The searched for name.
578
:param parent_id: The parent for which the check is made.
580
:param known_children: The already known children. This should have
581
been recently obtained from `self.by_parent.get(parent_id)`
582
(or will be if None is passed).
584
if known_children is None:
585
known_children = self.by_parent().get(parent_id, [])
586
for child in known_children:
587
if self.final_name(child) == name:
589
parent_path = self._tree_id_paths.get(parent_id, None)
590
if parent_path is None:
591
# No parent... no children
593
child_path = joinpath(parent_path, name)
594
child_id = self._tree_path_ids.get(child_path, None)
596
# Not known by the tree transform yet, check the filesystem
597
return osutils.lexists(self._tree.abspath(child_path))
599
raise AssertionError('child_id is missing: %s, %s, %s'
600
% (name, parent_id, child_id))
602
def _available_backup_name(self, name, target_id):
603
"""Find an available backup name.
605
:param name: The basename of the file.
607
:param target_id: The directory trans_id where the backup should
610
known_children = self.by_parent().get(target_id, [])
611
return osutils.available_backup_name(
613
lambda base: self._has_named_child(
614
base, target_id, known_children))
616
def _parent_loops(self):
617
"""No entry should be its own ancestor"""
619
for trans_id in self._new_parent:
622
while parent_id != ROOT_PARENT:
625
parent_id = self.final_parent(parent_id)
628
if parent_id == trans_id:
629
conflicts.append(('parent loop', trans_id))
630
if parent_id in seen:
634
def _unversioned_parents(self, by_parent):
635
"""If parent directories are versioned, children must be versioned."""
637
for parent_id, children in viewitems(by_parent):
638
if parent_id == ROOT_PARENT:
640
if self.final_file_id(parent_id) is not None:
642
for child_id in children:
643
if self.final_file_id(child_id) is not None:
644
conflicts.append(('unversioned parent', parent_id))
648
def _improper_versioning(self):
649
"""Cannot version a file with no contents, or a bad type.
651
However, existing entries with no contents are okay.
654
for trans_id in self._new_id:
655
kind = self.final_kind(trans_id)
657
conflicts.append(('versioning no contents', trans_id))
659
if not self._tree.versionable_kind(kind):
660
conflicts.append(('versioning bad kind', trans_id, kind))
663
def _executability_conflicts(self):
664
"""Check for bad executability changes.
666
Only versioned files may have their executability set, because
667
1. only versioned entries can have executability under windows
668
2. only files can be executable. (The execute bit on a directory
669
does not indicate searchability)
672
for trans_id in self._new_executability:
673
if self.final_file_id(trans_id) is None:
674
conflicts.append(('unversioned executability', trans_id))
676
if self.final_kind(trans_id) != "file":
677
conflicts.append(('non-file executability', trans_id))
680
def _overwrite_conflicts(self):
681
"""Check for overwrites (not permitted on Win32)"""
683
for trans_id in self._new_contents:
684
if self.tree_kind(trans_id) is None:
686
if trans_id not in self._removed_contents:
687
conflicts.append(('overwrite', trans_id,
688
self.final_name(trans_id)))
691
def _duplicate_entries(self, by_parent):
692
"""No directory may have two entries with the same name."""
694
if (self._new_name, self._new_parent) == ({}, {}):
696
for children in viewvalues(by_parent):
698
for child_tid in children:
699
name = self.final_name(child_tid)
701
# Keep children only if they still exist in the end
702
if not self._case_sensitive_target:
704
name_ids.append((name, child_tid))
708
for name, trans_id in name_ids:
709
kind = self.final_kind(trans_id)
710
file_id = self.final_file_id(trans_id)
711
if kind is None and file_id is None:
713
if name == last_name:
714
conflicts.append(('duplicate', last_trans_id, trans_id,
717
last_trans_id = trans_id
720
def _duplicate_ids(self):
721
"""Each inventory id may only be used once"""
723
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
725
all_ids = self._tree.all_file_ids()
726
active_tree_ids = all_ids.difference(removed_tree_ids)
727
for trans_id, file_id in viewitems(self._new_id):
728
if file_id in active_tree_ids:
729
path = self._tree.id2path(file_id)
730
old_trans_id = self.trans_id_tree_path(path)
731
conflicts.append(('duplicate id', old_trans_id, trans_id))
734
def _parent_type_conflicts(self, by_parent):
735
"""Children must have a directory parent"""
737
for parent_id, children in viewitems(by_parent):
738
if parent_id == ROOT_PARENT:
741
for child_id in children:
742
if self.final_kind(child_id) is not None:
747
# There is at least a child, so we need an existing directory to
749
kind = self.final_kind(parent_id)
751
# The directory will be deleted
752
conflicts.append(('missing parent', parent_id))
753
elif kind != "directory":
754
# Meh, we need a *directory* to put something in it
755
conflicts.append(('non-directory parent', parent_id))
758
def _set_executability(self, path, trans_id):
759
"""Set the executability of versioned files """
760
if self._tree._supports_executable():
761
new_executability = self._new_executability[trans_id]
762
abspath = self._tree.abspath(path)
763
current_mode = os.stat(abspath).st_mode
764
if new_executability:
767
to_mode = current_mode | (0o100 & ~umask)
768
# Enable x-bit for others only if they can read it.
769
if current_mode & 0o004:
770
to_mode |= 0o001 & ~umask
771
if current_mode & 0o040:
772
to_mode |= 0o010 & ~umask
774
to_mode = current_mode & ~0o111
775
osutils.chmod_if_possible(abspath, to_mode)
777
def _new_entry(self, name, parent_id, file_id):
778
"""Helper function to create a new filesystem entry."""
779
trans_id = self.create_path(name, parent_id)
780
if file_id is not None:
781
self.version_file(file_id, trans_id)
784
def new_file(self, name, parent_id, contents, file_id=None,
785
executable=None, sha1=None):
786
"""Convenience method to create files.
788
name is the name of the file to create.
789
parent_id is the transaction id of the parent directory of the file.
790
contents is an iterator of bytestrings, which will be used to produce
792
:param file_id: The inventory ID of the file, if it is to be versioned.
793
:param executable: Only valid when a file_id has been supplied.
795
trans_id = self._new_entry(name, parent_id, file_id)
796
# TODO: rather than scheduling a set_executable call,
797
# have create_file create the file with the right mode.
798
self.create_file(contents, trans_id, sha1=sha1)
799
if executable is not None:
800
self.set_executability(executable, trans_id)
803
def new_directory(self, name, parent_id, file_id=None):
804
"""Convenience method to create directories.
806
name is the name of the directory to create.
807
parent_id is the transaction id of the parent directory of the
809
file_id is the inventory ID of the directory, if it is to be versioned.
811
trans_id = self._new_entry(name, parent_id, file_id)
812
self.create_directory(trans_id)
815
def new_symlink(self, name, parent_id, target, file_id=None):
816
"""Convenience method to create symbolic link.
818
name is the name of the symlink to create.
819
parent_id is the transaction id of the parent directory of the symlink.
820
target is a bytestring of the target of the symlink.
821
file_id is the inventory ID of the file, if it is to be versioned.
823
trans_id = self._new_entry(name, parent_id, file_id)
824
self.create_symlink(target, trans_id)
827
def new_orphan(self, trans_id, parent_id):
828
"""Schedule an item to be orphaned.
830
When a directory is about to be removed, its children, if they are not
831
versioned are moved out of the way: they don't have a parent anymore.
833
:param trans_id: The trans_id of the existing item.
834
:param parent_id: The parent trans_id of the item.
836
raise NotImplementedError(self.new_orphan)
838
def _get_potential_orphans(self, dir_id):
839
"""Find the potential orphans in a directory.
841
A directory can't be safely deleted if there are versioned files in it.
842
If all the contained files are unversioned then they can be orphaned.
844
The 'None' return value means that the directory contains at least one
845
versioned file and should not be deleted.
847
:param dir_id: The directory trans id.
849
:return: A list of the orphan trans ids or None if at least one
850
versioned file is present.
853
# Find the potential orphans, stop if one item should be kept
854
for child_tid in self.by_parent()[dir_id]:
855
if child_tid in self._removed_contents:
856
# The child is removed as part of the transform. Since it was
857
# versioned before, it's not an orphan
859
elif self.final_file_id(child_tid) is None:
860
# The child is not versioned
861
orphans.append(child_tid)
863
# We have a versioned file here, searching for orphans is
869
def _affected_ids(self):
870
"""Return the set of transform ids affected by the transform"""
871
trans_ids = set(self._removed_id)
872
trans_ids.update(self._new_id)
873
trans_ids.update(self._removed_contents)
874
trans_ids.update(self._new_contents)
875
trans_ids.update(self._new_executability)
876
trans_ids.update(self._new_name)
877
trans_ids.update(self._new_parent)
880
def _get_file_id_maps(self):
881
"""Return mapping of file_ids to trans_ids in the to and from states"""
882
trans_ids = self._affected_ids()
885
# Build up two dicts: trans_ids associated with file ids in the
886
# FROM state, vs the TO state.
887
for trans_id in trans_ids:
888
from_file_id = self.tree_file_id(trans_id)
889
if from_file_id is not None:
890
from_trans_ids[from_file_id] = trans_id
891
to_file_id = self.final_file_id(trans_id)
892
if to_file_id is not None:
893
to_trans_ids[to_file_id] = trans_id
894
return from_trans_ids, to_trans_ids
896
def _from_file_data(self, from_trans_id, from_versioned, from_path):
897
"""Get data about a file in the from (tree) state
899
Return a (name, parent, kind, executable) tuple
901
from_path = self._tree_id_paths.get(from_trans_id)
903
# get data from working tree if versioned
904
from_entry = next(self._tree.iter_entries_by_dir(
905
specific_files=[from_path]))[1]
906
from_name = from_entry.name
907
from_parent = from_entry.parent_id
910
if from_path is None:
911
# File does not exist in FROM state
915
# File exists, but is not versioned. Have to use path-
917
from_name = os.path.basename(from_path)
918
tree_parent = self.get_tree_parent(from_trans_id)
919
from_parent = self.tree_file_id(tree_parent)
920
if from_path is not None:
921
from_kind, from_executable, from_stats = \
922
self._tree._comparison_data(from_entry, from_path)
925
from_executable = False
926
return from_name, from_parent, from_kind, from_executable
928
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
929
"""Get data about a file in the to (target) state
931
Return a (name, parent, kind, executable) tuple
933
to_name = self.final_name(to_trans_id)
934
to_kind = self.final_kind(to_trans_id)
935
to_parent = self.final_file_id(self.final_parent(to_trans_id))
936
if to_trans_id in self._new_executability:
937
to_executable = self._new_executability[to_trans_id]
938
elif to_trans_id == from_trans_id:
939
to_executable = from_executable
941
to_executable = False
942
return to_name, to_parent, to_kind, to_executable
944
def iter_changes(self):
945
"""Produce output in the same format as Tree.iter_changes.
947
Will produce nonsensical results if invoked while inventory/filesystem
948
conflicts (as reported by TreeTransform.find_conflicts()) are present.
950
This reads the Transform, but only reproduces changes involving a
951
file_id. Files that are not versioned in either of the FROM or TO
952
states are not reflected.
954
final_paths = FinalPaths(self)
955
from_trans_ids, to_trans_ids = self._get_file_id_maps()
957
# Now iterate through all active file_ids
958
for file_id in set(from_trans_ids).union(to_trans_ids):
960
from_trans_id = from_trans_ids.get(file_id)
961
# find file ids, and determine versioning state
962
if from_trans_id is None:
963
from_versioned = False
964
from_trans_id = to_trans_ids[file_id]
966
from_versioned = True
967
to_trans_id = to_trans_ids.get(file_id)
968
if to_trans_id is None:
970
to_trans_id = from_trans_id
974
if not from_versioned:
977
from_path = self._tree_id_paths.get(from_trans_id)
981
to_path = final_paths.get_path(to_trans_id)
983
from_name, from_parent, from_kind, from_executable = \
984
self._from_file_data(from_trans_id, from_versioned, from_path)
986
to_name, to_parent, to_kind, to_executable = \
987
self._to_file_data(to_trans_id, from_trans_id, from_executable)
989
if from_kind != to_kind:
991
elif to_kind in ('file', 'symlink') and (
992
to_trans_id != from_trans_id
993
or to_trans_id in self._new_contents):
995
if (not modified and from_versioned == to_versioned
996
and from_parent == to_parent and from_name == to_name
997
and from_executable == to_executable):
999
results.append((file_id, (from_path, to_path), modified,
1000
(from_versioned, to_versioned),
1001
(from_parent, to_parent),
1002
(from_name, to_name),
1003
(from_kind, to_kind),
1004
(from_executable, to_executable)))
1008
return (paths[0] or '', paths[1] or '')
1009
return iter(sorted(results, key=path_key))
1011
def get_preview_tree(self):
1012
"""Return a tree representing the result of the transform.
1014
The tree is a snapshot, and altering the TreeTransform will invalidate
1017
return _PreviewTree(self)
1019
def commit(self, branch, message, merge_parents=None, strict=False,
1020
timestamp=None, timezone=None, committer=None, authors=None,
1021
revprops=None, revision_id=None):
1022
"""Commit the result of this TreeTransform to a branch.
1024
:param branch: The branch to commit to.
1025
:param message: The message to attach to the commit.
1026
:param merge_parents: Additional parent revision-ids specified by
1028
:param strict: If True, abort the commit if there are unversioned
1030
:param timestamp: if not None, seconds-since-epoch for the time and
1031
date. (May be a float.)
1032
:param timezone: Optional timezone for timestamp, as an offset in
1034
:param committer: Optional committer in email-id format.
1035
(e.g. "J Random Hacker <jrandom@example.com>")
1036
:param authors: Optional list of authors in email-id format.
1037
:param revprops: Optional dictionary of revision properties.
1038
:param revision_id: Optional revision id. (Specifying a revision-id
1039
may reduce performance for some non-native formats.)
1040
:return: The revision_id of the revision committed.
1042
self._check_malformed()
1044
unversioned = set(self._new_contents).difference(set(self._new_id))
1045
for trans_id in unversioned:
1046
if self.final_file_id(trans_id) is None:
1047
raise errors.StrictCommitFailed()
1049
revno, last_rev_id = branch.last_revision_info()
1050
if last_rev_id == _mod_revision.NULL_REVISION:
1051
if merge_parents is not None:
1052
raise ValueError('Cannot supply merge parents for first'
1056
parent_ids = [last_rev_id]
1057
if merge_parents is not None:
1058
parent_ids.extend(merge_parents)
1059
if self._tree.get_revision_id() != last_rev_id:
1060
raise ValueError('TreeTransform not based on branch basis: %s' %
1061
self._tree.get_revision_id().decode('utf-8'))
1062
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1063
builder = branch.get_commit_builder(parent_ids,
1064
timestamp=timestamp,
1066
committer=committer,
1068
revision_id=revision_id)
1069
preview = self.get_preview_tree()
1070
list(builder.record_iter_changes(preview, last_rev_id,
1071
self.iter_changes()))
1072
builder.finish_inventory()
1073
revision_id = builder.commit(message)
1074
branch.set_last_revision_info(revno + 1, revision_id)
1077
def _text_parent(self, trans_id):
1078
path = self.tree_path(trans_id)
1080
if path is None or self._tree.kind(path) != 'file':
1082
except errors.NoSuchFile:
1086
def _get_parents_texts(self, trans_id):
1087
"""Get texts for compression parents of this file."""
1088
path = self._text_parent(trans_id)
1091
return (self._tree.get_file_text(path),)
1093
def _get_parents_lines(self, trans_id):
1094
"""Get lines for compression parents of this file."""
1095
path = self._text_parent(trans_id)
1098
return (self._tree.get_file_lines(path),)
1100
def serialize(self, serializer):
1101
"""Serialize this TreeTransform.
1103
:param serializer: A Serialiser like pack.ContainerSerializer.
1105
new_name = {k.encode('utf-8'): v.encode('utf-8')
1106
for k, v in viewitems(self._new_name)}
1107
new_parent = {k.encode('utf-8'): v.encode('utf-8')
1108
for k, v in viewitems(self._new_parent)}
1109
new_id = {k.encode('utf-8'): v
1110
for k, v in viewitems(self._new_id)}
1111
new_executability = {k.encode('utf-8'): int(v)
1112
for k, v in viewitems(self._new_executability)}
1113
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
1114
for k, v in viewitems(self._tree_path_ids)}
1115
non_present_ids = {k: v.encode('utf-8')
1116
for k, v in viewitems(self._non_present_ids)}
1117
removed_contents = [trans_id.encode('utf-8')
1118
for trans_id in self._removed_contents]
1119
removed_id = [trans_id.encode('utf-8')
1120
for trans_id in self._removed_id]
1122
b'_id_number': self._id_number,
1123
b'_new_name': new_name,
1124
b'_new_parent': new_parent,
1125
b'_new_executability': new_executability,
1127
b'_tree_path_ids': tree_path_ids,
1128
b'_removed_id': removed_id,
1129
b'_removed_contents': removed_contents,
1130
b'_non_present_ids': non_present_ids,
1132
yield serializer.bytes_record(bencode.bencode(attribs),
1134
for trans_id, kind in sorted(viewitems(self._new_contents)):
1136
with open(self._limbo_name(trans_id), 'rb') as cur_file:
1137
lines = cur_file.readlines()
1138
parents = self._get_parents_lines(trans_id)
1139
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1140
content = b''.join(mpdiff.to_patch())
1141
if kind == 'directory':
1143
if kind == 'symlink':
1144
content = self._read_symlink_target(trans_id)
1145
if not isinstance(content, bytes):
1146
content = content.encode('utf-8')
1147
yield serializer.bytes_record(
1148
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
1150
def deserialize(self, records):
1151
"""Deserialize a stored TreeTransform.
1153
:param records: An iterable of (names, content) tuples, as per
1154
pack.ContainerPushParser.
1156
names, content = next(records)
1157
attribs = bencode.bdecode(content)
1158
self._id_number = attribs[b'_id_number']
1159
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
1160
for k, v in viewitems(attribs[b'_new_name'])}
1161
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
1162
for k, v in viewitems(attribs[b'_new_parent'])}
1163
self._new_executability = {
1164
k.decode('utf-8'): bool(v)
1165
for k, v in viewitems(attribs[b'_new_executability'])}
1166
self._new_id = {k.decode('utf-8'): v
1167
for k, v in viewitems(attribs[b'_new_id'])}
1168
self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
1169
self._tree_path_ids = {}
1170
self._tree_id_paths = {}
1171
for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
1172
path = bytepath.decode('utf-8')
1173
trans_id = trans_id.decode('utf-8')
1174
self._tree_path_ids[path] = trans_id
1175
self._tree_id_paths[trans_id] = path
1176
self._removed_id = {trans_id.decode('utf-8')
1177
for trans_id in attribs[b'_removed_id']}
1178
self._removed_contents = set(
1179
trans_id.decode('utf-8')
1180
for trans_id in attribs[b'_removed_contents'])
1181
self._non_present_ids = {
1182
k: v.decode('utf-8')
1183
for k, v in viewitems(attribs[b'_non_present_ids'])}
1184
for ((trans_id, kind),), content in records:
1185
trans_id = trans_id.decode('utf-8')
1186
kind = kind.decode('ascii')
1188
mpdiff = multiparent.MultiParent.from_patch(content)
1189
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1190
self.create_file(lines, trans_id)
1191
if kind == 'directory':
1192
self.create_directory(trans_id)
1193
if kind == 'symlink':
1194
self.create_symlink(content.decode('utf-8'), trans_id)
1197
class DiskTreeTransform(TreeTransformBase):
1198
"""Tree transform storing its contents on disk."""
1200
def __init__(self, tree, limbodir, pb=None,
1201
case_sensitive=True):
1203
:param tree: The tree that will be transformed, but not necessarily
1205
:param limbodir: A directory where new files can be stored until
1206
they are installed in their proper places
1208
:param case_sensitive: If True, the target of the transform is
1209
case sensitive, not just case preserving.
1211
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1212
self._limbodir = limbodir
1213
self._deletiondir = None
1214
# A mapping of transform ids to their limbo filename
1215
self._limbo_files = {}
1216
self._possibly_stale_limbo_files = set()
1217
# A mapping of transform ids to a set of the transform ids of children
1218
# that their limbo directory has
1219
self._limbo_children = {}
1220
# Map transform ids to maps of child filename to child transform id
1221
self._limbo_children_names = {}
1222
# List of transform ids that need to be renamed from limbo into place
1223
self._needs_rename = set()
1224
self._creation_mtime = None
1227
"""Release the working tree lock, if held, clean up limbo dir.
1229
This is required if apply has not been invoked, but can be invoked
1232
if self._tree is None:
1235
limbo_paths = list(viewvalues(self._limbo_files))
1236
limbo_paths.extend(self._possibly_stale_limbo_files)
1237
limbo_paths.sort(reverse=True)
1238
for path in limbo_paths:
1241
except OSError as e:
1242
if e.errno != errno.ENOENT:
1244
# XXX: warn? perhaps we just got interrupted at an
1245
# inconvenient moment, but perhaps files are disappearing
1248
delete_any(self._limbodir)
1250
# We don't especially care *why* the dir is immortal.
1251
raise ImmortalLimbo(self._limbodir)
1253
if self._deletiondir is not None:
1254
delete_any(self._deletiondir)
1256
raise errors.ImmortalPendingDeletion(self._deletiondir)
1258
TreeTransformBase.finalize(self)
1260
def _limbo_supports_executable(self):
1261
"""Check if the limbo path supports the executable bit."""
1262
# FIXME: Check actual file system capabilities of limbodir
1263
return osutils.supports_executable()
1265
def _limbo_name(self, trans_id):
1266
"""Generate the limbo name of a file"""
1267
limbo_name = self._limbo_files.get(trans_id)
1268
if limbo_name is None:
1269
limbo_name = self._generate_limbo_path(trans_id)
1270
self._limbo_files[trans_id] = limbo_name
1273
def _generate_limbo_path(self, trans_id):
1274
"""Generate a limbo path using the trans_id as the relative path.
1276
This is suitable as a fallback, and when the transform should not be
1277
sensitive to the path encoding of the limbo directory.
1279
self._needs_rename.add(trans_id)
1280
return pathjoin(self._limbodir, trans_id)
1282
def adjust_path(self, name, parent, trans_id):
1283
previous_parent = self._new_parent.get(trans_id)
1284
previous_name = self._new_name.get(trans_id)
1285
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1286
if (trans_id in self._limbo_files
1287
and trans_id not in self._needs_rename):
1288
self._rename_in_limbo([trans_id])
1289
if previous_parent != parent:
1290
self._limbo_children[previous_parent].remove(trans_id)
1291
if previous_parent != parent or previous_name != name:
1292
del self._limbo_children_names[previous_parent][previous_name]
1294
def _rename_in_limbo(self, trans_ids):
1295
"""Fix limbo names so that the right final path is produced.
1297
This means we outsmarted ourselves-- we tried to avoid renaming
1298
these files later by creating them with their final names in their
1299
final parents. But now the previous name or parent is no longer
1300
suitable, so we have to rename them.
1302
Even for trans_ids that have no new contents, we must remove their
1303
entries from _limbo_files, because they are now stale.
1305
for trans_id in trans_ids:
1306
old_path = self._limbo_files[trans_id]
1307
self._possibly_stale_limbo_files.add(old_path)
1308
del self._limbo_files[trans_id]
1309
if trans_id not in self._new_contents:
1311
new_path = self._limbo_name(trans_id)
1312
os.rename(old_path, new_path)
1313
self._possibly_stale_limbo_files.remove(old_path)
1314
for descendant in self._limbo_descendants(trans_id):
1315
desc_path = self._limbo_files[descendant]
1316
desc_path = new_path + desc_path[len(old_path):]
1317
self._limbo_files[descendant] = desc_path
1319
def _limbo_descendants(self, trans_id):
1320
"""Return the set of trans_ids whose limbo paths descend from this."""
1321
descendants = set(self._limbo_children.get(trans_id, []))
1322
for descendant in list(descendants):
1323
descendants.update(self._limbo_descendants(descendant))
1326
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1327
"""Schedule creation of a new file.
1331
:param contents: an iterator of strings, all of which will be written
1332
to the target destination.
1333
:param trans_id: TreeTransform handle
1334
:param mode_id: If not None, force the mode of the target file to match
1335
the mode of the object referenced by mode_id.
1336
Otherwise, we will try to preserve mode bits of an existing file.
1337
:param sha1: If the sha1 of this content is already known, pass it in.
1338
We can use it to prevent future sha1 computations.
1340
name = self._limbo_name(trans_id)
1341
with open(name, 'wb') as f:
1342
unique_add(self._new_contents, trans_id, 'file')
1343
f.writelines(contents)
1344
self._set_mtime(name)
1345
self._set_mode(trans_id, mode_id, S_ISREG)
1346
# It is unfortunate we have to use lstat instead of fstat, but we just
1347
# used utime and chmod on the file, so we need the accurate final
1349
if sha1 is not None:
1350
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1352
def _read_symlink_target(self, trans_id):
1353
return os.readlink(self._limbo_name(trans_id))
1355
def _set_mtime(self, path):
1356
"""All files that are created get the same mtime.
1358
This time is set by the first object to be created.
1360
if self._creation_mtime is None:
1361
self._creation_mtime = time.time()
1362
os.utime(path, (self._creation_mtime, self._creation_mtime))
1364
def create_hardlink(self, path, trans_id):
1365
"""Schedule creation of a hard link"""
1366
name = self._limbo_name(trans_id)
1369
except OSError as e:
1370
if e.errno != errno.EPERM:
1372
raise errors.HardLinkNotSupported(path)
1374
unique_add(self._new_contents, trans_id, 'file')
1375
except BaseException:
1376
# Clean up the file, it never got registered so
1377
# TreeTransform.finalize() won't clean it up.
1381
def create_directory(self, trans_id):
1382
"""Schedule creation of a new directory.
1384
See also new_directory.
1386
os.mkdir(self._limbo_name(trans_id))
1387
unique_add(self._new_contents, trans_id, 'directory')
1389
def create_symlink(self, target, trans_id):
1390
"""Schedule creation of a new symbolic link.
1392
target is a bytestring.
1393
See also new_symlink.
1396
os.symlink(target, self._limbo_name(trans_id))
1397
unique_add(self._new_contents, trans_id, 'symlink')
1400
path = FinalPaths(self).get_path(trans_id)
1403
raise UnableCreateSymlink(path=path)
1405
def cancel_creation(self, trans_id):
1406
"""Cancel the creation of new file contents."""
1407
del self._new_contents[trans_id]
1408
if trans_id in self._observed_sha1s:
1409
del self._observed_sha1s[trans_id]
1410
children = self._limbo_children.get(trans_id)
1411
# if this is a limbo directory with children, move them before removing
1413
if children is not None:
1414
self._rename_in_limbo(children)
1415
del self._limbo_children[trans_id]
1416
del self._limbo_children_names[trans_id]
1417
delete_any(self._limbo_name(trans_id))
1419
def new_orphan(self, trans_id, parent_id):
1420
conf = self._tree.get_config_stack()
1421
handle_orphan = conf.get('transform.orphan_policy')
1422
handle_orphan(self, trans_id, parent_id)
1425
class OrphaningError(errors.BzrError):
1427
# Only bugs could lead to such exception being seen by the user
1428
internal_error = True
1429
_fmt = "Error while orphaning %s in %s directory"
1431
def __init__(self, orphan, parent):
1432
errors.BzrError.__init__(self)
1433
self.orphan = orphan
1434
self.parent = parent
1437
class OrphaningForbidden(OrphaningError):
1439
_fmt = "Policy: %s doesn't allow creating orphans."
1441
def __init__(self, policy):
1442
errors.BzrError.__init__(self)
1443
self.policy = policy
1446
def move_orphan(tt, orphan_id, parent_id):
1447
"""See TreeTransformBase.new_orphan.
1449
This creates a new orphan in the `brz-orphans` dir at the root of the
1452
:param tt: The TreeTransform orphaning `trans_id`.
1454
:param orphan_id: The trans id that should be orphaned.
1456
:param parent_id: The orphan parent trans id.
1458
# Add the orphan dir if it doesn't exist
1459
orphan_dir_basename = 'brz-orphans'
1460
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1461
if tt.final_kind(od_id) is None:
1462
tt.create_directory(od_id)
1463
parent_path = tt._tree_id_paths[parent_id]
1464
# Find a name that doesn't exist yet in the orphan dir
1465
actual_name = tt.final_name(orphan_id)
1466
new_name = tt._available_backup_name(actual_name, od_id)
1467
tt.adjust_path(new_name, od_id, orphan_id)
1468
trace.warning('%s has been orphaned in %s'
1469
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1472
def refuse_orphan(tt, orphan_id, parent_id):
1473
"""See TreeTransformBase.new_orphan.
1475
This refuses to create orphan, letting the caller handle the conflict.
1477
raise OrphaningForbidden('never')
1480
orphaning_registry = registry.Registry()
1481
orphaning_registry.register(
1482
u'conflict', refuse_orphan,
1483
'Leave orphans in place and create a conflict on the directory.')
1484
orphaning_registry.register(
1485
u'move', move_orphan,
1486
'Move orphans into the brz-orphans directory.')
1487
orphaning_registry._set_default_key(u'conflict')
1490
opt_transform_orphan = _mod_config.RegistryOption(
1491
'transform.orphan_policy', orphaning_registry,
1492
help='Policy for orphaned files during transform operations.',
1496
class TreeTransform(DiskTreeTransform):
1497
"""Represent a tree transformation.
1499
This object is designed to support incremental generation of the transform,
1502
However, it gives optimum performance when parent directories are created
1503
before their contents. The transform is then able to put child files
1504
directly in their parent directory, avoiding later renames.
1506
It is easy to produce malformed transforms, but they are generally
1507
harmless. Attempting to apply a malformed transform will cause an
1508
exception to be raised before any modifications are made to the tree.
1510
Many kinds of malformed transforms can be corrected with the
1511
resolve_conflicts function. The remaining ones indicate programming error,
1512
such as trying to create a file with no path.
1514
Two sets of file creation methods are supplied. Convenience methods are:
1519
These are composed of the low-level methods:
1521
* create_file or create_directory or create_symlink
1525
Transform/Transaction ids
1526
-------------------------
1527
trans_ids are temporary ids assigned to all files involved in a transform.
1528
It's possible, even common, that not all files in the Tree have trans_ids.
1530
trans_ids are used because filenames and file_ids are not good enough
1531
identifiers; filenames change, and not all files have file_ids. File-ids
1532
are also associated with trans-ids, so that moving a file moves its
1535
trans_ids are only valid for the TreeTransform that generated them.
1539
Limbo is a temporary directory use to hold new versions of files.
1540
Files are added to limbo by create_file, create_directory, create_symlink,
1541
and their convenience variants (new_*). Files may be removed from limbo
1542
using cancel_creation. Files are renamed from limbo into their final
1543
location as part of TreeTransform.apply
1545
Limbo must be cleaned up, by either calling TreeTransform.apply or
1546
calling TreeTransform.finalize.
1548
Files are placed into limbo inside their parent directories, where
1549
possible. This reduces subsequent renames, and makes operations involving
1550
lots of files faster. This optimization is only possible if the parent
1551
directory is created *before* creating any of its children, so avoid
1552
creating children before parents, where possible.
1556
This temporary directory is used by _FileMover for storing files that are
1557
about to be deleted. In case of rollback, the files will be restored.
1558
FileMover does not delete files until it is sure that a rollback will not
1562
def __init__(self, tree, pb=None):
1563
"""Note: a tree_write lock is taken on the tree.
1565
Use TreeTransform.finalize() to release the lock (can be omitted if
1566
TreeTransform.apply() called).
1568
tree.lock_tree_write()
1570
limbodir = urlutils.local_path_from_url(
1571
tree._transport.abspath('limbo'))
1572
osutils.ensure_empty_directory_exists(
1574
errors.ExistingLimbo)
1575
deletiondir = urlutils.local_path_from_url(
1576
tree._transport.abspath('pending-deletion'))
1577
osutils.ensure_empty_directory_exists(
1579
errors.ExistingPendingDeletion)
1580
except BaseException:
1584
# Cache of realpath results, to speed up canonical_path
1585
self._realpaths = {}
1586
# Cache of relpath results, to speed up canonical_path
1588
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1589
tree.case_sensitive)
1590
self._deletiondir = deletiondir
1592
def canonical_path(self, path):
1593
"""Get the canonical tree-relative path"""
1594
# don't follow final symlinks
1595
abs = self._tree.abspath(path)
1596
if abs in self._relpaths:
1597
return self._relpaths[abs]
1598
dirname, basename = os.path.split(abs)
1599
if dirname not in self._realpaths:
1600
self._realpaths[dirname] = os.path.realpath(dirname)
1601
dirname = self._realpaths[dirname]
1602
abs = pathjoin(dirname, basename)
1603
if dirname in self._relpaths:
1604
relpath = pathjoin(self._relpaths[dirname], basename)
1605
relpath = relpath.rstrip('/\\')
1607
relpath = self._tree.relpath(abs)
1608
self._relpaths[abs] = relpath
1611
def tree_kind(self, trans_id):
1612
"""Determine the file kind in the working tree.
1614
:returns: The file kind or None if the file does not exist
1616
path = self._tree_id_paths.get(trans_id)
1620
return file_kind(self._tree.abspath(path))
1621
except errors.NoSuchFile:
1624
def _set_mode(self, trans_id, mode_id, typefunc):
1625
"""Set the mode of new file contents.
1626
The mode_id is the existing file to get the mode from (often the same
1627
as trans_id). The operation is only performed if there's a mode match
1628
according to typefunc.
1633
old_path = self._tree_id_paths[mode_id]
1637
mode = os.stat(self._tree.abspath(old_path)).st_mode
1638
except OSError as e:
1639
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1640
# Either old_path doesn't exist, or the parent of the
1641
# target is not a directory (but will be one eventually)
1642
# Either way, we know it doesn't exist *right now*
1643
# See also bug #248448
1648
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1650
def iter_tree_children(self, parent_id):
1651
"""Iterate through the entry's tree children, if any"""
1653
path = self._tree_id_paths[parent_id]
1657
children = os.listdir(self._tree.abspath(path))
1658
except OSError as e:
1659
if not (osutils._is_error_enotdir(e) or
1660
e.errno in (errno.ENOENT, errno.ESRCH)):
1664
for child in children:
1665
childpath = joinpath(path, child)
1666
if self._tree.is_control_filename(childpath):
1668
yield self.trans_id_tree_path(childpath)
1670
def _generate_limbo_path(self, trans_id):
1671
"""Generate a limbo path using the final path if possible.
1673
This optimizes the performance of applying the tree transform by
1674
avoiding renames. These renames can be avoided only when the parent
1675
directory is already scheduled for creation.
1677
If the final path cannot be used, falls back to using the trans_id as
1680
parent = self._new_parent.get(trans_id)
1681
# if the parent directory is already in limbo (e.g. when building a
1682
# tree), choose a limbo name inside the parent, to reduce further
1684
use_direct_path = False
1685
if self._new_contents.get(parent) == 'directory':
1686
filename = self._new_name.get(trans_id)
1687
if filename is not None:
1688
if parent not in self._limbo_children:
1689
self._limbo_children[parent] = set()
1690
self._limbo_children_names[parent] = {}
1691
use_direct_path = True
1692
# the direct path can only be used if no other file has
1693
# already taken this pathname, i.e. if the name is unused, or
1694
# if it is already associated with this trans_id.
1695
elif self._case_sensitive_target:
1696
if (self._limbo_children_names[parent].get(filename)
1697
in (trans_id, None)):
1698
use_direct_path = True
1700
for l_filename, l_trans_id in viewitems(
1701
self._limbo_children_names[parent]):
1702
if l_trans_id == trans_id:
1704
if l_filename.lower() == filename.lower():
1707
use_direct_path = True
1709
if not use_direct_path:
1710
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1712
limbo_name = pathjoin(self._limbo_files[parent], filename)
1713
self._limbo_children[parent].add(trans_id)
1714
self._limbo_children_names[parent][filename] = trans_id
1717
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1718
"""Apply all changes to the inventory and filesystem.
1720
If filesystem or inventory conflicts are present, MalformedTransform
1723
If apply succeeds, finalize is not necessary.
1725
:param no_conflicts: if True, the caller guarantees there are no
1726
conflicts, so no check is made.
1727
:param precomputed_delta: An inventory delta to use instead of
1729
:param _mover: Supply an alternate FileMover, for testing
1731
for hook in MutableTree.hooks['pre_transform']:
1732
hook(self._tree, self)
1733
if not no_conflicts:
1734
self._check_malformed()
1735
with ui.ui_factory.nested_progress_bar() as child_pb:
1736
if precomputed_delta is None:
1737
child_pb.update(gettext('Apply phase'), 0, 2)
1738
inventory_delta = self._generate_inventory_delta()
1741
inventory_delta = precomputed_delta
1744
mover = _FileMover()
1748
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1749
self._apply_removals(mover)
1750
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1751
modified_paths = self._apply_insertions(mover)
1752
except BaseException:
1756
mover.apply_deletions()
1757
if self.final_file_id(self.root) is None:
1758
inventory_delta = [e for e in inventory_delta if e[0] != '']
1759
self._tree.apply_inventory_delta(inventory_delta)
1760
self._apply_observed_sha1s()
1763
return _TransformResults(modified_paths, self.rename_count)
1765
def _generate_inventory_delta(self):
1766
"""Generate an inventory delta for the current transform."""
1767
inventory_delta = []
1768
new_paths = self._inventory_altered()
1769
total_entries = len(new_paths) + len(self._removed_id)
1770
with ui.ui_factory.nested_progress_bar() as child_pb:
1771
for num, trans_id in enumerate(self._removed_id):
1773
child_pb.update(gettext('removing file'),
1775
if trans_id == self._new_root:
1776
file_id = self._tree.get_root_id()
1778
file_id = self.tree_file_id(trans_id)
1779
# File-id isn't really being deleted, just moved
1780
if file_id in self._r_new_id:
1782
path = self._tree_id_paths[trans_id]
1783
inventory_delta.append((path, None, file_id, None))
1784
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1786
for num, (path, trans_id) in enumerate(new_paths):
1788
child_pb.update(gettext('adding file'),
1789
num + len(self._removed_id), total_entries)
1790
file_id = new_path_file_ids[trans_id]
1793
kind = self.final_kind(trans_id)
1795
kind = self._tree.stored_kind(
1796
self._tree.id2path(file_id), file_id)
1797
parent_trans_id = self.final_parent(trans_id)
1798
parent_file_id = new_path_file_ids.get(parent_trans_id)
1799
if parent_file_id is None:
1800
parent_file_id = self.final_file_id(parent_trans_id)
1801
if trans_id in self._new_reference_revision:
1802
new_entry = inventory.TreeReference(
1804
self._new_name[trans_id],
1805
self.final_file_id(self._new_parent[trans_id]),
1806
None, self._new_reference_revision[trans_id])
1808
new_entry = inventory.make_entry(kind,
1809
self.final_name(trans_id),
1810
parent_file_id, file_id)
1812
old_path = self._tree.id2path(new_entry.file_id)
1813
except errors.NoSuchId:
1815
new_executability = self._new_executability.get(trans_id)
1816
if new_executability is not None:
1817
new_entry.executable = new_executability
1818
inventory_delta.append(
1819
(old_path, path, new_entry.file_id, new_entry))
1820
return inventory_delta
1822
def _apply_removals(self, mover):
1823
"""Perform tree operations that remove directory/inventory names.
1825
That is, delete files that are to be deleted, and put any files that
1826
need renaming into limbo. This must be done in strict child-to-parent
1829
If inventory_delta is None, no inventory delta generation is performed.
1831
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1832
with ui.ui_factory.nested_progress_bar() as child_pb:
1833
for num, (path, trans_id) in enumerate(tree_paths):
1834
# do not attempt to move root into a subdirectory of itself.
1837
child_pb.update(gettext('removing file'), num, len(tree_paths))
1838
full_path = self._tree.abspath(path)
1839
if trans_id in self._removed_contents:
1840
delete_path = os.path.join(self._deletiondir, trans_id)
1841
mover.pre_delete(full_path, delete_path)
1842
elif (trans_id in self._new_name or
1843
trans_id in self._new_parent):
1845
mover.rename(full_path, self._limbo_name(trans_id))
1846
except errors.TransformRenameFailed as e:
1847
if e.errno != errno.ENOENT:
1850
self.rename_count += 1
1852
def _apply_insertions(self, mover):
1853
"""Perform tree operations that insert directory/inventory names.
1855
That is, create any files that need to be created, and restore from
1856
limbo any files that needed renaming. This must be done in strict
1857
parent-to-child order.
1859
If inventory_delta is None, no inventory delta is calculated, and
1860
no list of modified paths is returned.
1862
new_paths = self.new_paths(filesystem_only=True)
1864
with ui.ui_factory.nested_progress_bar() as child_pb:
1865
for num, (path, trans_id) in enumerate(new_paths):
1867
child_pb.update(gettext('adding file'),
1868
num, len(new_paths))
1869
full_path = self._tree.abspath(path)
1870
if trans_id in self._needs_rename:
1872
mover.rename(self._limbo_name(trans_id), full_path)
1873
except errors.TransformRenameFailed as e:
1874
# We may be renaming a dangling inventory id
1875
if e.errno != errno.ENOENT:
1878
self.rename_count += 1
1879
# TODO: if trans_id in self._observed_sha1s, we should
1880
# re-stat the final target, since ctime will be
1881
# updated by the change.
1882
if (trans_id in self._new_contents
1883
or self.path_changed(trans_id)):
1884
if trans_id in self._new_contents:
1885
modified_paths.append(full_path)
1886
if trans_id in self._new_executability:
1887
self._set_executability(path, trans_id)
1888
if trans_id in self._observed_sha1s:
1889
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1890
st = osutils.lstat(full_path)
1891
self._observed_sha1s[trans_id] = (o_sha1, st)
1892
for path, trans_id in new_paths:
1893
# new_paths includes stuff like workingtree conflicts. Only the
1894
# stuff in new_contents actually comes from limbo.
1895
if trans_id in self._limbo_files:
1896
del self._limbo_files[trans_id]
1897
self._new_contents.clear()
1898
return modified_paths
1900
def _apply_observed_sha1s(self):
1901
"""After we have finished renaming everything, update observed sha1s
1903
This has to be done after self._tree.apply_inventory_delta, otherwise
1904
it doesn't know anything about the files we are updating. Also, we want
1905
to do this as late as possible, so that most entries end up cached.
1907
# TODO: this doesn't update the stat information for directories. So
1908
# the first 'bzr status' will still need to rewrite
1909
# .bzr/checkout/dirstate. However, we at least don't need to
1910
# re-read all of the files.
1911
# TODO: If the operation took a while, we could do a time.sleep(3) here
1912
# to allow the clock to tick over and ensure we won't have any
1913
# problems. (we could observe start time, and finish time, and if
1914
# it is less than eg 10% overhead, add a sleep call.)
1915
paths = FinalPaths(self)
1916
for trans_id, observed in viewitems(self._observed_sha1s):
1917
path = paths.get_path(trans_id)
1918
self._tree._observed_sha1(path, observed)
1921
class TransformPreview(DiskTreeTransform):
1922
"""A TreeTransform for generating preview trees.
1924
Unlike TreeTransform, this version works when the input tree is a
1925
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1926
unversioned files in the input tree.
1929
def __init__(self, tree, pb=None, case_sensitive=True):
1931
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1932
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1934
def canonical_path(self, path):
1937
def tree_kind(self, trans_id):
1938
path = self._tree_id_paths.get(trans_id)
1941
kind = self._tree.path_content_summary(path)[0]
1942
if kind == 'missing':
1946
def _set_mode(self, trans_id, mode_id, typefunc):
1947
"""Set the mode of new file contents.
1948
The mode_id is the existing file to get the mode from (often the same
1949
as trans_id). The operation is only performed if there's a mode match
1950
according to typefunc.
1952
# is it ok to ignore this? probably
1955
def iter_tree_children(self, parent_id):
1956
"""Iterate through the entry's tree children, if any"""
1958
path = self._tree_id_paths[parent_id]
1962
entry = next(self._tree.iter_entries_by_dir(
1963
specific_files=[path]))[1]
1964
except StopIteration:
1966
children = getattr(entry, 'children', {})
1967
for child in children:
1968
childpath = joinpath(path, child)
1969
yield self.trans_id_tree_path(childpath)
1971
def new_orphan(self, trans_id, parent_id):
1972
raise NotImplementedError(self.new_orphan)
1975
class _PreviewTree(inventorytree.InventoryTree):
1976
"""Partial implementation of Tree to support show_diff_trees"""
1978
def __init__(self, transform):
1979
self._transform = transform
1980
self._final_paths = FinalPaths(transform)
1981
self.__by_parent = None
1982
self._parent_ids = []
1983
self._all_children_cache = {}
1984
self._path2trans_id_cache = {}
1985
self._final_name_cache = {}
1986
self._iter_changes_cache = dict((c[0], c) for c in
1987
self._transform.iter_changes())
1989
def _content_change(self, file_id):
1990
"""Return True if the content of this file changed"""
1991
changes = self._iter_changes_cache.get(file_id)
1992
# changes[2] is true if the file content changed. See
1993
# InterTree.iter_changes.
1994
return (changes is not None and changes[2])
1996
def _get_repository(self):
1997
repo = getattr(self._transform._tree, '_repository', None)
1999
repo = self._transform._tree.branch.repository
2002
def _iter_parent_trees(self):
2003
for revision_id in self.get_parent_ids():
2005
yield self.revision_tree(revision_id)
2006
except errors.NoSuchRevisionInTree:
2007
yield self._get_repository().revision_tree(revision_id)
2009
def _get_file_revision(self, path, file_id, vf, tree_revision):
2011
(file_id, t.get_file_revision(t.id2path(file_id)))
2012
for t in self._iter_parent_trees()]
2013
vf.add_lines((file_id, tree_revision), parent_keys,
2014
self.get_file_lines(path))
2015
repo = self._get_repository()
2016
base_vf = repo.texts
2017
if base_vf not in vf.fallback_versionedfiles:
2018
vf.fallback_versionedfiles.append(base_vf)
2019
return tree_revision
2021
def _stat_limbo_file(self, trans_id):
2022
name = self._transform._limbo_name(trans_id)
2023
return os.lstat(name)
2026
def _by_parent(self):
2027
if self.__by_parent is None:
2028
self.__by_parent = self._transform.by_parent()
2029
return self.__by_parent
2031
def _comparison_data(self, entry, path):
2032
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2033
if kind == 'missing':
2037
file_id = self._transform.final_file_id(self._path2trans_id(path))
2038
executable = self.is_executable(path)
2039
return kind, executable, None
2041
def is_locked(self):
2044
def lock_read(self):
2045
# Perhaps in theory, this should lock the TreeTransform?
2046
return lock.LogicalLockResult(self.unlock)
2052
def root_inventory(self):
2053
"""This Tree does not use inventory as its backing data."""
2054
raise NotImplementedError(_PreviewTree.root_inventory)
2056
def get_root_id(self):
2057
return self._transform.final_file_id(self._transform.root)
2059
def all_file_ids(self):
2060
tree_ids = set(self._transform._tree.all_file_ids())
2061
tree_ids.difference_update(self._transform.tree_file_id(t)
2062
for t in self._transform._removed_id)
2063
tree_ids.update(viewvalues(self._transform._new_id))
2066
def all_versioned_paths(self):
2067
return {self.id2path(fid) for fid in self.all_file_ids()}
2069
def _has_id(self, file_id, fallback_check):
2070
if file_id in self._transform._r_new_id:
2072
elif file_id in {self._transform.tree_file_id(trans_id) for
2073
trans_id in self._transform._removed_id}:
2076
return fallback_check(file_id)
2078
def has_id(self, file_id):
2079
return self._has_id(file_id, self._transform._tree.has_id)
2081
def has_or_had_id(self, file_id):
2082
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2084
def _path2trans_id(self, path):
2085
# We must not use None here, because that is a valid value to store.
2086
trans_id = self._path2trans_id_cache.get(path, object)
2087
if trans_id is not object:
2089
segments = splitpath(path)
2090
cur_parent = self._transform.root
2091
for cur_segment in segments:
2092
for child in self._all_children(cur_parent):
2093
final_name = self._final_name_cache.get(child)
2094
if final_name is None:
2095
final_name = self._transform.final_name(child)
2096
self._final_name_cache[child] = final_name
2097
if final_name == cur_segment:
2101
self._path2trans_id_cache[path] = None
2103
self._path2trans_id_cache[path] = cur_parent
2106
def path2id(self, path):
2107
if isinstance(path, list):
2110
path = osutils.pathjoin(*path)
2111
return self._transform.final_file_id(self._path2trans_id(path))
2113
def id2path(self, file_id):
2114
trans_id = self._transform.trans_id_file_id(file_id)
2116
return self._final_paths._determine_path(trans_id)
2118
raise errors.NoSuchId(self, file_id)
2120
def _all_children(self, trans_id):
2121
children = self._all_children_cache.get(trans_id)
2122
if children is not None:
2124
children = set(self._transform.iter_tree_children(trans_id))
2125
# children in the _new_parent set are provided by _by_parent.
2126
children.difference_update(self._transform._new_parent)
2127
children.update(self._by_parent.get(trans_id, []))
2128
self._all_children_cache[trans_id] = children
2131
def _iter_children(self, file_id):
2132
trans_id = self._transform.trans_id_file_id(file_id)
2133
for child_trans_id in self._all_children(trans_id):
2134
yield self._transform.final_file_id(child_trans_id)
2137
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2138
in self._transform._tree.extras())
2139
possible_extras.update(self._transform._new_contents)
2140
possible_extras.update(self._transform._removed_id)
2141
for trans_id in possible_extras:
2142
if self._transform.final_file_id(trans_id) is None:
2143
yield self._final_paths._determine_path(trans_id)
2145
def _make_inv_entries(self, ordered_entries, specific_files=None):
2146
for trans_id, parent_file_id in ordered_entries:
2147
file_id = self._transform.final_file_id(trans_id)
2150
if (specific_files is not None
2151
and self._final_paths.get_path(trans_id) not in specific_files):
2153
kind = self._transform.final_kind(trans_id)
2155
kind = self._transform._tree.stored_kind(
2156
self._transform._tree.id2path(file_id),
2158
new_entry = inventory.make_entry(
2160
self._transform.final_name(trans_id),
2161
parent_file_id, file_id)
2162
yield new_entry, trans_id
2164
def _list_files_by_dir(self):
2165
todo = [ROOT_PARENT]
2167
while len(todo) > 0:
2169
parent_file_id = self._transform.final_file_id(parent)
2170
children = list(self._all_children(parent))
2171
paths = dict(zip(children, self._final_paths.get_paths(children)))
2172
children.sort(key=paths.get)
2173
todo.extend(reversed(children))
2174
for trans_id in children:
2175
ordered_ids.append((trans_id, parent_file_id))
2178
def iter_child_entries(self, path):
2179
trans_id = self._path2trans_id(path)
2180
if trans_id is None:
2181
raise errors.NoSuchFile(path)
2182
todo = [(child_trans_id, trans_id) for child_trans_id in
2183
self._all_children(trans_id)]
2184
for entry, trans_id in self._make_inv_entries(todo):
2187
def iter_entries_by_dir(self, specific_files=None):
2188
# This may not be a maximally efficient implementation, but it is
2189
# reasonably straightforward. An implementation that grafts the
2190
# TreeTransform changes onto the tree's iter_entries_by_dir results
2191
# might be more efficient, but requires tricky inferences about stack
2193
ordered_ids = self._list_files_by_dir()
2194
for entry, trans_id in self._make_inv_entries(ordered_ids,
2196
yield self._final_paths.get_path(trans_id), entry
2198
def _iter_entries_for_dir(self, dir_path):
2199
"""Return path, entry for items in a directory without recursing down."""
2201
dir_trans_id = self._path2trans_id(dir_path)
2202
dir_id = self._transform.final_file_id(dir_trans_id)
2203
for child_trans_id in self._all_children(dir_trans_id):
2204
ordered_ids.append((child_trans_id, dir_id))
2206
for entry, trans_id in self._make_inv_entries(ordered_ids):
2207
path_entries.append((self._final_paths.get_path(trans_id), entry))
2211
def list_files(self, include_root=False, from_dir=None, recursive=True):
2212
"""See WorkingTree.list_files."""
2213
# XXX This should behave like WorkingTree.list_files, but is really
2214
# more like RevisionTree.list_files.
2218
prefix = from_dir + '/'
2219
entries = self.iter_entries_by_dir()
2220
for path, entry in entries:
2221
if entry.name == '' and not include_root:
2224
if not path.startswith(prefix):
2226
path = path[len(prefix):]
2227
yield path, 'V', entry.kind, entry.file_id, entry
2229
if from_dir is None and include_root is True:
2230
root_entry = inventory.make_entry('directory', '',
2231
ROOT_PARENT, self.get_root_id())
2232
yield '', 'V', 'directory', root_entry.file_id, root_entry
2233
entries = self._iter_entries_for_dir(from_dir or '')
2234
for path, entry in entries:
2235
yield path, 'V', entry.kind, entry.file_id, entry
2237
def kind(self, path):
2238
trans_id = self._path2trans_id(path)
2239
if trans_id is None:
2240
raise errors.NoSuchFile(path)
2241
return self._transform.final_kind(trans_id)
2243
def stored_kind(self, path):
2244
trans_id = self._path2trans_id(path)
2245
if trans_id is None:
2246
raise errors.NoSuchFile(path)
2248
return self._transform._new_contents[trans_id]
2250
return self._transform._tree.stored_kind(path)
2252
def get_file_mtime(self, path):
2253
"""See Tree.get_file_mtime"""
2254
file_id = self.path2id(path)
2256
raise errors.NoSuchFile(path)
2257
if not self._content_change(file_id):
2258
return self._transform._tree.get_file_mtime(
2259
self._transform._tree.id2path(file_id))
2260
trans_id = self._path2trans_id(path)
2261
return self._stat_limbo_file(trans_id).st_mtime
2263
def get_file_size(self, path):
2264
"""See Tree.get_file_size"""
2265
trans_id = self._path2trans_id(path)
2266
if trans_id is None:
2267
raise errors.NoSuchFile(path)
2268
kind = self._transform.final_kind(trans_id)
2271
if trans_id in self._transform._new_contents:
2272
return self._stat_limbo_file(trans_id).st_size
2273
if self.kind(path) == 'file':
2274
return self._transform._tree.get_file_size(path)
2278
def get_file_verifier(self, path, stat_value=None):
2279
trans_id = self._path2trans_id(path)
2280
if trans_id is None:
2281
raise errors.NoSuchFile(path)
2282
kind = self._transform._new_contents.get(trans_id)
2284
return self._transform._tree.get_file_verifier(path)
2286
with self.get_file(path) as fileobj:
2287
return ("SHA1", sha_file(fileobj))
2289
def get_file_sha1(self, path, stat_value=None):
2290
trans_id = self._path2trans_id(path)
2291
if trans_id is None:
2292
raise errors.NoSuchFile(path)
2293
kind = self._transform._new_contents.get(trans_id)
2295
return self._transform._tree.get_file_sha1(path)
2297
with self.get_file(path) as fileobj:
2298
return sha_file(fileobj)
2300
def is_executable(self, path):
2301
trans_id = self._path2trans_id(path)
2302
if trans_id is None:
2305
return self._transform._new_executability[trans_id]
2308
return self._transform._tree.is_executable(path)
2309
except OSError as e:
2310
if e.errno == errno.ENOENT:
2313
except errors.NoSuchFile:
2316
def has_filename(self, path):
2317
trans_id = self._path2trans_id(path)
2318
if trans_id in self._transform._new_contents:
2320
elif trans_id in self._transform._removed_contents:
2323
return self._transform._tree.has_filename(path)
2325
def path_content_summary(self, path):
2326
trans_id = self._path2trans_id(path)
2327
tt = self._transform
2328
tree_path = tt._tree_id_paths.get(trans_id)
2329
kind = tt._new_contents.get(trans_id)
2331
if tree_path is None or trans_id in tt._removed_contents:
2332
return 'missing', None, None, None
2333
summary = tt._tree.path_content_summary(tree_path)
2334
kind, size, executable, link_or_sha1 = summary
2337
limbo_name = tt._limbo_name(trans_id)
2338
if trans_id in tt._new_reference_revision:
2339
kind = 'tree-reference'
2341
statval = os.lstat(limbo_name)
2342
size = statval.st_size
2343
if not tt._limbo_supports_executable():
2346
executable = statval.st_mode & S_IEXEC
2350
if kind == 'symlink':
2351
link_or_sha1 = os.readlink(limbo_name)
2352
if not isinstance(link_or_sha1, text_type):
2353
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2354
executable = tt._new_executability.get(trans_id, executable)
2355
return kind, size, executable, link_or_sha1
2357
def iter_changes(self, from_tree, include_unchanged=False,
2358
specific_files=None, pb=None, extra_trees=None,
2359
require_versioned=True, want_unversioned=False):
2360
"""See InterTree.iter_changes.
2362
This has a fast path that is only used when the from_tree matches
2363
the transform tree, and no fancy options are supplied.
2365
if (from_tree is not self._transform._tree or include_unchanged
2366
or specific_files or want_unversioned):
2367
return tree.InterTree(from_tree, self).iter_changes(
2368
include_unchanged=include_unchanged,
2369
specific_files=specific_files,
2371
extra_trees=extra_trees,
2372
require_versioned=require_versioned,
2373
want_unversioned=want_unversioned)
2374
if want_unversioned:
2375
raise ValueError('want_unversioned is not supported')
2376
return self._transform.iter_changes()
2378
def get_file(self, path):
2379
"""See Tree.get_file"""
2380
file_id = self.path2id(path)
2381
if not self._content_change(file_id):
2382
return self._transform._tree.get_file(path)
2383
trans_id = self._path2trans_id(path)
2384
name = self._transform._limbo_name(trans_id)
2385
return open(name, 'rb')
2387
def get_file_with_stat(self, path):
2388
return self.get_file(path), None
2390
def annotate_iter(self, path,
2391
default_revision=_mod_revision.CURRENT_REVISION):
2392
file_id = self.path2id(path)
2393
changes = self._iter_changes_cache.get(file_id)
2397
changed_content, versioned, kind = (changes[2], changes[3],
2401
get_old = (kind[0] == 'file' and versioned[0])
2403
old_annotation = self._transform._tree.annotate_iter(
2404
path, default_revision=default_revision)
2408
return old_annotation
2409
if not changed_content:
2410
return old_annotation
2411
# TODO: This is doing something similar to what WT.annotate_iter is
2412
# doing, however it fails slightly because it doesn't know what
2413
# the *other* revision_id is, so it doesn't know how to give the
2414
# other as the origin for some lines, they all get
2415
# 'default_revision'
2416
# It would be nice to be able to use the new Annotator based
2417
# approach, as well.
2418
return annotate.reannotate([old_annotation],
2419
self.get_file(path).readlines(),
2422
def get_symlink_target(self, path):
2423
"""See Tree.get_symlink_target"""
2424
file_id = self.path2id(path)
2425
if not self._content_change(file_id):
2426
return self._transform._tree.get_symlink_target(path)
2427
trans_id = self._path2trans_id(path)
2428
name = self._transform._limbo_name(trans_id)
2429
return osutils.readlink(name)
2431
def walkdirs(self, prefix=''):
2432
pending = [self._transform.root]
2433
while len(pending) > 0:
2434
parent_id = pending.pop()
2437
prefix = prefix.rstrip('/')
2438
parent_path = self._final_paths.get_path(parent_id)
2439
parent_file_id = self._transform.final_file_id(parent_id)
2440
for child_id in self._all_children(parent_id):
2441
path_from_root = self._final_paths.get_path(child_id)
2442
basename = self._transform.final_name(child_id)
2443
file_id = self._transform.final_file_id(child_id)
2444
kind = self._transform.final_kind(child_id)
2445
if kind is not None:
2446
versioned_kind = kind
2449
versioned_kind = self._transform._tree.stored_kind(
2450
self._transform._tree.id2path(file_id))
2451
if versioned_kind == 'directory':
2452
subdirs.append(child_id)
2453
children.append((path_from_root, basename, kind, None,
2454
file_id, versioned_kind))
2456
if parent_path.startswith(prefix):
2457
yield (parent_path, parent_file_id), children
2458
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2461
def get_parent_ids(self):
2462
return self._parent_ids
2464
def set_parent_ids(self, parent_ids):
2465
self._parent_ids = parent_ids
2467
def get_revision_tree(self, revision_id):
2468
return self._transform._tree.get_revision_tree(revision_id)
2471
def joinpath(parent, child):
2472
"""Join tree-relative paths, handling the tree root specially"""
2473
if parent is None or parent == "":
2476
return pathjoin(parent, child)
2479
class FinalPaths(object):
2480
"""Make path calculation cheap by memoizing paths.
2482
The underlying tree must not be manipulated between calls, or else
2483
the results will likely be incorrect.
2486
def __init__(self, transform):
2487
object.__init__(self)
2488
self._known_paths = {}
2489
self.transform = transform
2491
def _determine_path(self, trans_id):
2492
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2494
name = self.transform.final_name(trans_id)
2495
parent_id = self.transform.final_parent(trans_id)
2496
if parent_id == self.transform.root:
2499
return pathjoin(self.get_path(parent_id), name)
2501
def get_path(self, trans_id):
2502
"""Find the final path associated with a trans_id"""
2503
if trans_id not in self._known_paths:
2504
self._known_paths[trans_id] = self._determine_path(trans_id)
2505
return self._known_paths[trans_id]
2507
def get_paths(self, trans_ids):
2508
return [(self.get_path(t), t) for t in trans_ids]
2511
def topology_sorted_ids(tree):
2512
"""Determine the topological order of the ids in a tree"""
2513
file_ids = list(tree)
2514
file_ids.sort(key=tree.id2path)
2518
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2519
delta_from_tree=False):
2520
"""Create working tree for a branch, using a TreeTransform.
2522
This function should be used on empty trees, having a tree root at most.
2523
(see merge and revert functionality for working with existing trees)
2525
Existing files are handled like so:
2527
- Existing bzrdirs take precedence over creating new items. They are
2528
created as '%s.diverted' % name.
2529
- Otherwise, if the content on disk matches the content we are building,
2530
it is silently replaced.
2531
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2533
:param tree: The tree to convert wt into a copy of
2534
:param wt: The working tree that files will be placed into
2535
:param accelerator_tree: A tree which can be used for retrieving file
2536
contents more quickly than tree itself, i.e. a workingtree. tree
2537
will be used for cases where accelerator_tree's content is different.
2538
:param hardlink: If true, hard-link files to accelerator_tree, where
2539
possible. accelerator_tree must implement abspath, i.e. be a
2541
:param delta_from_tree: If true, build_tree may use the input Tree to
2542
generate the inventory delta.
2544
with wt.lock_tree_write(), tree.lock_read():
2545
if accelerator_tree is not None:
2546
accelerator_tree.lock_read()
2548
return _build_tree(tree, wt, accelerator_tree, hardlink,
2551
if accelerator_tree is not None:
2552
accelerator_tree.unlock()
2555
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2556
"""See build_tree."""
2557
for num, _unused in enumerate(wt.all_versioned_paths()):
2558
if num > 0: # more than just a root
2559
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2561
top_pb = ui.ui_factory.nested_progress_bar()
2562
pp = ProgressPhase("Build phase", 2, top_pb)
2563
if tree.get_root_id() is not None:
2564
# This is kind of a hack: we should be altering the root
2565
# as part of the regular tree shape diff logic.
2566
# The conditional test here is to avoid doing an
2567
# expensive operation (flush) every time the root id
2568
# is set within the tree, nor setting the root and thus
2569
# marking the tree as dirty, because we use two different
2570
# idioms here: tree interfaces and inventory interfaces.
2571
if wt.get_root_id() != tree.get_root_id():
2572
wt.set_root_id(tree.get_root_id())
2574
tt = TreeTransform(wt)
2578
file_trans_id[wt.get_root_id()] = tt.trans_id_tree_path('')
2579
with ui.ui_factory.nested_progress_bar() as pb:
2580
deferred_contents = []
2582
total = len(tree.all_versioned_paths())
2584
precomputed_delta = []
2586
precomputed_delta = None
2587
# Check if tree inventory has content. If so, we populate
2588
# existing_files with the directory content. If there are no
2589
# entries we skip populating existing_files as its not used.
2590
# This improves performance and unncessary work on large
2591
# directory trees. (#501307)
2593
existing_files = set()
2594
for dir, files in wt.walkdirs():
2595
existing_files.update(f[0] for f in files)
2596
for num, (tree_path, entry) in \
2597
enumerate(tree.iter_entries_by_dir()):
2598
pb.update(gettext("Building tree"), num
2599
- len(deferred_contents), total)
2600
if entry.parent_id is None:
2603
file_id = entry.file_id
2605
precomputed_delta.append((None, tree_path, file_id, entry))
2606
if tree_path in existing_files:
2607
target_path = wt.abspath(tree_path)
2608
kind = file_kind(target_path)
2609
if kind == "directory":
2611
controldir.ControlDir.open(target_path)
2612
except errors.NotBranchError:
2616
if (file_id not in divert
2617
and _content_match(tree, entry, tree_path, file_id, kind,
2619
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2620
if kind == 'directory':
2622
parent_id = file_trans_id[entry.parent_id]
2623
if entry.kind == 'file':
2624
# We *almost* replicate new_by_entry, so that we can defer
2625
# getting the file text, and get them all at once.
2626
trans_id = tt.create_path(entry.name, parent_id)
2627
file_trans_id[file_id] = trans_id
2628
tt.version_file(file_id, trans_id)
2629
executable = tree.is_executable(tree_path)
2631
tt.set_executability(executable, trans_id)
2632
trans_data = (trans_id, file_id,
2633
tree_path, entry.text_sha1)
2634
deferred_contents.append((tree_path, trans_data))
2636
file_trans_id[file_id] = new_by_entry(
2637
tree_path, tt, entry, parent_id, tree)
2639
new_trans_id = file_trans_id[file_id]
2640
old_parent = tt.trans_id_tree_path(tree_path)
2641
_reparent_children(tt, old_parent, new_trans_id)
2642
offset = num + 1 - len(deferred_contents)
2643
_create_files(tt, tree, deferred_contents, pb, offset,
2644
accelerator_tree, hardlink)
2646
divert_trans = set(file_trans_id[f] for f in divert)
2649
return resolve_checkout(t, c, divert_trans)
2650
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2651
if len(raw_conflicts) > 0:
2652
precomputed_delta = None
2653
conflicts = cook_conflicts(raw_conflicts, tt)
2654
for conflict in conflicts:
2655
trace.warning(text_type(conflict))
2657
wt.add_conflicts(conflicts)
2658
except errors.UnsupportedOperation:
2660
result = tt.apply(no_conflicts=True,
2661
precomputed_delta=precomputed_delta)
2668
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2670
total = len(desired_files) + offset
2672
if accelerator_tree is None:
2673
new_desired_files = desired_files
2675
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2676
unchanged = [(p[0], p[1]) for (f, p, c, v, d, n, k, e)
2677
in iter if not (c or e[0] != e[1])]
2678
if accelerator_tree.supports_content_filtering():
2679
unchanged = [(tp, ap) for (tp, ap) in unchanged
2680
if not next(accelerator_tree.iter_search_rules([ap]))]
2681
unchanged = dict(unchanged)
2682
new_desired_files = []
2684
for unused_tree_path, (trans_id, file_id, tree_path, text_sha1) in desired_files:
2685
accelerator_path = unchanged.get(tree_path)
2686
if accelerator_path is None:
2687
new_desired_files.append((tree_path,
2688
(trans_id, file_id, tree_path, text_sha1)))
2690
pb.update(gettext('Adding file contents'), count + offset, total)
2692
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2695
with accelerator_tree.get_file(accelerator_path, file_id) as f:
2696
chunks = osutils.file_iterator(f)
2697
if wt.supports_content_filtering():
2698
filters = wt._content_filter_stack(tree_path)
2699
chunks = filtered_output_bytes(chunks, filters,
2700
ContentFilterContext(tree_path, tree))
2701
tt.create_file(chunks, trans_id, sha1=text_sha1)
2704
for count, ((trans_id, file_id, tree_path, text_sha1), contents) in enumerate(
2705
tree.iter_files_bytes(new_desired_files)):
2706
if wt.supports_content_filtering():
2707
filters = wt._content_filter_stack(tree_path)
2708
contents = filtered_output_bytes(contents, filters,
2709
ContentFilterContext(tree_path, tree))
2710
tt.create_file(contents, trans_id, sha1=text_sha1)
2711
pb.update(gettext('Adding file contents'), count + offset, total)
2714
def _reparent_children(tt, old_parent, new_parent):
2715
for child in tt.iter_tree_children(old_parent):
2716
tt.adjust_path(tt.final_name(child), new_parent, child)
2719
def _reparent_transform_children(tt, old_parent, new_parent):
2720
by_parent = tt.by_parent()
2721
for child in by_parent[old_parent]:
2722
tt.adjust_path(tt.final_name(child), new_parent, child)
2723
return by_parent[old_parent]
2726
def _content_match(tree, entry, tree_path, file_id, kind, target_path):
2727
if entry.kind != kind:
2729
if entry.kind == "directory":
2731
if entry.kind == "file":
2732
with open(target_path, 'rb') as f1, \
2733
tree.get_file(tree_path) as f2:
2734
if osutils.compare_files(f1, f2):
2736
elif entry.kind == "symlink":
2737
if tree.get_symlink_target(tree_path) == os.readlink(target_path):
2742
def resolve_checkout(tt, conflicts, divert):
2743
new_conflicts = set()
2744
for c_type, conflict in ((c[0], c) for c in conflicts):
2745
# Anything but a 'duplicate' would indicate programmer error
2746
if c_type != 'duplicate':
2747
raise AssertionError(c_type)
2748
# Now figure out which is new and which is old
2749
if tt.new_contents(conflict[1]):
2750
new_file = conflict[1]
2751
old_file = conflict[2]
2753
new_file = conflict[2]
2754
old_file = conflict[1]
2756
# We should only get here if the conflict wasn't completely
2758
final_parent = tt.final_parent(old_file)
2759
if new_file in divert:
2760
new_name = tt.final_name(old_file) + '.diverted'
2761
tt.adjust_path(new_name, final_parent, new_file)
2762
new_conflicts.add((c_type, 'Diverted to',
2763
new_file, old_file))
2765
new_name = tt.final_name(old_file) + '.moved'
2766
tt.adjust_path(new_name, final_parent, old_file)
2767
new_conflicts.add((c_type, 'Moved existing file to',
2768
old_file, new_file))
2769
return new_conflicts
2772
def new_by_entry(path, tt, entry, parent_id, tree):
2773
"""Create a new file according to its inventory entry"""
2777
with tree.get_file(path) as f:
2778
executable = tree.is_executable(path)
2780
name, parent_id, osutils.file_iterator(f), entry.file_id,
2782
elif kind in ('directory', 'tree-reference'):
2783
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2784
if kind == 'tree-reference':
2785
tt.set_tree_reference(entry.reference_revision, trans_id)
2787
elif kind == 'symlink':
2788
target = tree.get_symlink_target(path)
2789
return tt.new_symlink(name, parent_id, target, entry.file_id)
2791
raise errors.BadFileKindError(name, kind)
2794
def create_from_tree(tt, trans_id, tree, path, file_id=None, chunks=None,
2795
filter_tree_path=None):
2796
"""Create new file contents according to tree contents.
2798
:param filter_tree_path: the tree path to use to lookup
2799
content filters to apply to the bytes output in the working tree.
2800
This only applies if the working tree supports content filtering.
2802
kind = tree.kind(path)
2803
if kind == 'directory':
2804
tt.create_directory(trans_id)
2805
elif kind == "file":
2807
f = tree.get_file(path)
2808
chunks = osutils.file_iterator(f)
2813
if wt.supports_content_filtering() and filter_tree_path is not None:
2814
filters = wt._content_filter_stack(filter_tree_path)
2815
chunks = filtered_output_bytes(
2817
ContentFilterContext(filter_tree_path, tree))
2818
tt.create_file(chunks, trans_id)
2822
elif kind == "symlink":
2823
tt.create_symlink(tree.get_symlink_target(path), trans_id)
2825
raise AssertionError('Unknown kind %r' % kind)
2828
def create_entry_executability(tt, entry, trans_id):
2829
"""Set the executability of a trans_id according to an inventory entry"""
2830
if entry.kind == "file":
2831
tt.set_executability(entry.executable, trans_id)
2834
def revert(working_tree, target_tree, filenames, backups=False,
2835
pb=None, change_reporter=None):
2836
"""Revert a working tree's contents to those of a target tree."""
2837
pb = ui.ui_factory.nested_progress_bar()
2839
with target_tree.lock_read(), TreeTransform(working_tree, pb) as tt:
2840
pp = ProgressPhase("Revert phase", 3, pb)
2841
conflicts, merge_modified = _prepare_revert_transform(
2842
working_tree, target_tree, tt, filenames, backups, pp)
2844
change_reporter = delta._ChangeReporter(
2845
unversioned_filter=working_tree.is_ignored)
2846
delta.report_changes(tt.iter_changes(), change_reporter)
2847
for conflict in conflicts:
2848
trace.warning(text_type(conflict))
2851
if working_tree.supports_merge_modified():
2852
working_tree.set_merge_modified(merge_modified)
2858
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2859
backups, pp, basis_tree=None,
2860
merge_modified=None):
2861
with ui.ui_factory.nested_progress_bar() as child_pb:
2862
if merge_modified is None:
2863
merge_modified = working_tree.merge_modified()
2864
merge_modified = _alter_files(working_tree, target_tree, tt,
2865
child_pb, filenames, backups,
2866
merge_modified, basis_tree)
2867
with ui.ui_factory.nested_progress_bar() as child_pb:
2868
raw_conflicts = resolve_conflicts(
2869
tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
2870
conflicts = cook_conflicts(raw_conflicts, tt)
2871
return conflicts, merge_modified
2874
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2875
backups, merge_modified, basis_tree=None):
2876
if basis_tree is not None:
2877
basis_tree.lock_read()
2878
# We ask the working_tree for its changes relative to the target, rather
2879
# than the target changes relative to the working tree. Because WT4 has an
2880
# optimizer to compare itself to a target, but no optimizer for the
2882
change_list = working_tree.iter_changes(
2883
target_tree, specific_files=specific_files, pb=pb)
2884
if not target_tree.is_versioned(u''):
2890
for id_num, (file_id, path, changed_content, versioned, parent, name,
2891
kind, executable) in enumerate(change_list):
2892
target_path, wt_path = path
2893
target_versioned, wt_versioned = versioned
2894
target_parent, wt_parent = parent
2895
target_name, wt_name = name
2896
target_kind, wt_kind = kind
2897
target_executable, wt_executable = executable
2898
if skip_root and wt_parent is None:
2900
trans_id = tt.trans_id_file_id(file_id)
2903
keep_content = False
2904
if wt_kind == 'file' and (backups or target_kind is None):
2905
wt_sha1 = working_tree.get_file_sha1(wt_path)
2906
if merge_modified.get(file_id) != wt_sha1:
2907
# acquire the basis tree lazily to prevent the
2908
# expense of accessing it when it's not needed ?
2909
# (Guessing, RBC, 200702)
2910
if basis_tree is None:
2911
basis_tree = working_tree.basis_tree()
2912
basis_tree.lock_read()
2913
basis_path = find_previous_path(
2914
working_tree, basis_tree, wt_path)
2915
if basis_path is None:
2916
if target_kind is None and not target_versioned:
2919
if wt_sha1 != basis_tree.get_file_sha1(basis_path):
2921
if wt_kind is not None:
2922
if not keep_content:
2923
tt.delete_contents(trans_id)
2924
elif target_kind is not None:
2925
parent_trans_id = tt.trans_id_file_id(wt_parent)
2926
backup_name = tt._available_backup_name(
2927
wt_name, parent_trans_id)
2928
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2929
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2930
if wt_versioned and target_versioned:
2931
tt.unversion_file(trans_id)
2932
tt.version_file(file_id, new_trans_id)
2933
# New contents should have the same unix perms as old
2936
trans_id = new_trans_id
2937
if target_kind in ('directory', 'tree-reference'):
2938
tt.create_directory(trans_id)
2939
if target_kind == 'tree-reference':
2940
revision = target_tree.get_reference_revision(
2942
tt.set_tree_reference(revision, trans_id)
2943
elif target_kind == 'symlink':
2944
tt.create_symlink(target_tree.get_symlink_target(
2945
target_path), trans_id)
2946
elif target_kind == 'file':
2947
deferred_files.append(
2948
(target_path, (trans_id, mode_id, file_id)))
2949
if basis_tree is None:
2950
basis_tree = working_tree.basis_tree()
2951
basis_tree.lock_read()
2952
new_sha1 = target_tree.get_file_sha1(target_path)
2953
basis_path = find_previous_path(target_tree, basis_tree, target_path)
2954
if (basis_path is not None and
2955
new_sha1 == basis_tree.get_file_sha1(basis_path)):
2956
if file_id in merge_modified:
2957
del merge_modified[file_id]
2959
merge_modified[file_id] = new_sha1
2961
# preserve the execute bit when backing up
2962
if keep_content and wt_executable == target_executable:
2963
tt.set_executability(target_executable, trans_id)
2964
elif target_kind is not None:
2965
raise AssertionError(target_kind)
2966
if not wt_versioned and target_versioned:
2967
tt.version_file(file_id, trans_id)
2968
if wt_versioned and not target_versioned:
2969
tt.unversion_file(trans_id)
2970
if (target_name is not None
2971
and (wt_name != target_name or wt_parent != target_parent)):
2972
if target_name == '' and target_parent is None:
2973
parent_trans = ROOT_PARENT
2975
parent_trans = tt.trans_id_file_id(target_parent)
2976
if wt_parent is None and wt_versioned:
2977
tt.adjust_root_path(target_name, parent_trans)
2979
tt.adjust_path(target_name, parent_trans, trans_id)
2980
if wt_executable != target_executable and target_kind == "file":
2981
tt.set_executability(target_executable, trans_id)
2982
if working_tree.supports_content_filtering():
2983
for (trans_id, mode_id, file_id), bytes in (
2984
target_tree.iter_files_bytes(deferred_files)):
2985
# We're reverting a tree to the target tree so using the
2986
# target tree to find the file path seems the best choice
2987
# here IMO - Ian C 27/Oct/2009
2988
filter_tree_path = target_tree.id2path(file_id)
2989
filters = working_tree._content_filter_stack(filter_tree_path)
2990
bytes = filtered_output_bytes(
2992
ContentFilterContext(filter_tree_path, working_tree))
2993
tt.create_file(bytes, trans_id, mode_id)
2995
for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
2997
tt.create_file(bytes, trans_id, mode_id)
2998
tt.fixup_new_roots()
3000
if basis_tree is not None:
3002
return merge_modified
3005
def resolve_conflicts(tt, pb=None, pass_func=None):
3006
"""Make many conflict-resolution attempts, but die if they fail"""
3007
if pass_func is None:
3008
pass_func = conflict_pass
3009
new_conflicts = set()
3010
with ui.ui_factory.nested_progress_bar() as pb:
3012
pb.update(gettext('Resolution pass'), n + 1, 10)
3013
conflicts = tt.find_conflicts()
3014
if len(conflicts) == 0:
3015
return new_conflicts
3016
new_conflicts.update(pass_func(tt, conflicts))
3017
raise MalformedTransform(conflicts=conflicts)
3020
def conflict_pass(tt, conflicts, path_tree=None):
3021
"""Resolve some classes of conflicts.
3023
:param tt: The transform to resolve conflicts in
3024
:param conflicts: The conflicts to resolve
3025
:param path_tree: A Tree to get supplemental paths from
3027
new_conflicts = set()
3028
for c_type, conflict in ((c[0], c) for c in conflicts):
3029
if c_type == 'duplicate id':
3030
tt.unversion_file(conflict[1])
3031
new_conflicts.add((c_type, 'Unversioned existing file',
3032
conflict[1], conflict[2], ))
3033
elif c_type == 'duplicate':
3034
# files that were renamed take precedence
3035
final_parent = tt.final_parent(conflict[1])
3036
if tt.path_changed(conflict[1]):
3037
existing_file, new_file = conflict[2], conflict[1]
3039
existing_file, new_file = conflict[1], conflict[2]
3040
new_name = tt.final_name(existing_file) + '.moved'
3041
tt.adjust_path(new_name, final_parent, existing_file)
3042
new_conflicts.add((c_type, 'Moved existing file to',
3043
existing_file, new_file))
3044
elif c_type == 'parent loop':
3045
# break the loop by undoing one of the ops that caused the loop
3047
while not tt.path_changed(cur):
3048
cur = tt.final_parent(cur)
3049
new_conflicts.add((c_type, 'Cancelled move', cur,
3050
tt.final_parent(cur),))
3051
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3053
elif c_type == 'missing parent':
3054
trans_id = conflict[1]
3055
if trans_id in tt._removed_contents:
3056
cancel_deletion = True
3057
orphans = tt._get_potential_orphans(trans_id)
3059
cancel_deletion = False
3060
# All children are orphans
3063
tt.new_orphan(o, trans_id)
3064
except OrphaningError:
3065
# Something bad happened so we cancel the directory
3066
# deletion which will leave it in place with a
3067
# conflict. The user can deal with it from there.
3068
# Note that this also catch the case where we don't
3069
# want to create orphans and leave the directory in
3071
cancel_deletion = True
3074
# Cancel the directory deletion
3075
tt.cancel_deletion(trans_id)
3076
new_conflicts.add(('deleting parent', 'Not deleting',
3081
tt.final_name(trans_id)
3083
if path_tree is not None:
3084
file_id = tt.final_file_id(trans_id)
3086
file_id = tt.inactive_file_id(trans_id)
3087
_, entry = next(path_tree.iter_entries_by_dir(
3088
specific_files=[path_tree.id2path(file_id)]))
3089
# special-case the other tree root (move its
3090
# children to current root)
3091
if entry.parent_id is None:
3093
moved = _reparent_transform_children(
3094
tt, trans_id, tt.root)
3096
new_conflicts.add((c_type, 'Moved to root',
3099
parent_trans_id = tt.trans_id_file_id(
3101
tt.adjust_path(entry.name, parent_trans_id,
3104
tt.create_directory(trans_id)
3105
new_conflicts.add((c_type, 'Created directory', trans_id))
3106
elif c_type == 'unversioned parent':
3107
file_id = tt.inactive_file_id(conflict[1])
3108
# special-case the other tree root (move its children instead)
3109
if path_tree and path_tree.path2id('') == file_id:
3110
# This is the root entry, skip it
3112
tt.version_file(file_id, conflict[1])
3113
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3114
elif c_type == 'non-directory parent':
3115
parent_id = conflict[1]
3116
parent_parent = tt.final_parent(parent_id)
3117
parent_name = tt.final_name(parent_id)
3118
parent_file_id = tt.final_file_id(parent_id)
3119
new_parent_id = tt.new_directory(parent_name + '.new',
3120
parent_parent, parent_file_id)
3121
_reparent_transform_children(tt, parent_id, new_parent_id)
3122
if parent_file_id is not None:
3123
tt.unversion_file(parent_id)
3124
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3125
elif c_type == 'versioning no contents':
3126
tt.cancel_versioning(conflict[1])
3127
return new_conflicts
3130
def cook_conflicts(raw_conflicts, tt):
3131
"""Generate a list of cooked conflicts, sorted by file path"""
3132
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3133
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3136
def iter_cook_conflicts(raw_conflicts, tt):
3138
for conflict in raw_conflicts:
3139
c_type = conflict[0]
3140
action = conflict[1]
3141
modified_path = fp.get_path(conflict[2])
3142
modified_id = tt.final_file_id(conflict[2])
3143
if len(conflict) == 3:
3144
yield conflicts.Conflict.factory(
3145
c_type, action=action, path=modified_path, file_id=modified_id)
3148
conflicting_path = fp.get_path(conflict[3])
3149
conflicting_id = tt.final_file_id(conflict[3])
3150
yield conflicts.Conflict.factory(
3151
c_type, action=action, path=modified_path,
3152
file_id=modified_id,
3153
conflict_path=conflicting_path,
3154
conflict_file_id=conflicting_id)
3157
class _FileMover(object):
3158
"""Moves and deletes files for TreeTransform, tracking operations"""
3161
self.past_renames = []
3162
self.pending_deletions = []
3164
def rename(self, from_, to):
3165
"""Rename a file from one path to another."""
3167
os.rename(from_, to)
3168
except OSError as e:
3169
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3170
raise errors.FileExists(to, str(e))
3171
# normal OSError doesn't include filenames so it's hard to see where
3172
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3173
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3174
self.past_renames.append((from_, to))
3176
def pre_delete(self, from_, to):
3177
"""Rename a file out of the way and mark it for deletion.
3179
Unlike os.unlink, this works equally well for files and directories.
3180
:param from_: The current file path
3181
:param to: A temporary path for the file
3183
self.rename(from_, to)
3184
self.pending_deletions.append(to)
3187
"""Reverse all renames that have been performed"""
3188
for from_, to in reversed(self.past_renames):
3190
os.rename(to, from_)
3191
except OSError as e:
3192
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3193
# after rollback, don't reuse _FileMover
3194
self.past_renames = None
3195
self.pending_deletions = None
3197
def apply_deletions(self):
3198
"""Apply all marked deletions"""
3199
for path in self.pending_deletions:
3201
# after apply_deletions, don't reuse _FileMover
3202
self.past_renames = None
3203
self.pending_deletions = None
3206
def link_tree(target_tree, source_tree):
3207
"""Where possible, hard-link files in a tree to those in another tree.
3209
:param target_tree: Tree to change
3210
:param source_tree: Tree to hard-link from
3212
tt = TreeTransform(target_tree)
3214
for (file_id, paths, changed_content, versioned, parent, name, kind,
3215
executable) in target_tree.iter_changes(source_tree,
3216
include_unchanged=True):
3219
if kind != ('file', 'file'):
3221
if executable[0] != executable[1]:
3223
trans_id = tt.trans_id_tree_path(paths[1])
3224
tt.delete_contents(trans_id)
3225
tt.create_hardlink(source_tree.abspath(paths[0]), trans_id)