1
# Copyright (C) 2006, 2007 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
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
30
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
31
ReusingTransform, NotVersionedError, CantMoveRoot,
32
ExistingLimbo, ImmortalLimbo, NoFinalPath)
33
from bzrlib.inventory import InventoryEntry
34
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
36
from bzrlib.progress import DummyProgress, ProgressPhase
37
from bzrlib.symbol_versioning import deprecated_function, zero_fifteen
38
from bzrlib.trace import mutter, warning
39
from bzrlib import tree
41
import bzrlib.urlutils as urlutils
44
ROOT_PARENT = "root-parent"
47
def unique_add(map, key, value):
49
raise DuplicateKey(key=key)
53
class _TransformResults(object):
54
def __init__(self, modified_paths, rename_count):
56
self.modified_paths = modified_paths
57
self.rename_count = rename_count
60
class TreeTransform(object):
61
"""Represent a tree transformation.
63
This object is designed to support incremental generation of the transform,
66
However, it gives optimum performance when parent directories are created
67
before their contents. The transform is then able to put child files
68
directly in their parent directory, avoiding later renames.
70
It is easy to produce malformed transforms, but they are generally
71
harmless. Attempting to apply a malformed transform will cause an
72
exception to be raised before any modifications are made to the tree.
74
Many kinds of malformed transforms can be corrected with the
75
resolve_conflicts function. The remaining ones indicate programming error,
76
such as trying to create a file with no path.
78
Two sets of file creation methods are supplied. Convenience methods are:
83
These are composed of the low-level methods:
85
* create_file or create_directory or create_symlink
89
def __init__(self, tree, pb=DummyProgress()):
90
"""Note: a tree_write lock is taken on the tree.
92
Use TreeTransform.finalize() to release the lock (can be omitted if
93
TreeTransform.apply() called).
97
self._tree.lock_tree_write()
99
control_files = self._tree._control_files
100
self._limbodir = urlutils.local_path_from_url(
101
control_files.controlfilename('limbo'))
103
os.mkdir(self._limbodir)
105
if e.errno == errno.EEXIST:
106
raise ExistingLimbo(self._limbodir)
113
self._new_parent = {}
114
self._new_contents = {}
115
# A mapping of transform ids to their limbo filename
116
self._limbo_files = {}
117
# A mapping of transform ids to a set of the transform ids of children
118
# that their limbo directory has
119
self._limbo_children = {}
120
# Map transform ids to maps of child filename to child transform id
121
self._limbo_children_names = {}
122
# List of transform ids that need to be renamed from limbo into place
123
self._needs_rename = set()
124
self._removed_contents = set()
125
self._new_executability = {}
126
self._new_reference_revision = {}
128
self._non_present_ids = {}
130
self._removed_id = set()
131
self._tree_path_ids = {}
132
self._tree_id_paths = {}
133
# Cache of realpath results, to speed up canonical_path
135
# Cache of relpath results, to speed up canonical_path
137
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
140
self.rename_count = 0
142
def __get_root(self):
143
return self._new_root
145
root = property(__get_root)
148
"""Release the working tree lock, if held, clean up limbo dir.
150
This is required if apply has not been invoked, but can be invoked
153
if self._tree is None:
156
entries = [(self._limbo_name(t), t, k) for t, k in
157
self._new_contents.iteritems()]
158
entries.sort(reverse=True)
159
for path, trans_id, kind in entries:
160
if kind == "directory":
165
os.rmdir(self._limbodir)
167
# We don't especially care *why* the dir is immortal.
168
raise ImmortalLimbo(self._limbodir)
173
def _assign_id(self):
174
"""Produce a new tranform id"""
175
new_id = "new-%s" % self._id_number
179
def create_path(self, name, parent):
180
"""Assign a transaction id to a new path"""
181
trans_id = self._assign_id()
182
unique_add(self._new_name, trans_id, name)
183
unique_add(self._new_parent, trans_id, parent)
186
def adjust_path(self, name, parent, trans_id):
187
"""Change the path that is assigned to a transaction id."""
188
if trans_id == self._new_root:
190
previous_parent = self._new_parent.get(trans_id)
191
previous_name = self._new_name.get(trans_id)
192
self._new_name[trans_id] = name
193
self._new_parent[trans_id] = parent
194
if (trans_id in self._limbo_files and
195
trans_id not in self._needs_rename):
196
self._rename_in_limbo([trans_id])
197
self._limbo_children[previous_parent].remove(trans_id)
198
del self._limbo_children_names[previous_parent][previous_name]
200
def _rename_in_limbo(self, trans_ids):
201
"""Fix limbo names so that the right final path is produced.
203
This means we outsmarted ourselves-- we tried to avoid renaming
204
these files later by creating them with their final names in their
205
final parents. But now the previous name or parent is no longer
206
suitable, so we have to rename them.
208
for trans_id in trans_ids:
209
old_path = self._limbo_files[trans_id]
210
new_path = self._limbo_name(trans_id, from_scratch=True)
211
os.rename(old_path, new_path)
213
def adjust_root_path(self, name, parent):
214
"""Emulate moving the root by moving all children, instead.
216
We do this by undoing the association of root's transaction id with the
217
current tree. This allows us to create a new directory with that
218
transaction id. We unversion the root directory and version the
219
physically new directory, and hope someone versions the tree root
222
old_root = self._new_root
223
old_root_file_id = self.final_file_id(old_root)
224
# force moving all children of root
225
for child_id in self.iter_tree_children(old_root):
226
if child_id != parent:
227
self.adjust_path(self.final_name(child_id),
228
self.final_parent(child_id), child_id)
229
file_id = self.final_file_id(child_id)
230
if file_id is not None:
231
self.unversion_file(child_id)
232
self.version_file(file_id, child_id)
234
# the physical root needs a new transaction id
235
self._tree_path_ids.pop("")
236
self._tree_id_paths.pop(old_root)
237
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
238
if parent == old_root:
239
parent = self._new_root
240
self.adjust_path(name, parent, old_root)
241
self.create_directory(old_root)
242
self.version_file(old_root_file_id, old_root)
243
self.unversion_file(self._new_root)
245
def trans_id_tree_file_id(self, inventory_id):
246
"""Determine the transaction id of a working tree file.
248
This reflects only files that already exist, not ones that will be
249
added by transactions.
251
path = self._tree.inventory.id2path(inventory_id)
252
return self.trans_id_tree_path(path)
254
def trans_id_file_id(self, file_id):
255
"""Determine or set the transaction id associated with a file ID.
256
A new id is only created for file_ids that were never present. If
257
a transaction has been unversioned, it is deliberately still returned.
258
(this will likely lead to an unversioned parent conflict.)
260
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
261
return self._r_new_id[file_id]
262
elif file_id in self._tree.inventory:
263
return self.trans_id_tree_file_id(file_id)
264
elif file_id in self._non_present_ids:
265
return self._non_present_ids[file_id]
267
trans_id = self._assign_id()
268
self._non_present_ids[file_id] = trans_id
271
def canonical_path(self, path):
272
"""Get the canonical tree-relative path"""
273
# don't follow final symlinks
274
abs = self._tree.abspath(path)
275
if abs in self._relpaths:
276
return self._relpaths[abs]
277
dirname, basename = os.path.split(abs)
278
if dirname not in self._realpaths:
279
self._realpaths[dirname] = os.path.realpath(dirname)
280
dirname = self._realpaths[dirname]
281
abs = pathjoin(dirname, basename)
282
if dirname in self._relpaths:
283
relpath = pathjoin(self._relpaths[dirname], basename)
284
relpath = relpath.rstrip('/\\')
286
relpath = self._tree.relpath(abs)
287
self._relpaths[abs] = relpath
290
def trans_id_tree_path(self, path):
291
"""Determine (and maybe set) the transaction ID for a tree path."""
292
path = self.canonical_path(path)
293
if path not in self._tree_path_ids:
294
self._tree_path_ids[path] = self._assign_id()
295
self._tree_id_paths[self._tree_path_ids[path]] = path
296
return self._tree_path_ids[path]
298
def get_tree_parent(self, trans_id):
299
"""Determine id of the parent in the tree."""
300
path = self._tree_id_paths[trans_id]
303
return self.trans_id_tree_path(os.path.dirname(path))
305
def create_file(self, contents, trans_id, mode_id=None):
306
"""Schedule creation of a new file.
310
Contents is an iterator of strings, all of which will be written
311
to the target destination.
313
New file takes the permissions of any existing file with that id,
314
unless mode_id is specified.
316
name = self._limbo_name(trans_id)
320
unique_add(self._new_contents, trans_id, 'file')
322
# Clean up the file, it never got registered so
323
# TreeTransform.finalize() won't clean it up.
328
f.writelines(contents)
331
self._set_mode(trans_id, mode_id, S_ISREG)
333
def _set_mode(self, trans_id, mode_id, typefunc):
334
"""Set the mode of new file contents.
335
The mode_id is the existing file to get the mode from (often the same
336
as trans_id). The operation is only performed if there's a mode match
337
according to typefunc.
342
old_path = self._tree_id_paths[mode_id]
346
mode = os.stat(self._tree.abspath(old_path)).st_mode
348
if e.errno == errno.ENOENT:
353
os.chmod(self._limbo_name(trans_id), mode)
355
def create_directory(self, trans_id):
356
"""Schedule creation of a new directory.
358
See also new_directory.
360
os.mkdir(self._limbo_name(trans_id))
361
unique_add(self._new_contents, trans_id, 'directory')
363
def create_symlink(self, target, trans_id):
364
"""Schedule creation of a new symbolic link.
366
target is a bytestring.
367
See also new_symlink.
369
os.symlink(target, self._limbo_name(trans_id))
370
unique_add(self._new_contents, trans_id, 'symlink')
372
def cancel_creation(self, trans_id):
373
"""Cancel the creation of new file contents."""
374
del self._new_contents[trans_id]
375
children = self._limbo_children.get(trans_id)
376
# if this is a limbo directory with children, move them before removing
378
if children is not None:
379
self._rename_in_limbo(children)
380
del self._limbo_children[trans_id]
381
del self._limbo_children_names[trans_id]
382
delete_any(self._limbo_name(trans_id))
384
def delete_contents(self, trans_id):
385
"""Schedule the contents of a path entry for deletion"""
386
self.tree_kind(trans_id)
387
self._removed_contents.add(trans_id)
389
def cancel_deletion(self, trans_id):
390
"""Cancel a scheduled deletion"""
391
self._removed_contents.remove(trans_id)
393
def unversion_file(self, trans_id):
394
"""Schedule a path entry to become unversioned"""
395
self._removed_id.add(trans_id)
397
def delete_versioned(self, trans_id):
398
"""Delete and unversion a versioned file"""
399
self.delete_contents(trans_id)
400
self.unversion_file(trans_id)
402
def set_executability(self, executability, trans_id):
403
"""Schedule setting of the 'execute' bit
404
To unschedule, set to None
406
if executability is None:
407
del self._new_executability[trans_id]
409
unique_add(self._new_executability, trans_id, executability)
411
def set_tree_reference(self, revision_id, trans_id):
412
"""Set the reference associated with a directory"""
413
unique_add(self._new_reference_revision, trans_id, revision_id)
415
def version_file(self, file_id, trans_id):
416
"""Schedule a file to become versioned."""
417
assert file_id is not None
418
unique_add(self._new_id, trans_id, file_id)
419
unique_add(self._r_new_id, file_id, trans_id)
421
def cancel_versioning(self, trans_id):
422
"""Undo a previous versioning of a file"""
423
file_id = self._new_id[trans_id]
424
del self._new_id[trans_id]
425
del self._r_new_id[file_id]
428
"""Determine the paths of all new and changed files"""
430
fp = FinalPaths(self)
431
for id_set in (self._new_name, self._new_parent, self._new_contents,
432
self._new_id, self._new_executability):
433
new_ids.update(id_set)
434
new_paths = [(fp.get_path(t), t) for t in new_ids]
438
def tree_kind(self, trans_id):
439
"""Determine the file kind in the working tree.
441
Raises NoSuchFile if the file does not exist
443
path = self._tree_id_paths.get(trans_id)
445
raise NoSuchFile(None)
447
return file_kind(self._tree.abspath(path))
449
if e.errno != errno.ENOENT:
452
raise NoSuchFile(path)
454
def final_kind(self, trans_id):
455
"""Determine the final file kind, after any changes applied.
457
Raises NoSuchFile if the file does not exist/has no contents.
458
(It is conceivable that a path would be created without the
459
corresponding contents insertion command)
461
if trans_id in self._new_contents:
462
return self._new_contents[trans_id]
463
elif trans_id in self._removed_contents:
464
raise NoSuchFile(None)
466
return self.tree_kind(trans_id)
468
def tree_file_id(self, trans_id):
469
"""Determine the file id associated with the trans_id in the tree"""
471
path = self._tree_id_paths[trans_id]
473
# the file is a new, unversioned file, or invalid trans_id
475
# the file is old; the old id is still valid
476
if self._new_root == trans_id:
477
return self._tree.inventory.root.file_id
478
return self._tree.inventory.path2id(path)
480
def final_file_id(self, trans_id):
481
"""Determine the file id after any changes are applied, or None.
483
None indicates that the file will not be versioned after changes are
487
# there is a new id for this file
488
assert self._new_id[trans_id] is not None
489
return self._new_id[trans_id]
491
if trans_id in self._removed_id:
493
return self.tree_file_id(trans_id)
495
def inactive_file_id(self, trans_id):
496
"""Return the inactive file_id associated with a transaction id.
497
That is, the one in the tree or in non_present_ids.
498
The file_id may actually be active, too.
500
file_id = self.tree_file_id(trans_id)
501
if file_id is not None:
503
for key, value in self._non_present_ids.iteritems():
504
if value == trans_id:
507
def final_parent(self, trans_id):
508
"""Determine the parent file_id, after any changes are applied.
510
ROOT_PARENT is returned for the tree root.
513
return self._new_parent[trans_id]
515
return self.get_tree_parent(trans_id)
517
def final_name(self, trans_id):
518
"""Determine the final filename, after all changes are applied."""
520
return self._new_name[trans_id]
523
return os.path.basename(self._tree_id_paths[trans_id])
525
raise NoFinalPath(trans_id, self)
528
"""Return a map of parent: children for known parents.
530
Only new paths and parents of tree files with assigned ids are used.
533
items = list(self._new_parent.iteritems())
534
items.extend((t, self.final_parent(t)) for t in
535
self._tree_id_paths.keys())
536
for trans_id, parent_id in items:
537
if parent_id not in by_parent:
538
by_parent[parent_id] = set()
539
by_parent[parent_id].add(trans_id)
542
def path_changed(self, trans_id):
543
"""Return True if a trans_id's path has changed."""
544
return (trans_id in self._new_name) or (trans_id in self._new_parent)
546
def new_contents(self, trans_id):
547
return (trans_id in self._new_contents)
549
def find_conflicts(self):
550
"""Find any violations of inventory or filesystem invariants"""
551
if self.__done is True:
552
raise ReusingTransform()
554
# ensure all children of all existent parents are known
555
# all children of non-existent parents are known, by definition.
556
self._add_tree_children()
557
by_parent = self.by_parent()
558
conflicts.extend(self._unversioned_parents(by_parent))
559
conflicts.extend(self._parent_loops())
560
conflicts.extend(self._duplicate_entries(by_parent))
561
conflicts.extend(self._duplicate_ids())
562
conflicts.extend(self._parent_type_conflicts(by_parent))
563
conflicts.extend(self._improper_versioning())
564
conflicts.extend(self._executability_conflicts())
565
conflicts.extend(self._overwrite_conflicts())
568
def _add_tree_children(self):
569
"""Add all the children of all active parents to the known paths.
571
Active parents are those which gain children, and those which are
572
removed. This is a necessary first step in detecting conflicts.
574
parents = self.by_parent().keys()
575
parents.extend([t for t in self._removed_contents if
576
self.tree_kind(t) == 'directory'])
577
for trans_id in self._removed_id:
578
file_id = self.tree_file_id(trans_id)
579
if self._tree.inventory[file_id].kind == 'directory':
580
parents.append(trans_id)
582
for parent_id in parents:
583
# ensure that all children are registered with the transaction
584
list(self.iter_tree_children(parent_id))
586
def iter_tree_children(self, parent_id):
587
"""Iterate through the entry's tree children, if any"""
589
path = self._tree_id_paths[parent_id]
593
children = os.listdir(self._tree.abspath(path))
595
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
599
for child in children:
600
childpath = joinpath(path, child)
601
if self._tree.is_control_filename(childpath):
603
yield self.trans_id_tree_path(childpath)
605
def has_named_child(self, by_parent, parent_id, name):
607
children = by_parent[parent_id]
610
for child in children:
611
if self.final_name(child) == name:
614
path = self._tree_id_paths[parent_id]
617
childpath = joinpath(path, name)
618
child_id = self._tree_path_ids.get(childpath)
620
return lexists(self._tree.abspath(childpath))
622
if self.final_parent(child_id) != parent_id:
624
if child_id in self._removed_contents:
625
# XXX What about dangling file-ids?
630
def _parent_loops(self):
631
"""No entry should be its own ancestor"""
633
for trans_id in self._new_parent:
636
while parent_id is not ROOT_PARENT:
639
parent_id = self.final_parent(parent_id)
642
if parent_id == trans_id:
643
conflicts.append(('parent loop', trans_id))
644
if parent_id in seen:
648
def _unversioned_parents(self, by_parent):
649
"""If parent directories are versioned, children must be versioned."""
651
for parent_id, children in by_parent.iteritems():
652
if parent_id is ROOT_PARENT:
654
if self.final_file_id(parent_id) is not None:
656
for child_id in children:
657
if self.final_file_id(child_id) is not None:
658
conflicts.append(('unversioned parent', parent_id))
662
def _improper_versioning(self):
663
"""Cannot version a file with no contents, or a bad type.
665
However, existing entries with no contents are okay.
668
for trans_id in self._new_id.iterkeys():
670
kind = self.final_kind(trans_id)
672
conflicts.append(('versioning no contents', trans_id))
674
if not InventoryEntry.versionable_kind(kind):
675
conflicts.append(('versioning bad kind', trans_id, kind))
678
def _executability_conflicts(self):
679
"""Check for bad executability changes.
681
Only versioned files may have their executability set, because
682
1. only versioned entries can have executability under windows
683
2. only files can be executable. (The execute bit on a directory
684
does not indicate searchability)
687
for trans_id in self._new_executability:
688
if self.final_file_id(trans_id) is None:
689
conflicts.append(('unversioned executability', trans_id))
692
non_file = self.final_kind(trans_id) != "file"
696
conflicts.append(('non-file executability', trans_id))
699
def _overwrite_conflicts(self):
700
"""Check for overwrites (not permitted on Win32)"""
702
for trans_id in self._new_contents:
704
self.tree_kind(trans_id)
707
if trans_id not in self._removed_contents:
708
conflicts.append(('overwrite', trans_id,
709
self.final_name(trans_id)))
712
def _duplicate_entries(self, by_parent):
713
"""No directory may have two entries with the same name."""
715
for children in by_parent.itervalues():
716
name_ids = [(self.final_name(t), t) for t in children]
720
for name, trans_id in name_ids:
722
kind = self.final_kind(trans_id)
725
file_id = self.final_file_id(trans_id)
726
if kind is None and file_id is None:
728
if name == last_name:
729
conflicts.append(('duplicate', last_trans_id, trans_id,
732
last_trans_id = trans_id
735
def _duplicate_ids(self):
736
"""Each inventory id may only be used once"""
738
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
740
active_tree_ids = set((f for f in self._tree.inventory if
741
f not in removed_tree_ids))
742
for trans_id, file_id in self._new_id.iteritems():
743
if file_id in active_tree_ids:
744
old_trans_id = self.trans_id_tree_file_id(file_id)
745
conflicts.append(('duplicate id', old_trans_id, trans_id))
748
def _parent_type_conflicts(self, by_parent):
749
"""parents must have directory 'contents'."""
751
for parent_id, children in by_parent.iteritems():
752
if parent_id is ROOT_PARENT:
754
if not self._any_contents(children):
756
for child in children:
758
self.final_kind(child)
762
kind = self.final_kind(parent_id)
766
conflicts.append(('missing parent', parent_id))
767
elif kind != "directory":
768
conflicts.append(('non-directory parent', parent_id))
771
def _any_contents(self, trans_ids):
772
"""Return true if any of the trans_ids, will have contents."""
773
for trans_id in trans_ids:
775
kind = self.final_kind(trans_id)
782
"""Apply all changes to the inventory and filesystem.
784
If filesystem or inventory conflicts are present, MalformedTransform
787
If apply succeeds, finalize is not necessary.
789
conflicts = self.find_conflicts()
790
if len(conflicts) != 0:
791
raise MalformedTransform(conflicts=conflicts)
792
inv = self._tree.inventory
794
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
796
child_pb.update('Apply phase', 0, 2)
797
self._apply_removals(inv, inventory_delta)
798
child_pb.update('Apply phase', 1, 2)
799
modified_paths = self._apply_insertions(inv, inventory_delta)
802
self._tree.apply_inventory_delta(inventory_delta)
805
return _TransformResults(modified_paths, self.rename_count)
807
def _limbo_name(self, trans_id, from_scratch=False):
808
"""Generate the limbo name of a file"""
810
limbo_name = self._limbo_files.get(trans_id)
811
if limbo_name is not None:
813
parent = self._new_parent.get(trans_id)
814
# if the parent directory is already in limbo (e.g. when building a
815
# tree), choose a limbo name inside the parent, to reduce further
817
use_direct_path = False
818
if self._new_contents.get(parent) == 'directory':
819
filename = self._new_name.get(trans_id)
820
if filename is not None:
821
if parent not in self._limbo_children:
822
self._limbo_children[parent] = set()
823
self._limbo_children_names[parent] = {}
824
use_direct_path = True
825
# the direct path can only be used if no other file has
826
# already taken this pathname, i.e. if the name is unused, or
827
# if it is already associated with this trans_id.
828
elif (self._limbo_children_names[parent].get(filename)
829
in (trans_id, None)):
830
use_direct_path = True
832
limbo_name = pathjoin(self._limbo_files[parent], filename)
833
self._limbo_children[parent].add(trans_id)
834
self._limbo_children_names[parent][filename] = trans_id
836
limbo_name = pathjoin(self._limbodir, trans_id)
837
self._needs_rename.add(trans_id)
838
self._limbo_files[trans_id] = limbo_name
841
def _apply_removals(self, inv, inventory_delta):
842
"""Perform tree operations that remove directory/inventory names.
844
That is, delete files that are to be deleted, and put any files that
845
need renaming into limbo. This must be done in strict child-to-parent
848
tree_paths = list(self._tree_path_ids.iteritems())
849
tree_paths.sort(reverse=True)
850
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
852
for num, data in enumerate(tree_paths):
853
path, trans_id = data
854
child_pb.update('removing file', num, len(tree_paths))
855
full_path = self._tree.abspath(path)
856
if trans_id in self._removed_contents:
857
delete_any(full_path)
858
elif trans_id in self._new_name or trans_id in \
861
os.rename(full_path, self._limbo_name(trans_id))
863
if e.errno != errno.ENOENT:
866
self.rename_count += 1
867
if trans_id in self._removed_id:
868
if trans_id == self._new_root:
869
file_id = self._tree.inventory.root.file_id
871
file_id = self.tree_file_id(trans_id)
872
assert file_id is not None
873
inventory_delta.append((path, None, file_id, None))
877
def _apply_insertions(self, inv, inventory_delta):
878
"""Perform tree operations that insert directory/inventory names.
880
That is, create any files that need to be created, and restore from
881
limbo any files that needed renaming. This must be done in strict
882
parent-to-child order.
884
new_paths = self.new_paths()
886
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
888
for num, (path, trans_id) in enumerate(new_paths):
890
child_pb.update('adding file', num, len(new_paths))
892
kind = self._new_contents[trans_id]
894
kind = contents = None
895
if trans_id in self._new_contents or \
896
self.path_changed(trans_id):
897
full_path = self._tree.abspath(path)
898
if trans_id in self._needs_rename:
900
os.rename(self._limbo_name(trans_id), full_path)
902
# We may be renaming a dangling inventory id
903
if e.errno != errno.ENOENT:
906
self.rename_count += 1
907
if trans_id in self._new_contents:
908
modified_paths.append(full_path)
909
del self._new_contents[trans_id]
911
if trans_id in self._new_id:
913
kind = file_kind(self._tree.abspath(path))
914
if trans_id in self._new_reference_revision:
915
new_entry = inventory.TreeReference(
916
self._new_id[trans_id],
917
self._new_name[trans_id],
918
self.final_file_id(self._new_parent[trans_id]),
919
None, self._new_reference_revision[trans_id])
921
new_entry = inventory.make_entry(kind,
922
self.final_name(trans_id),
923
self.final_file_id(self.final_parent(trans_id)),
924
self._new_id[trans_id])
926
if trans_id in self._new_name or trans_id in\
928
trans_id in self._new_executability:
929
file_id = self.final_file_id(trans_id)
930
if file_id is not None:
932
new_entry = entry.copy()
934
if trans_id in self._new_name or trans_id in\
936
if new_entry is not None:
937
new_entry.name = self.final_name(trans_id)
938
parent = self.final_parent(trans_id)
939
parent_id = self.final_file_id(parent)
940
new_entry.parent_id = parent_id
942
if trans_id in self._new_executability:
943
self._set_executability(path, new_entry, trans_id)
944
if new_entry is not None:
945
if new_entry.file_id in inv:
946
old_path = inv.id2path(new_entry.file_id)
949
inventory_delta.append((old_path, path,
954
return modified_paths
956
def _set_executability(self, path, entry, trans_id):
957
"""Set the executability of versioned files """
958
new_executability = self._new_executability[trans_id]
959
entry.executable = new_executability
960
if supports_executable():
961
abspath = self._tree.abspath(path)
962
current_mode = os.stat(abspath).st_mode
963
if new_executability:
966
to_mode = current_mode | (0100 & ~umask)
967
# Enable x-bit for others only if they can read it.
968
if current_mode & 0004:
969
to_mode |= 0001 & ~umask
970
if current_mode & 0040:
971
to_mode |= 0010 & ~umask
973
to_mode = current_mode & ~0111
974
os.chmod(abspath, to_mode)
976
def _new_entry(self, name, parent_id, file_id):
977
"""Helper function to create a new filesystem entry."""
978
trans_id = self.create_path(name, parent_id)
979
if file_id is not None:
980
self.version_file(file_id, trans_id)
983
def new_file(self, name, parent_id, contents, file_id=None,
985
"""Convenience method to create files.
987
name is the name of the file to create.
988
parent_id is the transaction id of the parent directory of the file.
989
contents is an iterator of bytestrings, which will be used to produce
991
:param file_id: The inventory ID of the file, if it is to be versioned.
992
:param executable: Only valid when a file_id has been supplied.
994
trans_id = self._new_entry(name, parent_id, file_id)
995
# TODO: rather than scheduling a set_executable call,
996
# have create_file create the file with the right mode.
997
self.create_file(contents, trans_id)
998
if executable is not None:
999
self.set_executability(executable, trans_id)
1002
def new_directory(self, name, parent_id, file_id=None):
1003
"""Convenience method to create directories.
1005
name is the name of the directory to create.
1006
parent_id is the transaction id of the parent directory of the
1008
file_id is the inventory ID of the directory, if it is to be versioned.
1010
trans_id = self._new_entry(name, parent_id, file_id)
1011
self.create_directory(trans_id)
1014
def new_symlink(self, name, parent_id, target, file_id=None):
1015
"""Convenience method to create symbolic link.
1017
name is the name of the symlink to create.
1018
parent_id is the transaction id of the parent directory of the symlink.
1019
target is a bytestring of the target of the symlink.
1020
file_id is the inventory ID of the file, if it is to be versioned.
1022
trans_id = self._new_entry(name, parent_id, file_id)
1023
self.create_symlink(target, trans_id)
1026
def _affected_ids(self):
1027
"""Return the set of transform ids affected by the transform"""
1028
trans_ids = set(self._removed_id)
1029
trans_ids.update(self._new_id.keys())
1030
trans_ids.update(self._removed_contents)
1031
trans_ids.update(self._new_contents.keys())
1032
trans_ids.update(self._new_executability.keys())
1033
trans_ids.update(self._new_name.keys())
1034
trans_ids.update(self._new_parent.keys())
1037
def _get_file_id_maps(self):
1038
"""Return mapping of file_ids to trans_ids in the to and from states"""
1039
trans_ids = self._affected_ids()
1042
# Build up two dicts: trans_ids associated with file ids in the
1043
# FROM state, vs the TO state.
1044
for trans_id in trans_ids:
1045
from_file_id = self.tree_file_id(trans_id)
1046
if from_file_id is not None:
1047
from_trans_ids[from_file_id] = trans_id
1048
to_file_id = self.final_file_id(trans_id)
1049
if to_file_id is not None:
1050
to_trans_ids[to_file_id] = trans_id
1051
return from_trans_ids, to_trans_ids
1053
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1054
"""Get data about a file in the from (tree) state
1056
Return a (name, parent, kind, executable) tuple
1058
from_path = self._tree_id_paths.get(from_trans_id)
1060
# get data from working tree if versioned
1061
from_entry = self._tree.inventory[file_id]
1062
from_name = from_entry.name
1063
from_parent = from_entry.parent_id
1066
if from_path is None:
1067
# File does not exist in FROM state
1071
# File exists, but is not versioned. Have to use path-
1073
from_name = os.path.basename(from_path)
1074
tree_parent = self.get_tree_parent(from_trans_id)
1075
from_parent = self.tree_file_id(tree_parent)
1076
if from_path is not None:
1077
from_kind, from_executable, from_stats = \
1078
self._tree._comparison_data(from_entry, from_path)
1081
from_executable = False
1082
return from_name, from_parent, from_kind, from_executable
1084
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1085
"""Get data about a file in the to (target) state
1087
Return a (name, parent, kind, executable) tuple
1089
to_name = self.final_name(to_trans_id)
1091
to_kind = self.final_kind(to_trans_id)
1094
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1095
if to_trans_id in self._new_executability:
1096
to_executable = self._new_executability[to_trans_id]
1097
elif to_trans_id == from_trans_id:
1098
to_executable = from_executable
1100
to_executable = False
1101
return to_name, to_parent, to_kind, to_executable
1103
def _iter_changes(self):
1104
"""Produce output in the same format as Tree._iter_changes.
1106
Will produce nonsensical results if invoked while inventory/filesystem
1107
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1109
This reads the Transform, but only reproduces changes involving a
1110
file_id. Files that are not versioned in either of the FROM or TO
1111
states are not reflected.
1113
final_paths = FinalPaths(self)
1114
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1116
# Now iterate through all active file_ids
1117
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1119
from_trans_id = from_trans_ids.get(file_id)
1120
# find file ids, and determine versioning state
1121
if from_trans_id is None:
1122
from_versioned = False
1123
from_trans_id = to_trans_ids[file_id]
1125
from_versioned = True
1126
to_trans_id = to_trans_ids.get(file_id)
1127
if to_trans_id is None:
1128
to_versioned = False
1129
to_trans_id = from_trans_id
1133
from_name, from_parent, from_kind, from_executable = \
1134
self._from_file_data(from_trans_id, from_versioned, file_id)
1136
to_name, to_parent, to_kind, to_executable = \
1137
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1139
if not from_versioned:
1142
from_path = self._tree_id_paths.get(from_trans_id)
1143
if not to_versioned:
1146
to_path = final_paths.get_path(to_trans_id)
1147
if from_kind != to_kind:
1149
elif to_kind in ('file', 'symlink') and (
1150
to_trans_id != from_trans_id or
1151
to_trans_id in self._new_contents):
1153
if (not modified and from_versioned == to_versioned and
1154
from_parent==to_parent and from_name == to_name and
1155
from_executable == to_executable):
1157
results.append((file_id, (from_path, to_path), modified,
1158
(from_versioned, to_versioned),
1159
(from_parent, to_parent),
1160
(from_name, to_name),
1161
(from_kind, to_kind),
1162
(from_executable, to_executable)))
1163
return iter(sorted(results, key=lambda x:x[1]))
1166
def joinpath(parent, child):
1167
"""Join tree-relative paths, handling the tree root specially"""
1168
if parent is None or parent == "":
1171
return pathjoin(parent, child)
1174
class FinalPaths(object):
1175
"""Make path calculation cheap by memoizing paths.
1177
The underlying tree must not be manipulated between calls, or else
1178
the results will likely be incorrect.
1180
def __init__(self, transform):
1181
object.__init__(self)
1182
self._known_paths = {}
1183
self.transform = transform
1185
def _determine_path(self, trans_id):
1186
if trans_id == self.transform.root:
1188
name = self.transform.final_name(trans_id)
1189
parent_id = self.transform.final_parent(trans_id)
1190
if parent_id == self.transform.root:
1193
return pathjoin(self.get_path(parent_id), name)
1195
def get_path(self, trans_id):
1196
"""Find the final path associated with a trans_id"""
1197
if trans_id not in self._known_paths:
1198
self._known_paths[trans_id] = self._determine_path(trans_id)
1199
return self._known_paths[trans_id]
1201
def topology_sorted_ids(tree):
1202
"""Determine the topological order of the ids in a tree"""
1203
file_ids = list(tree)
1204
file_ids.sort(key=tree.id2path)
1208
def build_tree(tree, wt):
1209
"""Create working tree for a branch, using a TreeTransform.
1211
This function should be used on empty trees, having a tree root at most.
1212
(see merge and revert functionality for working with existing trees)
1214
Existing files are handled like so:
1216
- Existing bzrdirs take precedence over creating new items. They are
1217
created as '%s.diverted' % name.
1218
- Otherwise, if the content on disk matches the content we are building,
1219
it is silently replaced.
1220
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1222
wt.lock_tree_write()
1226
return _build_tree(tree, wt)
1232
def _build_tree(tree, wt):
1233
"""See build_tree."""
1234
if len(wt.inventory) > 1: # more than just a root
1235
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1237
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1238
pp = ProgressPhase("Build phase", 2, top_pb)
1239
if tree.inventory.root is not None:
1240
# This is kind of a hack: we should be altering the root
1241
# as part of the regular tree shape diff logic.
1242
# The conditional test here is to avoid doing an
1243
# expensive operation (flush) every time the root id
1244
# is set within the tree, nor setting the root and thus
1245
# marking the tree as dirty, because we use two different
1246
# idioms here: tree interfaces and inventory interfaces.
1247
if wt.path2id('') != tree.inventory.root.file_id:
1248
wt.set_root_id(tree.inventory.root.file_id)
1250
tt = TreeTransform(wt)
1254
file_trans_id[wt.get_root_id()] = \
1255
tt.trans_id_tree_file_id(wt.get_root_id())
1256
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1258
for num, (tree_path, entry) in \
1259
enumerate(tree.inventory.iter_entries_by_dir()):
1260
pb.update("Building tree", num, len(tree.inventory))
1261
if entry.parent_id is None:
1264
file_id = entry.file_id
1265
target_path = wt.abspath(tree_path)
1267
kind = file_kind(target_path)
1271
if kind == "directory":
1273
bzrdir.BzrDir.open(target_path)
1274
except errors.NotBranchError:
1278
if (file_id not in divert and
1279
_content_match(tree, entry, file_id, kind,
1281
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1282
if kind == 'directory':
1284
if entry.parent_id not in file_trans_id:
1285
raise AssertionError(
1286
'entry %s parent id %r is not in file_trans_id %r'
1287
% (entry, entry.parent_id, file_trans_id))
1288
parent_id = file_trans_id[entry.parent_id]
1289
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1292
new_trans_id = file_trans_id[file_id]
1293
old_parent = tt.trans_id_tree_path(tree_path)
1294
_reparent_children(tt, old_parent, new_trans_id)
1298
divert_trans = set(file_trans_id[f] for f in divert)
1299
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1300
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1301
conflicts = cook_conflicts(raw_conflicts, tt)
1302
for conflict in conflicts:
1305
wt.add_conflicts(conflicts)
1306
except errors.UnsupportedOperation:
1315
def _reparent_children(tt, old_parent, new_parent):
1316
for child in tt.iter_tree_children(old_parent):
1317
tt.adjust_path(tt.final_name(child), new_parent, child)
1320
def _content_match(tree, entry, file_id, kind, target_path):
1321
if entry.kind != kind:
1323
if entry.kind == "directory":
1325
if entry.kind == "file":
1326
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1328
elif entry.kind == "symlink":
1329
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1334
def resolve_checkout(tt, conflicts, divert):
1335
new_conflicts = set()
1336
for c_type, conflict in ((c[0], c) for c in conflicts):
1337
# Anything but a 'duplicate' would indicate programmer error
1338
assert c_type == 'duplicate', c_type
1339
# Now figure out which is new and which is old
1340
if tt.new_contents(conflict[1]):
1341
new_file = conflict[1]
1342
old_file = conflict[2]
1344
new_file = conflict[2]
1345
old_file = conflict[1]
1347
# We should only get here if the conflict wasn't completely
1349
final_parent = tt.final_parent(old_file)
1350
if new_file in divert:
1351
new_name = tt.final_name(old_file)+'.diverted'
1352
tt.adjust_path(new_name, final_parent, new_file)
1353
new_conflicts.add((c_type, 'Diverted to',
1354
new_file, old_file))
1356
new_name = tt.final_name(old_file)+'.moved'
1357
tt.adjust_path(new_name, final_parent, old_file)
1358
new_conflicts.add((c_type, 'Moved existing file to',
1359
old_file, new_file))
1360
return new_conflicts
1363
def new_by_entry(tt, entry, parent_id, tree):
1364
"""Create a new file according to its inventory entry"""
1368
contents = tree.get_file(entry.file_id).readlines()
1369
executable = tree.is_executable(entry.file_id)
1370
return tt.new_file(name, parent_id, contents, entry.file_id,
1372
elif kind in ('directory', 'tree-reference'):
1373
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1374
if kind == 'tree-reference':
1375
tt.set_tree_reference(entry.reference_revision, trans_id)
1377
elif kind == 'symlink':
1378
target = tree.get_symlink_target(entry.file_id)
1379
return tt.new_symlink(name, parent_id, target, entry.file_id)
1381
raise errors.BadFileKindError(name, kind)
1383
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1384
"""Create new file contents according to an inventory entry."""
1385
if entry.kind == "file":
1387
lines = tree.get_file(entry.file_id).readlines()
1388
tt.create_file(lines, trans_id, mode_id=mode_id)
1389
elif entry.kind == "symlink":
1390
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1391
elif entry.kind == "directory":
1392
tt.create_directory(trans_id)
1394
def create_entry_executability(tt, entry, trans_id):
1395
"""Set the executability of a trans_id according to an inventory entry"""
1396
if entry.kind == "file":
1397
tt.set_executability(entry.executable, trans_id)
1400
@deprecated_function(zero_fifteen)
1401
def find_interesting(working_tree, target_tree, filenames):
1402
"""Find the ids corresponding to specified filenames.
1404
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1406
working_tree.lock_read()
1408
target_tree.lock_read()
1410
return working_tree.paths2ids(filenames, [target_tree])
1412
target_tree.unlock()
1414
working_tree.unlock()
1417
def change_entry(tt, file_id, working_tree, target_tree,
1418
trans_id_file_id, backups, trans_id, by_parent):
1419
"""Replace a file_id's contents with those from a target tree."""
1420
e_trans_id = trans_id_file_id(file_id)
1421
entry = target_tree.inventory[file_id]
1422
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1425
mode_id = e_trans_id
1428
tt.delete_contents(e_trans_id)
1430
parent_trans_id = trans_id_file_id(entry.parent_id)
1431
backup_name = get_backup_name(entry, by_parent,
1432
parent_trans_id, tt)
1433
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1434
tt.unversion_file(e_trans_id)
1435
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1436
tt.version_file(file_id, e_trans_id)
1437
trans_id[file_id] = e_trans_id
1438
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1439
create_entry_executability(tt, entry, e_trans_id)
1442
tt.set_executability(entry.executable, e_trans_id)
1443
if tt.final_name(e_trans_id) != entry.name:
1446
parent_id = tt.final_parent(e_trans_id)
1447
parent_file_id = tt.final_file_id(parent_id)
1448
if parent_file_id != entry.parent_id:
1453
parent_trans_id = trans_id_file_id(entry.parent_id)
1454
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1457
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1458
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1461
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1462
"""Produce a backup-style name that appears to be available"""
1466
yield "%s.~%d~" % (name, counter)
1468
for new_name in name_gen():
1469
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1473
def _entry_changes(file_id, entry, working_tree):
1474
"""Determine in which ways the inventory entry has changed.
1476
Returns booleans: has_contents, content_mod, meta_mod
1477
has_contents means there are currently contents, but they differ
1478
contents_mod means contents need to be modified
1479
meta_mod means the metadata needs to be modified
1481
cur_entry = working_tree.inventory[file_id]
1483
working_kind = working_tree.kind(file_id)
1486
has_contents = False
1489
if has_contents is True:
1490
if entry.kind != working_kind:
1491
contents_mod, meta_mod = True, False
1493
cur_entry._read_tree_state(working_tree.id2path(file_id),
1495
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1496
cur_entry._forget_tree_state()
1497
return has_contents, contents_mod, meta_mod
1500
def revert(working_tree, target_tree, filenames, backups=False,
1501
pb=DummyProgress(), change_reporter=None):
1502
"""Revert a working tree's contents to those of a target tree."""
1503
target_tree.lock_read()
1504
tt = TreeTransform(working_tree, pb)
1506
pp = ProgressPhase("Revert phase", 3, pb)
1508
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1510
merge_modified = _alter_files(working_tree, target_tree, tt,
1511
child_pb, filenames, backups)
1515
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1517
raw_conflicts = resolve_conflicts(tt, child_pb)
1520
conflicts = cook_conflicts(raw_conflicts, tt)
1522
change_reporter = delta._ChangeReporter(
1523
unversioned_filter=working_tree.is_ignored)
1524
delta.report_changes(tt._iter_changes(), change_reporter)
1525
for conflict in conflicts:
1529
working_tree.set_merge_modified(merge_modified)
1531
target_tree.unlock()
1537
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1539
merge_modified = working_tree.merge_modified()
1540
change_list = target_tree._iter_changes(working_tree,
1541
specific_files=specific_files, pb=pb)
1542
if target_tree.inventory.root is None:
1548
for id_num, (file_id, path, changed_content, versioned, parent, name,
1549
kind, executable) in enumerate(change_list):
1550
if skip_root and file_id[0] is not None and parent[0] is None:
1552
trans_id = tt.trans_id_file_id(file_id)
1555
keep_content = False
1556
if kind[0] == 'file' and (backups or kind[1] is None):
1557
wt_sha1 = working_tree.get_file_sha1(file_id)
1558
if merge_modified.get(file_id) != wt_sha1:
1559
# acquire the basis tree lazily to prevent the
1560
# expense of accessing it when it's not needed ?
1561
# (Guessing, RBC, 200702)
1562
if basis_tree is None:
1563
basis_tree = working_tree.basis_tree()
1564
basis_tree.lock_read()
1565
if file_id in basis_tree:
1566
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1568
elif kind[1] is None and not versioned[1]:
1570
if kind[0] is not None:
1571
if not keep_content:
1572
tt.delete_contents(trans_id)
1573
elif kind[1] is not None:
1574
parent_trans_id = tt.trans_id_file_id(parent[0])
1575
by_parent = tt.by_parent()
1576
backup_name = _get_backup_name(name[0], by_parent,
1577
parent_trans_id, tt)
1578
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1579
new_trans_id = tt.create_path(name[0], parent_trans_id)
1580
if versioned == (True, True):
1581
tt.unversion_file(trans_id)
1582
tt.version_file(file_id, new_trans_id)
1583
# New contents should have the same unix perms as old
1586
trans_id = new_trans_id
1587
if kind[1] == 'directory':
1588
tt.create_directory(trans_id)
1589
elif kind[1] == 'symlink':
1590
tt.create_symlink(target_tree.get_symlink_target(file_id),
1592
elif kind[1] == 'file':
1593
tt.create_file(target_tree.get_file_lines(file_id),
1595
if basis_tree is None:
1596
basis_tree = working_tree.basis_tree()
1597
basis_tree.lock_read()
1598
new_sha1 = target_tree.get_file_sha1(file_id)
1599
if (file_id in basis_tree and new_sha1 ==
1600
basis_tree.get_file_sha1(file_id)):
1601
if file_id in merge_modified:
1602
del merge_modified[file_id]
1604
merge_modified[file_id] = new_sha1
1606
# preserve the execute bit when backing up
1607
if keep_content and executable[0] == executable[1]:
1608
tt.set_executability(executable[1], trans_id)
1610
assert kind[1] is None
1611
if versioned == (False, True):
1612
tt.version_file(file_id, trans_id)
1613
if versioned == (True, False):
1614
tt.unversion_file(trans_id)
1615
if (name[1] is not None and
1616
(name[0] != name[1] or parent[0] != parent[1])):
1618
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1619
if executable[0] != executable[1] and kind[1] == "file":
1620
tt.set_executability(executable[1], trans_id)
1622
if basis_tree is not None:
1624
return merge_modified
1627
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1628
"""Make many conflict-resolution attempts, but die if they fail"""
1629
if pass_func is None:
1630
pass_func = conflict_pass
1631
new_conflicts = set()
1634
pb.update('Resolution pass', n+1, 10)
1635
conflicts = tt.find_conflicts()
1636
if len(conflicts) == 0:
1637
return new_conflicts
1638
new_conflicts.update(pass_func(tt, conflicts))
1639
raise MalformedTransform(conflicts=conflicts)
1644
def conflict_pass(tt, conflicts):
1645
"""Resolve some classes of conflicts."""
1646
new_conflicts = set()
1647
for c_type, conflict in ((c[0], c) for c in conflicts):
1648
if c_type == 'duplicate id':
1649
tt.unversion_file(conflict[1])
1650
new_conflicts.add((c_type, 'Unversioned existing file',
1651
conflict[1], conflict[2], ))
1652
elif c_type == 'duplicate':
1653
# files that were renamed take precedence
1654
new_name = tt.final_name(conflict[1])+'.moved'
1655
final_parent = tt.final_parent(conflict[1])
1656
if tt.path_changed(conflict[1]):
1657
tt.adjust_path(new_name, final_parent, conflict[2])
1658
new_conflicts.add((c_type, 'Moved existing file to',
1659
conflict[2], conflict[1]))
1661
tt.adjust_path(new_name, final_parent, conflict[1])
1662
new_conflicts.add((c_type, 'Moved existing file to',
1663
conflict[1], conflict[2]))
1664
elif c_type == 'parent loop':
1665
# break the loop by undoing one of the ops that caused the loop
1667
while not tt.path_changed(cur):
1668
cur = tt.final_parent(cur)
1669
new_conflicts.add((c_type, 'Cancelled move', cur,
1670
tt.final_parent(cur),))
1671
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1673
elif c_type == 'missing parent':
1674
trans_id = conflict[1]
1676
tt.cancel_deletion(trans_id)
1677
new_conflicts.add(('deleting parent', 'Not deleting',
1680
tt.create_directory(trans_id)
1681
new_conflicts.add((c_type, 'Created directory', trans_id))
1682
elif c_type == 'unversioned parent':
1683
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1684
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1685
return new_conflicts
1688
def cook_conflicts(raw_conflicts, tt):
1689
"""Generate a list of cooked conflicts, sorted by file path"""
1690
from bzrlib.conflicts import Conflict
1691
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1692
return sorted(conflict_iter, key=Conflict.sort_key)
1695
def iter_cook_conflicts(raw_conflicts, tt):
1696
from bzrlib.conflicts import Conflict
1698
for conflict in raw_conflicts:
1699
c_type = conflict[0]
1700
action = conflict[1]
1701
modified_path = fp.get_path(conflict[2])
1702
modified_id = tt.final_file_id(conflict[2])
1703
if len(conflict) == 3:
1704
yield Conflict.factory(c_type, action=action, path=modified_path,
1705
file_id=modified_id)
1708
conflicting_path = fp.get_path(conflict[3])
1709
conflicting_id = tt.final_file_id(conflict[3])
1710
yield Conflict.factory(c_type, action=action, path=modified_path,
1711
file_id=modified_id,
1712
conflict_path=conflicting_path,
1713
conflict_file_id=conflicting_id)