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
20
from stat import S_ISREG, S_IEXEC
24
config as _mod_config,
33
lazy_import.lazy_import(globals(), """
36
revision as _mod_revision,
40
from breezy.i18n import gettext
43
from .errors import (DuplicateKey,
44
BzrError, InternalBzrError)
45
from .filters import filtered_output_bytes, ContentFilterContext
46
from .mutabletree import MutableTree
47
from .osutils import (
55
from .progress import ProgressPhase
62
ROOT_PARENT = "root-parent"
65
class NoFinalPath(BzrError):
67
_fmt = ("No final name for trans_id %(trans_id)r\n"
68
"root trans-id: %(root_trans_id)r\n")
70
def __init__(self, trans_id, transform):
71
self.trans_id = trans_id
72
self.root_trans_id = transform.root
75
class ReusingTransform(BzrError):
77
_fmt = "Attempt to reuse a transform that has already been applied."
80
class MalformedTransform(InternalBzrError):
82
_fmt = "Tree transform is malformed %(conflicts)r"
85
class CantMoveRoot(BzrError):
87
_fmt = "Moving the root directory is not supported at this time"
90
class ImmortalLimbo(BzrError):
92
_fmt = """Unable to delete transform temporary directory %(limbo_dir)s.
93
Please examine %(limbo_dir)s to see if it contains any files you wish to
94
keep, and delete it when you are done."""
96
def __init__(self, limbo_dir):
97
BzrError.__init__(self)
98
self.limbo_dir = limbo_dir
101
class TransformRenameFailed(BzrError):
103
_fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s"
105
def __init__(self, from_path, to_path, why, errno):
106
self.from_path = from_path
107
self.to_path = to_path
112
def unique_add(map, key, value):
114
raise DuplicateKey(key=key)
118
class _TransformResults(object):
120
def __init__(self, modified_paths, rename_count):
121
object.__init__(self)
122
self.modified_paths = modified_paths
123
self.rename_count = rename_count
126
class TreeTransform(object):
127
"""Represent a tree transformation.
129
This object is designed to support incremental generation of the transform,
132
However, it gives optimum performance when parent directories are created
133
before their contents. The transform is then able to put child files
134
directly in their parent directory, avoiding later renames.
136
It is easy to produce malformed transforms, but they are generally
137
harmless. Attempting to apply a malformed transform will cause an
138
exception to be raised before any modifications are made to the tree.
140
Many kinds of malformed transforms can be corrected with the
141
resolve_conflicts function. The remaining ones indicate programming error,
142
such as trying to create a file with no path.
144
Two sets of file creation methods are supplied. Convenience methods are:
149
These are composed of the low-level methods:
151
* create_file or create_directory or create_symlink
155
Transform/Transaction ids
156
-------------------------
157
trans_ids are temporary ids assigned to all files involved in a transform.
158
It's possible, even common, that not all files in the Tree have trans_ids.
160
trans_ids are only valid for the TreeTransform that generated them.
163
def __init__(self, tree, pb=None):
168
# Mapping of path in old tree -> trans_id
169
self._tree_path_ids = {}
170
# Mapping trans_id -> path in old tree
171
self._tree_id_paths = {}
172
# mapping of trans_id -> new basename
174
# mapping of trans_id -> new parent trans_id
175
self._new_parent = {}
176
# mapping of trans_id with new contents -> new file_kind
177
self._new_contents = {}
178
# Set of trans_ids whose contents will be removed
179
self._removed_contents = set()
180
# Mapping of trans_id -> new execute-bit value
181
self._new_executability = {}
182
# Mapping of trans_id -> new tree-reference value
183
self._new_reference_revision = {}
184
# Set of trans_ids that will be removed
185
self._removed_id = set()
186
# Indicator of whether the transform has been applied
190
"""Support Context Manager API."""
193
def __exit__(self, exc_type, exc_val, exc_tb):
194
"""Support Context Manager API."""
197
def iter_tree_children(self, trans_id):
198
"""Iterate through the entry's tree children, if any.
200
:param trans_id: trans id to iterate
201
:returns: Iterator over paths
203
raise NotImplementedError(self.iter_tree_children)
205
def canonical_path(self, path):
208
def tree_kind(self, trans_id):
209
raise NotImplementedError(self.tree_kind)
212
"""Return a map of parent: children for known parents.
214
Only new paths and parents of tree files with assigned ids are used.
217
items = list(self._new_parent.items())
218
items.extend((t, self.final_parent(t))
219
for t in list(self._tree_id_paths))
220
for trans_id, parent_id in items:
221
if parent_id not in by_parent:
222
by_parent[parent_id] = set()
223
by_parent[parent_id].add(trans_id)
227
"""Release the working tree lock, if held.
229
This is required if apply has not been invoked, but can be invoked
232
raise NotImplementedError(self.finalize)
234
def create_path(self, name, parent):
235
"""Assign a transaction id to a new path"""
236
trans_id = self.assign_id()
237
unique_add(self._new_name, trans_id, name)
238
unique_add(self._new_parent, trans_id, parent)
241
def adjust_path(self, name, parent, trans_id):
242
"""Change the path that is assigned to a transaction id."""
244
raise ValueError("Parent trans-id may not be None")
245
if trans_id == self.root:
247
self._new_name[trans_id] = name
248
self._new_parent[trans_id] = parent
250
def adjust_root_path(self, name, parent):
251
"""Emulate moving the root by moving all children, instead.
253
We do this by undoing the association of root's transaction id with the
254
current tree. This allows us to create a new directory with that
255
transaction id. We unversion the root directory and version the
256
physically new directory, and hope someone versions the tree root
259
raise NotImplementedError(self.adjust_root_path)
261
def fixup_new_roots(self):
262
"""Reinterpret requests to change the root directory
264
Instead of creating a root directory, or moving an existing directory,
265
all the attributes and children of the new root are applied to the
266
existing root directory.
268
This means that the old root trans-id becomes obsolete, so it is
269
recommended only to invoke this after the root trans-id has become
272
raise NotImplementedError(self.fixup_new_roots)
275
"""Produce a new tranform id"""
276
new_id = "new-%s" % self._id_number
280
def trans_id_tree_path(self, path):
281
"""Determine (and maybe set) the transaction ID for a tree path."""
282
path = self.canonical_path(path)
283
if path not in self._tree_path_ids:
284
self._tree_path_ids[path] = self.assign_id()
285
self._tree_id_paths[self._tree_path_ids[path]] = path
286
return self._tree_path_ids[path]
288
def get_tree_parent(self, trans_id):
289
"""Determine id of the parent in the tree."""
290
path = self._tree_id_paths[trans_id]
293
return self.trans_id_tree_path(os.path.dirname(path))
295
def delete_contents(self, trans_id):
296
"""Schedule the contents of a path entry for deletion"""
297
kind = self.tree_kind(trans_id)
299
self._removed_contents.add(trans_id)
301
def cancel_deletion(self, trans_id):
302
"""Cancel a scheduled deletion"""
303
self._removed_contents.remove(trans_id)
305
def delete_versioned(self, trans_id):
306
"""Delete and unversion a versioned file"""
307
self.delete_contents(trans_id)
308
self.unversion_file(trans_id)
310
def set_executability(self, executability, trans_id):
311
"""Schedule setting of the 'execute' bit
312
To unschedule, set to None
314
if executability is None:
315
del self._new_executability[trans_id]
317
unique_add(self._new_executability, trans_id, executability)
319
def set_tree_reference(self, revision_id, trans_id):
320
"""Set the reference associated with a directory"""
321
unique_add(self._new_reference_revision, trans_id, revision_id)
323
def version_file(self, trans_id, file_id=None):
324
"""Schedule a file to become versioned."""
325
raise NotImplementedError(self.version_file)
327
def cancel_versioning(self, trans_id):
328
"""Undo a previous versioning of a file"""
329
raise NotImplementedError(self.cancel_versioning)
331
def unversion_file(self, trans_id):
332
"""Schedule a path entry to become unversioned"""
333
self._removed_id.add(trans_id)
335
def new_paths(self, filesystem_only=False):
336
"""Determine the paths of all new and changed files.
338
:param filesystem_only: if True, only calculate values for files
339
that require renames or execute bit changes.
341
raise NotImplementedError(self.new_paths)
343
def final_kind(self, trans_id):
344
"""Determine the final file kind, after any changes applied.
346
:return: None if the file does not exist/has no contents. (It is
347
conceivable that a path would be created without the corresponding
348
contents insertion command)
350
if trans_id in self._new_contents:
351
if trans_id in self._new_reference_revision:
352
return 'tree-reference'
353
return self._new_contents[trans_id]
354
elif trans_id in self._removed_contents:
357
return self.tree_kind(trans_id)
359
def tree_path(self, trans_id):
360
"""Determine the tree path associated with the trans_id."""
361
return self._tree_id_paths.get(trans_id)
363
def final_is_versioned(self, trans_id):
364
raise NotImplementedError(self.final_is_versioned)
366
def final_parent(self, trans_id):
367
"""Determine the parent file_id, after any changes are applied.
369
ROOT_PARENT is returned for the tree root.
372
return self._new_parent[trans_id]
374
return self.get_tree_parent(trans_id)
376
def final_name(self, trans_id):
377
"""Determine the final filename, after all changes are applied."""
379
return self._new_name[trans_id]
382
return os.path.basename(self._tree_id_paths[trans_id])
384
raise NoFinalPath(trans_id, self)
386
def path_changed(self, trans_id):
387
"""Return True if a trans_id's path has changed."""
388
return (trans_id in self._new_name) or (trans_id in self._new_parent)
390
def new_contents(self, trans_id):
391
return (trans_id in self._new_contents)
393
def find_raw_conflicts(self):
394
"""Find any violations of inventory or filesystem invariants"""
395
raise NotImplementedError(self.find_raw_conflicts)
397
def new_file(self, name, parent_id, contents, file_id=None,
398
executable=None, sha1=None):
399
"""Convenience method to create files.
401
name is the name of the file to create.
402
parent_id is the transaction id of the parent directory of the file.
403
contents is an iterator of bytestrings, which will be used to produce
405
:param file_id: The inventory ID of the file, if it is to be versioned.
406
:param executable: Only valid when a file_id has been supplied.
408
raise NotImplementedError(self.new_file)
410
def new_directory(self, name, parent_id, file_id=None):
411
"""Convenience method to create directories.
413
name is the name of the directory to create.
414
parent_id is the transaction id of the parent directory of the
416
file_id is the inventory ID of the directory, if it is to be versioned.
418
raise NotImplementedError(self.new_directory)
420
def new_symlink(self, name, parent_id, target, file_id=None):
421
"""Convenience method to create symbolic link.
423
name is the name of the symlink to create.
424
parent_id is the transaction id of the parent directory of the symlink.
425
target is a bytestring of the target of the symlink.
426
file_id is the inventory ID of the file, if it is to be versioned.
428
raise NotImplementedError(self.new_symlink)
430
def new_orphan(self, trans_id, parent_id):
431
"""Schedule an item to be orphaned.
433
When a directory is about to be removed, its children, if they are not
434
versioned are moved out of the way: they don't have a parent anymore.
436
:param trans_id: The trans_id of the existing item.
437
:param parent_id: The parent trans_id of the item.
439
raise NotImplementedError(self.new_orphan)
441
def iter_changes(self):
442
"""Produce output in the same format as Tree.iter_changes.
444
Will produce nonsensical results if invoked while inventory/filesystem
445
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
447
This reads the Transform, but only reproduces changes involving a
448
file_id. Files that are not versioned in either of the FROM or TO
449
states are not reflected.
451
raise NotImplementedError(self.iter_changes)
453
def get_preview_tree(self):
454
"""Return a tree representing the result of the transform.
456
The tree is a snapshot, and altering the TreeTransform will invalidate
459
raise NotImplementedError(self.get_preview_tree)
461
def commit(self, branch, message, merge_parents=None, strict=False,
462
timestamp=None, timezone=None, committer=None, authors=None,
463
revprops=None, revision_id=None):
464
"""Commit the result of this TreeTransform to a branch.
466
:param branch: The branch to commit to.
467
:param message: The message to attach to the commit.
468
:param merge_parents: Additional parent revision-ids specified by
470
:param strict: If True, abort the commit if there are unversioned
472
:param timestamp: if not None, seconds-since-epoch for the time and
473
date. (May be a float.)
474
:param timezone: Optional timezone for timestamp, as an offset in
476
:param committer: Optional committer in email-id format.
477
(e.g. "J Random Hacker <jrandom@example.com>")
478
:param authors: Optional list of authors in email-id format.
479
:param revprops: Optional dictionary of revision properties.
480
:param revision_id: Optional revision id. (Specifying a revision-id
481
may reduce performance for some non-native formats.)
482
:return: The revision_id of the revision committed.
484
raise NotImplementedError(self.commit)
486
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
487
"""Schedule creation of a new file.
491
:param contents: an iterator of strings, all of which will be written
492
to the target destination.
493
:param trans_id: TreeTransform handle
494
:param mode_id: If not None, force the mode of the target file to match
495
the mode of the object referenced by mode_id.
496
Otherwise, we will try to preserve mode bits of an existing file.
497
:param sha1: If the sha1 of this content is already known, pass it in.
498
We can use it to prevent future sha1 computations.
500
raise NotImplementedError(self.create_file)
502
def create_directory(self, trans_id):
503
"""Schedule creation of a new directory.
505
See also new_directory.
507
raise NotImplementedError(self.create_directory)
509
def create_symlink(self, target, trans_id):
510
"""Schedule creation of a new symbolic link.
512
target is a bytestring.
513
See also new_symlink.
515
raise NotImplementedError(self.create_symlink)
517
def create_hardlink(self, path, trans_id):
518
"""Schedule creation of a hard link"""
519
raise NotImplementedError(self.create_hardlink)
521
def cancel_creation(self, trans_id):
522
"""Cancel the creation of new file contents."""
523
raise NotImplementedError(self.cancel_creation)
525
def cook_conflicts(self, raw_conflicts):
528
raise NotImplementedError(self.cook_conflicts)
531
class OrphaningError(errors.BzrError):
533
# Only bugs could lead to such exception being seen by the user
534
internal_error = True
535
_fmt = "Error while orphaning %s in %s directory"
537
def __init__(self, orphan, parent):
538
errors.BzrError.__init__(self)
543
class OrphaningForbidden(OrphaningError):
545
_fmt = "Policy: %s doesn't allow creating orphans."
547
def __init__(self, policy):
548
errors.BzrError.__init__(self)
552
def move_orphan(tt, orphan_id, parent_id):
553
"""See TreeTransformBase.new_orphan.
555
This creates a new orphan in the `brz-orphans` dir at the root of the
558
:param tt: The TreeTransform orphaning `trans_id`.
560
:param orphan_id: The trans id that should be orphaned.
562
:param parent_id: The orphan parent trans id.
564
# Add the orphan dir if it doesn't exist
565
orphan_dir_basename = 'brz-orphans'
566
od_id = tt.trans_id_tree_path(orphan_dir_basename)
567
if tt.final_kind(od_id) is None:
568
tt.create_directory(od_id)
569
parent_path = tt._tree_id_paths[parent_id]
570
# Find a name that doesn't exist yet in the orphan dir
571
actual_name = tt.final_name(orphan_id)
572
new_name = tt._available_backup_name(actual_name, od_id)
573
tt.adjust_path(new_name, od_id, orphan_id)
574
trace.warning('%s has been orphaned in %s'
575
% (joinpath(parent_path, actual_name), orphan_dir_basename))
578
def refuse_orphan(tt, orphan_id, parent_id):
579
"""See TreeTransformBase.new_orphan.
581
This refuses to create orphan, letting the caller handle the conflict.
583
raise OrphaningForbidden('never')
586
orphaning_registry = registry.Registry()
587
orphaning_registry.register(
588
u'conflict', refuse_orphan,
589
'Leave orphans in place and create a conflict on the directory.')
590
orphaning_registry.register(
591
u'move', move_orphan,
592
'Move orphans into the brz-orphans directory.')
593
orphaning_registry._set_default_key(u'conflict')
596
opt_transform_orphan = _mod_config.RegistryOption(
597
'transform.orphan_policy', orphaning_registry,
598
help='Policy for orphaned files during transform operations.',
602
def joinpath(parent, child):
603
"""Join tree-relative paths, handling the tree root specially"""
604
if parent is None or parent == "":
607
return pathjoin(parent, child)
610
class FinalPaths(object):
611
"""Make path calculation cheap by memoizing paths.
613
The underlying tree must not be manipulated between calls, or else
614
the results will likely be incorrect.
617
def __init__(self, transform):
618
object.__init__(self)
619
self._known_paths = {}
620
self.transform = transform
622
def _determine_path(self, trans_id):
623
if trans_id == self.transform.root or trans_id == ROOT_PARENT:
625
name = self.transform.final_name(trans_id)
626
parent_id = self.transform.final_parent(trans_id)
627
if parent_id == self.transform.root:
630
return pathjoin(self.get_path(parent_id), name)
632
def get_path(self, trans_id):
633
"""Find the final path associated with a trans_id"""
634
if trans_id not in self._known_paths:
635
self._known_paths[trans_id] = self._determine_path(trans_id)
636
return self._known_paths[trans_id]
638
def get_paths(self, trans_ids):
639
return [(self.get_path(t), t) for t in trans_ids]
642
def _reparent_children(tt, old_parent, new_parent):
643
for child in tt.iter_tree_children(old_parent):
644
tt.adjust_path(tt.final_name(child), new_parent, child)
647
def _reparent_transform_children(tt, old_parent, new_parent):
648
by_parent = tt.by_parent()
649
for child in by_parent[old_parent]:
650
tt.adjust_path(tt.final_name(child), new_parent, child)
651
return by_parent[old_parent]
654
def new_by_entry(path, tt, entry, parent_id, tree):
655
"""Create a new file according to its inventory entry"""
659
with tree.get_file(path) as f:
660
executable = tree.is_executable(path)
662
name, parent_id, osutils.file_iterator(f), entry.file_id,
664
elif kind in ('directory', 'tree-reference'):
665
trans_id = tt.new_directory(name, parent_id, entry.file_id)
666
if kind == 'tree-reference':
667
tt.set_tree_reference(entry.reference_revision, trans_id)
669
elif kind == 'symlink':
670
target = tree.get_symlink_target(path)
671
return tt.new_symlink(name, parent_id, target, entry.file_id)
673
raise errors.BadFileKindError(name, kind)
676
def create_from_tree(tt, trans_id, tree, path, chunks=None,
677
filter_tree_path=None):
678
"""Create new file contents according to tree contents.
680
:param filter_tree_path: the tree path to use to lookup
681
content filters to apply to the bytes output in the working tree.
682
This only applies if the working tree supports content filtering.
684
kind = tree.kind(path)
685
if kind == 'directory':
686
tt.create_directory(trans_id)
689
f = tree.get_file(path)
690
chunks = osutils.file_iterator(f)
695
if wt.supports_content_filtering() and filter_tree_path is not None:
696
filters = wt._content_filter_stack(filter_tree_path)
697
chunks = filtered_output_bytes(
699
ContentFilterContext(filter_tree_path, tree))
700
tt.create_file(chunks, trans_id)
704
elif kind == "symlink":
705
tt.create_symlink(tree.get_symlink_target(path), trans_id)
707
raise AssertionError('Unknown kind %r' % kind)
710
def create_entry_executability(tt, entry, trans_id):
711
"""Set the executability of a trans_id according to an inventory entry"""
712
if entry.kind == "file":
713
tt.set_executability(entry.executable, trans_id)
716
def _prepare_revert_transform(es, working_tree, target_tree, tt, filenames,
717
backups, pp, basis_tree=None,
718
merge_modified=None):
719
with ui.ui_factory.nested_progress_bar() as child_pb:
720
if merge_modified is None:
721
merge_modified = working_tree.merge_modified()
722
merge_modified = _alter_files(es, working_tree, target_tree, tt,
723
child_pb, filenames, backups,
724
merge_modified, basis_tree)
725
with ui.ui_factory.nested_progress_bar() as child_pb:
726
raw_conflicts = resolve_conflicts(
727
tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
728
conflicts = tt.cook_conflicts(raw_conflicts)
729
return conflicts, merge_modified
732
def revert(working_tree, target_tree, filenames, backups=False,
733
pb=None, change_reporter=None, merge_modified=None, basis_tree=None):
734
"""Revert a working tree's contents to those of a target tree."""
735
with contextlib.ExitStack() as es:
736
pb = es.enter_context(ui.ui_factory.nested_progress_bar())
737
es.enter_context(target_tree.lock_read())
738
tt = es.enter_context(working_tree.transform(pb))
739
pp = ProgressPhase("Revert phase", 3, pb)
740
conflicts, merge_modified = _prepare_revert_transform(
741
es, working_tree, target_tree, tt, filenames, backups, pp)
744
change_reporter = delta._ChangeReporter(
745
unversioned_filter=working_tree.is_ignored)
746
delta.report_changes(tt.iter_changes(), change_reporter)
747
for conflict in conflicts:
748
trace.warning(str(conflict))
751
if working_tree.supports_merge_modified():
752
working_tree.set_merge_modified(merge_modified)
756
def _alter_files(es, working_tree, target_tree, tt, pb, specific_files,
757
backups, merge_modified, basis_tree=None):
758
if basis_tree is not None:
759
es.enter_context(basis_tree.lock_read())
760
# We ask the working_tree for its changes relative to the target, rather
761
# than the target changes relative to the working tree. Because WT4 has an
762
# optimizer to compare itself to a target, but no optimizer for the
764
change_list = working_tree.iter_changes(
765
target_tree, specific_files=specific_files, pb=pb)
766
if not target_tree.is_versioned(u''):
771
for id_num, change in enumerate(change_list):
772
target_path, wt_path = change.path
773
target_versioned, wt_versioned = change.versioned
774
target_parent = change.parent_id[0]
775
target_name, wt_name = change.name
776
target_kind, wt_kind = change.kind
777
target_executable, wt_executable = change.executable
778
if skip_root and wt_path == '':
781
if wt_path is not None:
782
trans_id = tt.trans_id_tree_path(wt_path)
784
trans_id = tt.assign_id()
785
if change.changed_content:
787
if wt_kind == 'file' and (backups or target_kind is None):
788
wt_sha1 = working_tree.get_file_sha1(wt_path)
789
if merge_modified.get(wt_path) != wt_sha1:
790
# acquire the basis tree lazily to prevent the
791
# expense of accessing it when it's not needed ?
792
# (Guessing, RBC, 200702)
793
if basis_tree is None:
794
basis_tree = working_tree.basis_tree()
795
es.enter_context(basis_tree.lock_read())
796
basis_inter = InterTree.get(basis_tree, working_tree)
797
basis_path = basis_inter.find_source_path(wt_path)
798
if basis_path is None:
799
if target_kind is None and not target_versioned:
802
if wt_sha1 != basis_tree.get_file_sha1(basis_path):
804
if wt_kind is not None:
806
tt.delete_contents(trans_id)
807
elif target_kind is not None:
808
parent_trans_id = tt.trans_id_tree_path(osutils.dirname(wt_path))
809
backup_name = tt._available_backup_name(
810
wt_name, parent_trans_id)
811
tt.adjust_path(backup_name, parent_trans_id, trans_id)
812
new_trans_id = tt.create_path(wt_name, parent_trans_id)
813
if wt_versioned and target_versioned:
814
tt.unversion_file(trans_id)
816
new_trans_id, file_id=getattr(change, 'file_id', None))
817
# New contents should have the same unix perms as old
820
trans_id = new_trans_id
821
if target_kind in ('directory', 'tree-reference'):
822
tt.create_directory(trans_id)
823
if target_kind == 'tree-reference':
824
revision = target_tree.get_reference_revision(
826
tt.set_tree_reference(revision, trans_id)
827
elif target_kind == 'symlink':
828
tt.create_symlink(target_tree.get_symlink_target(
829
target_path), trans_id)
830
elif target_kind == 'file':
831
deferred_files.append(
832
(target_path, (trans_id, mode_id, target_path)))
833
if basis_tree is None:
834
basis_tree = working_tree.basis_tree()
835
es.enter_context(basis_tree.lock_read())
836
new_sha1 = target_tree.get_file_sha1(target_path)
837
basis_inter = InterTree.get(basis_tree, target_tree)
838
basis_path = basis_inter.find_source_path(target_path)
839
if (basis_path is not None and
840
new_sha1 == basis_tree.get_file_sha1(basis_path)):
841
# If the new contents of the file match what is in basis,
842
# then there is no need to store in merge_modified.
843
if basis_path in merge_modified:
844
del merge_modified[basis_path]
846
merge_modified[target_path] = new_sha1
848
# preserve the execute bit when backing up
849
if keep_content and wt_executable == target_executable:
850
tt.set_executability(target_executable, trans_id)
851
elif target_kind is not None:
852
raise AssertionError(target_kind)
853
if not wt_versioned and target_versioned:
855
trans_id, file_id=getattr(change, 'file_id', None))
856
if wt_versioned and not target_versioned:
857
tt.unversion_file(trans_id)
858
if (target_name is not None
859
and (wt_name != target_name or change.is_reparented())):
860
if target_path == '':
861
parent_trans = ROOT_PARENT
863
parent_trans = tt.trans_id_file_id(target_parent)
864
if wt_path == '' and wt_versioned:
865
tt.adjust_root_path(target_name, parent_trans)
867
tt.adjust_path(target_name, parent_trans, trans_id)
868
if wt_executable != target_executable and target_kind == "file":
869
tt.set_executability(target_executable, trans_id)
870
if working_tree.supports_content_filtering():
871
for (trans_id, mode_id, target_path), bytes in (
872
target_tree.iter_files_bytes(deferred_files)):
873
# We're reverting a tree to the target tree so using the
874
# target tree to find the file path seems the best choice
875
# here IMO - Ian C 27/Oct/2009
876
filters = working_tree._content_filter_stack(target_path)
877
bytes = filtered_output_bytes(
879
ContentFilterContext(target_path, working_tree))
880
tt.create_file(bytes, trans_id, mode_id)
882
for (trans_id, mode_id, target_path), bytes in target_tree.iter_files_bytes(
884
tt.create_file(bytes, trans_id, mode_id)
886
return merge_modified
889
def resolve_conflicts(tt, pb=None, pass_func=None):
890
"""Make many conflict-resolution attempts, but die if they fail"""
891
if pass_func is None:
892
pass_func = conflict_pass
893
new_conflicts = set()
894
with ui.ui_factory.nested_progress_bar() as pb:
896
pb.update(gettext('Resolution pass'), n + 1, 10)
897
conflicts = tt.find_raw_conflicts()
898
if len(conflicts) == 0:
900
new_conflicts.update(pass_func(tt, conflicts))
901
raise MalformedTransform(conflicts=conflicts)
904
def resolve_duplicate_id(tt, path_tree, c_type, old_trans_id, trans_id):
905
tt.unversion_file(old_trans_id)
906
yield (c_type, 'Unversioned existing file', old_trans_id, trans_id)
909
def resolve_duplicate(tt, path_tree, c_type, last_trans_id, trans_id, name):
910
# files that were renamed take precedence
911
final_parent = tt.final_parent(last_trans_id)
912
if tt.path_changed(last_trans_id):
913
existing_file, new_file = trans_id, last_trans_id
915
existing_file, new_file = last_trans_id, trans_id
916
if (not tt._tree.has_versioned_directories() and
917
tt.final_kind(trans_id) == 'directory' and
918
tt.final_kind(last_trans_id) == 'directory'):
919
_reparent_transform_children(tt, existing_file, new_file)
920
tt.delete_contents(existing_file)
921
tt.unversion_file(existing_file)
922
tt.cancel_creation(existing_file)
924
new_name = tt.final_name(existing_file) + '.moved'
925
tt.adjust_path(new_name, final_parent, existing_file)
926
yield (c_type, 'Moved existing file to', existing_file, new_file)
929
def resolve_parent_loop(tt, path_tree, c_type, cur):
930
# break the loop by undoing one of the ops that caused the loop
931
while not tt.path_changed(cur):
932
cur = tt.final_parent(cur)
933
yield (c_type, 'Cancelled move', cur, tt.final_parent(cur),)
934
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
937
def resolve_missing_parent(tt, path_tree, c_type, trans_id):
938
if trans_id in tt._removed_contents:
939
cancel_deletion = True
940
orphans = tt._get_potential_orphans(trans_id)
942
cancel_deletion = False
943
# All children are orphans
946
tt.new_orphan(o, trans_id)
947
except OrphaningError:
948
# Something bad happened so we cancel the directory
949
# deletion which will leave it in place with a
950
# conflict. The user can deal with it from there.
951
# Note that this also catch the case where we don't
952
# want to create orphans and leave the directory in
954
cancel_deletion = True
957
# Cancel the directory deletion
958
tt.cancel_deletion(trans_id)
959
yield ('deleting parent', 'Not deleting', trans_id)
963
tt.final_name(trans_id)
965
if path_tree is not None:
966
file_id = tt.final_file_id(trans_id)
968
file_id = tt.inactive_file_id(trans_id)
969
_, entry = next(path_tree.iter_entries_by_dir(
970
specific_files=[path_tree.id2path(file_id)]))
971
# special-case the other tree root (move its
972
# children to current root)
973
if entry.parent_id is None:
975
moved = _reparent_transform_children(
976
tt, trans_id, tt.root)
978
yield (c_type, 'Moved to root', child)
980
parent_trans_id = tt.trans_id_file_id(
982
tt.adjust_path(entry.name, parent_trans_id,
985
tt.create_directory(trans_id)
986
yield (c_type, 'Created directory', trans_id)
989
def resolve_unversioned_parent(tt, path_tree, c_type, trans_id):
990
file_id = tt.inactive_file_id(trans_id)
991
# special-case the other tree root (move its children instead)
992
if path_tree and path_tree.path2id('') == file_id:
993
# This is the root entry, skip it
995
tt.version_file(trans_id, file_id=file_id)
996
yield (c_type, 'Versioned directory', trans_id)
999
def resolve_non_directory_parent(tt, path_tree, c_type, parent_id):
1000
parent_parent = tt.final_parent(parent_id)
1001
parent_name = tt.final_name(parent_id)
1002
# TODO(jelmer): Make this code transform-specific
1003
if tt._tree.supports_setting_file_ids():
1004
parent_file_id = tt.final_file_id(parent_id)
1006
parent_file_id = b'DUMMY'
1007
new_parent_id = tt.new_directory(parent_name + '.new',
1008
parent_parent, parent_file_id)
1009
_reparent_transform_children(tt, parent_id, new_parent_id)
1010
if parent_file_id is not None:
1011
tt.unversion_file(parent_id)
1012
yield (c_type, 'Created directory', new_parent_id)
1015
def resolve_versioning_no_contents(tt, path_tree, c_type, trans_id):
1016
tt.cancel_versioning(trans_id)
1020
CONFLICT_RESOLVERS = {
1021
'duplicate id': resolve_duplicate_id,
1022
'duplicate': resolve_duplicate,
1023
'parent loop': resolve_parent_loop,
1024
'missing parent': resolve_missing_parent,
1025
'unversioned parent': resolve_unversioned_parent,
1026
'non-directory parent': resolve_non_directory_parent,
1027
'versioning no contents': resolve_versioning_no_contents,
1031
def conflict_pass(tt, conflicts, path_tree=None):
1032
"""Resolve some classes of conflicts.
1034
:param tt: The transform to resolve conflicts in
1035
:param conflicts: The conflicts to resolve
1036
:param path_tree: A Tree to get supplemental paths from
1038
new_conflicts = set()
1039
for conflict in conflicts:
1040
resolver = CONFLICT_RESOLVERS.get(conflict[0])
1041
if resolver is None:
1043
new_conflicts.update(resolver(tt, path_tree, *conflict))
1044
return new_conflicts
1047
class _FileMover(object):
1048
"""Moves and deletes files for TreeTransform, tracking operations"""
1051
self.past_renames = []
1052
self.pending_deletions = []
1054
def rename(self, from_, to):
1055
"""Rename a file from one path to another."""
1057
os.rename(from_, to)
1058
except OSError as e:
1059
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
1060
raise errors.FileExists(to, str(e))
1061
# normal OSError doesn't include filenames so it's hard to see where
1062
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
1063
raise TransformRenameFailed(from_, to, str(e), e.errno)
1064
self.past_renames.append((from_, to))
1066
def pre_delete(self, from_, to):
1067
"""Rename a file out of the way and mark it for deletion.
1069
Unlike os.unlink, this works equally well for files and directories.
1070
:param from_: The current file path
1071
:param to: A temporary path for the file
1073
self.rename(from_, to)
1074
self.pending_deletions.append(to)
1077
"""Reverse all renames that have been performed"""
1078
for from_, to in reversed(self.past_renames):
1080
os.rename(to, from_)
1081
except OSError as e:
1082
raise TransformRenameFailed(to, from_, str(e), e.errno)
1083
# after rollback, don't reuse _FileMover
1084
self.past_renames = None
1085
self.pending_deletions = None
1087
def apply_deletions(self):
1088
"""Apply all marked deletions"""
1089
for path in self.pending_deletions:
1091
# after apply_deletions, don't reuse _FileMover
1092
self.past_renames = None
1093
self.pending_deletions = None
1096
def link_tree(target_tree, source_tree):
1097
"""Where possible, hard-link files in a tree to those in another tree.
1099
:param target_tree: Tree to change
1100
:param source_tree: Tree to hard-link from
1102
with target_tree.transform() as tt:
1103
for change in target_tree.iter_changes(source_tree, include_unchanged=True):
1104
if change.changed_content:
1106
if change.kind != ('file', 'file'):
1108
if change.executable[0] != change.executable[1]:
1110
trans_id = tt.trans_id_tree_path(change.path[1])
1111
tt.delete_contents(trans_id)
1112
tt.create_hardlink(source_tree.abspath(change.path[0]), trans_id)
1116
class PreviewTree(object):
1119
def __init__(self, transform):
1120
self._transform = transform
1121
self._parent_ids = []
1122
self.__by_parent = None
1123
self._path2trans_id_cache = {}
1124
self._all_children_cache = {}
1125
self._final_name_cache = {}
1127
def supports_setting_file_ids(self):
1128
raise NotImplementedError(self.supports_setting_file_ids)
1131
def _by_parent(self):
1132
if self.__by_parent is None:
1133
self.__by_parent = self._transform.by_parent()
1134
return self.__by_parent
1136
def get_parent_ids(self):
1137
return self._parent_ids
1139
def set_parent_ids(self, parent_ids):
1140
self._parent_ids = parent_ids
1142
def get_revision_tree(self, revision_id):
1143
return self._transform._tree.get_revision_tree(revision_id)
1145
def is_locked(self):
1148
def lock_read(self):
1149
# Perhaps in theory, this should lock the TreeTransform?
1150
return lock.LogicalLockResult(self.unlock)
1155
def _path2trans_id(self, path):
1156
"""Look up the trans id associated with a path.
1158
:param path: path to look up, None when the path does not exist
1161
# We must not use None here, because that is a valid value to store.
1162
trans_id = self._path2trans_id_cache.get(path, object)
1163
if trans_id is not object:
1165
segments = osutils.splitpath(path)
1166
cur_parent = self._transform.root
1167
for cur_segment in segments:
1168
for child in self._all_children(cur_parent):
1169
final_name = self._final_name_cache.get(child)
1170
if final_name is None:
1171
final_name = self._transform.final_name(child)
1172
self._final_name_cache[child] = final_name
1173
if final_name == cur_segment:
1177
self._path2trans_id_cache[path] = None
1179
self._path2trans_id_cache[path] = cur_parent
1182
def _all_children(self, trans_id):
1183
children = self._all_children_cache.get(trans_id)
1184
if children is not None:
1186
children = set(self._transform.iter_tree_children(trans_id))
1187
# children in the _new_parent set are provided by _by_parent.
1188
children.difference_update(self._transform._new_parent)
1189
children.update(self._by_parent.get(trans_id, []))
1190
self._all_children_cache[trans_id] = children
1193
def get_file_with_stat(self, path):
1194
return self.get_file(path), None
1196
def is_executable(self, path):
1197
trans_id = self._path2trans_id(path)
1198
if trans_id is None:
1201
return self._transform._new_executability[trans_id]
1204
return self._transform._tree.is_executable(path)
1205
except OSError as e:
1206
if e.errno == errno.ENOENT:
1209
except errors.NoSuchFile:
1212
def has_filename(self, path):
1213
trans_id = self._path2trans_id(path)
1214
if trans_id in self._transform._new_contents:
1216
elif trans_id in self._transform._removed_contents:
1219
return self._transform._tree.has_filename(path)
1221
def get_file_sha1(self, path, stat_value=None):
1222
trans_id = self._path2trans_id(path)
1223
if trans_id is None:
1224
raise errors.NoSuchFile(path)
1225
kind = self._transform._new_contents.get(trans_id)
1227
return self._transform._tree.get_file_sha1(path)
1229
with self.get_file(path) as fileobj:
1230
return osutils.sha_file(fileobj)
1232
def get_file_verifier(self, path, stat_value=None):
1233
trans_id = self._path2trans_id(path)
1234
if trans_id is None:
1235
raise errors.NoSuchFile(path)
1236
kind = self._transform._new_contents.get(trans_id)
1238
return self._transform._tree.get_file_verifier(path)
1240
with self.get_file(path) as fileobj:
1241
return ("SHA1", osutils.sha_file(fileobj))
1243
def kind(self, path):
1244
trans_id = self._path2trans_id(path)
1245
if trans_id is None:
1246
raise errors.NoSuchFile(path)
1247
return self._transform.final_kind(trans_id)
1249
def stored_kind(self, path):
1250
trans_id = self._path2trans_id(path)
1251
if trans_id is None:
1252
raise errors.NoSuchFile(path)
1254
return self._transform._new_contents[trans_id]
1256
return self._transform._tree.stored_kind(path)
1258
def _get_repository(self):
1259
repo = getattr(self._transform._tree, '_repository', None)
1261
repo = self._transform._tree.branch.repository
1264
def _iter_parent_trees(self):
1265
for revision_id in self.get_parent_ids():
1267
yield self.revision_tree(revision_id)
1268
except errors.NoSuchRevisionInTree:
1269
yield self._get_repository().revision_tree(revision_id)
1271
def get_file_size(self, path):
1272
"""See Tree.get_file_size"""
1273
trans_id = self._path2trans_id(path)
1274
if trans_id is None:
1275
raise errors.NoSuchFile(path)
1276
kind = self._transform.final_kind(trans_id)
1279
if trans_id in self._transform._new_contents:
1280
return self._stat_limbo_file(trans_id).st_size
1281
if self.kind(path) == 'file':
1282
return self._transform._tree.get_file_size(path)
1286
def get_reference_revision(self, path):
1287
trans_id = self._path2trans_id(path)
1288
if trans_id is None:
1289
raise errors.NoSuchFile(path)
1290
reference_revision = self._transform._new_reference_revision.get(trans_id)
1291
if reference_revision is None:
1292
return self._transform._tree.get_reference_revision(path)
1293
return reference_revision
1295
def tree_kind(self, trans_id):
1296
path = self._tree_id_paths.get(trans_id)
1299
kind = self._tree.path_content_summary(path)[0]
1300
if kind == 'missing':