1
# Copyright (C) 2006, 2007, 2008 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from stat import S_ISREG, S_IEXEC
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
30
revision as _mod_revision,
33
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
34
ReusingTransform, NotVersionedError, CantMoveRoot,
35
ExistingLimbo, ImmortalLimbo, NoFinalPath,
37
from bzrlib.inventory import InventoryEntry
38
from bzrlib.osutils import (
48
from bzrlib.progress import DummyProgress, ProgressPhase
49
from bzrlib.symbol_versioning import (
53
from bzrlib.trace import mutter, warning
54
from bzrlib import tree
56
import bzrlib.urlutils as urlutils
59
ROOT_PARENT = "root-parent"
62
def unique_add(map, key, value):
64
raise DuplicateKey(key=key)
68
class _TransformResults(object):
69
def __init__(self, modified_paths, rename_count):
71
self.modified_paths = modified_paths
72
self.rename_count = rename_count
75
class TreeTransformBase(object):
76
"""The base class for TreeTransform and TreeTransformBase"""
78
def __init__(self, tree, limbodir, pb=DummyProgress(),
82
:param tree: The tree that will be transformed, but not necessarily
84
:param limbodir: A directory where new files can be stored until
85
they are installed in their proper places
86
:param pb: A ProgressBar indicating how much progress is being made
87
:param case_sensitive: If True, the target of the transform is
88
case sensitive, not just case preserving.
92
self._limbodir = limbodir
93
self._deletiondir = None
95
# mapping of trans_id -> new basename
97
# mapping of trans_id -> new parent trans_id
99
# mapping of trans_id with new contents -> new file_kind
100
self._new_contents = {}
101
# A mapping of transform ids to their limbo filename
102
self._limbo_files = {}
103
# A mapping of transform ids to a set of the transform ids of children
104
# that their limbo directory has
105
self._limbo_children = {}
106
# Map transform ids to maps of child filename to child transform id
107
self._limbo_children_names = {}
108
# List of transform ids that need to be renamed from limbo into place
109
self._needs_rename = set()
110
# Set of trans_ids whose contents will be removed
111
self._removed_contents = set()
112
# Mapping of trans_id -> new execute-bit value
113
self._new_executability = {}
114
# Mapping of trans_id -> new tree-reference value
115
self._new_reference_revision = {}
116
# Mapping of trans_id -> new file_id
118
# Mapping of old file-id -> trans_id
119
self._non_present_ids = {}
120
# Mapping of new file_id -> trans_id
122
# Set of file_ids that will be removed
123
self._removed_id = set()
124
# Mapping of path in old tree -> trans_id
125
self._tree_path_ids = {}
126
# Mapping trans_id -> path in old tree
127
self._tree_id_paths = {}
128
# Cache of realpath results, to speed up canonical_path
130
# Cache of relpath results, to speed up canonical_path
132
# The trans_id that will be used as the tree root
133
root_id = tree.get_root_id()
134
if root_id is not None:
135
self._new_root = self.trans_id_tree_file_id(root_id)
137
self._new_root = None
138
# Indictor of whether the transform has been applied
142
# Whether the target is case sensitive
143
self._case_sensitive_target = case_sensitive
144
# A counter of how many files have been renamed
145
self.rename_count = 0
147
def __get_root(self):
148
return self._new_root
150
root = property(__get_root)
153
"""Release the working tree lock, if held, clean up limbo dir.
155
This is required if apply has not been invoked, but can be invoked
158
if self._tree is None:
161
entries = [(self._limbo_name(t), t, k) for t, k in
162
self._new_contents.iteritems()]
163
entries.sort(reverse=True)
164
for path, trans_id, kind in entries:
165
if kind == "directory":
170
os.rmdir(self._limbodir)
172
# We don't especially care *why* the dir is immortal.
173
raise ImmortalLimbo(self._limbodir)
175
if self._deletiondir is not None:
176
os.rmdir(self._deletiondir)
178
raise errors.ImmortalPendingDeletion(self._deletiondir)
183
def _assign_id(self):
184
"""Produce a new tranform id"""
185
new_id = "new-%s" % self._id_number
189
def create_path(self, name, parent):
190
"""Assign a transaction id to a new path"""
191
trans_id = self._assign_id()
192
unique_add(self._new_name, trans_id, name)
193
unique_add(self._new_parent, trans_id, parent)
196
def adjust_path(self, name, parent, trans_id):
197
"""Change the path that is assigned to a transaction id."""
198
if trans_id == self._new_root:
200
previous_parent = self._new_parent.get(trans_id)
201
previous_name = self._new_name.get(trans_id)
202
self._new_name[trans_id] = name
203
self._new_parent[trans_id] = parent
204
if parent == ROOT_PARENT:
205
if self._new_root is not None:
206
raise ValueError("Cannot have multiple roots.")
207
self._new_root = trans_id
208
if (trans_id in self._limbo_files and
209
trans_id not in self._needs_rename):
210
self._rename_in_limbo([trans_id])
211
self._limbo_children[previous_parent].remove(trans_id)
212
del self._limbo_children_names[previous_parent][previous_name]
214
def _rename_in_limbo(self, trans_ids):
215
"""Fix limbo names so that the right final path is produced.
217
This means we outsmarted ourselves-- we tried to avoid renaming
218
these files later by creating them with their final names in their
219
final parents. But now the previous name or parent is no longer
220
suitable, so we have to rename them.
222
Even for trans_ids that have no new contents, we must remove their
223
entries from _limbo_files, because they are now stale.
225
for trans_id in trans_ids:
226
old_path = self._limbo_files.pop(trans_id)
227
if trans_id not in self._new_contents:
229
new_path = self._limbo_name(trans_id)
230
os.rename(old_path, new_path)
232
def adjust_root_path(self, name, parent):
233
"""Emulate moving the root by moving all children, instead.
235
We do this by undoing the association of root's transaction id with the
236
current tree. This allows us to create a new directory with that
237
transaction id. We unversion the root directory and version the
238
physically new directory, and hope someone versions the tree root
241
old_root = self._new_root
242
old_root_file_id = self.final_file_id(old_root)
243
# force moving all children of root
244
for child_id in self.iter_tree_children(old_root):
245
if child_id != parent:
246
self.adjust_path(self.final_name(child_id),
247
self.final_parent(child_id), child_id)
248
file_id = self.final_file_id(child_id)
249
if file_id is not None:
250
self.unversion_file(child_id)
251
self.version_file(file_id, child_id)
253
# the physical root needs a new transaction id
254
self._tree_path_ids.pop("")
255
self._tree_id_paths.pop(old_root)
256
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
257
if parent == old_root:
258
parent = self._new_root
259
self.adjust_path(name, parent, old_root)
260
self.create_directory(old_root)
261
self.version_file(old_root_file_id, old_root)
262
self.unversion_file(self._new_root)
264
def trans_id_tree_file_id(self, inventory_id):
265
"""Determine the transaction id of a working tree file.
267
This reflects only files that already exist, not ones that will be
268
added by transactions.
270
if inventory_id is None:
271
raise ValueError('None is not a valid file id')
272
path = self._tree.id2path(inventory_id)
273
return self.trans_id_tree_path(path)
275
def trans_id_file_id(self, file_id):
276
"""Determine or set the transaction id associated with a file ID.
277
A new id is only created for file_ids that were never present. If
278
a transaction has been unversioned, it is deliberately still returned.
279
(this will likely lead to an unversioned parent conflict.)
282
raise ValueError('None is not a valid file id')
283
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
284
return self._r_new_id[file_id]
287
self._tree.iter_entries_by_dir([file_id]).next()
288
except StopIteration:
289
if file_id in self._non_present_ids:
290
return self._non_present_ids[file_id]
292
trans_id = self._assign_id()
293
self._non_present_ids[file_id] = trans_id
296
return self.trans_id_tree_file_id(file_id)
298
def canonical_path(self, path):
299
"""Get the canonical tree-relative path"""
300
# don't follow final symlinks
301
abs = self._tree.abspath(path)
302
if abs in self._relpaths:
303
return self._relpaths[abs]
304
dirname, basename = os.path.split(abs)
305
if dirname not in self._realpaths:
306
self._realpaths[dirname] = os.path.realpath(dirname)
307
dirname = self._realpaths[dirname]
308
abs = pathjoin(dirname, basename)
309
if dirname in self._relpaths:
310
relpath = pathjoin(self._relpaths[dirname], basename)
311
relpath = relpath.rstrip('/\\')
313
relpath = self._tree.relpath(abs)
314
self._relpaths[abs] = relpath
317
def trans_id_tree_path(self, path):
318
"""Determine (and maybe set) the transaction ID for a tree path."""
319
path = self.canonical_path(path)
320
if path not in self._tree_path_ids:
321
self._tree_path_ids[path] = self._assign_id()
322
self._tree_id_paths[self._tree_path_ids[path]] = path
323
return self._tree_path_ids[path]
325
def get_tree_parent(self, trans_id):
326
"""Determine id of the parent in the tree."""
327
path = self._tree_id_paths[trans_id]
330
return self.trans_id_tree_path(os.path.dirname(path))
332
def create_file(self, contents, trans_id, mode_id=None):
333
"""Schedule creation of a new file.
337
Contents is an iterator of strings, all of which will be written
338
to the target destination.
340
New file takes the permissions of any existing file with that id,
341
unless mode_id is specified.
343
name = self._limbo_name(trans_id)
347
unique_add(self._new_contents, trans_id, 'file')
349
# Clean up the file, it never got registered so
350
# TreeTransform.finalize() won't clean it up.
355
f.writelines(contents)
358
self._set_mode(trans_id, mode_id, S_ISREG)
360
def _set_mode(self, trans_id, mode_id, typefunc):
361
"""Set the mode of new file contents.
362
The mode_id is the existing file to get the mode from (often the same
363
as trans_id). The operation is only performed if there's a mode match
364
according to typefunc.
369
old_path = self._tree_id_paths[mode_id]
373
mode = os.stat(self._tree.abspath(old_path)).st_mode
375
if e.errno in (errno.ENOENT, errno.ENOTDIR):
376
# Either old_path doesn't exist, or the parent of the
377
# target is not a directory (but will be one eventually)
378
# Either way, we know it doesn't exist *right now*
379
# See also bug #248448
384
os.chmod(self._limbo_name(trans_id), mode)
386
def create_hardlink(self, path, trans_id):
387
"""Schedule creation of a hard link"""
388
name = self._limbo_name(trans_id)
392
if e.errno != errno.EPERM:
394
raise errors.HardLinkNotSupported(path)
396
unique_add(self._new_contents, trans_id, 'file')
398
# Clean up the file, it never got registered so
399
# TreeTransform.finalize() won't clean it up.
403
def create_directory(self, trans_id):
404
"""Schedule creation of a new directory.
406
See also new_directory.
408
os.mkdir(self._limbo_name(trans_id))
409
unique_add(self._new_contents, trans_id, 'directory')
411
def create_symlink(self, target, trans_id):
412
"""Schedule creation of a new symbolic link.
414
target is a bytestring.
415
See also new_symlink.
418
os.symlink(target, self._limbo_name(trans_id))
419
unique_add(self._new_contents, trans_id, 'symlink')
422
path = FinalPaths(self).get_path(trans_id)
425
raise UnableCreateSymlink(path=path)
427
def cancel_creation(self, trans_id):
428
"""Cancel the creation of new file contents."""
429
del self._new_contents[trans_id]
430
children = self._limbo_children.get(trans_id)
431
# if this is a limbo directory with children, move them before removing
433
if children is not None:
434
self._rename_in_limbo(children)
435
del self._limbo_children[trans_id]
436
del self._limbo_children_names[trans_id]
437
delete_any(self._limbo_name(trans_id))
439
def delete_contents(self, trans_id):
440
"""Schedule the contents of a path entry for deletion"""
441
self.tree_kind(trans_id)
442
self._removed_contents.add(trans_id)
444
def cancel_deletion(self, trans_id):
445
"""Cancel a scheduled deletion"""
446
self._removed_contents.remove(trans_id)
448
def unversion_file(self, trans_id):
449
"""Schedule a path entry to become unversioned"""
450
self._removed_id.add(trans_id)
452
def delete_versioned(self, trans_id):
453
"""Delete and unversion a versioned file"""
454
self.delete_contents(trans_id)
455
self.unversion_file(trans_id)
457
def set_executability(self, executability, trans_id):
458
"""Schedule setting of the 'execute' bit
459
To unschedule, set to None
461
if executability is None:
462
del self._new_executability[trans_id]
464
unique_add(self._new_executability, trans_id, executability)
466
def set_tree_reference(self, revision_id, trans_id):
467
"""Set the reference associated with a directory"""
468
unique_add(self._new_reference_revision, trans_id, revision_id)
470
def version_file(self, file_id, trans_id):
471
"""Schedule a file to become versioned."""
474
unique_add(self._new_id, trans_id, file_id)
475
unique_add(self._r_new_id, file_id, trans_id)
477
def cancel_versioning(self, trans_id):
478
"""Undo a previous versioning of a file"""
479
file_id = self._new_id[trans_id]
480
del self._new_id[trans_id]
481
del self._r_new_id[file_id]
483
def new_paths(self, filesystem_only=False):
484
"""Determine the paths of all new and changed files.
486
:param filesystem_only: if True, only calculate values for files
487
that require renames or execute bit changes.
491
stale_ids = self._needs_rename.difference(self._new_name)
492
stale_ids.difference_update(self._new_parent)
493
stale_ids.difference_update(self._new_contents)
494
stale_ids.difference_update(self._new_id)
495
needs_rename = self._needs_rename.difference(stale_ids)
496
id_sets = (needs_rename, self._new_executability)
498
id_sets = (self._new_name, self._new_parent, self._new_contents,
499
self._new_id, self._new_executability)
500
for id_set in id_sets:
501
new_ids.update(id_set)
502
return sorted(FinalPaths(self).get_paths(new_ids))
504
def _inventory_altered(self):
505
"""Get the trans_ids and paths of files needing new inv entries."""
507
for id_set in [self._new_name, self._new_parent, self._new_id,
508
self._new_executability]:
509
new_ids.update(id_set)
510
changed_kind = set(self._removed_contents)
511
changed_kind.intersection_update(self._new_contents)
512
changed_kind.difference_update(new_ids)
513
changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
515
new_ids.update(changed_kind)
516
return sorted(FinalPaths(self).get_paths(new_ids))
518
def tree_kind(self, trans_id):
519
"""Determine the file kind in the working tree.
521
Raises NoSuchFile if the file does not exist
523
path = self._tree_id_paths.get(trans_id)
525
raise NoSuchFile(None)
527
return file_kind(self._tree.abspath(path))
529
if e.errno != errno.ENOENT:
532
raise NoSuchFile(path)
534
def final_kind(self, trans_id):
535
"""Determine the final file kind, after any changes applied.
537
Raises NoSuchFile if the file does not exist/has no contents.
538
(It is conceivable that a path would be created without the
539
corresponding contents insertion command)
541
if trans_id in self._new_contents:
542
return self._new_contents[trans_id]
543
elif trans_id in self._removed_contents:
544
raise NoSuchFile(None)
546
return self.tree_kind(trans_id)
548
def tree_file_id(self, trans_id):
549
"""Determine the file id associated with the trans_id in the tree"""
551
path = self._tree_id_paths[trans_id]
553
# the file is a new, unversioned file, or invalid trans_id
555
# the file is old; the old id is still valid
556
if self._new_root == trans_id:
557
return self._tree.get_root_id()
558
return self._tree.path2id(path)
560
def final_file_id(self, trans_id):
561
"""Determine the file id after any changes are applied, or None.
563
None indicates that the file will not be versioned after changes are
567
return self._new_id[trans_id]
569
if trans_id in self._removed_id:
571
return self.tree_file_id(trans_id)
573
def inactive_file_id(self, trans_id):
574
"""Return the inactive file_id associated with a transaction id.
575
That is, the one in the tree or in non_present_ids.
576
The file_id may actually be active, too.
578
file_id = self.tree_file_id(trans_id)
579
if file_id is not None:
581
for key, value in self._non_present_ids.iteritems():
582
if value == trans_id:
585
def final_parent(self, trans_id):
586
"""Determine the parent file_id, after any changes are applied.
588
ROOT_PARENT is returned for the tree root.
591
return self._new_parent[trans_id]
593
return self.get_tree_parent(trans_id)
595
def final_name(self, trans_id):
596
"""Determine the final filename, after all changes are applied."""
598
return self._new_name[trans_id]
601
return os.path.basename(self._tree_id_paths[trans_id])
603
raise NoFinalPath(trans_id, self)
606
"""Return a map of parent: children for known parents.
608
Only new paths and parents of tree files with assigned ids are used.
611
items = list(self._new_parent.iteritems())
612
items.extend((t, self.final_parent(t)) for t in
613
self._tree_id_paths.keys())
614
for trans_id, parent_id in items:
615
if parent_id not in by_parent:
616
by_parent[parent_id] = set()
617
by_parent[parent_id].add(trans_id)
620
def path_changed(self, trans_id):
621
"""Return True if a trans_id's path has changed."""
622
return (trans_id in self._new_name) or (trans_id in self._new_parent)
624
def new_contents(self, trans_id):
625
return (trans_id in self._new_contents)
627
def find_conflicts(self):
628
"""Find any violations of inventory or filesystem invariants"""
629
if self._done is True:
630
raise ReusingTransform()
632
# ensure all children of all existent parents are known
633
# all children of non-existent parents are known, by definition.
634
self._add_tree_children()
635
by_parent = self.by_parent()
636
conflicts.extend(self._unversioned_parents(by_parent))
637
conflicts.extend(self._parent_loops())
638
conflicts.extend(self._duplicate_entries(by_parent))
639
conflicts.extend(self._duplicate_ids())
640
conflicts.extend(self._parent_type_conflicts(by_parent))
641
conflicts.extend(self._improper_versioning())
642
conflicts.extend(self._executability_conflicts())
643
conflicts.extend(self._overwrite_conflicts())
646
def _add_tree_children(self):
647
"""Add all the children of all active parents to the known paths.
649
Active parents are those which gain children, and those which are
650
removed. This is a necessary first step in detecting conflicts.
652
parents = self.by_parent().keys()
653
parents.extend([t for t in self._removed_contents if
654
self.tree_kind(t) == 'directory'])
655
for trans_id in self._removed_id:
656
file_id = self.tree_file_id(trans_id)
657
if file_id is not None:
658
if self._tree.inventory[file_id].kind == 'directory':
659
parents.append(trans_id)
660
elif self.tree_kind(trans_id) == 'directory':
661
parents.append(trans_id)
663
for parent_id in parents:
664
# ensure that all children are registered with the transaction
665
list(self.iter_tree_children(parent_id))
667
def iter_tree_children(self, parent_id):
668
"""Iterate through the entry's tree children, if any"""
670
path = self._tree_id_paths[parent_id]
674
children = os.listdir(self._tree.abspath(path))
676
if not (osutils._is_error_enotdir(e)
677
or e.errno in (errno.ENOENT, errno.ESRCH)):
681
for child in children:
682
childpath = joinpath(path, child)
683
if self._tree.is_control_filename(childpath):
685
yield self.trans_id_tree_path(childpath)
687
def has_named_child(self, by_parent, parent_id, name):
689
children = by_parent[parent_id]
692
for child in children:
693
if self.final_name(child) == name:
696
path = self._tree_id_paths[parent_id]
699
childpath = joinpath(path, name)
700
child_id = self._tree_path_ids.get(childpath)
702
return lexists(self._tree.abspath(childpath))
704
if self.final_parent(child_id) != parent_id:
706
if child_id in self._removed_contents:
707
# XXX What about dangling file-ids?
712
def _parent_loops(self):
713
"""No entry should be its own ancestor"""
715
for trans_id in self._new_parent:
718
while parent_id is not ROOT_PARENT:
721
parent_id = self.final_parent(parent_id)
724
if parent_id == trans_id:
725
conflicts.append(('parent loop', trans_id))
726
if parent_id in seen:
730
def _unversioned_parents(self, by_parent):
731
"""If parent directories are versioned, children must be versioned."""
733
for parent_id, children in by_parent.iteritems():
734
if parent_id is ROOT_PARENT:
736
if self.final_file_id(parent_id) is not None:
738
for child_id in children:
739
if self.final_file_id(child_id) is not None:
740
conflicts.append(('unversioned parent', parent_id))
744
def _improper_versioning(self):
745
"""Cannot version a file with no contents, or a bad type.
747
However, existing entries with no contents are okay.
750
for trans_id in self._new_id.iterkeys():
752
kind = self.final_kind(trans_id)
754
conflicts.append(('versioning no contents', trans_id))
756
if not InventoryEntry.versionable_kind(kind):
757
conflicts.append(('versioning bad kind', trans_id, kind))
760
def _executability_conflicts(self):
761
"""Check for bad executability changes.
763
Only versioned files may have their executability set, because
764
1. only versioned entries can have executability under windows
765
2. only files can be executable. (The execute bit on a directory
766
does not indicate searchability)
769
for trans_id in self._new_executability:
770
if self.final_file_id(trans_id) is None:
771
conflicts.append(('unversioned executability', trans_id))
774
non_file = self.final_kind(trans_id) != "file"
778
conflicts.append(('non-file executability', trans_id))
781
def _overwrite_conflicts(self):
782
"""Check for overwrites (not permitted on Win32)"""
784
for trans_id in self._new_contents:
786
self.tree_kind(trans_id)
789
if trans_id not in self._removed_contents:
790
conflicts.append(('overwrite', trans_id,
791
self.final_name(trans_id)))
794
def _duplicate_entries(self, by_parent):
795
"""No directory may have two entries with the same name."""
797
if (self._new_name, self._new_parent) == ({}, {}):
799
for children in by_parent.itervalues():
800
name_ids = [(self.final_name(t), t) for t in children]
801
if not self._case_sensitive_target:
802
name_ids = [(n.lower(), t) for n, t in name_ids]
806
for name, trans_id in name_ids:
808
kind = self.final_kind(trans_id)
811
file_id = self.final_file_id(trans_id)
812
if kind is None and file_id is None:
814
if name == last_name:
815
conflicts.append(('duplicate', last_trans_id, trans_id,
818
last_trans_id = trans_id
821
def _duplicate_ids(self):
822
"""Each inventory id may only be used once"""
824
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
826
all_ids = self._tree.all_file_ids()
827
active_tree_ids = all_ids.difference(removed_tree_ids)
828
for trans_id, file_id in self._new_id.iteritems():
829
if file_id in active_tree_ids:
830
old_trans_id = self.trans_id_tree_file_id(file_id)
831
conflicts.append(('duplicate id', old_trans_id, trans_id))
834
def _parent_type_conflicts(self, by_parent):
835
"""parents must have directory 'contents'."""
837
for parent_id, children in by_parent.iteritems():
838
if parent_id is ROOT_PARENT:
840
if not self._any_contents(children):
842
for child in children:
844
self.final_kind(child)
848
kind = self.final_kind(parent_id)
852
conflicts.append(('missing parent', parent_id))
853
elif kind != "directory":
854
conflicts.append(('non-directory parent', parent_id))
857
def _any_contents(self, trans_ids):
858
"""Return true if any of the trans_ids, will have contents."""
859
for trans_id in trans_ids:
861
kind = self.final_kind(trans_id)
867
def _limbo_name(self, trans_id):
868
"""Generate the limbo name of a file"""
869
limbo_name = self._limbo_files.get(trans_id)
870
if limbo_name is not None:
872
parent = self._new_parent.get(trans_id)
873
# if the parent directory is already in limbo (e.g. when building a
874
# tree), choose a limbo name inside the parent, to reduce further
876
use_direct_path = False
877
if self._new_contents.get(parent) == 'directory':
878
filename = self._new_name.get(trans_id)
879
if filename is not None:
880
if parent not in self._limbo_children:
881
self._limbo_children[parent] = set()
882
self._limbo_children_names[parent] = {}
883
use_direct_path = True
884
# the direct path can only be used if no other file has
885
# already taken this pathname, i.e. if the name is unused, or
886
# if it is already associated with this trans_id.
887
elif self._case_sensitive_target:
888
if (self._limbo_children_names[parent].get(filename)
889
in (trans_id, None)):
890
use_direct_path = True
892
for l_filename, l_trans_id in\
893
self._limbo_children_names[parent].iteritems():
894
if l_trans_id == trans_id:
896
if l_filename.lower() == filename.lower():
899
use_direct_path = True
902
limbo_name = pathjoin(self._limbo_files[parent], filename)
903
self._limbo_children[parent].add(trans_id)
904
self._limbo_children_names[parent][filename] = trans_id
906
limbo_name = pathjoin(self._limbodir, trans_id)
907
self._needs_rename.add(trans_id)
908
self._limbo_files[trans_id] = limbo_name
911
def _set_executability(self, path, trans_id):
912
"""Set the executability of versioned files """
913
if supports_executable():
914
new_executability = self._new_executability[trans_id]
915
abspath = self._tree.abspath(path)
916
current_mode = os.stat(abspath).st_mode
917
if new_executability:
920
to_mode = current_mode | (0100 & ~umask)
921
# Enable x-bit for others only if they can read it.
922
if current_mode & 0004:
923
to_mode |= 0001 & ~umask
924
if current_mode & 0040:
925
to_mode |= 0010 & ~umask
927
to_mode = current_mode & ~0111
928
os.chmod(abspath, to_mode)
930
def _new_entry(self, name, parent_id, file_id):
931
"""Helper function to create a new filesystem entry."""
932
trans_id = self.create_path(name, parent_id)
933
if file_id is not None:
934
self.version_file(file_id, trans_id)
937
def new_file(self, name, parent_id, contents, file_id=None,
939
"""Convenience method to create files.
941
name is the name of the file to create.
942
parent_id is the transaction id of the parent directory of the file.
943
contents is an iterator of bytestrings, which will be used to produce
945
:param file_id: The inventory ID of the file, if it is to be versioned.
946
:param executable: Only valid when a file_id has been supplied.
948
trans_id = self._new_entry(name, parent_id, file_id)
949
# TODO: rather than scheduling a set_executable call,
950
# have create_file create the file with the right mode.
951
self.create_file(contents, trans_id)
952
if executable is not None:
953
self.set_executability(executable, trans_id)
956
def new_directory(self, name, parent_id, file_id=None):
957
"""Convenience method to create directories.
959
name is the name of the directory to create.
960
parent_id is the transaction id of the parent directory of the
962
file_id is the inventory ID of the directory, if it is to be versioned.
964
trans_id = self._new_entry(name, parent_id, file_id)
965
self.create_directory(trans_id)
968
def new_symlink(self, name, parent_id, target, file_id=None):
969
"""Convenience method to create symbolic link.
971
name is the name of the symlink to create.
972
parent_id is the transaction id of the parent directory of the symlink.
973
target is a bytestring of the target of the symlink.
974
file_id is the inventory ID of the file, if it is to be versioned.
976
trans_id = self._new_entry(name, parent_id, file_id)
977
self.create_symlink(target, trans_id)
980
def _affected_ids(self):
981
"""Return the set of transform ids affected by the transform"""
982
trans_ids = set(self._removed_id)
983
trans_ids.update(self._new_id.keys())
984
trans_ids.update(self._removed_contents)
985
trans_ids.update(self._new_contents.keys())
986
trans_ids.update(self._new_executability.keys())
987
trans_ids.update(self._new_name.keys())
988
trans_ids.update(self._new_parent.keys())
991
def _get_file_id_maps(self):
992
"""Return mapping of file_ids to trans_ids in the to and from states"""
993
trans_ids = self._affected_ids()
996
# Build up two dicts: trans_ids associated with file ids in the
997
# FROM state, vs the TO state.
998
for trans_id in trans_ids:
999
from_file_id = self.tree_file_id(trans_id)
1000
if from_file_id is not None:
1001
from_trans_ids[from_file_id] = trans_id
1002
to_file_id = self.final_file_id(trans_id)
1003
if to_file_id is not None:
1004
to_trans_ids[to_file_id] = trans_id
1005
return from_trans_ids, to_trans_ids
1007
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1008
"""Get data about a file in the from (tree) state
1010
Return a (name, parent, kind, executable) tuple
1012
from_path = self._tree_id_paths.get(from_trans_id)
1014
# get data from working tree if versioned
1015
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1016
from_name = from_entry.name
1017
from_parent = from_entry.parent_id
1020
if from_path is None:
1021
# File does not exist in FROM state
1025
# File exists, but is not versioned. Have to use path-
1027
from_name = os.path.basename(from_path)
1028
tree_parent = self.get_tree_parent(from_trans_id)
1029
from_parent = self.tree_file_id(tree_parent)
1030
if from_path is not None:
1031
from_kind, from_executable, from_stats = \
1032
self._tree._comparison_data(from_entry, from_path)
1035
from_executable = False
1036
return from_name, from_parent, from_kind, from_executable
1038
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1039
"""Get data about a file in the to (target) state
1041
Return a (name, parent, kind, executable) tuple
1043
to_name = self.final_name(to_trans_id)
1045
to_kind = self.final_kind(to_trans_id)
1048
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1049
if to_trans_id in self._new_executability:
1050
to_executable = self._new_executability[to_trans_id]
1051
elif to_trans_id == from_trans_id:
1052
to_executable = from_executable
1054
to_executable = False
1055
return to_name, to_parent, to_kind, to_executable
1057
def iter_changes(self):
1058
"""Produce output in the same format as Tree.iter_changes.
1060
Will produce nonsensical results if invoked while inventory/filesystem
1061
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1063
This reads the Transform, but only reproduces changes involving a
1064
file_id. Files that are not versioned in either of the FROM or TO
1065
states are not reflected.
1067
final_paths = FinalPaths(self)
1068
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1070
# Now iterate through all active file_ids
1071
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1073
from_trans_id = from_trans_ids.get(file_id)
1074
# find file ids, and determine versioning state
1075
if from_trans_id is None:
1076
from_versioned = False
1077
from_trans_id = to_trans_ids[file_id]
1079
from_versioned = True
1080
to_trans_id = to_trans_ids.get(file_id)
1081
if to_trans_id is None:
1082
to_versioned = False
1083
to_trans_id = from_trans_id
1087
from_name, from_parent, from_kind, from_executable = \
1088
self._from_file_data(from_trans_id, from_versioned, file_id)
1090
to_name, to_parent, to_kind, to_executable = \
1091
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1093
if not from_versioned:
1096
from_path = self._tree_id_paths.get(from_trans_id)
1097
if not to_versioned:
1100
to_path = final_paths.get_path(to_trans_id)
1101
if from_kind != to_kind:
1103
elif to_kind in ('file', 'symlink') and (
1104
to_trans_id != from_trans_id or
1105
to_trans_id in self._new_contents):
1107
if (not modified and from_versioned == to_versioned and
1108
from_parent==to_parent and from_name == to_name and
1109
from_executable == to_executable):
1111
results.append((file_id, (from_path, to_path), modified,
1112
(from_versioned, to_versioned),
1113
(from_parent, to_parent),
1114
(from_name, to_name),
1115
(from_kind, to_kind),
1116
(from_executable, to_executable)))
1117
return iter(sorted(results, key=lambda x:x[1]))
1119
def get_preview_tree(self):
1120
"""Return a tree representing the result of the transform.
1122
This tree only supports the subset of Tree functionality required
1123
by show_diff_trees. It must only be compared to tt._tree.
1125
return _PreviewTree(self)
1128
class TreeTransform(TreeTransformBase):
1129
"""Represent a tree transformation.
1131
This object is designed to support incremental generation of the transform,
1134
However, it gives optimum performance when parent directories are created
1135
before their contents. The transform is then able to put child files
1136
directly in their parent directory, avoiding later renames.
1138
It is easy to produce malformed transforms, but they are generally
1139
harmless. Attempting to apply a malformed transform will cause an
1140
exception to be raised before any modifications are made to the tree.
1142
Many kinds of malformed transforms can be corrected with the
1143
resolve_conflicts function. The remaining ones indicate programming error,
1144
such as trying to create a file with no path.
1146
Two sets of file creation methods are supplied. Convenience methods are:
1151
These are composed of the low-level methods:
1153
* create_file or create_directory or create_symlink
1157
Transform/Transaction ids
1158
-------------------------
1159
trans_ids are temporary ids assigned to all files involved in a transform.
1160
It's possible, even common, that not all files in the Tree have trans_ids.
1162
trans_ids are used because filenames and file_ids are not good enough
1163
identifiers; filenames change, and not all files have file_ids. File-ids
1164
are also associated with trans-ids, so that moving a file moves its
1167
trans_ids are only valid for the TreeTransform that generated them.
1171
Limbo is a temporary directory use to hold new versions of files.
1172
Files are added to limbo by create_file, create_directory, create_symlink,
1173
and their convenience variants (new_*). Files may be removed from limbo
1174
using cancel_creation. Files are renamed from limbo into their final
1175
location as part of TreeTransform.apply
1177
Limbo must be cleaned up, by either calling TreeTransform.apply or
1178
calling TreeTransform.finalize.
1180
Files are placed into limbo inside their parent directories, where
1181
possible. This reduces subsequent renames, and makes operations involving
1182
lots of files faster. This optimization is only possible if the parent
1183
directory is created *before* creating any of its children, so avoid
1184
creating children before parents, where possible.
1188
This temporary directory is used by _FileMover for storing files that are
1189
about to be deleted. In case of rollback, the files will be restored.
1190
FileMover does not delete files until it is sure that a rollback will not
1193
def __init__(self, tree, pb=DummyProgress()):
1194
"""Note: a tree_write lock is taken on the tree.
1196
Use TreeTransform.finalize() to release the lock (can be omitted if
1197
TreeTransform.apply() called).
1199
tree.lock_tree_write()
1202
limbodir = urlutils.local_path_from_url(
1203
tree._transport.abspath('limbo'))
1207
if e.errno == errno.EEXIST:
1208
raise ExistingLimbo(limbodir)
1209
deletiondir = urlutils.local_path_from_url(
1210
tree._transport.abspath('pending-deletion'))
1212
os.mkdir(deletiondir)
1214
if e.errno == errno.EEXIST:
1215
raise errors.ExistingPendingDeletion(deletiondir)
1220
TreeTransformBase.__init__(self, tree, limbodir, pb,
1221
tree.case_sensitive)
1222
self._deletiondir = deletiondir
1224
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1225
"""Apply all changes to the inventory and filesystem.
1227
If filesystem or inventory conflicts are present, MalformedTransform
1230
If apply succeeds, finalize is not necessary.
1232
:param no_conflicts: if True, the caller guarantees there are no
1233
conflicts, so no check is made.
1234
:param precomputed_delta: An inventory delta to use instead of
1236
:param _mover: Supply an alternate FileMover, for testing
1238
if not no_conflicts:
1239
conflicts = self.find_conflicts()
1240
if len(conflicts) != 0:
1241
raise MalformedTransform(conflicts=conflicts)
1242
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1244
if precomputed_delta is None:
1245
child_pb.update('Apply phase', 0, 2)
1246
inventory_delta = self._generate_inventory_delta()
1249
inventory_delta = precomputed_delta
1252
mover = _FileMover()
1256
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1257
self._apply_removals(mover)
1258
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1259
modified_paths = self._apply_insertions(mover)
1264
mover.apply_deletions()
1267
self._tree.apply_inventory_delta(inventory_delta)
1270
return _TransformResults(modified_paths, self.rename_count)
1272
def _generate_inventory_delta(self):
1273
"""Generate an inventory delta for the current transform."""
1274
inventory_delta = []
1275
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1276
new_paths = self._inventory_altered()
1277
total_entries = len(new_paths) + len(self._removed_id)
1279
for num, trans_id in enumerate(self._removed_id):
1281
child_pb.update('removing file', num, total_entries)
1282
if trans_id == self._new_root:
1283
file_id = self._tree.get_root_id()
1285
file_id = self.tree_file_id(trans_id)
1286
# File-id isn't really being deleted, just moved
1287
if file_id in self._r_new_id:
1289
path = self._tree_id_paths[trans_id]
1290
inventory_delta.append((path, None, file_id, None))
1291
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1293
entries = self._tree.iter_entries_by_dir(
1294
new_path_file_ids.values())
1295
old_paths = dict((e.file_id, p) for p, e in entries)
1297
for num, (path, trans_id) in enumerate(new_paths):
1299
child_pb.update('adding file',
1300
num + len(self._removed_id), total_entries)
1301
file_id = new_path_file_ids[trans_id]
1306
kind = self.final_kind(trans_id)
1308
kind = self._tree.stored_kind(file_id)
1309
parent_trans_id = self.final_parent(trans_id)
1310
parent_file_id = new_path_file_ids.get(parent_trans_id)
1311
if parent_file_id is None:
1312
parent_file_id = self.final_file_id(parent_trans_id)
1313
if trans_id in self._new_reference_revision:
1314
new_entry = inventory.TreeReference(
1316
self._new_name[trans_id],
1317
self.final_file_id(self._new_parent[trans_id]),
1318
None, self._new_reference_revision[trans_id])
1320
new_entry = inventory.make_entry(kind,
1321
self.final_name(trans_id),
1322
parent_file_id, file_id)
1323
old_path = old_paths.get(new_entry.file_id)
1324
new_executability = self._new_executability.get(trans_id)
1325
if new_executability is not None:
1326
new_entry.executable = new_executability
1327
inventory_delta.append(
1328
(old_path, path, new_entry.file_id, new_entry))
1331
return inventory_delta
1333
def _apply_removals(self, mover):
1334
"""Perform tree operations that remove directory/inventory names.
1336
That is, delete files that are to be deleted, and put any files that
1337
need renaming into limbo. This must be done in strict child-to-parent
1340
If inventory_delta is None, no inventory delta generation is performed.
1342
tree_paths = list(self._tree_path_ids.iteritems())
1343
tree_paths.sort(reverse=True)
1344
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1346
for num, data in enumerate(tree_paths):
1347
path, trans_id = data
1348
child_pb.update('removing file', num, len(tree_paths))
1349
full_path = self._tree.abspath(path)
1350
if trans_id in self._removed_contents:
1351
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1353
elif trans_id in self._new_name or trans_id in \
1356
mover.rename(full_path, self._limbo_name(trans_id))
1358
if e.errno != errno.ENOENT:
1361
self.rename_count += 1
1365
def _apply_insertions(self, mover):
1366
"""Perform tree operations that insert directory/inventory names.
1368
That is, create any files that need to be created, and restore from
1369
limbo any files that needed renaming. This must be done in strict
1370
parent-to-child order.
1372
If inventory_delta is None, no inventory delta is calculated, and
1373
no list of modified paths is returned.
1375
new_paths = self.new_paths(filesystem_only=True)
1377
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1379
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1381
for num, (path, trans_id) in enumerate(new_paths):
1383
child_pb.update('adding file', num, len(new_paths))
1384
full_path = self._tree.abspath(path)
1385
if trans_id in self._needs_rename:
1387
mover.rename(self._limbo_name(trans_id), full_path)
1389
# We may be renaming a dangling inventory id
1390
if e.errno != errno.ENOENT:
1393
self.rename_count += 1
1394
if (trans_id in self._new_contents or
1395
self.path_changed(trans_id)):
1396
if trans_id in self._new_contents:
1397
modified_paths.append(full_path)
1398
if trans_id in self._new_executability:
1399
self._set_executability(path, trans_id)
1402
self._new_contents.clear()
1403
return modified_paths
1406
class TransformPreview(TreeTransformBase):
1407
"""A TreeTransform for generating preview trees.
1409
Unlike TreeTransform, this version works when the input tree is a
1410
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1411
unversioned files in the input tree.
1414
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1416
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1417
TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1419
def canonical_path(self, path):
1422
def tree_kind(self, trans_id):
1423
path = self._tree_id_paths.get(trans_id)
1425
raise NoSuchFile(None)
1426
file_id = self._tree.path2id(path)
1427
return self._tree.kind(file_id)
1429
def _set_mode(self, trans_id, mode_id, typefunc):
1430
"""Set the mode of new file contents.
1431
The mode_id is the existing file to get the mode from (often the same
1432
as trans_id). The operation is only performed if there's a mode match
1433
according to typefunc.
1435
# is it ok to ignore this? probably
1438
def iter_tree_children(self, parent_id):
1439
"""Iterate through the entry's tree children, if any"""
1441
path = self._tree_id_paths[parent_id]
1444
file_id = self.tree_file_id(parent_id)
1447
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1448
children = getattr(entry, 'children', {})
1449
for child in children:
1450
childpath = joinpath(path, child)
1451
yield self.trans_id_tree_path(childpath)
1454
class _PreviewTree(tree.Tree):
1455
"""Partial implementation of Tree to support show_diff_trees"""
1457
def __init__(self, transform):
1458
self._transform = transform
1459
self._final_paths = FinalPaths(transform)
1460
self.__by_parent = None
1461
self._parent_ids = []
1462
self._all_children_cache = {}
1463
self._path2trans_id_cache = {}
1464
self._final_name_cache = {}
1466
def _changes(self, file_id):
1467
for changes in self._transform.iter_changes():
1468
if changes[0] == file_id:
1471
def _content_change(self, file_id):
1472
"""Return True if the content of this file changed"""
1473
changes = self._changes(file_id)
1474
# changes[2] is true if the file content changed. See
1475
# InterTree.iter_changes.
1476
return (changes is not None and changes[2])
1478
def _get_repository(self):
1479
repo = getattr(self._transform._tree, '_repository', None)
1481
repo = self._transform._tree.branch.repository
1484
def _iter_parent_trees(self):
1485
for revision_id in self.get_parent_ids():
1487
yield self.revision_tree(revision_id)
1488
except errors.NoSuchRevisionInTree:
1489
yield self._get_repository().revision_tree(revision_id)
1491
def _get_file_revision(self, file_id, vf, tree_revision):
1492
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1493
self._iter_parent_trees()]
1494
vf.add_lines((file_id, tree_revision), parent_keys,
1495
self.get_file(file_id).readlines())
1496
repo = self._get_repository()
1497
base_vf = repo.texts
1498
if base_vf not in vf.fallback_versionedfiles:
1499
vf.fallback_versionedfiles.append(base_vf)
1500
return tree_revision
1502
def _stat_limbo_file(self, file_id):
1503
trans_id = self._transform.trans_id_file_id(file_id)
1504
name = self._transform._limbo_name(trans_id)
1505
return os.lstat(name)
1508
def _by_parent(self):
1509
if self.__by_parent is None:
1510
self.__by_parent = self._transform.by_parent()
1511
return self.__by_parent
1513
def _comparison_data(self, entry, path):
1514
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1515
if kind == 'missing':
1519
file_id = self._transform.final_file_id(self._path2trans_id(path))
1520
executable = self.is_executable(file_id, path)
1521
return kind, executable, None
1523
def lock_read(self):
1524
# Perhaps in theory, this should lock the TreeTransform?
1531
def inventory(self):
1532
"""This Tree does not use inventory as its backing data."""
1533
raise NotImplementedError(_PreviewTree.inventory)
1535
def get_root_id(self):
1536
return self._transform.final_file_id(self._transform.root)
1538
def all_file_ids(self):
1539
tree_ids = set(self._transform._tree.all_file_ids())
1540
tree_ids.difference_update(self._transform.tree_file_id(t)
1541
for t in self._transform._removed_id)
1542
tree_ids.update(self._transform._new_id.values())
1546
return iter(self.all_file_ids())
1548
def has_id(self, file_id):
1549
if file_id in self._transform._r_new_id:
1551
elif file_id in self._transform._removed_id:
1554
return self._transform._tree.has_id(file_id)
1556
def _path2trans_id(self, path):
1557
# We must not use None here, because that is a valid value to store.
1558
trans_id = self._path2trans_id_cache.get(path, object)
1559
if trans_id is not object:
1561
segments = splitpath(path)
1562
cur_parent = self._transform.root
1563
for cur_segment in segments:
1564
for child in self._all_children(cur_parent):
1565
final_name = self._final_name_cache.get(child)
1566
if final_name is None:
1567
final_name = self._transform.final_name(child)
1568
self._final_name_cache[child] = final_name
1569
if final_name == cur_segment:
1573
self._path2trans_id_cache[path] = None
1575
self._path2trans_id_cache[path] = cur_parent
1578
def path2id(self, path):
1579
return self._transform.final_file_id(self._path2trans_id(path))
1581
def id2path(self, file_id):
1582
trans_id = self._transform.trans_id_file_id(file_id)
1584
return self._final_paths._determine_path(trans_id)
1586
raise errors.NoSuchId(self, file_id)
1588
def _all_children(self, trans_id):
1589
children = self._all_children_cache.get(trans_id)
1590
if children is not None:
1592
children = set(self._transform.iter_tree_children(trans_id))
1593
# children in the _new_parent set are provided by _by_parent.
1594
children.difference_update(self._transform._new_parent.keys())
1595
children.update(self._by_parent.get(trans_id, []))
1596
self._all_children_cache[trans_id] = children
1599
def iter_children(self, file_id):
1600
trans_id = self._transform.trans_id_file_id(file_id)
1601
for child_trans_id in self._all_children(trans_id):
1602
yield self._transform.final_file_id(child_trans_id)
1605
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1606
in self._transform._tree.extras())
1607
possible_extras.update(self._transform._new_contents)
1608
possible_extras.update(self._transform._removed_id)
1609
for trans_id in possible_extras:
1610
if self._transform.final_file_id(trans_id) is None:
1611
yield self._final_paths._determine_path(trans_id)
1613
def _make_inv_entries(self, ordered_entries, specific_file_ids):
1614
for trans_id, parent_file_id in ordered_entries:
1615
file_id = self._transform.final_file_id(trans_id)
1618
if (specific_file_ids is not None
1619
and file_id not in specific_file_ids):
1622
kind = self._transform.final_kind(trans_id)
1624
kind = self._transform._tree.stored_kind(file_id)
1625
new_entry = inventory.make_entry(
1627
self._transform.final_name(trans_id),
1628
parent_file_id, file_id)
1629
yield new_entry, trans_id
1631
def _list_files_by_dir(self):
1632
todo = [ROOT_PARENT]
1634
while len(todo) > 0:
1636
parent_file_id = self._transform.final_file_id(parent)
1637
children = list(self._all_children(parent))
1638
paths = dict(zip(children, self._final_paths.get_paths(children)))
1639
children.sort(key=paths.get)
1640
todo.extend(reversed(children))
1641
for trans_id in children:
1642
ordered_ids.append((trans_id, parent_file_id))
1645
def iter_entries_by_dir(self, specific_file_ids=None):
1646
# This may not be a maximally efficient implementation, but it is
1647
# reasonably straightforward. An implementation that grafts the
1648
# TreeTransform changes onto the tree's iter_entries_by_dir results
1649
# might be more efficient, but requires tricky inferences about stack
1651
ordered_ids = self._list_files_by_dir()
1652
for entry, trans_id in self._make_inv_entries(ordered_ids,
1654
yield unicode(self._final_paths.get_path(trans_id)), entry
1656
def list_files(self, include_root=False):
1657
"""See Tree.list_files."""
1658
# XXX This should behave like WorkingTree.list_files, but is really
1659
# more like RevisionTree.list_files.
1660
for path, entry in self.iter_entries_by_dir():
1661
if entry.name == '' and not include_root:
1663
yield path, 'V', entry.kind, entry.file_id, entry
1665
def kind(self, file_id):
1666
trans_id = self._transform.trans_id_file_id(file_id)
1667
return self._transform.final_kind(trans_id)
1669
def stored_kind(self, file_id):
1670
trans_id = self._transform.trans_id_file_id(file_id)
1672
return self._transform._new_contents[trans_id]
1674
return self._transform._tree.stored_kind(file_id)
1676
def get_file_mtime(self, file_id, path=None):
1677
"""See Tree.get_file_mtime"""
1678
if not self._content_change(file_id):
1679
return self._transform._tree.get_file_mtime(file_id, path)
1680
return self._stat_limbo_file(file_id).st_mtime
1682
def _file_size(self, entry, stat_value):
1683
return self.get_file_size(entry.file_id)
1685
def get_file_size(self, file_id):
1686
"""See Tree.get_file_size"""
1687
if self.kind(file_id) == 'file':
1688
return self._transform._tree.get_file_size(file_id)
1692
def get_file_sha1(self, file_id, path=None, stat_value=None):
1693
trans_id = self._transform.trans_id_file_id(file_id)
1694
kind = self._transform._new_contents.get(trans_id)
1696
return self._transform._tree.get_file_sha1(file_id)
1698
fileobj = self.get_file(file_id)
1700
return sha_file(fileobj)
1704
def is_executable(self, file_id, path=None):
1707
trans_id = self._transform.trans_id_file_id(file_id)
1709
return self._transform._new_executability[trans_id]
1712
return self._transform._tree.is_executable(file_id, path)
1714
if e.errno == errno.ENOENT:
1717
except errors.NoSuchId:
1720
def path_content_summary(self, path):
1721
trans_id = self._path2trans_id(path)
1722
tt = self._transform
1723
tree_path = tt._tree_id_paths.get(trans_id)
1724
kind = tt._new_contents.get(trans_id)
1726
if tree_path is None or trans_id in tt._removed_contents:
1727
return 'missing', None, None, None
1728
summary = tt._tree.path_content_summary(tree_path)
1729
kind, size, executable, link_or_sha1 = summary
1732
limbo_name = tt._limbo_name(trans_id)
1733
if trans_id in tt._new_reference_revision:
1734
kind = 'tree-reference'
1736
statval = os.lstat(limbo_name)
1737
size = statval.st_size
1738
if not supports_executable():
1741
executable = statval.st_mode & S_IEXEC
1745
if kind == 'symlink':
1746
link_or_sha1 = os.readlink(limbo_name)
1747
if supports_executable():
1748
executable = tt._new_executability.get(trans_id, executable)
1749
return kind, size, executable, link_or_sha1
1751
def iter_changes(self, from_tree, include_unchanged=False,
1752
specific_files=None, pb=None, extra_trees=None,
1753
require_versioned=True, want_unversioned=False):
1754
"""See InterTree.iter_changes.
1756
This has a fast path that is only used when the from_tree matches
1757
the transform tree, and no fancy options are supplied.
1759
if (from_tree is not self._transform._tree or include_unchanged or
1760
specific_files or want_unversioned):
1761
return tree.InterTree(from_tree, self).iter_changes(
1762
include_unchanged=include_unchanged,
1763
specific_files=specific_files,
1765
extra_trees=extra_trees,
1766
require_versioned=require_versioned,
1767
want_unversioned=want_unversioned)
1768
if want_unversioned:
1769
raise ValueError('want_unversioned is not supported')
1770
return self._transform.iter_changes()
1772
def get_file(self, file_id, path=None):
1773
"""See Tree.get_file"""
1774
if not self._content_change(file_id):
1775
return self._transform._tree.get_file(file_id, path)
1776
trans_id = self._transform.trans_id_file_id(file_id)
1777
name = self._transform._limbo_name(trans_id)
1778
return open(name, 'rb')
1780
def annotate_iter(self, file_id,
1781
default_revision=_mod_revision.CURRENT_REVISION):
1782
changes = self._changes(file_id)
1786
changed_content, versioned, kind = (changes[2], changes[3],
1790
get_old = (kind[0] == 'file' and versioned[0])
1792
old_annotation = self._transform._tree.annotate_iter(file_id,
1793
default_revision=default_revision)
1797
return old_annotation
1798
if not changed_content:
1799
return old_annotation
1800
return annotate.reannotate([old_annotation],
1801
self.get_file(file_id).readlines(),
1804
def get_symlink_target(self, file_id):
1805
"""See Tree.get_symlink_target"""
1806
if not self._content_change(file_id):
1807
return self._transform._tree.get_symlink_target(file_id)
1808
trans_id = self._transform.trans_id_file_id(file_id)
1809
name = self._transform._limbo_name(trans_id)
1810
return os.readlink(name)
1812
def walkdirs(self, prefix=''):
1813
pending = [self._transform.root]
1814
while len(pending) > 0:
1815
parent_id = pending.pop()
1818
prefix = prefix.rstrip('/')
1819
parent_path = self._final_paths.get_path(parent_id)
1820
parent_file_id = self._transform.final_file_id(parent_id)
1821
for child_id in self._all_children(parent_id):
1822
path_from_root = self._final_paths.get_path(child_id)
1823
basename = self._transform.final_name(child_id)
1824
file_id = self._transform.final_file_id(child_id)
1826
kind = self._transform.final_kind(child_id)
1827
versioned_kind = kind
1830
versioned_kind = self._transform._tree.stored_kind(file_id)
1831
if versioned_kind == 'directory':
1832
subdirs.append(child_id)
1833
children.append((path_from_root, basename, kind, None,
1834
file_id, versioned_kind))
1836
if parent_path.startswith(prefix):
1837
yield (parent_path, parent_file_id), children
1838
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1841
def get_parent_ids(self):
1842
return self._parent_ids
1844
def set_parent_ids(self, parent_ids):
1845
self._parent_ids = parent_ids
1847
def get_revision_tree(self, revision_id):
1848
return self._transform._tree.get_revision_tree(revision_id)
1851
def joinpath(parent, child):
1852
"""Join tree-relative paths, handling the tree root specially"""
1853
if parent is None or parent == "":
1856
return pathjoin(parent, child)
1859
class FinalPaths(object):
1860
"""Make path calculation cheap by memoizing paths.
1862
The underlying tree must not be manipulated between calls, or else
1863
the results will likely be incorrect.
1865
def __init__(self, transform):
1866
object.__init__(self)
1867
self._known_paths = {}
1868
self.transform = transform
1870
def _determine_path(self, trans_id):
1871
if trans_id == self.transform.root:
1873
name = self.transform.final_name(trans_id)
1874
parent_id = self.transform.final_parent(trans_id)
1875
if parent_id == self.transform.root:
1878
return pathjoin(self.get_path(parent_id), name)
1880
def get_path(self, trans_id):
1881
"""Find the final path associated with a trans_id"""
1882
if trans_id not in self._known_paths:
1883
self._known_paths[trans_id] = self._determine_path(trans_id)
1884
return self._known_paths[trans_id]
1886
def get_paths(self, trans_ids):
1887
return [(self.get_path(t), t) for t in trans_ids]
1891
def topology_sorted_ids(tree):
1892
"""Determine the topological order of the ids in a tree"""
1893
file_ids = list(tree)
1894
file_ids.sort(key=tree.id2path)
1898
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1899
delta_from_tree=False):
1900
"""Create working tree for a branch, using a TreeTransform.
1902
This function should be used on empty trees, having a tree root at most.
1903
(see merge and revert functionality for working with existing trees)
1905
Existing files are handled like so:
1907
- Existing bzrdirs take precedence over creating new items. They are
1908
created as '%s.diverted' % name.
1909
- Otherwise, if the content on disk matches the content we are building,
1910
it is silently replaced.
1911
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1913
:param tree: The tree to convert wt into a copy of
1914
:param wt: The working tree that files will be placed into
1915
:param accelerator_tree: A tree which can be used for retrieving file
1916
contents more quickly than tree itself, i.e. a workingtree. tree
1917
will be used for cases where accelerator_tree's content is different.
1918
:param hardlink: If true, hard-link files to accelerator_tree, where
1919
possible. accelerator_tree must implement abspath, i.e. be a
1921
:param delta_from_tree: If true, build_tree may use the input Tree to
1922
generate the inventory delta.
1924
wt.lock_tree_write()
1928
if accelerator_tree is not None:
1929
accelerator_tree.lock_read()
1931
return _build_tree(tree, wt, accelerator_tree, hardlink,
1934
if accelerator_tree is not None:
1935
accelerator_tree.unlock()
1942
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1943
"""See build_tree."""
1944
for num, _unused in enumerate(wt.all_file_ids()):
1945
if num > 0: # more than just a root
1946
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1947
existing_files = set()
1948
for dir, files in wt.walkdirs():
1949
existing_files.update(f[0] for f in files)
1951
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1952
pp = ProgressPhase("Build phase", 2, top_pb)
1953
if tree.inventory.root is not None:
1954
# This is kind of a hack: we should be altering the root
1955
# as part of the regular tree shape diff logic.
1956
# The conditional test here is to avoid doing an
1957
# expensive operation (flush) every time the root id
1958
# is set within the tree, nor setting the root and thus
1959
# marking the tree as dirty, because we use two different
1960
# idioms here: tree interfaces and inventory interfaces.
1961
if wt.get_root_id() != tree.get_root_id():
1962
wt.set_root_id(tree.get_root_id())
1964
tt = TreeTransform(wt)
1968
file_trans_id[wt.get_root_id()] = \
1969
tt.trans_id_tree_file_id(wt.get_root_id())
1970
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1972
deferred_contents = []
1974
total = len(tree.inventory)
1976
precomputed_delta = []
1978
precomputed_delta = None
1979
for num, (tree_path, entry) in \
1980
enumerate(tree.inventory.iter_entries_by_dir()):
1981
pb.update("Building tree", num - len(deferred_contents), total)
1982
if entry.parent_id is None:
1985
file_id = entry.file_id
1987
precomputed_delta.append((None, tree_path, file_id, entry))
1988
if tree_path in existing_files:
1989
target_path = wt.abspath(tree_path)
1990
kind = file_kind(target_path)
1991
if kind == "directory":
1993
bzrdir.BzrDir.open(target_path)
1994
except errors.NotBranchError:
1998
if (file_id not in divert and
1999
_content_match(tree, entry, file_id, kind,
2001
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2002
if kind == 'directory':
2004
parent_id = file_trans_id[entry.parent_id]
2005
if entry.kind == 'file':
2006
# We *almost* replicate new_by_entry, so that we can defer
2007
# getting the file text, and get them all at once.
2008
trans_id = tt.create_path(entry.name, parent_id)
2009
file_trans_id[file_id] = trans_id
2010
tt.version_file(file_id, trans_id)
2011
executable = tree.is_executable(file_id, tree_path)
2013
tt.set_executability(executable, trans_id)
2014
deferred_contents.append((file_id, trans_id))
2016
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2019
new_trans_id = file_trans_id[file_id]
2020
old_parent = tt.trans_id_tree_path(tree_path)
2021
_reparent_children(tt, old_parent, new_trans_id)
2022
offset = num + 1 - len(deferred_contents)
2023
_create_files(tt, tree, deferred_contents, pb, offset,
2024
accelerator_tree, hardlink)
2028
divert_trans = set(file_trans_id[f] for f in divert)
2029
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2030
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2031
if len(raw_conflicts) > 0:
2032
precomputed_delta = None
2033
conflicts = cook_conflicts(raw_conflicts, tt)
2034
for conflict in conflicts:
2037
wt.add_conflicts(conflicts)
2038
except errors.UnsupportedOperation:
2040
result = tt.apply(no_conflicts=True,
2041
precomputed_delta=precomputed_delta)
2048
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2050
total = len(desired_files) + offset
2051
if accelerator_tree is None:
2052
new_desired_files = desired_files
2054
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2055
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2056
in iter if not (c or e[0] != e[1]))
2057
new_desired_files = []
2059
for file_id, trans_id in desired_files:
2060
accelerator_path = unchanged.get(file_id)
2061
if accelerator_path is None:
2062
new_desired_files.append((file_id, trans_id))
2064
pb.update('Adding file contents', count + offset, total)
2066
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2069
contents = accelerator_tree.get_file(file_id, accelerator_path)
2071
tt.create_file(contents, trans_id)
2076
for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
2077
new_desired_files)):
2078
tt.create_file(contents, trans_id)
2079
pb.update('Adding file contents', count + offset, total)
2082
def _reparent_children(tt, old_parent, new_parent):
2083
for child in tt.iter_tree_children(old_parent):
2084
tt.adjust_path(tt.final_name(child), new_parent, child)
2086
def _reparent_transform_children(tt, old_parent, new_parent):
2087
by_parent = tt.by_parent()
2088
for child in by_parent[old_parent]:
2089
tt.adjust_path(tt.final_name(child), new_parent, child)
2090
return by_parent[old_parent]
2092
def _content_match(tree, entry, file_id, kind, target_path):
2093
if entry.kind != kind:
2095
if entry.kind == "directory":
2097
if entry.kind == "file":
2098
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2100
elif entry.kind == "symlink":
2101
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2106
def resolve_checkout(tt, conflicts, divert):
2107
new_conflicts = set()
2108
for c_type, conflict in ((c[0], c) for c in conflicts):
2109
# Anything but a 'duplicate' would indicate programmer error
2110
if c_type != 'duplicate':
2111
raise AssertionError(c_type)
2112
# Now figure out which is new and which is old
2113
if tt.new_contents(conflict[1]):
2114
new_file = conflict[1]
2115
old_file = conflict[2]
2117
new_file = conflict[2]
2118
old_file = conflict[1]
2120
# We should only get here if the conflict wasn't completely
2122
final_parent = tt.final_parent(old_file)
2123
if new_file in divert:
2124
new_name = tt.final_name(old_file)+'.diverted'
2125
tt.adjust_path(new_name, final_parent, new_file)
2126
new_conflicts.add((c_type, 'Diverted to',
2127
new_file, old_file))
2129
new_name = tt.final_name(old_file)+'.moved'
2130
tt.adjust_path(new_name, final_parent, old_file)
2131
new_conflicts.add((c_type, 'Moved existing file to',
2132
old_file, new_file))
2133
return new_conflicts
2136
def new_by_entry(tt, entry, parent_id, tree):
2137
"""Create a new file according to its inventory entry"""
2141
contents = tree.get_file(entry.file_id).readlines()
2142
executable = tree.is_executable(entry.file_id)
2143
return tt.new_file(name, parent_id, contents, entry.file_id,
2145
elif kind in ('directory', 'tree-reference'):
2146
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2147
if kind == 'tree-reference':
2148
tt.set_tree_reference(entry.reference_revision, trans_id)
2150
elif kind == 'symlink':
2151
target = tree.get_symlink_target(entry.file_id)
2152
return tt.new_symlink(name, parent_id, target, entry.file_id)
2154
raise errors.BadFileKindError(name, kind)
2157
@deprecated_function(deprecated_in((1, 9, 0)))
2158
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2159
"""Create new file contents according to an inventory entry.
2161
DEPRECATED. Use create_from_tree instead.
2163
if entry.kind == "file":
2165
lines = tree.get_file(entry.file_id).readlines()
2166
tt.create_file(lines, trans_id, mode_id=mode_id)
2167
elif entry.kind == "symlink":
2168
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2169
elif entry.kind == "directory":
2170
tt.create_directory(trans_id)
2173
def create_from_tree(tt, trans_id, tree, file_id, bytes=None):
2174
"""Create new file contents according to tree contents."""
2175
kind = tree.kind(file_id)
2176
if kind == 'directory':
2177
tt.create_directory(trans_id)
2178
elif kind == "file":
2180
tree_file = tree.get_file(file_id)
2182
bytes = tree_file.readlines()
2185
tt.create_file(bytes, trans_id)
2186
elif kind == "symlink":
2187
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2189
raise AssertionError('Unknown kind %r' % kind)
2192
def create_entry_executability(tt, entry, trans_id):
2193
"""Set the executability of a trans_id according to an inventory entry"""
2194
if entry.kind == "file":
2195
tt.set_executability(entry.executable, trans_id)
2198
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2199
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2202
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2203
"""Produce a backup-style name that appears to be available"""
2207
yield "%s.~%d~" % (name, counter)
2209
for new_name in name_gen():
2210
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2214
def _entry_changes(file_id, entry, working_tree):
2215
"""Determine in which ways the inventory entry has changed.
2217
Returns booleans: has_contents, content_mod, meta_mod
2218
has_contents means there are currently contents, but they differ
2219
contents_mod means contents need to be modified
2220
meta_mod means the metadata needs to be modified
2222
cur_entry = working_tree.inventory[file_id]
2224
working_kind = working_tree.kind(file_id)
2227
has_contents = False
2230
if has_contents is True:
2231
if entry.kind != working_kind:
2232
contents_mod, meta_mod = True, False
2234
cur_entry._read_tree_state(working_tree.id2path(file_id),
2236
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2237
cur_entry._forget_tree_state()
2238
return has_contents, contents_mod, meta_mod
2241
def revert(working_tree, target_tree, filenames, backups=False,
2242
pb=DummyProgress(), change_reporter=None):
2243
"""Revert a working tree's contents to those of a target tree."""
2244
target_tree.lock_read()
2245
tt = TreeTransform(working_tree, pb)
2247
pp = ProgressPhase("Revert phase", 3, pb)
2248
conflicts, merge_modified = _prepare_revert_transform(
2249
working_tree, target_tree, tt, filenames, backups, pp)
2251
change_reporter = delta._ChangeReporter(
2252
unversioned_filter=working_tree.is_ignored)
2253
delta.report_changes(tt.iter_changes(), change_reporter)
2254
for conflict in conflicts:
2258
working_tree.set_merge_modified(merge_modified)
2260
target_tree.unlock()
2266
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2267
backups, pp, basis_tree=None,
2268
merge_modified=None):
2270
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2272
if merge_modified is None:
2273
merge_modified = working_tree.merge_modified()
2274
merge_modified = _alter_files(working_tree, target_tree, tt,
2275
child_pb, filenames, backups,
2276
merge_modified, basis_tree)
2280
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2282
raw_conflicts = resolve_conflicts(tt, child_pb,
2283
lambda t, c: conflict_pass(t, c, target_tree))
2286
conflicts = cook_conflicts(raw_conflicts, tt)
2287
return conflicts, merge_modified
2290
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2291
backups, merge_modified, basis_tree=None):
2292
if basis_tree is not None:
2293
basis_tree.lock_read()
2294
change_list = target_tree.iter_changes(working_tree,
2295
specific_files=specific_files, pb=pb)
2296
if target_tree.get_root_id() is None:
2302
for id_num, (file_id, path, changed_content, versioned, parent, name,
2303
kind, executable) in enumerate(change_list):
2304
if skip_root and file_id[0] is not None and parent[0] is None:
2306
trans_id = tt.trans_id_file_id(file_id)
2309
keep_content = False
2310
if kind[0] == 'file' and (backups or kind[1] is None):
2311
wt_sha1 = working_tree.get_file_sha1(file_id)
2312
if merge_modified.get(file_id) != wt_sha1:
2313
# acquire the basis tree lazily to prevent the
2314
# expense of accessing it when it's not needed ?
2315
# (Guessing, RBC, 200702)
2316
if basis_tree is None:
2317
basis_tree = working_tree.basis_tree()
2318
basis_tree.lock_read()
2319
if file_id in basis_tree:
2320
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2322
elif kind[1] is None and not versioned[1]:
2324
if kind[0] is not None:
2325
if not keep_content:
2326
tt.delete_contents(trans_id)
2327
elif kind[1] is not None:
2328
parent_trans_id = tt.trans_id_file_id(parent[0])
2329
by_parent = tt.by_parent()
2330
backup_name = _get_backup_name(name[0], by_parent,
2331
parent_trans_id, tt)
2332
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2333
new_trans_id = tt.create_path(name[0], parent_trans_id)
2334
if versioned == (True, True):
2335
tt.unversion_file(trans_id)
2336
tt.version_file(file_id, new_trans_id)
2337
# New contents should have the same unix perms as old
2340
trans_id = new_trans_id
2341
if kind[1] in ('directory', 'tree-reference'):
2342
tt.create_directory(trans_id)
2343
if kind[1] == 'tree-reference':
2344
revision = target_tree.get_reference_revision(file_id,
2346
tt.set_tree_reference(revision, trans_id)
2347
elif kind[1] == 'symlink':
2348
tt.create_symlink(target_tree.get_symlink_target(file_id),
2350
elif kind[1] == 'file':
2351
deferred_files.append((file_id, (trans_id, mode_id)))
2352
if basis_tree is None:
2353
basis_tree = working_tree.basis_tree()
2354
basis_tree.lock_read()
2355
new_sha1 = target_tree.get_file_sha1(file_id)
2356
if (file_id in basis_tree and new_sha1 ==
2357
basis_tree.get_file_sha1(file_id)):
2358
if file_id in merge_modified:
2359
del merge_modified[file_id]
2361
merge_modified[file_id] = new_sha1
2363
# preserve the execute bit when backing up
2364
if keep_content and executable[0] == executable[1]:
2365
tt.set_executability(executable[1], trans_id)
2366
elif kind[1] is not None:
2367
raise AssertionError(kind[1])
2368
if versioned == (False, True):
2369
tt.version_file(file_id, trans_id)
2370
if versioned == (True, False):
2371
tt.unversion_file(trans_id)
2372
if (name[1] is not None and
2373
(name[0] != name[1] or parent[0] != parent[1])):
2374
if name[1] == '' and parent[1] is None:
2375
parent_trans = ROOT_PARENT
2377
parent_trans = tt.trans_id_file_id(parent[1])
2378
tt.adjust_path(name[1], parent_trans, trans_id)
2379
if executable[0] != executable[1] and kind[1] == "file":
2380
tt.set_executability(executable[1], trans_id)
2381
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2383
tt.create_file(bytes, trans_id, mode_id)
2385
if basis_tree is not None:
2387
return merge_modified
2390
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2391
"""Make many conflict-resolution attempts, but die if they fail"""
2392
if pass_func is None:
2393
pass_func = conflict_pass
2394
new_conflicts = set()
2397
pb.update('Resolution pass', n+1, 10)
2398
conflicts = tt.find_conflicts()
2399
if len(conflicts) == 0:
2400
return new_conflicts
2401
new_conflicts.update(pass_func(tt, conflicts))
2402
raise MalformedTransform(conflicts=conflicts)
2407
def conflict_pass(tt, conflicts, path_tree=None):
2408
"""Resolve some classes of conflicts.
2410
:param tt: The transform to resolve conflicts in
2411
:param conflicts: The conflicts to resolve
2412
:param path_tree: A Tree to get supplemental paths from
2414
new_conflicts = set()
2415
for c_type, conflict in ((c[0], c) for c in conflicts):
2416
if c_type == 'duplicate id':
2417
tt.unversion_file(conflict[1])
2418
new_conflicts.add((c_type, 'Unversioned existing file',
2419
conflict[1], conflict[2], ))
2420
elif c_type == 'duplicate':
2421
# files that were renamed take precedence
2422
final_parent = tt.final_parent(conflict[1])
2423
if tt.path_changed(conflict[1]):
2424
existing_file, new_file = conflict[2], conflict[1]
2426
existing_file, new_file = conflict[1], conflict[2]
2427
new_name = tt.final_name(existing_file)+'.moved'
2428
tt.adjust_path(new_name, final_parent, existing_file)
2429
new_conflicts.add((c_type, 'Moved existing file to',
2430
existing_file, new_file))
2431
elif c_type == 'parent loop':
2432
# break the loop by undoing one of the ops that caused the loop
2434
while not tt.path_changed(cur):
2435
cur = tt.final_parent(cur)
2436
new_conflicts.add((c_type, 'Cancelled move', cur,
2437
tt.final_parent(cur),))
2438
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2440
elif c_type == 'missing parent':
2441
trans_id = conflict[1]
2443
tt.cancel_deletion(trans_id)
2444
new_conflicts.add(('deleting parent', 'Not deleting',
2449
tt.final_name(trans_id)
2451
if path_tree is not None:
2452
file_id = tt.final_file_id(trans_id)
2454
file_id = tt.inactive_file_id(trans_id)
2455
entry = path_tree.inventory[file_id]
2456
# special-case the other tree root (move its
2457
# children to current root)
2458
if entry.parent_id is None:
2460
moved = _reparent_transform_children(
2461
tt, trans_id, tt.root)
2463
new_conflicts.add((c_type, 'Moved to root',
2466
parent_trans_id = tt.trans_id_file_id(
2468
tt.adjust_path(entry.name, parent_trans_id,
2471
tt.create_directory(trans_id)
2472
new_conflicts.add((c_type, 'Created directory', trans_id))
2473
elif c_type == 'unversioned parent':
2474
file_id = tt.inactive_file_id(conflict[1])
2475
# special-case the other tree root (move its children instead)
2476
if path_tree and file_id in path_tree:
2477
if path_tree.inventory[file_id].parent_id is None:
2479
tt.version_file(file_id, conflict[1])
2480
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2481
elif c_type == 'non-directory parent':
2482
parent_id = conflict[1]
2483
parent_parent = tt.final_parent(parent_id)
2484
parent_name = tt.final_name(parent_id)
2485
parent_file_id = tt.final_file_id(parent_id)
2486
new_parent_id = tt.new_directory(parent_name + '.new',
2487
parent_parent, parent_file_id)
2488
_reparent_transform_children(tt, parent_id, new_parent_id)
2489
if parent_file_id is not None:
2490
tt.unversion_file(parent_id)
2491
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2492
elif c_type == 'versioning no contents':
2493
tt.cancel_versioning(conflict[1])
2494
return new_conflicts
2497
def cook_conflicts(raw_conflicts, tt):
2498
"""Generate a list of cooked conflicts, sorted by file path"""
2499
from bzrlib.conflicts import Conflict
2500
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2501
return sorted(conflict_iter, key=Conflict.sort_key)
2504
def iter_cook_conflicts(raw_conflicts, tt):
2505
from bzrlib.conflicts import Conflict
2507
for conflict in raw_conflicts:
2508
c_type = conflict[0]
2509
action = conflict[1]
2510
modified_path = fp.get_path(conflict[2])
2511
modified_id = tt.final_file_id(conflict[2])
2512
if len(conflict) == 3:
2513
yield Conflict.factory(c_type, action=action, path=modified_path,
2514
file_id=modified_id)
2517
conflicting_path = fp.get_path(conflict[3])
2518
conflicting_id = tt.final_file_id(conflict[3])
2519
yield Conflict.factory(c_type, action=action, path=modified_path,
2520
file_id=modified_id,
2521
conflict_path=conflicting_path,
2522
conflict_file_id=conflicting_id)
2525
class _FileMover(object):
2526
"""Moves and deletes files for TreeTransform, tracking operations"""
2529
self.past_renames = []
2530
self.pending_deletions = []
2532
def rename(self, from_, to):
2533
"""Rename a file from one path to another. Functions like os.rename"""
2535
os.rename(from_, to)
2537
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2538
raise errors.FileExists(to, str(e))
2540
self.past_renames.append((from_, to))
2542
def pre_delete(self, from_, to):
2543
"""Rename a file out of the way and mark it for deletion.
2545
Unlike os.unlink, this works equally well for files and directories.
2546
:param from_: The current file path
2547
:param to: A temporary path for the file
2549
self.rename(from_, to)
2550
self.pending_deletions.append(to)
2553
"""Reverse all renames that have been performed"""
2554
for from_, to in reversed(self.past_renames):
2555
os.rename(to, from_)
2556
# after rollback, don't reuse _FileMover
2558
pending_deletions = None
2560
def apply_deletions(self):
2561
"""Apply all marked deletions"""
2562
for path in self.pending_deletions:
2564
# after apply_deletions, don't reuse _FileMover
2566
pending_deletions = None