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,
34
lazy_import.lazy_import(globals(), """
38
revision as _mod_revision,
42
from breezy.i18n import gettext
45
from .errors import (DuplicateKey,
46
BzrError, InternalBzrError)
47
from .filters import filtered_output_bytes, ContentFilterContext
48
from .mutabletree import MutableTree
49
from .osutils import (
57
from .progress import ProgressPhase
69
ROOT_PARENT = "root-parent"
72
class NoFinalPath(BzrError):
74
_fmt = ("No final name for trans_id %(trans_id)r\n"
75
"root trans-id: %(root_trans_id)r\n")
77
def __init__(self, trans_id, transform):
78
self.trans_id = trans_id
79
self.root_trans_id = transform.root
82
class ReusingTransform(BzrError):
84
_fmt = "Attempt to reuse a transform that has already been applied."
87
class MalformedTransform(InternalBzrError):
89
_fmt = "Tree transform is malformed %(conflicts)r"
92
class CantMoveRoot(BzrError):
94
_fmt = "Moving the root directory is not supported at this time"
97
class ImmortalLimbo(BzrError):
99
_fmt = """Unable to delete transform temporary directory %(limbo_dir)s.
100
Please examine %(limbo_dir)s to see if it contains any files you wish to
101
keep, and delete it when you are done."""
103
def __init__(self, limbo_dir):
104
BzrError.__init__(self)
105
self.limbo_dir = limbo_dir
108
class TransformRenameFailed(BzrError):
110
_fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s"
112
def __init__(self, from_path, to_path, why, errno):
113
self.from_path = from_path
114
self.to_path = to_path
119
def unique_add(map, key, value):
121
raise DuplicateKey(key=key)
125
class _TransformResults(object):
127
def __init__(self, modified_paths, rename_count):
128
object.__init__(self)
129
self.modified_paths = modified_paths
130
self.rename_count = rename_count
133
class TreeTransform(object):
134
"""Represent a tree transformation.
136
This object is designed to support incremental generation of the transform,
139
However, it gives optimum performance when parent directories are created
140
before their contents. The transform is then able to put child files
141
directly in their parent directory, avoiding later renames.
143
It is easy to produce malformed transforms, but they are generally
144
harmless. Attempting to apply a malformed transform will cause an
145
exception to be raised before any modifications are made to the tree.
147
Many kinds of malformed transforms can be corrected with the
148
resolve_conflicts function. The remaining ones indicate programming error,
149
such as trying to create a file with no path.
151
Two sets of file creation methods are supplied. Convenience methods are:
156
These are composed of the low-level methods:
158
* create_file or create_directory or create_symlink
162
Transform/Transaction ids
163
-------------------------
164
trans_ids are temporary ids assigned to all files involved in a transform.
165
It's possible, even common, that not all files in the Tree have trans_ids.
167
trans_ids are only valid for the TreeTransform that generated them.
170
def __init__(self, tree, pb=None):
175
# Mapping of path in old tree -> trans_id
176
self._tree_path_ids = {}
177
# Mapping trans_id -> path in old tree
178
self._tree_id_paths = {}
179
# mapping of trans_id -> new basename
181
# mapping of trans_id -> new parent trans_id
182
self._new_parent = {}
183
# mapping of trans_id with new contents -> new file_kind
184
self._new_contents = {}
185
# Set of trans_ids whose contents will be removed
186
self._removed_contents = set()
187
# Mapping of trans_id -> new execute-bit value
188
self._new_executability = {}
189
# Mapping of trans_id -> new tree-reference value
190
self._new_reference_revision = {}
191
# Set of trans_ids that will be removed
192
self._removed_id = set()
193
# Indicator of whether the transform has been applied
197
"""Support Context Manager API."""
200
def __exit__(self, exc_type, exc_val, exc_tb):
201
"""Support Context Manager API."""
204
def iter_tree_children(self, trans_id):
205
"""Iterate through the entry's tree children, if any.
207
:param trans_id: trans id to iterate
208
:returns: Iterator over paths
210
raise NotImplementedError(self.iter_tree_children)
212
def canonical_path(self, path):
215
def tree_kind(self, trans_id):
216
raise NotImplementedError(self.tree_kind)
219
"""Return a map of parent: children for known parents.
221
Only new paths and parents of tree files with assigned ids are used.
224
items = list(viewitems(self._new_parent))
225
items.extend((t, self.final_parent(t))
226
for t in list(self._tree_id_paths))
227
for trans_id, parent_id in items:
228
if parent_id not in by_parent:
229
by_parent[parent_id] = set()
230
by_parent[parent_id].add(trans_id)
234
"""Release the working tree lock, if held.
236
This is required if apply has not been invoked, but can be invoked
239
raise NotImplementedError(self.finalize)
241
def create_path(self, name, parent):
242
"""Assign a transaction id to a new path"""
243
trans_id = self.assign_id()
244
unique_add(self._new_name, trans_id, name)
245
unique_add(self._new_parent, trans_id, parent)
248
def adjust_path(self, name, parent, trans_id):
249
"""Change the path that is assigned to a transaction id."""
251
raise ValueError("Parent trans-id may not be None")
252
if trans_id == self.root:
254
self._new_name[trans_id] = name
255
self._new_parent[trans_id] = parent
257
def adjust_root_path(self, name, parent):
258
"""Emulate moving the root by moving all children, instead.
260
We do this by undoing the association of root's transaction id with the
261
current tree. This allows us to create a new directory with that
262
transaction id. We unversion the root directory and version the
263
physically new directory, and hope someone versions the tree root
266
raise NotImplementedError(self.adjust_root_path)
268
def fixup_new_roots(self):
269
"""Reinterpret requests to change the root directory
271
Instead of creating a root directory, or moving an existing directory,
272
all the attributes and children of the new root are applied to the
273
existing root directory.
275
This means that the old root trans-id becomes obsolete, so it is
276
recommended only to invoke this after the root trans-id has become
279
raise NotImplementedError(self.fixup_new_roots)
282
"""Produce a new tranform id"""
283
new_id = "new-%s" % self._id_number
287
def trans_id_tree_path(self, path):
288
"""Determine (and maybe set) the transaction ID for a tree path."""
289
path = self.canonical_path(path)
290
if path not in self._tree_path_ids:
291
self._tree_path_ids[path] = self.assign_id()
292
self._tree_id_paths[self._tree_path_ids[path]] = path
293
return self._tree_path_ids[path]
295
def get_tree_parent(self, trans_id):
296
"""Determine id of the parent in the tree."""
297
path = self._tree_id_paths[trans_id]
300
return self.trans_id_tree_path(os.path.dirname(path))
302
def delete_contents(self, trans_id):
303
"""Schedule the contents of a path entry for deletion"""
304
kind = self.tree_kind(trans_id)
306
self._removed_contents.add(trans_id)
308
def cancel_deletion(self, trans_id):
309
"""Cancel a scheduled deletion"""
310
self._removed_contents.remove(trans_id)
312
def delete_versioned(self, trans_id):
313
"""Delete and unversion a versioned file"""
314
self.delete_contents(trans_id)
315
self.unversion_file(trans_id)
317
def set_executability(self, executability, trans_id):
318
"""Schedule setting of the 'execute' bit
319
To unschedule, set to None
321
if executability is None:
322
del self._new_executability[trans_id]
324
unique_add(self._new_executability, trans_id, executability)
326
def set_tree_reference(self, revision_id, trans_id):
327
"""Set the reference associated with a directory"""
328
unique_add(self._new_reference_revision, trans_id, revision_id)
330
def version_file(self, trans_id, file_id=None):
331
"""Schedule a file to become versioned."""
332
raise NotImplementedError(self.version_file)
334
def cancel_versioning(self, trans_id):
335
"""Undo a previous versioning of a file"""
336
raise NotImplementedError(self.cancel_versioning)
338
def unversion_file(self, trans_id):
339
"""Schedule a path entry to become unversioned"""
340
self._removed_id.add(trans_id)
342
def new_paths(self, filesystem_only=False):
343
"""Determine the paths of all new and changed files.
345
:param filesystem_only: if True, only calculate values for files
346
that require renames or execute bit changes.
348
raise NotImplementedError(self.new_paths)
350
def final_kind(self, trans_id):
351
"""Determine the final file kind, after any changes applied.
353
:return: None if the file does not exist/has no contents. (It is
354
conceivable that a path would be created without the corresponding
355
contents insertion command)
357
if trans_id in self._new_contents:
358
if trans_id in self._new_reference_revision:
359
return 'tree-reference'
360
return self._new_contents[trans_id]
361
elif trans_id in self._removed_contents:
364
return self.tree_kind(trans_id)
366
def tree_path(self, trans_id):
367
"""Determine the tree path associated with the trans_id."""
368
return self._tree_id_paths.get(trans_id)
370
def final_is_versioned(self, trans_id):
371
raise NotImplementedError(self.final_is_versioned)
373
def final_parent(self, trans_id):
374
"""Determine the parent file_id, after any changes are applied.
376
ROOT_PARENT is returned for the tree root.
379
return self._new_parent[trans_id]
381
return self.get_tree_parent(trans_id)
383
def final_name(self, trans_id):
384
"""Determine the final filename, after all changes are applied."""
386
return self._new_name[trans_id]
389
return os.path.basename(self._tree_id_paths[trans_id])
391
raise NoFinalPath(trans_id, self)
393
def path_changed(self, trans_id):
394
"""Return True if a trans_id's path has changed."""
395
return (trans_id in self._new_name) or (trans_id in self._new_parent)
397
def new_contents(self, trans_id):
398
return (trans_id in self._new_contents)
400
def find_raw_conflicts(self):
401
"""Find any violations of inventory or filesystem invariants"""
402
raise NotImplementedError(self.find_raw_conflicts)
404
def new_file(self, name, parent_id, contents, file_id=None,
405
executable=None, sha1=None):
406
"""Convenience method to create files.
408
name is the name of the file to create.
409
parent_id is the transaction id of the parent directory of the file.
410
contents is an iterator of bytestrings, which will be used to produce
412
:param file_id: The inventory ID of the file, if it is to be versioned.
413
:param executable: Only valid when a file_id has been supplied.
415
raise NotImplementedError(self.new_file)
417
def new_directory(self, name, parent_id, file_id=None):
418
"""Convenience method to create directories.
420
name is the name of the directory to create.
421
parent_id is the transaction id of the parent directory of the
423
file_id is the inventory ID of the directory, if it is to be versioned.
425
raise NotImplementedError(self.new_directory)
427
def new_symlink(self, name, parent_id, target, file_id=None):
428
"""Convenience method to create symbolic link.
430
name is the name of the symlink to create.
431
parent_id is the transaction id of the parent directory of the symlink.
432
target is a bytestring of the target of the symlink.
433
file_id is the inventory ID of the file, if it is to be versioned.
435
raise NotImplementedError(self.new_symlink)
437
def new_orphan(self, trans_id, parent_id):
438
"""Schedule an item to be orphaned.
440
When a directory is about to be removed, its children, if they are not
441
versioned are moved out of the way: they don't have a parent anymore.
443
:param trans_id: The trans_id of the existing item.
444
:param parent_id: The parent trans_id of the item.
446
raise NotImplementedError(self.new_orphan)
448
def iter_changes(self):
449
"""Produce output in the same format as Tree.iter_changes.
451
Will produce nonsensical results if invoked while inventory/filesystem
452
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
454
This reads the Transform, but only reproduces changes involving a
455
file_id. Files that are not versioned in either of the FROM or TO
456
states are not reflected.
458
raise NotImplementedError(self.iter_changes)
460
def get_preview_tree(self):
461
"""Return a tree representing the result of the transform.
463
The tree is a snapshot, and altering the TreeTransform will invalidate
466
raise NotImplementedError(self.get_preview_tree)
468
def commit(self, branch, message, merge_parents=None, strict=False,
469
timestamp=None, timezone=None, committer=None, authors=None,
470
revprops=None, revision_id=None):
471
"""Commit the result of this TreeTransform to a branch.
473
:param branch: The branch to commit to.
474
:param message: The message to attach to the commit.
475
:param merge_parents: Additional parent revision-ids specified by
477
:param strict: If True, abort the commit if there are unversioned
479
:param timestamp: if not None, seconds-since-epoch for the time and
480
date. (May be a float.)
481
:param timezone: Optional timezone for timestamp, as an offset in
483
:param committer: Optional committer in email-id format.
484
(e.g. "J Random Hacker <jrandom@example.com>")
485
:param authors: Optional list of authors in email-id format.
486
:param revprops: Optional dictionary of revision properties.
487
:param revision_id: Optional revision id. (Specifying a revision-id
488
may reduce performance for some non-native formats.)
489
:return: The revision_id of the revision committed.
491
raise NotImplementedError(self.commit)
493
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
494
"""Schedule creation of a new file.
498
:param contents: an iterator of strings, all of which will be written
499
to the target destination.
500
:param trans_id: TreeTransform handle
501
:param mode_id: If not None, force the mode of the target file to match
502
the mode of the object referenced by mode_id.
503
Otherwise, we will try to preserve mode bits of an existing file.
504
:param sha1: If the sha1 of this content is already known, pass it in.
505
We can use it to prevent future sha1 computations.
507
raise NotImplementedError(self.create_file)
509
def create_directory(self, trans_id):
510
"""Schedule creation of a new directory.
512
See also new_directory.
514
raise NotImplementedError(self.create_directory)
516
def create_symlink(self, target, trans_id):
517
"""Schedule creation of a new symbolic link.
519
target is a bytestring.
520
See also new_symlink.
522
raise NotImplementedError(self.create_symlink)
524
def create_hardlink(self, path, trans_id):
525
"""Schedule creation of a hard link"""
526
raise NotImplementedError(self.create_hardlink)
528
def cancel_creation(self, trans_id):
529
"""Cancel the creation of new file contents."""
530
raise NotImplementedError(self.cancel_creation)
532
def cook_conflicts(self, raw_conflicts):
535
raise NotImplementedError(self.cook_conflicts)
538
class OrphaningError(errors.BzrError):
540
# Only bugs could lead to such exception being seen by the user
541
internal_error = True
542
_fmt = "Error while orphaning %s in %s directory"
544
def __init__(self, orphan, parent):
545
errors.BzrError.__init__(self)
550
class OrphaningForbidden(OrphaningError):
552
_fmt = "Policy: %s doesn't allow creating orphans."
554
def __init__(self, policy):
555
errors.BzrError.__init__(self)
559
def move_orphan(tt, orphan_id, parent_id):
560
"""See TreeTransformBase.new_orphan.
562
This creates a new orphan in the `brz-orphans` dir at the root of the
565
:param tt: The TreeTransform orphaning `trans_id`.
567
:param orphan_id: The trans id that should be orphaned.
569
:param parent_id: The orphan parent trans id.
571
# Add the orphan dir if it doesn't exist
572
orphan_dir_basename = 'brz-orphans'
573
od_id = tt.trans_id_tree_path(orphan_dir_basename)
574
if tt.final_kind(od_id) is None:
575
tt.create_directory(od_id)
576
parent_path = tt._tree_id_paths[parent_id]
577
# Find a name that doesn't exist yet in the orphan dir
578
actual_name = tt.final_name(orphan_id)
579
new_name = tt._available_backup_name(actual_name, od_id)
580
tt.adjust_path(new_name, od_id, orphan_id)
581
trace.warning('%s has been orphaned in %s'
582
% (joinpath(parent_path, actual_name), orphan_dir_basename))
585
def refuse_orphan(tt, orphan_id, parent_id):
586
"""See TreeTransformBase.new_orphan.
588
This refuses to create orphan, letting the caller handle the conflict.
590
raise OrphaningForbidden('never')
593
orphaning_registry = registry.Registry()
594
orphaning_registry.register(
595
u'conflict', refuse_orphan,
596
'Leave orphans in place and create a conflict on the directory.')
597
orphaning_registry.register(
598
u'move', move_orphan,
599
'Move orphans into the brz-orphans directory.')
600
orphaning_registry._set_default_key(u'conflict')
603
opt_transform_orphan = _mod_config.RegistryOption(
604
'transform.orphan_policy', orphaning_registry,
605
help='Policy for orphaned files during transform operations.',
609
def joinpath(parent, child):
610
"""Join tree-relative paths, handling the tree root specially"""
611
if parent is None or parent == "":
614
return pathjoin(parent, child)
617
class FinalPaths(object):
618
"""Make path calculation cheap by memoizing paths.
620
The underlying tree must not be manipulated between calls, or else
621
the results will likely be incorrect.
624
def __init__(self, transform):
625
object.__init__(self)
626
self._known_paths = {}
627
self.transform = transform
629
def _determine_path(self, trans_id):
630
if trans_id == self.transform.root or trans_id == ROOT_PARENT:
632
name = self.transform.final_name(trans_id)
633
parent_id = self.transform.final_parent(trans_id)
634
if parent_id == self.transform.root:
637
return pathjoin(self.get_path(parent_id), name)
639
def get_path(self, trans_id):
640
"""Find the final path associated with a trans_id"""
641
if trans_id not in self._known_paths:
642
self._known_paths[trans_id] = self._determine_path(trans_id)
643
return self._known_paths[trans_id]
645
def get_paths(self, trans_ids):
646
return [(self.get_path(t), t) for t in trans_ids]
649
def _reparent_children(tt, old_parent, new_parent):
650
for child in tt.iter_tree_children(old_parent):
651
tt.adjust_path(tt.final_name(child), new_parent, child)
654
def _reparent_transform_children(tt, old_parent, new_parent):
655
by_parent = tt.by_parent()
656
for child in by_parent[old_parent]:
657
tt.adjust_path(tt.final_name(child), new_parent, child)
658
return by_parent[old_parent]
661
def _content_match(tree, entry, tree_path, kind, target_path):
662
if entry.kind != kind:
664
if entry.kind == "directory":
666
if entry.kind == "file":
667
with open(target_path, 'rb') as f1, \
668
tree.get_file(tree_path) as f2:
669
if osutils.compare_files(f1, f2):
671
elif entry.kind == "symlink":
672
if tree.get_symlink_target(tree_path) == os.readlink(target_path):
677
def new_by_entry(path, tt, entry, parent_id, tree):
678
"""Create a new file according to its inventory entry"""
682
with tree.get_file(path) as f:
683
executable = tree.is_executable(path)
685
name, parent_id, osutils.file_iterator(f), entry.file_id,
687
elif kind in ('directory', 'tree-reference'):
688
trans_id = tt.new_directory(name, parent_id, entry.file_id)
689
if kind == 'tree-reference':
690
tt.set_tree_reference(entry.reference_revision, trans_id)
692
elif kind == 'symlink':
693
target = tree.get_symlink_target(path)
694
return tt.new_symlink(name, parent_id, target, entry.file_id)
696
raise errors.BadFileKindError(name, kind)
699
def create_from_tree(tt, trans_id, tree, path, chunks=None,
700
filter_tree_path=None):
701
"""Create new file contents according to tree contents.
703
:param filter_tree_path: the tree path to use to lookup
704
content filters to apply to the bytes output in the working tree.
705
This only applies if the working tree supports content filtering.
707
kind = tree.kind(path)
708
if kind == 'directory':
709
tt.create_directory(trans_id)
712
f = tree.get_file(path)
713
chunks = osutils.file_iterator(f)
718
if wt.supports_content_filtering() and filter_tree_path is not None:
719
filters = wt._content_filter_stack(filter_tree_path)
720
chunks = filtered_output_bytes(
722
ContentFilterContext(filter_tree_path, tree))
723
tt.create_file(chunks, trans_id)
727
elif kind == "symlink":
728
tt.create_symlink(tree.get_symlink_target(path), trans_id)
730
raise AssertionError('Unknown kind %r' % kind)
733
def create_entry_executability(tt, entry, trans_id):
734
"""Set the executability of a trans_id according to an inventory entry"""
735
if entry.kind == "file":
736
tt.set_executability(entry.executable, trans_id)
739
def _prepare_revert_transform(es, working_tree, target_tree, tt, filenames,
740
backups, pp, basis_tree=None,
741
merge_modified=None):
742
with ui.ui_factory.nested_progress_bar() as child_pb:
743
if merge_modified is None:
744
merge_modified = working_tree.merge_modified()
745
merge_modified = _alter_files(es, working_tree, target_tree, tt,
746
child_pb, filenames, backups,
747
merge_modified, basis_tree)
748
with ui.ui_factory.nested_progress_bar() as child_pb:
749
raw_conflicts = resolve_conflicts(
750
tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
751
conflicts = tt.cook_conflicts(raw_conflicts)
752
return conflicts, merge_modified
755
def revert(working_tree, target_tree, filenames, backups=False,
756
pb=None, change_reporter=None, merge_modified=None, basis_tree=None):
757
"""Revert a working tree's contents to those of a target tree."""
758
with cleanup.ExitStack() as es:
759
pb = es.enter_context(ui.ui_factory.nested_progress_bar())
760
es.enter_context(target_tree.lock_read())
761
tt = es.enter_context(working_tree.transform(pb))
762
pp = ProgressPhase("Revert phase", 3, pb)
763
conflicts, merge_modified = _prepare_revert_transform(
764
es, working_tree, target_tree, tt, filenames, backups, pp)
767
change_reporter = delta._ChangeReporter(
768
unversioned_filter=working_tree.is_ignored)
769
delta.report_changes(tt.iter_changes(), change_reporter)
770
for conflict in conflicts:
771
trace.warning(text_type(conflict))
774
if working_tree.supports_merge_modified():
775
working_tree.set_merge_modified(merge_modified)
779
def _alter_files(es, working_tree, target_tree, tt, pb, specific_files,
780
backups, merge_modified, basis_tree=None):
781
if basis_tree is not None:
782
es.enter_context(basis_tree.lock_read())
783
# We ask the working_tree for its changes relative to the target, rather
784
# than the target changes relative to the working tree. Because WT4 has an
785
# optimizer to compare itself to a target, but no optimizer for the
787
change_list = working_tree.iter_changes(
788
target_tree, specific_files=specific_files, pb=pb)
789
if not target_tree.is_versioned(u''):
794
for id_num, change in enumerate(change_list):
795
target_path, wt_path = change.path
796
target_versioned, wt_versioned = change.versioned
797
target_parent = change.parent_id[0]
798
target_name, wt_name = change.name
799
target_kind, wt_kind = change.kind
800
target_executable, wt_executable = change.executable
801
if skip_root and wt_path == '':
804
if wt_path is not None:
805
trans_id = tt.trans_id_tree_path(wt_path)
807
trans_id = tt.assign_id()
808
if change.changed_content:
810
if wt_kind == 'file' and (backups or target_kind is None):
811
wt_sha1 = working_tree.get_file_sha1(wt_path)
812
if merge_modified.get(wt_path) != wt_sha1:
813
# acquire the basis tree lazily to prevent the
814
# expense of accessing it when it's not needed ?
815
# (Guessing, RBC, 200702)
816
if basis_tree is None:
817
basis_tree = working_tree.basis_tree()
818
es.enter_context(basis_tree.lock_read())
819
basis_inter = InterTree.get(basis_tree, working_tree)
820
basis_path = basis_inter.find_source_path(wt_path)
821
if basis_path is None:
822
if target_kind is None and not target_versioned:
825
if wt_sha1 != basis_tree.get_file_sha1(basis_path):
827
if wt_kind is not None:
829
tt.delete_contents(trans_id)
830
elif target_kind is not None:
831
parent_trans_id = tt.trans_id_tree_path(osutils.dirname(wt_path))
832
backup_name = tt._available_backup_name(
833
wt_name, parent_trans_id)
834
tt.adjust_path(backup_name, parent_trans_id, trans_id)
835
new_trans_id = tt.create_path(wt_name, parent_trans_id)
836
if wt_versioned and target_versioned:
837
tt.unversion_file(trans_id)
839
new_trans_id, file_id=getattr(change, 'file_id', None))
840
# New contents should have the same unix perms as old
843
trans_id = new_trans_id
844
if target_kind in ('directory', 'tree-reference'):
845
tt.create_directory(trans_id)
846
if target_kind == 'tree-reference':
847
revision = target_tree.get_reference_revision(
849
tt.set_tree_reference(revision, trans_id)
850
elif target_kind == 'symlink':
851
tt.create_symlink(target_tree.get_symlink_target(
852
target_path), trans_id)
853
elif target_kind == 'file':
854
deferred_files.append(
855
(target_path, (trans_id, mode_id, target_path)))
856
if basis_tree is None:
857
basis_tree = working_tree.basis_tree()
858
es.enter_context(basis_tree.lock_read())
859
new_sha1 = target_tree.get_file_sha1(target_path)
860
basis_inter = InterTree.get(basis_tree, target_tree)
861
basis_path = basis_inter.find_source_path(target_path)
862
if (basis_path is not None and
863
new_sha1 == basis_tree.get_file_sha1(basis_path)):
864
# If the new contents of the file match what is in basis,
865
# then there is no need to store in merge_modified.
866
if basis_path in merge_modified:
867
del merge_modified[basis_path]
869
merge_modified[target_path] = new_sha1
871
# preserve the execute bit when backing up
872
if keep_content and wt_executable == target_executable:
873
tt.set_executability(target_executable, trans_id)
874
elif target_kind is not None:
875
raise AssertionError(target_kind)
876
if not wt_versioned and target_versioned:
878
trans_id, file_id=getattr(change, 'file_id', None))
879
if wt_versioned and not target_versioned:
880
tt.unversion_file(trans_id)
881
if (target_name is not None
882
and (wt_name != target_name or change.is_reparented())):
883
if target_path == '':
884
parent_trans = ROOT_PARENT
886
parent_trans = tt.trans_id_file_id(target_parent)
887
if wt_path == '' and wt_versioned:
888
tt.adjust_root_path(target_name, parent_trans)
890
tt.adjust_path(target_name, parent_trans, trans_id)
891
if wt_executable != target_executable and target_kind == "file":
892
tt.set_executability(target_executable, trans_id)
893
if working_tree.supports_content_filtering():
894
for (trans_id, mode_id, target_path), bytes in (
895
target_tree.iter_files_bytes(deferred_files)):
896
# We're reverting a tree to the target tree so using the
897
# target tree to find the file path seems the best choice
898
# here IMO - Ian C 27/Oct/2009
899
filters = working_tree._content_filter_stack(target_path)
900
bytes = filtered_output_bytes(
902
ContentFilterContext(target_path, working_tree))
903
tt.create_file(bytes, trans_id, mode_id)
905
for (trans_id, mode_id, target_path), bytes in target_tree.iter_files_bytes(
907
tt.create_file(bytes, trans_id, mode_id)
909
return merge_modified
912
def resolve_conflicts(tt, pb=None, pass_func=None):
913
"""Make many conflict-resolution attempts, but die if they fail"""
914
if pass_func is None:
915
pass_func = conflict_pass
916
new_conflicts = set()
917
with ui.ui_factory.nested_progress_bar() as pb:
919
pb.update(gettext('Resolution pass'), n + 1, 10)
920
conflicts = tt.find_raw_conflicts()
921
if len(conflicts) == 0:
923
new_conflicts.update(pass_func(tt, conflicts))
924
raise MalformedTransform(conflicts=conflicts)
927
def resolve_duplicate_id(tt, path_tree, c_type, old_trans_id, trans_id):
928
tt.unversion_file(old_trans_id)
929
yield (c_type, 'Unversioned existing file', old_trans_id, trans_id)
932
def resolve_duplicate(tt, path_tree, c_type, last_trans_id, trans_id, name):
933
# files that were renamed take precedence
934
final_parent = tt.final_parent(last_trans_id)
935
if tt.path_changed(last_trans_id):
936
existing_file, new_file = trans_id, last_trans_id
938
existing_file, new_file = last_trans_id, trans_id
939
if (not tt._tree.has_versioned_directories() and
940
tt.final_kind(trans_id) == 'directory' and
941
tt.final_kind(last_trans_id) == 'directory'):
942
_reparent_transform_children(tt, existing_file, new_file)
943
tt.delete_contents(existing_file)
944
tt.unversion_file(existing_file)
945
tt.cancel_creation(existing_file)
947
new_name = tt.final_name(existing_file) + '.moved'
948
tt.adjust_path(new_name, final_parent, existing_file)
949
yield (c_type, 'Moved existing file to', existing_file, new_file)
952
def resolve_parent_loop(tt, path_tree, c_type, cur):
953
# break the loop by undoing one of the ops that caused the loop
954
while not tt.path_changed(cur):
955
cur = tt.final_parent(cur)
956
yield (c_type, 'Cancelled move', cur, tt.final_parent(cur),)
957
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
960
def resolve_missing_parent(tt, path_tree, c_type, trans_id):
961
if trans_id in tt._removed_contents:
962
cancel_deletion = True
963
orphans = tt._get_potential_orphans(trans_id)
965
cancel_deletion = False
966
# All children are orphans
969
tt.new_orphan(o, trans_id)
970
except OrphaningError:
971
# Something bad happened so we cancel the directory
972
# deletion which will leave it in place with a
973
# conflict. The user can deal with it from there.
974
# Note that this also catch the case where we don't
975
# want to create orphans and leave the directory in
977
cancel_deletion = True
980
# Cancel the directory deletion
981
tt.cancel_deletion(trans_id)
982
yield ('deleting parent', 'Not deleting', trans_id)
986
tt.final_name(trans_id)
988
if path_tree is not None:
989
file_id = tt.final_file_id(trans_id)
991
file_id = tt.inactive_file_id(trans_id)
992
_, entry = next(path_tree.iter_entries_by_dir(
993
specific_files=[path_tree.id2path(file_id)]))
994
# special-case the other tree root (move its
995
# children to current root)
996
if entry.parent_id is None:
998
moved = _reparent_transform_children(
999
tt, trans_id, tt.root)
1001
yield (c_type, 'Moved to root', child)
1003
parent_trans_id = tt.trans_id_file_id(
1005
tt.adjust_path(entry.name, parent_trans_id,
1008
tt.create_directory(trans_id)
1009
yield (c_type, 'Created directory', trans_id)
1012
def resolve_unversioned_parent(tt, path_tree, c_type, trans_id):
1013
file_id = tt.inactive_file_id(trans_id)
1014
# special-case the other tree root (move its children instead)
1015
if path_tree and path_tree.path2id('') == file_id:
1016
# This is the root entry, skip it
1018
tt.version_file(trans_id, file_id=file_id)
1019
yield (c_type, 'Versioned directory', trans_id)
1022
def resolve_non_directory_parent(tt, path_tree, c_type, parent_id):
1023
parent_parent = tt.final_parent(parent_id)
1024
parent_name = tt.final_name(parent_id)
1025
# TODO(jelmer): Make this code transform-specific
1026
if tt._tree.supports_setting_file_ids():
1027
parent_file_id = tt.final_file_id(parent_id)
1029
parent_file_id = b'DUMMY'
1030
new_parent_id = tt.new_directory(parent_name + '.new',
1031
parent_parent, parent_file_id)
1032
_reparent_transform_children(tt, parent_id, new_parent_id)
1033
if parent_file_id is not None:
1034
tt.unversion_file(parent_id)
1035
yield (c_type, 'Created directory', new_parent_id)
1038
def resolve_versioning_no_contents(tt, path_tree, c_type, trans_id):
1039
tt.cancel_versioning(trans_id)
1043
CONFLICT_RESOLVERS = {
1044
'duplicate id': resolve_duplicate_id,
1045
'duplicate': resolve_duplicate,
1046
'parent loop': resolve_parent_loop,
1047
'missing parent': resolve_missing_parent,
1048
'unversioned parent': resolve_unversioned_parent,
1049
'non-directory parent': resolve_non_directory_parent,
1050
'versioning no contents': resolve_versioning_no_contents,
1054
def conflict_pass(tt, conflicts, path_tree=None):
1055
"""Resolve some classes of conflicts.
1057
:param tt: The transform to resolve conflicts in
1058
:param conflicts: The conflicts to resolve
1059
:param path_tree: A Tree to get supplemental paths from
1061
new_conflicts = set()
1062
for conflict in conflicts:
1063
resolver = CONFLICT_RESOLVERS.get(conflict[0])
1064
if resolver is None:
1066
new_conflicts.update(resolver(tt, path_tree, *conflict))
1067
return new_conflicts
1070
class _FileMover(object):
1071
"""Moves and deletes files for TreeTransform, tracking operations"""
1074
self.past_renames = []
1075
self.pending_deletions = []
1077
def rename(self, from_, to):
1078
"""Rename a file from one path to another."""
1080
os.rename(from_, to)
1081
except OSError as e:
1082
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
1083
raise errors.FileExists(to, str(e))
1084
# normal OSError doesn't include filenames so it's hard to see where
1085
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
1086
raise TransformRenameFailed(from_, to, str(e), e.errno)
1087
self.past_renames.append((from_, to))
1089
def pre_delete(self, from_, to):
1090
"""Rename a file out of the way and mark it for deletion.
1092
Unlike os.unlink, this works equally well for files and directories.
1093
:param from_: The current file path
1094
:param to: A temporary path for the file
1096
self.rename(from_, to)
1097
self.pending_deletions.append(to)
1100
"""Reverse all renames that have been performed"""
1101
for from_, to in reversed(self.past_renames):
1103
os.rename(to, from_)
1104
except OSError as e:
1105
raise TransformRenameFailed(to, from_, str(e), e.errno)
1106
# after rollback, don't reuse _FileMover
1107
self.past_renames = None
1108
self.pending_deletions = None
1110
def apply_deletions(self):
1111
"""Apply all marked deletions"""
1112
for path in self.pending_deletions:
1114
# after apply_deletions, don't reuse _FileMover
1115
self.past_renames = None
1116
self.pending_deletions = None
1119
def link_tree(target_tree, source_tree):
1120
"""Where possible, hard-link files in a tree to those in another tree.
1122
:param target_tree: Tree to change
1123
:param source_tree: Tree to hard-link from
1125
with target_tree.transform() as tt:
1126
for change in target_tree.iter_changes(source_tree, include_unchanged=True):
1127
if change.changed_content:
1129
if change.kind != ('file', 'file'):
1131
if change.executable[0] != change.executable[1]:
1133
trans_id = tt.trans_id_tree_path(change.path[1])
1134
tt.delete_contents(trans_id)
1135
tt.create_hardlink(source_tree.abspath(change.path[0]), trans_id)
1139
class PreviewTree(object):
1142
def __init__(self, transform):
1143
self._transform = transform
1144
self._parent_ids = []
1145
self.__by_parent = None
1146
self._path2trans_id_cache = {}
1147
self._all_children_cache = {}
1148
self._final_name_cache = {}
1150
def supports_setting_file_ids(self):
1151
raise NotImplementedError(self.supports_setting_file_ids)
1154
def _by_parent(self):
1155
if self.__by_parent is None:
1156
self.__by_parent = self._transform.by_parent()
1157
return self.__by_parent
1159
def get_parent_ids(self):
1160
return self._parent_ids
1162
def set_parent_ids(self, parent_ids):
1163
self._parent_ids = parent_ids
1165
def get_revision_tree(self, revision_id):
1166
return self._transform._tree.get_revision_tree(revision_id)
1168
def is_locked(self):
1171
def lock_read(self):
1172
# Perhaps in theory, this should lock the TreeTransform?
1173
return lock.LogicalLockResult(self.unlock)
1178
def _path2trans_id(self, path):
1179
"""Look up the trans id associated with a path.
1181
:param path: path to look up, None when the path does not exist
1184
# We must not use None here, because that is a valid value to store.
1185
trans_id = self._path2trans_id_cache.get(path, object)
1186
if trans_id is not object:
1188
segments = osutils.splitpath(path)
1189
cur_parent = self._transform.root
1190
for cur_segment in segments:
1191
for child in self._all_children(cur_parent):
1192
final_name = self._final_name_cache.get(child)
1193
if final_name is None:
1194
final_name = self._transform.final_name(child)
1195
self._final_name_cache[child] = final_name
1196
if final_name == cur_segment:
1200
self._path2trans_id_cache[path] = None
1202
self._path2trans_id_cache[path] = cur_parent
1205
def _all_children(self, trans_id):
1206
children = self._all_children_cache.get(trans_id)
1207
if children is not None:
1209
children = set(self._transform.iter_tree_children(trans_id))
1210
# children in the _new_parent set are provided by _by_parent.
1211
children.difference_update(self._transform._new_parent)
1212
children.update(self._by_parent.get(trans_id, []))
1213
self._all_children_cache[trans_id] = children
1216
def get_file_with_stat(self, path):
1217
return self.get_file(path), None
1219
def is_executable(self, path):
1220
trans_id = self._path2trans_id(path)
1221
if trans_id is None:
1224
return self._transform._new_executability[trans_id]
1227
return self._transform._tree.is_executable(path)
1228
except OSError as e:
1229
if e.errno == errno.ENOENT:
1232
except errors.NoSuchFile:
1235
def has_filename(self, path):
1236
trans_id = self._path2trans_id(path)
1237
if trans_id in self._transform._new_contents:
1239
elif trans_id in self._transform._removed_contents:
1242
return self._transform._tree.has_filename(path)
1244
def get_file_sha1(self, path, stat_value=None):
1245
trans_id = self._path2trans_id(path)
1246
if trans_id is None:
1247
raise errors.NoSuchFile(path)
1248
kind = self._transform._new_contents.get(trans_id)
1250
return self._transform._tree.get_file_sha1(path)
1252
with self.get_file(path) as fileobj:
1253
return osutils.sha_file(fileobj)
1255
def get_file_verifier(self, path, stat_value=None):
1256
trans_id = self._path2trans_id(path)
1257
if trans_id is None:
1258
raise errors.NoSuchFile(path)
1259
kind = self._transform._new_contents.get(trans_id)
1261
return self._transform._tree.get_file_verifier(path)
1263
with self.get_file(path) as fileobj:
1264
return ("SHA1", osutils.sha_file(fileobj))
1266
def kind(self, path):
1267
trans_id = self._path2trans_id(path)
1268
if trans_id is None:
1269
raise errors.NoSuchFile(path)
1270
return self._transform.final_kind(trans_id)
1272
def stored_kind(self, path):
1273
trans_id = self._path2trans_id(path)
1274
if trans_id is None:
1275
raise errors.NoSuchFile(path)
1277
return self._transform._new_contents[trans_id]
1279
return self._transform._tree.stored_kind(path)
1281
def _get_repository(self):
1282
repo = getattr(self._transform._tree, '_repository', None)
1284
repo = self._transform._tree.branch.repository
1287
def _iter_parent_trees(self):
1288
for revision_id in self.get_parent_ids():
1290
yield self.revision_tree(revision_id)
1291
except errors.NoSuchRevisionInTree:
1292
yield self._get_repository().revision_tree(revision_id)
1294
def get_file_size(self, path):
1295
"""See Tree.get_file_size"""
1296
trans_id = self._path2trans_id(path)
1297
if trans_id is None:
1298
raise errors.NoSuchFile(path)
1299
kind = self._transform.final_kind(trans_id)
1302
if trans_id in self._transform._new_contents:
1303
return self._stat_limbo_file(trans_id).st_size
1304
if self.kind(path) == 'file':
1305
return self._transform._tree.get_file_size(path)
1309
def get_reference_revision(self, path):
1310
trans_id = self._path2trans_id(path)
1311
if trans_id is None:
1312
raise errors.NoSuchFile(path)
1313
reference_revision = self._transform._new_reference_revision.get(trans_id)
1314
if reference_revision is None:
1315
return self._transform._tree.get_reference_revision(path)
1316
return reference_revision
1318
def tree_kind(self, trans_id):
1319
path = self._tree_id_paths.get(trans_id)
1322
kind = self._tree.path_content_summary(path)[0]
1323
if kind == 'missing':