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 implementation does not support include_unchanged, specific_files,
1757
or want_unversioned. extra_trees, require_versioned, and pb are
1760
if from_tree is not self._transform._tree:
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 include_unchanged:
1769
raise ValueError('include_unchanged is not supported')
1770
if specific_files is not None:
1771
raise ValueError('specific_files is not supported')
1772
if want_unversioned:
1773
raise ValueError('want_unversioned is not supported')
1774
return self._transform.iter_changes()
1776
def get_file(self, file_id, path=None):
1777
"""See Tree.get_file"""
1778
if not self._content_change(file_id):
1779
return self._transform._tree.get_file(file_id, path)
1780
trans_id = self._transform.trans_id_file_id(file_id)
1781
name = self._transform._limbo_name(trans_id)
1782
return open(name, 'rb')
1784
def get_file_text(self, file_id):
1785
text_file = self.get_file(file_id)
1787
return text_file.read()
1791
def annotate_iter(self, file_id,
1792
default_revision=_mod_revision.CURRENT_REVISION):
1793
changes = self._changes(file_id)
1797
changed_content, versioned, kind = (changes[2], changes[3],
1801
get_old = (kind[0] == 'file' and versioned[0])
1803
old_annotation = self._transform._tree.annotate_iter(file_id,
1804
default_revision=default_revision)
1808
return old_annotation
1809
if not changed_content:
1810
return old_annotation
1811
return annotate.reannotate([old_annotation],
1812
self.get_file(file_id).readlines(),
1815
def get_symlink_target(self, file_id):
1816
"""See Tree.get_symlink_target"""
1817
if not self._content_change(file_id):
1818
return self._transform._tree.get_symlink_target(file_id)
1819
trans_id = self._transform.trans_id_file_id(file_id)
1820
name = self._transform._limbo_name(trans_id)
1821
return os.readlink(name)
1823
def walkdirs(self, prefix=''):
1824
pending = [self._transform.root]
1825
while len(pending) > 0:
1826
parent_id = pending.pop()
1829
prefix = prefix.rstrip('/')
1830
parent_path = self._final_paths.get_path(parent_id)
1831
parent_file_id = self._transform.final_file_id(parent_id)
1832
for child_id in self._all_children(parent_id):
1833
path_from_root = self._final_paths.get_path(child_id)
1834
basename = self._transform.final_name(child_id)
1835
file_id = self._transform.final_file_id(child_id)
1837
kind = self._transform.final_kind(child_id)
1838
versioned_kind = kind
1841
versioned_kind = self._transform._tree.stored_kind(file_id)
1842
if versioned_kind == 'directory':
1843
subdirs.append(child_id)
1844
children.append((path_from_root, basename, kind, None,
1845
file_id, versioned_kind))
1847
if parent_path.startswith(prefix):
1848
yield (parent_path, parent_file_id), children
1849
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1852
def get_parent_ids(self):
1853
return self._parent_ids
1855
def set_parent_ids(self, parent_ids):
1856
self._parent_ids = parent_ids
1858
def get_revision_tree(self, revision_id):
1859
return self._transform._tree.get_revision_tree(revision_id)
1862
def joinpath(parent, child):
1863
"""Join tree-relative paths, handling the tree root specially"""
1864
if parent is None or parent == "":
1867
return pathjoin(parent, child)
1870
class FinalPaths(object):
1871
"""Make path calculation cheap by memoizing paths.
1873
The underlying tree must not be manipulated between calls, or else
1874
the results will likely be incorrect.
1876
def __init__(self, transform):
1877
object.__init__(self)
1878
self._known_paths = {}
1879
self.transform = transform
1881
def _determine_path(self, trans_id):
1882
if trans_id == self.transform.root:
1884
name = self.transform.final_name(trans_id)
1885
parent_id = self.transform.final_parent(trans_id)
1886
if parent_id == self.transform.root:
1889
return pathjoin(self.get_path(parent_id), name)
1891
def get_path(self, trans_id):
1892
"""Find the final path associated with a trans_id"""
1893
if trans_id not in self._known_paths:
1894
self._known_paths[trans_id] = self._determine_path(trans_id)
1895
return self._known_paths[trans_id]
1897
def get_paths(self, trans_ids):
1898
return [(self.get_path(t), t) for t in trans_ids]
1902
def topology_sorted_ids(tree):
1903
"""Determine the topological order of the ids in a tree"""
1904
file_ids = list(tree)
1905
file_ids.sort(key=tree.id2path)
1909
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1910
delta_from_tree=False):
1911
"""Create working tree for a branch, using a TreeTransform.
1913
This function should be used on empty trees, having a tree root at most.
1914
(see merge and revert functionality for working with existing trees)
1916
Existing files are handled like so:
1918
- Existing bzrdirs take precedence over creating new items. They are
1919
created as '%s.diverted' % name.
1920
- Otherwise, if the content on disk matches the content we are building,
1921
it is silently replaced.
1922
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1924
:param tree: The tree to convert wt into a copy of
1925
:param wt: The working tree that files will be placed into
1926
:param accelerator_tree: A tree which can be used for retrieving file
1927
contents more quickly than tree itself, i.e. a workingtree. tree
1928
will be used for cases where accelerator_tree's content is different.
1929
:param hardlink: If true, hard-link files to accelerator_tree, where
1930
possible. accelerator_tree must implement abspath, i.e. be a
1932
:param delta_from_tree: If true, build_tree may use the input Tree to
1933
generate the inventory delta.
1935
wt.lock_tree_write()
1939
if accelerator_tree is not None:
1940
accelerator_tree.lock_read()
1942
return _build_tree(tree, wt, accelerator_tree, hardlink,
1945
if accelerator_tree is not None:
1946
accelerator_tree.unlock()
1953
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1954
"""See build_tree."""
1955
for num, _unused in enumerate(wt.all_file_ids()):
1956
if num > 0: # more than just a root
1957
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1958
existing_files = set()
1959
for dir, files in wt.walkdirs():
1960
existing_files.update(f[0] for f in files)
1962
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1963
pp = ProgressPhase("Build phase", 2, top_pb)
1964
if tree.inventory.root is not None:
1965
# This is kind of a hack: we should be altering the root
1966
# as part of the regular tree shape diff logic.
1967
# The conditional test here is to avoid doing an
1968
# expensive operation (flush) every time the root id
1969
# is set within the tree, nor setting the root and thus
1970
# marking the tree as dirty, because we use two different
1971
# idioms here: tree interfaces and inventory interfaces.
1972
if wt.get_root_id() != tree.get_root_id():
1973
wt.set_root_id(tree.get_root_id())
1975
tt = TreeTransform(wt)
1979
file_trans_id[wt.get_root_id()] = \
1980
tt.trans_id_tree_file_id(wt.get_root_id())
1981
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1983
deferred_contents = []
1985
total = len(tree.inventory)
1987
precomputed_delta = []
1989
precomputed_delta = None
1990
for num, (tree_path, entry) in \
1991
enumerate(tree.inventory.iter_entries_by_dir()):
1992
pb.update("Building tree", num - len(deferred_contents), total)
1993
if entry.parent_id is None:
1996
file_id = entry.file_id
1998
precomputed_delta.append((None, tree_path, file_id, entry))
1999
if tree_path in existing_files:
2000
target_path = wt.abspath(tree_path)
2001
kind = file_kind(target_path)
2002
if kind == "directory":
2004
bzrdir.BzrDir.open(target_path)
2005
except errors.NotBranchError:
2009
if (file_id not in divert and
2010
_content_match(tree, entry, file_id, kind,
2012
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2013
if kind == 'directory':
2015
parent_id = file_trans_id[entry.parent_id]
2016
if entry.kind == 'file':
2017
# We *almost* replicate new_by_entry, so that we can defer
2018
# getting the file text, and get them all at once.
2019
trans_id = tt.create_path(entry.name, parent_id)
2020
file_trans_id[file_id] = trans_id
2021
tt.version_file(file_id, trans_id)
2022
executable = tree.is_executable(file_id, tree_path)
2024
tt.set_executability(executable, trans_id)
2025
deferred_contents.append((file_id, trans_id))
2027
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2030
new_trans_id = file_trans_id[file_id]
2031
old_parent = tt.trans_id_tree_path(tree_path)
2032
_reparent_children(tt, old_parent, new_trans_id)
2033
offset = num + 1 - len(deferred_contents)
2034
_create_files(tt, tree, deferred_contents, pb, offset,
2035
accelerator_tree, hardlink)
2039
divert_trans = set(file_trans_id[f] for f in divert)
2040
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2041
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2042
if len(raw_conflicts) > 0:
2043
precomputed_delta = None
2044
conflicts = cook_conflicts(raw_conflicts, tt)
2045
for conflict in conflicts:
2048
wt.add_conflicts(conflicts)
2049
except errors.UnsupportedOperation:
2051
result = tt.apply(no_conflicts=True,
2052
precomputed_delta=precomputed_delta)
2059
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2061
total = len(desired_files) + offset
2062
if accelerator_tree is None:
2063
new_desired_files = desired_files
2065
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2066
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2067
in iter if not (c or e[0] != e[1]))
2068
new_desired_files = []
2070
for file_id, trans_id in desired_files:
2071
accelerator_path = unchanged.get(file_id)
2072
if accelerator_path is None:
2073
new_desired_files.append((file_id, trans_id))
2075
pb.update('Adding file contents', count + offset, total)
2077
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2080
contents = accelerator_tree.get_file(file_id, accelerator_path)
2082
tt.create_file(contents, trans_id)
2087
for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
2088
new_desired_files)):
2089
tt.create_file(contents, trans_id)
2090
pb.update('Adding file contents', count + offset, total)
2093
def _reparent_children(tt, old_parent, new_parent):
2094
for child in tt.iter_tree_children(old_parent):
2095
tt.adjust_path(tt.final_name(child), new_parent, child)
2097
def _reparent_transform_children(tt, old_parent, new_parent):
2098
by_parent = tt.by_parent()
2099
for child in by_parent[old_parent]:
2100
tt.adjust_path(tt.final_name(child), new_parent, child)
2101
return by_parent[old_parent]
2103
def _content_match(tree, entry, file_id, kind, target_path):
2104
if entry.kind != kind:
2106
if entry.kind == "directory":
2108
if entry.kind == "file":
2109
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2111
elif entry.kind == "symlink":
2112
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2117
def resolve_checkout(tt, conflicts, divert):
2118
new_conflicts = set()
2119
for c_type, conflict in ((c[0], c) for c in conflicts):
2120
# Anything but a 'duplicate' would indicate programmer error
2121
if c_type != 'duplicate':
2122
raise AssertionError(c_type)
2123
# Now figure out which is new and which is old
2124
if tt.new_contents(conflict[1]):
2125
new_file = conflict[1]
2126
old_file = conflict[2]
2128
new_file = conflict[2]
2129
old_file = conflict[1]
2131
# We should only get here if the conflict wasn't completely
2133
final_parent = tt.final_parent(old_file)
2134
if new_file in divert:
2135
new_name = tt.final_name(old_file)+'.diverted'
2136
tt.adjust_path(new_name, final_parent, new_file)
2137
new_conflicts.add((c_type, 'Diverted to',
2138
new_file, old_file))
2140
new_name = tt.final_name(old_file)+'.moved'
2141
tt.adjust_path(new_name, final_parent, old_file)
2142
new_conflicts.add((c_type, 'Moved existing file to',
2143
old_file, new_file))
2144
return new_conflicts
2147
def new_by_entry(tt, entry, parent_id, tree):
2148
"""Create a new file according to its inventory entry"""
2152
contents = tree.get_file(entry.file_id).readlines()
2153
executable = tree.is_executable(entry.file_id)
2154
return tt.new_file(name, parent_id, contents, entry.file_id,
2156
elif kind in ('directory', 'tree-reference'):
2157
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2158
if kind == 'tree-reference':
2159
tt.set_tree_reference(entry.reference_revision, trans_id)
2161
elif kind == 'symlink':
2162
target = tree.get_symlink_target(entry.file_id)
2163
return tt.new_symlink(name, parent_id, target, entry.file_id)
2165
raise errors.BadFileKindError(name, kind)
2168
@deprecated_function(deprecated_in((1, 9, 0)))
2169
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2170
"""Create new file contents according to an inventory entry.
2172
DEPRECATED. Use create_from_tree instead.
2174
if entry.kind == "file":
2176
lines = tree.get_file(entry.file_id).readlines()
2177
tt.create_file(lines, trans_id, mode_id=mode_id)
2178
elif entry.kind == "symlink":
2179
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2180
elif entry.kind == "directory":
2181
tt.create_directory(trans_id)
2184
def create_from_tree(tt, trans_id, tree, file_id, bytes=None):
2185
"""Create new file contents according to tree contents."""
2186
kind = tree.kind(file_id)
2187
if kind == 'directory':
2188
tt.create_directory(trans_id)
2189
elif kind == "file":
2191
tree_file = tree.get_file(file_id)
2193
bytes = tree_file.readlines()
2196
tt.create_file(bytes, trans_id)
2197
elif kind == "symlink":
2198
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2200
raise AssertionError('Unknown kind %r' % kind)
2203
def create_entry_executability(tt, entry, trans_id):
2204
"""Set the executability of a trans_id according to an inventory entry"""
2205
if entry.kind == "file":
2206
tt.set_executability(entry.executable, trans_id)
2209
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2210
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2213
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2214
"""Produce a backup-style name that appears to be available"""
2218
yield "%s.~%d~" % (name, counter)
2220
for new_name in name_gen():
2221
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2225
def _entry_changes(file_id, entry, working_tree):
2226
"""Determine in which ways the inventory entry has changed.
2228
Returns booleans: has_contents, content_mod, meta_mod
2229
has_contents means there are currently contents, but they differ
2230
contents_mod means contents need to be modified
2231
meta_mod means the metadata needs to be modified
2233
cur_entry = working_tree.inventory[file_id]
2235
working_kind = working_tree.kind(file_id)
2238
has_contents = False
2241
if has_contents is True:
2242
if entry.kind != working_kind:
2243
contents_mod, meta_mod = True, False
2245
cur_entry._read_tree_state(working_tree.id2path(file_id),
2247
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2248
cur_entry._forget_tree_state()
2249
return has_contents, contents_mod, meta_mod
2252
def revert(working_tree, target_tree, filenames, backups=False,
2253
pb=DummyProgress(), change_reporter=None):
2254
"""Revert a working tree's contents to those of a target tree."""
2255
target_tree.lock_read()
2256
tt = TreeTransform(working_tree, pb)
2258
pp = ProgressPhase("Revert phase", 3, pb)
2259
conflicts, merge_modified = _prepare_revert_transform(
2260
working_tree, target_tree, tt, filenames, backups, pp)
2262
change_reporter = delta._ChangeReporter(
2263
unversioned_filter=working_tree.is_ignored)
2264
delta.report_changes(tt.iter_changes(), change_reporter)
2265
for conflict in conflicts:
2269
working_tree.set_merge_modified(merge_modified)
2271
target_tree.unlock()
2277
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2278
backups, pp, basis_tree=None,
2279
merge_modified=None):
2281
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2283
if merge_modified is None:
2284
merge_modified = working_tree.merge_modified()
2285
merge_modified = _alter_files(working_tree, target_tree, tt,
2286
child_pb, filenames, backups,
2287
merge_modified, basis_tree)
2291
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2293
raw_conflicts = resolve_conflicts(tt, child_pb,
2294
lambda t, c: conflict_pass(t, c, target_tree))
2297
conflicts = cook_conflicts(raw_conflicts, tt)
2298
return conflicts, merge_modified
2301
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2302
backups, merge_modified, basis_tree=None):
2303
if basis_tree is not None:
2304
basis_tree.lock_read()
2305
change_list = target_tree.iter_changes(working_tree,
2306
specific_files=specific_files, pb=pb)
2307
if target_tree.get_root_id() is None:
2313
for id_num, (file_id, path, changed_content, versioned, parent, name,
2314
kind, executable) in enumerate(change_list):
2315
if skip_root and file_id[0] is not None and parent[0] is None:
2317
trans_id = tt.trans_id_file_id(file_id)
2320
keep_content = False
2321
if kind[0] == 'file' and (backups or kind[1] is None):
2322
wt_sha1 = working_tree.get_file_sha1(file_id)
2323
if merge_modified.get(file_id) != wt_sha1:
2324
# acquire the basis tree lazily to prevent the
2325
# expense of accessing it when it's not needed ?
2326
# (Guessing, RBC, 200702)
2327
if basis_tree is None:
2328
basis_tree = working_tree.basis_tree()
2329
basis_tree.lock_read()
2330
if file_id in basis_tree:
2331
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2333
elif kind[1] is None and not versioned[1]:
2335
if kind[0] is not None:
2336
if not keep_content:
2337
tt.delete_contents(trans_id)
2338
elif kind[1] is not None:
2339
parent_trans_id = tt.trans_id_file_id(parent[0])
2340
by_parent = tt.by_parent()
2341
backup_name = _get_backup_name(name[0], by_parent,
2342
parent_trans_id, tt)
2343
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2344
new_trans_id = tt.create_path(name[0], parent_trans_id)
2345
if versioned == (True, True):
2346
tt.unversion_file(trans_id)
2347
tt.version_file(file_id, new_trans_id)
2348
# New contents should have the same unix perms as old
2351
trans_id = new_trans_id
2352
if kind[1] in ('directory', 'tree-reference'):
2353
tt.create_directory(trans_id)
2354
if kind[1] == 'tree-reference':
2355
revision = target_tree.get_reference_revision(file_id,
2357
tt.set_tree_reference(revision, trans_id)
2358
elif kind[1] == 'symlink':
2359
tt.create_symlink(target_tree.get_symlink_target(file_id),
2361
elif kind[1] == 'file':
2362
deferred_files.append((file_id, (trans_id, mode_id)))
2363
if basis_tree is None:
2364
basis_tree = working_tree.basis_tree()
2365
basis_tree.lock_read()
2366
new_sha1 = target_tree.get_file_sha1(file_id)
2367
if (file_id in basis_tree and new_sha1 ==
2368
basis_tree.get_file_sha1(file_id)):
2369
if file_id in merge_modified:
2370
del merge_modified[file_id]
2372
merge_modified[file_id] = new_sha1
2374
# preserve the execute bit when backing up
2375
if keep_content and executable[0] == executable[1]:
2376
tt.set_executability(executable[1], trans_id)
2377
elif kind[1] is not None:
2378
raise AssertionError(kind[1])
2379
if versioned == (False, True):
2380
tt.version_file(file_id, trans_id)
2381
if versioned == (True, False):
2382
tt.unversion_file(trans_id)
2383
if (name[1] is not None and
2384
(name[0] != name[1] or parent[0] != parent[1])):
2385
if name[1] == '' and parent[1] is None:
2386
parent_trans = ROOT_PARENT
2388
parent_trans = tt.trans_id_file_id(parent[1])
2389
tt.adjust_path(name[1], parent_trans, trans_id)
2390
if executable[0] != executable[1] and kind[1] == "file":
2391
tt.set_executability(executable[1], trans_id)
2392
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2394
tt.create_file(bytes, trans_id, mode_id)
2396
if basis_tree is not None:
2398
return merge_modified
2401
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2402
"""Make many conflict-resolution attempts, but die if they fail"""
2403
if pass_func is None:
2404
pass_func = conflict_pass
2405
new_conflicts = set()
2408
pb.update('Resolution pass', n+1, 10)
2409
conflicts = tt.find_conflicts()
2410
if len(conflicts) == 0:
2411
return new_conflicts
2412
new_conflicts.update(pass_func(tt, conflicts))
2413
raise MalformedTransform(conflicts=conflicts)
2418
def conflict_pass(tt, conflicts, path_tree=None):
2419
"""Resolve some classes of conflicts.
2421
:param tt: The transform to resolve conflicts in
2422
:param conflicts: The conflicts to resolve
2423
:param path_tree: A Tree to get supplemental paths from
2425
new_conflicts = set()
2426
for c_type, conflict in ((c[0], c) for c in conflicts):
2427
if c_type == 'duplicate id':
2428
tt.unversion_file(conflict[1])
2429
new_conflicts.add((c_type, 'Unversioned existing file',
2430
conflict[1], conflict[2], ))
2431
elif c_type == 'duplicate':
2432
# files that were renamed take precedence
2433
final_parent = tt.final_parent(conflict[1])
2434
if tt.path_changed(conflict[1]):
2435
existing_file, new_file = conflict[2], conflict[1]
2437
existing_file, new_file = conflict[1], conflict[2]
2438
new_name = tt.final_name(existing_file)+'.moved'
2439
tt.adjust_path(new_name, final_parent, existing_file)
2440
new_conflicts.add((c_type, 'Moved existing file to',
2441
existing_file, new_file))
2442
elif c_type == 'parent loop':
2443
# break the loop by undoing one of the ops that caused the loop
2445
while not tt.path_changed(cur):
2446
cur = tt.final_parent(cur)
2447
new_conflicts.add((c_type, 'Cancelled move', cur,
2448
tt.final_parent(cur),))
2449
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2451
elif c_type == 'missing parent':
2452
trans_id = conflict[1]
2454
tt.cancel_deletion(trans_id)
2455
new_conflicts.add(('deleting parent', 'Not deleting',
2460
tt.final_name(trans_id)
2462
if path_tree is not None:
2463
file_id = tt.final_file_id(trans_id)
2465
file_id = tt.inactive_file_id(trans_id)
2466
entry = path_tree.inventory[file_id]
2467
# special-case the other tree root (move its
2468
# children to current root)
2469
if entry.parent_id is None:
2471
moved = _reparent_transform_children(
2472
tt, trans_id, tt.root)
2474
new_conflicts.add((c_type, 'Moved to root',
2477
parent_trans_id = tt.trans_id_file_id(
2479
tt.adjust_path(entry.name, parent_trans_id,
2482
tt.create_directory(trans_id)
2483
new_conflicts.add((c_type, 'Created directory', trans_id))
2484
elif c_type == 'unversioned parent':
2485
file_id = tt.inactive_file_id(conflict[1])
2486
# special-case the other tree root (move its children instead)
2487
if path_tree and file_id in path_tree:
2488
if path_tree.inventory[file_id].parent_id is None:
2490
tt.version_file(file_id, conflict[1])
2491
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2492
elif c_type == 'non-directory parent':
2493
parent_id = conflict[1]
2494
parent_parent = tt.final_parent(parent_id)
2495
parent_name = tt.final_name(parent_id)
2496
parent_file_id = tt.final_file_id(parent_id)
2497
new_parent_id = tt.new_directory(parent_name + '.new',
2498
parent_parent, parent_file_id)
2499
_reparent_transform_children(tt, parent_id, new_parent_id)
2500
if parent_file_id is not None:
2501
tt.unversion_file(parent_id)
2502
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2503
elif c_type == 'versioning no contents':
2504
tt.cancel_versioning(conflict[1])
2505
return new_conflicts
2508
def cook_conflicts(raw_conflicts, tt):
2509
"""Generate a list of cooked conflicts, sorted by file path"""
2510
from bzrlib.conflicts import Conflict
2511
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2512
return sorted(conflict_iter, key=Conflict.sort_key)
2515
def iter_cook_conflicts(raw_conflicts, tt):
2516
from bzrlib.conflicts import Conflict
2518
for conflict in raw_conflicts:
2519
c_type = conflict[0]
2520
action = conflict[1]
2521
modified_path = fp.get_path(conflict[2])
2522
modified_id = tt.final_file_id(conflict[2])
2523
if len(conflict) == 3:
2524
yield Conflict.factory(c_type, action=action, path=modified_path,
2525
file_id=modified_id)
2528
conflicting_path = fp.get_path(conflict[3])
2529
conflicting_id = tt.final_file_id(conflict[3])
2530
yield Conflict.factory(c_type, action=action, path=modified_path,
2531
file_id=modified_id,
2532
conflict_path=conflicting_path,
2533
conflict_file_id=conflicting_id)
2536
class _FileMover(object):
2537
"""Moves and deletes files for TreeTransform, tracking operations"""
2540
self.past_renames = []
2541
self.pending_deletions = []
2543
def rename(self, from_, to):
2544
"""Rename a file from one path to another. Functions like os.rename"""
2546
os.rename(from_, to)
2548
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2549
raise errors.FileExists(to, str(e))
2551
self.past_renames.append((from_, to))
2553
def pre_delete(self, from_, to):
2554
"""Rename a file out of the way and mark it for deletion.
2556
Unlike os.unlink, this works equally well for files and directories.
2557
:param from_: The current file path
2558
:param to: A temporary path for the file
2560
self.rename(from_, to)
2561
self.pending_deletions.append(to)
2564
"""Reverse all renames that have been performed"""
2565
for from_, to in reversed(self.past_renames):
2566
os.rename(to, from_)
2567
# after rollback, don't reuse _FileMover
2569
pending_deletions = None
2571
def apply_deletions(self):
2572
"""Apply all marked deletions"""
2573
for path in self.pending_deletions:
2575
# after apply_deletions, don't reuse _FileMover
2577
pending_deletions = None