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
Even for trans_ids that have no new contents, we must remove their
209
entries from _limbo_files, because they are now stale.
211
for trans_id in trans_ids:
212
old_path = self._limbo_files.pop(trans_id)
213
if trans_id not in self._new_contents:
215
new_path = self._limbo_name(trans_id)
216
os.rename(old_path, new_path)
218
def adjust_root_path(self, name, parent):
219
"""Emulate moving the root by moving all children, instead.
221
We do this by undoing the association of root's transaction id with the
222
current tree. This allows us to create a new directory with that
223
transaction id. We unversion the root directory and version the
224
physically new directory, and hope someone versions the tree root
227
old_root = self._new_root
228
old_root_file_id = self.final_file_id(old_root)
229
# force moving all children of root
230
for child_id in self.iter_tree_children(old_root):
231
if child_id != parent:
232
self.adjust_path(self.final_name(child_id),
233
self.final_parent(child_id), child_id)
234
file_id = self.final_file_id(child_id)
235
if file_id is not None:
236
self.unversion_file(child_id)
237
self.version_file(file_id, child_id)
239
# the physical root needs a new transaction id
240
self._tree_path_ids.pop("")
241
self._tree_id_paths.pop(old_root)
242
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
243
if parent == old_root:
244
parent = self._new_root
245
self.adjust_path(name, parent, old_root)
246
self.create_directory(old_root)
247
self.version_file(old_root_file_id, old_root)
248
self.unversion_file(self._new_root)
250
def trans_id_tree_file_id(self, inventory_id):
251
"""Determine the transaction id of a working tree file.
253
This reflects only files that already exist, not ones that will be
254
added by transactions.
256
path = self._tree.inventory.id2path(inventory_id)
257
return self.trans_id_tree_path(path)
259
def trans_id_file_id(self, file_id):
260
"""Determine or set the transaction id associated with a file ID.
261
A new id is only created for file_ids that were never present. If
262
a transaction has been unversioned, it is deliberately still returned.
263
(this will likely lead to an unversioned parent conflict.)
265
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
266
return self._r_new_id[file_id]
267
elif file_id in self._tree.inventory:
268
return self.trans_id_tree_file_id(file_id)
269
elif file_id in self._non_present_ids:
270
return self._non_present_ids[file_id]
272
trans_id = self._assign_id()
273
self._non_present_ids[file_id] = trans_id
276
def canonical_path(self, path):
277
"""Get the canonical tree-relative path"""
278
# don't follow final symlinks
279
abs = self._tree.abspath(path)
280
if abs in self._relpaths:
281
return self._relpaths[abs]
282
dirname, basename = os.path.split(abs)
283
if dirname not in self._realpaths:
284
self._realpaths[dirname] = os.path.realpath(dirname)
285
dirname = self._realpaths[dirname]
286
abs = pathjoin(dirname, basename)
287
if dirname in self._relpaths:
288
relpath = pathjoin(self._relpaths[dirname], basename)
289
relpath = relpath.rstrip('/\\')
291
relpath = self._tree.relpath(abs)
292
self._relpaths[abs] = relpath
295
def trans_id_tree_path(self, path):
296
"""Determine (and maybe set) the transaction ID for a tree path."""
297
path = self.canonical_path(path)
298
if path not in self._tree_path_ids:
299
self._tree_path_ids[path] = self._assign_id()
300
self._tree_id_paths[self._tree_path_ids[path]] = path
301
return self._tree_path_ids[path]
303
def get_tree_parent(self, trans_id):
304
"""Determine id of the parent in the tree."""
305
path = self._tree_id_paths[trans_id]
308
return self.trans_id_tree_path(os.path.dirname(path))
310
def create_file(self, contents, trans_id, mode_id=None):
311
"""Schedule creation of a new file.
315
Contents is an iterator of strings, all of which will be written
316
to the target destination.
318
New file takes the permissions of any existing file with that id,
319
unless mode_id is specified.
321
name = self._limbo_name(trans_id)
325
unique_add(self._new_contents, trans_id, 'file')
327
# Clean up the file, it never got registered so
328
# TreeTransform.finalize() won't clean it up.
333
f.writelines(contents)
336
self._set_mode(trans_id, mode_id, S_ISREG)
338
def _set_mode(self, trans_id, mode_id, typefunc):
339
"""Set the mode of new file contents.
340
The mode_id is the existing file to get the mode from (often the same
341
as trans_id). The operation is only performed if there's a mode match
342
according to typefunc.
347
old_path = self._tree_id_paths[mode_id]
351
mode = os.stat(self._tree.abspath(old_path)).st_mode
353
if e.errno == errno.ENOENT:
358
os.chmod(self._limbo_name(trans_id), mode)
360
def create_directory(self, trans_id):
361
"""Schedule creation of a new directory.
363
See also new_directory.
365
os.mkdir(self._limbo_name(trans_id))
366
unique_add(self._new_contents, trans_id, 'directory')
368
def create_symlink(self, target, trans_id):
369
"""Schedule creation of a new symbolic link.
371
target is a bytestring.
372
See also new_symlink.
374
os.symlink(target, self._limbo_name(trans_id))
375
unique_add(self._new_contents, trans_id, 'symlink')
377
def cancel_creation(self, trans_id):
378
"""Cancel the creation of new file contents."""
379
del self._new_contents[trans_id]
380
children = self._limbo_children.get(trans_id)
381
# if this is a limbo directory with children, move them before removing
383
if children is not None:
384
self._rename_in_limbo(children)
385
del self._limbo_children[trans_id]
386
del self._limbo_children_names[trans_id]
387
delete_any(self._limbo_name(trans_id))
389
def delete_contents(self, trans_id):
390
"""Schedule the contents of a path entry for deletion"""
391
self.tree_kind(trans_id)
392
self._removed_contents.add(trans_id)
394
def cancel_deletion(self, trans_id):
395
"""Cancel a scheduled deletion"""
396
self._removed_contents.remove(trans_id)
398
def unversion_file(self, trans_id):
399
"""Schedule a path entry to become unversioned"""
400
self._removed_id.add(trans_id)
402
def delete_versioned(self, trans_id):
403
"""Delete and unversion a versioned file"""
404
self.delete_contents(trans_id)
405
self.unversion_file(trans_id)
407
def set_executability(self, executability, trans_id):
408
"""Schedule setting of the 'execute' bit
409
To unschedule, set to None
411
if executability is None:
412
del self._new_executability[trans_id]
414
unique_add(self._new_executability, trans_id, executability)
416
def set_tree_reference(self, revision_id, trans_id):
417
"""Set the reference associated with a directory"""
418
unique_add(self._new_reference_revision, trans_id, revision_id)
420
def version_file(self, file_id, trans_id):
421
"""Schedule a file to become versioned."""
422
assert file_id is not None
423
unique_add(self._new_id, trans_id, file_id)
424
unique_add(self._r_new_id, file_id, trans_id)
426
def cancel_versioning(self, trans_id):
427
"""Undo a previous versioning of a file"""
428
file_id = self._new_id[trans_id]
429
del self._new_id[trans_id]
430
del self._r_new_id[file_id]
433
"""Determine the paths of all new and changed files"""
435
fp = FinalPaths(self)
436
for id_set in (self._new_name, self._new_parent, self._new_contents,
437
self._new_id, self._new_executability):
438
new_ids.update(id_set)
439
new_paths = [(fp.get_path(t), t) for t in new_ids]
443
def tree_kind(self, trans_id):
444
"""Determine the file kind in the working tree.
446
Raises NoSuchFile if the file does not exist
448
path = self._tree_id_paths.get(trans_id)
450
raise NoSuchFile(None)
452
return file_kind(self._tree.abspath(path))
454
if e.errno != errno.ENOENT:
457
raise NoSuchFile(path)
459
def final_kind(self, trans_id):
460
"""Determine the final file kind, after any changes applied.
462
Raises NoSuchFile if the file does not exist/has no contents.
463
(It is conceivable that a path would be created without the
464
corresponding contents insertion command)
466
if trans_id in self._new_contents:
467
return self._new_contents[trans_id]
468
elif trans_id in self._removed_contents:
469
raise NoSuchFile(None)
471
return self.tree_kind(trans_id)
473
def tree_file_id(self, trans_id):
474
"""Determine the file id associated with the trans_id in the tree"""
476
path = self._tree_id_paths[trans_id]
478
# the file is a new, unversioned file, or invalid trans_id
480
# the file is old; the old id is still valid
481
if self._new_root == trans_id:
482
return self._tree.inventory.root.file_id
483
return self._tree.inventory.path2id(path)
485
def final_file_id(self, trans_id):
486
"""Determine the file id after any changes are applied, or None.
488
None indicates that the file will not be versioned after changes are
492
# there is a new id for this file
493
assert self._new_id[trans_id] is not None
494
return self._new_id[trans_id]
496
if trans_id in self._removed_id:
498
return self.tree_file_id(trans_id)
500
def inactive_file_id(self, trans_id):
501
"""Return the inactive file_id associated with a transaction id.
502
That is, the one in the tree or in non_present_ids.
503
The file_id may actually be active, too.
505
file_id = self.tree_file_id(trans_id)
506
if file_id is not None:
508
for key, value in self._non_present_ids.iteritems():
509
if value == trans_id:
512
def final_parent(self, trans_id):
513
"""Determine the parent file_id, after any changes are applied.
515
ROOT_PARENT is returned for the tree root.
518
return self._new_parent[trans_id]
520
return self.get_tree_parent(trans_id)
522
def final_name(self, trans_id):
523
"""Determine the final filename, after all changes are applied."""
525
return self._new_name[trans_id]
528
return os.path.basename(self._tree_id_paths[trans_id])
530
raise NoFinalPath(trans_id, self)
533
"""Return a map of parent: children for known parents.
535
Only new paths and parents of tree files with assigned ids are used.
538
items = list(self._new_parent.iteritems())
539
items.extend((t, self.final_parent(t)) for t in
540
self._tree_id_paths.keys())
541
for trans_id, parent_id in items:
542
if parent_id not in by_parent:
543
by_parent[parent_id] = set()
544
by_parent[parent_id].add(trans_id)
547
def path_changed(self, trans_id):
548
"""Return True if a trans_id's path has changed."""
549
return (trans_id in self._new_name) or (trans_id in self._new_parent)
551
def new_contents(self, trans_id):
552
return (trans_id in self._new_contents)
554
def find_conflicts(self):
555
"""Find any violations of inventory or filesystem invariants"""
556
if self.__done is True:
557
raise ReusingTransform()
559
# ensure all children of all existent parents are known
560
# all children of non-existent parents are known, by definition.
561
self._add_tree_children()
562
by_parent = self.by_parent()
563
conflicts.extend(self._unversioned_parents(by_parent))
564
conflicts.extend(self._parent_loops())
565
conflicts.extend(self._duplicate_entries(by_parent))
566
conflicts.extend(self._duplicate_ids())
567
conflicts.extend(self._parent_type_conflicts(by_parent))
568
conflicts.extend(self._improper_versioning())
569
conflicts.extend(self._executability_conflicts())
570
conflicts.extend(self._overwrite_conflicts())
573
def _add_tree_children(self):
574
"""Add all the children of all active parents to the known paths.
576
Active parents are those which gain children, and those which are
577
removed. This is a necessary first step in detecting conflicts.
579
parents = self.by_parent().keys()
580
parents.extend([t for t in self._removed_contents if
581
self.tree_kind(t) == 'directory'])
582
for trans_id in self._removed_id:
583
file_id = self.tree_file_id(trans_id)
584
if self._tree.inventory[file_id].kind == 'directory':
585
parents.append(trans_id)
587
for parent_id in parents:
588
# ensure that all children are registered with the transaction
589
list(self.iter_tree_children(parent_id))
591
def iter_tree_children(self, parent_id):
592
"""Iterate through the entry's tree children, if any"""
594
path = self._tree_id_paths[parent_id]
598
children = os.listdir(self._tree.abspath(path))
600
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
604
for child in children:
605
childpath = joinpath(path, child)
606
if self._tree.is_control_filename(childpath):
608
yield self.trans_id_tree_path(childpath)
610
def has_named_child(self, by_parent, parent_id, name):
612
children = by_parent[parent_id]
615
for child in children:
616
if self.final_name(child) == name:
619
path = self._tree_id_paths[parent_id]
622
childpath = joinpath(path, name)
623
child_id = self._tree_path_ids.get(childpath)
625
return lexists(self._tree.abspath(childpath))
627
if self.final_parent(child_id) != parent_id:
629
if child_id in self._removed_contents:
630
# XXX What about dangling file-ids?
635
def _parent_loops(self):
636
"""No entry should be its own ancestor"""
638
for trans_id in self._new_parent:
641
while parent_id is not ROOT_PARENT:
644
parent_id = self.final_parent(parent_id)
647
if parent_id == trans_id:
648
conflicts.append(('parent loop', trans_id))
649
if parent_id in seen:
653
def _unversioned_parents(self, by_parent):
654
"""If parent directories are versioned, children must be versioned."""
656
for parent_id, children in by_parent.iteritems():
657
if parent_id is ROOT_PARENT:
659
if self.final_file_id(parent_id) is not None:
661
for child_id in children:
662
if self.final_file_id(child_id) is not None:
663
conflicts.append(('unversioned parent', parent_id))
667
def _improper_versioning(self):
668
"""Cannot version a file with no contents, or a bad type.
670
However, existing entries with no contents are okay.
673
for trans_id in self._new_id.iterkeys():
675
kind = self.final_kind(trans_id)
677
conflicts.append(('versioning no contents', trans_id))
679
if not InventoryEntry.versionable_kind(kind):
680
conflicts.append(('versioning bad kind', trans_id, kind))
683
def _executability_conflicts(self):
684
"""Check for bad executability changes.
686
Only versioned files may have their executability set, because
687
1. only versioned entries can have executability under windows
688
2. only files can be executable. (The execute bit on a directory
689
does not indicate searchability)
692
for trans_id in self._new_executability:
693
if self.final_file_id(trans_id) is None:
694
conflicts.append(('unversioned executability', trans_id))
697
non_file = self.final_kind(trans_id) != "file"
701
conflicts.append(('non-file executability', trans_id))
704
def _overwrite_conflicts(self):
705
"""Check for overwrites (not permitted on Win32)"""
707
for trans_id in self._new_contents:
709
self.tree_kind(trans_id)
712
if trans_id not in self._removed_contents:
713
conflicts.append(('overwrite', trans_id,
714
self.final_name(trans_id)))
717
def _duplicate_entries(self, by_parent):
718
"""No directory may have two entries with the same name."""
720
if (self._new_name, self._new_parent) == ({}, {}):
722
for children in by_parent.itervalues():
723
name_ids = [(self.final_name(t), t) for t in children]
727
for name, trans_id in name_ids:
729
kind = self.final_kind(trans_id)
732
file_id = self.final_file_id(trans_id)
733
if kind is None and file_id is None:
735
if name == last_name:
736
conflicts.append(('duplicate', last_trans_id, trans_id,
739
last_trans_id = trans_id
742
def _duplicate_ids(self):
743
"""Each inventory id may only be used once"""
745
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
747
active_tree_ids = set((f for f in self._tree.inventory if
748
f not in removed_tree_ids))
749
for trans_id, file_id in self._new_id.iteritems():
750
if file_id in active_tree_ids:
751
old_trans_id = self.trans_id_tree_file_id(file_id)
752
conflicts.append(('duplicate id', old_trans_id, trans_id))
755
def _parent_type_conflicts(self, by_parent):
756
"""parents must have directory 'contents'."""
758
for parent_id, children in by_parent.iteritems():
759
if parent_id is ROOT_PARENT:
761
if not self._any_contents(children):
763
for child in children:
765
self.final_kind(child)
769
kind = self.final_kind(parent_id)
773
conflicts.append(('missing parent', parent_id))
774
elif kind != "directory":
775
conflicts.append(('non-directory parent', parent_id))
778
def _any_contents(self, trans_ids):
779
"""Return true if any of the trans_ids, will have contents."""
780
for trans_id in trans_ids:
782
kind = self.final_kind(trans_id)
788
def apply(self, no_conflicts=False):
789
"""Apply all changes to the inventory and filesystem.
791
If filesystem or inventory conflicts are present, MalformedTransform
794
If apply succeeds, finalize is not necessary.
796
:param no_conflicts: if True, the caller guarantees there are no
797
conflicts, so no check is made.
800
conflicts = self.find_conflicts()
801
if len(conflicts) != 0:
802
raise MalformedTransform(conflicts=conflicts)
803
inv = self._tree.inventory
805
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
807
child_pb.update('Apply phase', 0, 2)
808
self._apply_removals(inv, inventory_delta)
809
child_pb.update('Apply phase', 1, 2)
810
modified_paths = self._apply_insertions(inv, inventory_delta)
813
self._tree.apply_inventory_delta(inventory_delta)
816
return _TransformResults(modified_paths, self.rename_count)
818
def _limbo_name(self, trans_id):
819
"""Generate the limbo name of a file"""
820
limbo_name = self._limbo_files.get(trans_id)
821
if limbo_name is not None:
823
parent = self._new_parent.get(trans_id)
824
# if the parent directory is already in limbo (e.g. when building a
825
# tree), choose a limbo name inside the parent, to reduce further
827
use_direct_path = False
828
if self._new_contents.get(parent) == 'directory':
829
filename = self._new_name.get(trans_id)
830
if filename is not None:
831
if parent not in self._limbo_children:
832
self._limbo_children[parent] = set()
833
self._limbo_children_names[parent] = {}
834
use_direct_path = True
835
# the direct path can only be used if no other file has
836
# already taken this pathname, i.e. if the name is unused, or
837
# if it is already associated with this trans_id.
838
elif (self._limbo_children_names[parent].get(filename)
839
in (trans_id, None)):
840
use_direct_path = True
842
limbo_name = pathjoin(self._limbo_files[parent], filename)
843
self._limbo_children[parent].add(trans_id)
844
self._limbo_children_names[parent][filename] = trans_id
846
limbo_name = pathjoin(self._limbodir, trans_id)
847
self._needs_rename.add(trans_id)
848
self._limbo_files[trans_id] = limbo_name
851
def _apply_removals(self, inv, inventory_delta):
852
"""Perform tree operations that remove directory/inventory names.
854
That is, delete files that are to be deleted, and put any files that
855
need renaming into limbo. This must be done in strict child-to-parent
858
tree_paths = list(self._tree_path_ids.iteritems())
859
tree_paths.sort(reverse=True)
860
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
862
for num, data in enumerate(tree_paths):
863
path, trans_id = data
864
child_pb.update('removing file', num, len(tree_paths))
865
full_path = self._tree.abspath(path)
866
if trans_id in self._removed_contents:
867
delete_any(full_path)
868
elif trans_id in self._new_name or trans_id in \
871
os.rename(full_path, self._limbo_name(trans_id))
873
if e.errno != errno.ENOENT:
876
self.rename_count += 1
877
if trans_id in self._removed_id:
878
if trans_id == self._new_root:
879
file_id = self._tree.inventory.root.file_id
881
file_id = self.tree_file_id(trans_id)
882
assert file_id is not None
883
inventory_delta.append((path, None, file_id, None))
887
def _apply_insertions(self, inv, inventory_delta):
888
"""Perform tree operations that insert directory/inventory names.
890
That is, create any files that need to be created, and restore from
891
limbo any files that needed renaming. This must be done in strict
892
parent-to-child order.
894
new_paths = self.new_paths()
896
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
898
for num, (path, trans_id) in enumerate(new_paths):
900
child_pb.update('adding file', num, len(new_paths))
902
kind = self._new_contents[trans_id]
904
kind = contents = None
905
if trans_id in self._new_contents or \
906
self.path_changed(trans_id):
907
full_path = self._tree.abspath(path)
908
if trans_id in self._needs_rename:
910
os.rename(self._limbo_name(trans_id), full_path)
912
# We may be renaming a dangling inventory id
913
if e.errno != errno.ENOENT:
916
self.rename_count += 1
917
if trans_id in self._new_contents:
918
modified_paths.append(full_path)
919
del self._new_contents[trans_id]
921
if trans_id in self._new_id:
923
kind = file_kind(self._tree.abspath(path))
924
if trans_id in self._new_reference_revision:
925
new_entry = inventory.TreeReference(
926
self._new_id[trans_id],
927
self._new_name[trans_id],
928
self.final_file_id(self._new_parent[trans_id]),
929
None, self._new_reference_revision[trans_id])
931
new_entry = inventory.make_entry(kind,
932
self.final_name(trans_id),
933
self.final_file_id(self.final_parent(trans_id)),
934
self._new_id[trans_id])
936
if trans_id in self._new_name or trans_id in\
938
trans_id in self._new_executability:
939
file_id = self.final_file_id(trans_id)
940
if file_id is not None:
942
new_entry = entry.copy()
944
if trans_id in self._new_name or trans_id in\
946
if new_entry is not None:
947
new_entry.name = self.final_name(trans_id)
948
parent = self.final_parent(trans_id)
949
parent_id = self.final_file_id(parent)
950
new_entry.parent_id = parent_id
952
if trans_id in self._new_executability:
953
self._set_executability(path, new_entry, trans_id)
954
if new_entry is not None:
955
if new_entry.file_id in inv:
956
old_path = inv.id2path(new_entry.file_id)
959
inventory_delta.append((old_path, path,
964
return modified_paths
966
def _set_executability(self, path, entry, trans_id):
967
"""Set the executability of versioned files """
968
new_executability = self._new_executability[trans_id]
969
entry.executable = new_executability
970
if supports_executable():
971
abspath = self._tree.abspath(path)
972
current_mode = os.stat(abspath).st_mode
973
if new_executability:
976
to_mode = current_mode | (0100 & ~umask)
977
# Enable x-bit for others only if they can read it.
978
if current_mode & 0004:
979
to_mode |= 0001 & ~umask
980
if current_mode & 0040:
981
to_mode |= 0010 & ~umask
983
to_mode = current_mode & ~0111
984
os.chmod(abspath, to_mode)
986
def _new_entry(self, name, parent_id, file_id):
987
"""Helper function to create a new filesystem entry."""
988
trans_id = self.create_path(name, parent_id)
989
if file_id is not None:
990
self.version_file(file_id, trans_id)
993
def new_file(self, name, parent_id, contents, file_id=None,
995
"""Convenience method to create files.
997
name is the name of the file to create.
998
parent_id is the transaction id of the parent directory of the file.
999
contents is an iterator of bytestrings, which will be used to produce
1001
:param file_id: The inventory ID of the file, if it is to be versioned.
1002
:param executable: Only valid when a file_id has been supplied.
1004
trans_id = self._new_entry(name, parent_id, file_id)
1005
# TODO: rather than scheduling a set_executable call,
1006
# have create_file create the file with the right mode.
1007
self.create_file(contents, trans_id)
1008
if executable is not None:
1009
self.set_executability(executable, trans_id)
1012
def new_directory(self, name, parent_id, file_id=None):
1013
"""Convenience method to create directories.
1015
name is the name of the directory to create.
1016
parent_id is the transaction id of the parent directory of the
1018
file_id is the inventory ID of the directory, if it is to be versioned.
1020
trans_id = self._new_entry(name, parent_id, file_id)
1021
self.create_directory(trans_id)
1024
def new_symlink(self, name, parent_id, target, file_id=None):
1025
"""Convenience method to create symbolic link.
1027
name is the name of the symlink to create.
1028
parent_id is the transaction id of the parent directory of the symlink.
1029
target is a bytestring of the target of the symlink.
1030
file_id is the inventory ID of the file, if it is to be versioned.
1032
trans_id = self._new_entry(name, parent_id, file_id)
1033
self.create_symlink(target, trans_id)
1036
def _affected_ids(self):
1037
"""Return the set of transform ids affected by the transform"""
1038
trans_ids = set(self._removed_id)
1039
trans_ids.update(self._new_id.keys())
1040
trans_ids.update(self._removed_contents)
1041
trans_ids.update(self._new_contents.keys())
1042
trans_ids.update(self._new_executability.keys())
1043
trans_ids.update(self._new_name.keys())
1044
trans_ids.update(self._new_parent.keys())
1047
def _get_file_id_maps(self):
1048
"""Return mapping of file_ids to trans_ids in the to and from states"""
1049
trans_ids = self._affected_ids()
1052
# Build up two dicts: trans_ids associated with file ids in the
1053
# FROM state, vs the TO state.
1054
for trans_id in trans_ids:
1055
from_file_id = self.tree_file_id(trans_id)
1056
if from_file_id is not None:
1057
from_trans_ids[from_file_id] = trans_id
1058
to_file_id = self.final_file_id(trans_id)
1059
if to_file_id is not None:
1060
to_trans_ids[to_file_id] = trans_id
1061
return from_trans_ids, to_trans_ids
1063
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1064
"""Get data about a file in the from (tree) state
1066
Return a (name, parent, kind, executable) tuple
1068
from_path = self._tree_id_paths.get(from_trans_id)
1070
# get data from working tree if versioned
1071
from_entry = self._tree.inventory[file_id]
1072
from_name = from_entry.name
1073
from_parent = from_entry.parent_id
1076
if from_path is None:
1077
# File does not exist in FROM state
1081
# File exists, but is not versioned. Have to use path-
1083
from_name = os.path.basename(from_path)
1084
tree_parent = self.get_tree_parent(from_trans_id)
1085
from_parent = self.tree_file_id(tree_parent)
1086
if from_path is not None:
1087
from_kind, from_executable, from_stats = \
1088
self._tree._comparison_data(from_entry, from_path)
1091
from_executable = False
1092
return from_name, from_parent, from_kind, from_executable
1094
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1095
"""Get data about a file in the to (target) state
1097
Return a (name, parent, kind, executable) tuple
1099
to_name = self.final_name(to_trans_id)
1101
to_kind = self.final_kind(to_trans_id)
1104
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1105
if to_trans_id in self._new_executability:
1106
to_executable = self._new_executability[to_trans_id]
1107
elif to_trans_id == from_trans_id:
1108
to_executable = from_executable
1110
to_executable = False
1111
return to_name, to_parent, to_kind, to_executable
1113
def _iter_changes(self):
1114
"""Produce output in the same format as Tree._iter_changes.
1116
Will produce nonsensical results if invoked while inventory/filesystem
1117
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1119
This reads the Transform, but only reproduces changes involving a
1120
file_id. Files that are not versioned in either of the FROM or TO
1121
states are not reflected.
1123
final_paths = FinalPaths(self)
1124
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1126
# Now iterate through all active file_ids
1127
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1129
from_trans_id = from_trans_ids.get(file_id)
1130
# find file ids, and determine versioning state
1131
if from_trans_id is None:
1132
from_versioned = False
1133
from_trans_id = to_trans_ids[file_id]
1135
from_versioned = True
1136
to_trans_id = to_trans_ids.get(file_id)
1137
if to_trans_id is None:
1138
to_versioned = False
1139
to_trans_id = from_trans_id
1143
from_name, from_parent, from_kind, from_executable = \
1144
self._from_file_data(from_trans_id, from_versioned, file_id)
1146
to_name, to_parent, to_kind, to_executable = \
1147
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1149
if not from_versioned:
1152
from_path = self._tree_id_paths.get(from_trans_id)
1153
if not to_versioned:
1156
to_path = final_paths.get_path(to_trans_id)
1157
if from_kind != to_kind:
1159
elif to_kind in ('file', 'symlink') and (
1160
to_trans_id != from_trans_id or
1161
to_trans_id in self._new_contents):
1163
if (not modified and from_versioned == to_versioned and
1164
from_parent==to_parent and from_name == to_name and
1165
from_executable == to_executable):
1167
results.append((file_id, (from_path, to_path), modified,
1168
(from_versioned, to_versioned),
1169
(from_parent, to_parent),
1170
(from_name, to_name),
1171
(from_kind, to_kind),
1172
(from_executable, to_executable)))
1173
return iter(sorted(results, key=lambda x:x[1]))
1176
def joinpath(parent, child):
1177
"""Join tree-relative paths, handling the tree root specially"""
1178
if parent is None or parent == "":
1181
return pathjoin(parent, child)
1184
class FinalPaths(object):
1185
"""Make path calculation cheap by memoizing paths.
1187
The underlying tree must not be manipulated between calls, or else
1188
the results will likely be incorrect.
1190
def __init__(self, transform):
1191
object.__init__(self)
1192
self._known_paths = {}
1193
self.transform = transform
1195
def _determine_path(self, trans_id):
1196
if trans_id == self.transform.root:
1198
name = self.transform.final_name(trans_id)
1199
parent_id = self.transform.final_parent(trans_id)
1200
if parent_id == self.transform.root:
1203
return pathjoin(self.get_path(parent_id), name)
1205
def get_path(self, trans_id):
1206
"""Find the final path associated with a trans_id"""
1207
if trans_id not in self._known_paths:
1208
self._known_paths[trans_id] = self._determine_path(trans_id)
1209
return self._known_paths[trans_id]
1211
def topology_sorted_ids(tree):
1212
"""Determine the topological order of the ids in a tree"""
1213
file_ids = list(tree)
1214
file_ids.sort(key=tree.id2path)
1218
def build_tree(tree, wt):
1219
"""Create working tree for a branch, using a TreeTransform.
1221
This function should be used on empty trees, having a tree root at most.
1222
(see merge and revert functionality for working with existing trees)
1224
Existing files are handled like so:
1226
- Existing bzrdirs take precedence over creating new items. They are
1227
created as '%s.diverted' % name.
1228
- Otherwise, if the content on disk matches the content we are building,
1229
it is silently replaced.
1230
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1232
wt.lock_tree_write()
1236
return _build_tree(tree, wt)
1242
def _build_tree(tree, wt):
1243
"""See build_tree."""
1244
if len(wt.inventory) > 1: # more than just a root
1245
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1247
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1248
pp = ProgressPhase("Build phase", 2, top_pb)
1249
if tree.inventory.root is not None:
1250
# This is kind of a hack: we should be altering the root
1251
# as part of the regular tree shape diff logic.
1252
# The conditional test here is to avoid doing an
1253
# expensive operation (flush) every time the root id
1254
# is set within the tree, nor setting the root and thus
1255
# marking the tree as dirty, because we use two different
1256
# idioms here: tree interfaces and inventory interfaces.
1257
if wt.path2id('') != tree.inventory.root.file_id:
1258
wt.set_root_id(tree.inventory.root.file_id)
1260
tt = TreeTransform(wt)
1264
file_trans_id[wt.get_root_id()] = \
1265
tt.trans_id_tree_file_id(wt.get_root_id())
1266
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1268
for num, (tree_path, entry) in \
1269
enumerate(tree.inventory.iter_entries_by_dir()):
1270
pb.update("Building tree", num, len(tree.inventory))
1271
if entry.parent_id is None:
1274
file_id = entry.file_id
1275
target_path = wt.abspath(tree_path)
1277
kind = file_kind(target_path)
1281
if kind == "directory":
1283
bzrdir.BzrDir.open(target_path)
1284
except errors.NotBranchError:
1288
if (file_id not in divert and
1289
_content_match(tree, entry, file_id, kind,
1291
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1292
if kind == 'directory':
1294
if entry.parent_id not in file_trans_id:
1295
raise AssertionError(
1296
'entry %s parent id %r is not in file_trans_id %r'
1297
% (entry, entry.parent_id, file_trans_id))
1298
parent_id = file_trans_id[entry.parent_id]
1299
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1302
new_trans_id = file_trans_id[file_id]
1303
old_parent = tt.trans_id_tree_path(tree_path)
1304
_reparent_children(tt, old_parent, new_trans_id)
1308
divert_trans = set(file_trans_id[f] for f in divert)
1309
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1310
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1311
conflicts = cook_conflicts(raw_conflicts, tt)
1312
for conflict in conflicts:
1315
wt.add_conflicts(conflicts)
1316
except errors.UnsupportedOperation:
1325
def _reparent_children(tt, old_parent, new_parent):
1326
for child in tt.iter_tree_children(old_parent):
1327
tt.adjust_path(tt.final_name(child), new_parent, child)
1330
def _content_match(tree, entry, file_id, kind, target_path):
1331
if entry.kind != kind:
1333
if entry.kind == "directory":
1335
if entry.kind == "file":
1336
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1338
elif entry.kind == "symlink":
1339
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1344
def resolve_checkout(tt, conflicts, divert):
1345
new_conflicts = set()
1346
for c_type, conflict in ((c[0], c) for c in conflicts):
1347
# Anything but a 'duplicate' would indicate programmer error
1348
assert c_type == 'duplicate', c_type
1349
# Now figure out which is new and which is old
1350
if tt.new_contents(conflict[1]):
1351
new_file = conflict[1]
1352
old_file = conflict[2]
1354
new_file = conflict[2]
1355
old_file = conflict[1]
1357
# We should only get here if the conflict wasn't completely
1359
final_parent = tt.final_parent(old_file)
1360
if new_file in divert:
1361
new_name = tt.final_name(old_file)+'.diverted'
1362
tt.adjust_path(new_name, final_parent, new_file)
1363
new_conflicts.add((c_type, 'Diverted to',
1364
new_file, old_file))
1366
new_name = tt.final_name(old_file)+'.moved'
1367
tt.adjust_path(new_name, final_parent, old_file)
1368
new_conflicts.add((c_type, 'Moved existing file to',
1369
old_file, new_file))
1370
return new_conflicts
1373
def new_by_entry(tt, entry, parent_id, tree):
1374
"""Create a new file according to its inventory entry"""
1378
contents = tree.get_file(entry.file_id).readlines()
1379
executable = tree.is_executable(entry.file_id)
1380
return tt.new_file(name, parent_id, contents, entry.file_id,
1382
elif kind in ('directory', 'tree-reference'):
1383
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1384
if kind == 'tree-reference':
1385
tt.set_tree_reference(entry.reference_revision, trans_id)
1387
elif kind == 'symlink':
1388
target = tree.get_symlink_target(entry.file_id)
1389
return tt.new_symlink(name, parent_id, target, entry.file_id)
1391
raise errors.BadFileKindError(name, kind)
1393
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1394
"""Create new file contents according to an inventory entry."""
1395
if entry.kind == "file":
1397
lines = tree.get_file(entry.file_id).readlines()
1398
tt.create_file(lines, trans_id, mode_id=mode_id)
1399
elif entry.kind == "symlink":
1400
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1401
elif entry.kind == "directory":
1402
tt.create_directory(trans_id)
1404
def create_entry_executability(tt, entry, trans_id):
1405
"""Set the executability of a trans_id according to an inventory entry"""
1406
if entry.kind == "file":
1407
tt.set_executability(entry.executable, trans_id)
1410
@deprecated_function(zero_fifteen)
1411
def find_interesting(working_tree, target_tree, filenames):
1412
"""Find the ids corresponding to specified filenames.
1414
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1416
working_tree.lock_read()
1418
target_tree.lock_read()
1420
return working_tree.paths2ids(filenames, [target_tree])
1422
target_tree.unlock()
1424
working_tree.unlock()
1427
def change_entry(tt, file_id, working_tree, target_tree,
1428
trans_id_file_id, backups, trans_id, by_parent):
1429
"""Replace a file_id's contents with those from a target tree."""
1430
e_trans_id = trans_id_file_id(file_id)
1431
entry = target_tree.inventory[file_id]
1432
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1435
mode_id = e_trans_id
1438
tt.delete_contents(e_trans_id)
1440
parent_trans_id = trans_id_file_id(entry.parent_id)
1441
backup_name = get_backup_name(entry, by_parent,
1442
parent_trans_id, tt)
1443
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1444
tt.unversion_file(e_trans_id)
1445
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1446
tt.version_file(file_id, e_trans_id)
1447
trans_id[file_id] = e_trans_id
1448
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1449
create_entry_executability(tt, entry, e_trans_id)
1452
tt.set_executability(entry.executable, e_trans_id)
1453
if tt.final_name(e_trans_id) != entry.name:
1456
parent_id = tt.final_parent(e_trans_id)
1457
parent_file_id = tt.final_file_id(parent_id)
1458
if parent_file_id != entry.parent_id:
1463
parent_trans_id = trans_id_file_id(entry.parent_id)
1464
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1467
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1468
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1471
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1472
"""Produce a backup-style name that appears to be available"""
1476
yield "%s.~%d~" % (name, counter)
1478
for new_name in name_gen():
1479
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1483
def _entry_changes(file_id, entry, working_tree):
1484
"""Determine in which ways the inventory entry has changed.
1486
Returns booleans: has_contents, content_mod, meta_mod
1487
has_contents means there are currently contents, but they differ
1488
contents_mod means contents need to be modified
1489
meta_mod means the metadata needs to be modified
1491
cur_entry = working_tree.inventory[file_id]
1493
working_kind = working_tree.kind(file_id)
1496
has_contents = False
1499
if has_contents is True:
1500
if entry.kind != working_kind:
1501
contents_mod, meta_mod = True, False
1503
cur_entry._read_tree_state(working_tree.id2path(file_id),
1505
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1506
cur_entry._forget_tree_state()
1507
return has_contents, contents_mod, meta_mod
1510
def revert(working_tree, target_tree, filenames, backups=False,
1511
pb=DummyProgress(), change_reporter=None):
1512
"""Revert a working tree's contents to those of a target tree."""
1513
target_tree.lock_read()
1514
tt = TreeTransform(working_tree, pb)
1516
pp = ProgressPhase("Revert phase", 3, pb)
1518
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1520
merge_modified = _alter_files(working_tree, target_tree, tt,
1521
child_pb, filenames, backups)
1525
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1527
raw_conflicts = resolve_conflicts(tt, child_pb)
1530
conflicts = cook_conflicts(raw_conflicts, tt)
1532
change_reporter = delta._ChangeReporter(
1533
unversioned_filter=working_tree.is_ignored)
1534
delta.report_changes(tt._iter_changes(), change_reporter)
1535
for conflict in conflicts:
1539
working_tree.set_merge_modified(merge_modified)
1541
target_tree.unlock()
1547
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1549
merge_modified = working_tree.merge_modified()
1550
change_list = target_tree._iter_changes(working_tree,
1551
specific_files=specific_files, pb=pb)
1552
if target_tree.inventory.root is None:
1558
for id_num, (file_id, path, changed_content, versioned, parent, name,
1559
kind, executable) in enumerate(change_list):
1560
if skip_root and file_id[0] is not None and parent[0] is None:
1562
trans_id = tt.trans_id_file_id(file_id)
1565
keep_content = False
1566
if kind[0] == 'file' and (backups or kind[1] is None):
1567
wt_sha1 = working_tree.get_file_sha1(file_id)
1568
if merge_modified.get(file_id) != wt_sha1:
1569
# acquire the basis tree lazily to prevent the
1570
# expense of accessing it when it's not needed ?
1571
# (Guessing, RBC, 200702)
1572
if basis_tree is None:
1573
basis_tree = working_tree.basis_tree()
1574
basis_tree.lock_read()
1575
if file_id in basis_tree:
1576
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1578
elif kind[1] is None and not versioned[1]:
1580
if kind[0] is not None:
1581
if not keep_content:
1582
tt.delete_contents(trans_id)
1583
elif kind[1] is not None:
1584
parent_trans_id = tt.trans_id_file_id(parent[0])
1585
by_parent = tt.by_parent()
1586
backup_name = _get_backup_name(name[0], by_parent,
1587
parent_trans_id, tt)
1588
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1589
new_trans_id = tt.create_path(name[0], parent_trans_id)
1590
if versioned == (True, True):
1591
tt.unversion_file(trans_id)
1592
tt.version_file(file_id, new_trans_id)
1593
# New contents should have the same unix perms as old
1596
trans_id = new_trans_id
1597
if kind[1] == 'directory':
1598
tt.create_directory(trans_id)
1599
elif kind[1] == 'symlink':
1600
tt.create_symlink(target_tree.get_symlink_target(file_id),
1602
elif kind[1] == 'file':
1603
tt.create_file(target_tree.get_file_lines(file_id),
1605
if basis_tree is None:
1606
basis_tree = working_tree.basis_tree()
1607
basis_tree.lock_read()
1608
new_sha1 = target_tree.get_file_sha1(file_id)
1609
if (file_id in basis_tree and new_sha1 ==
1610
basis_tree.get_file_sha1(file_id)):
1611
if file_id in merge_modified:
1612
del merge_modified[file_id]
1614
merge_modified[file_id] = new_sha1
1616
# preserve the execute bit when backing up
1617
if keep_content and executable[0] == executable[1]:
1618
tt.set_executability(executable[1], trans_id)
1620
assert kind[1] is None
1621
if versioned == (False, True):
1622
tt.version_file(file_id, trans_id)
1623
if versioned == (True, False):
1624
tt.unversion_file(trans_id)
1625
if (name[1] is not None and
1626
(name[0] != name[1] or parent[0] != parent[1])):
1628
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1629
if executable[0] != executable[1] and kind[1] == "file":
1630
tt.set_executability(executable[1], trans_id)
1632
if basis_tree is not None:
1634
return merge_modified
1637
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1638
"""Make many conflict-resolution attempts, but die if they fail"""
1639
if pass_func is None:
1640
pass_func = conflict_pass
1641
new_conflicts = set()
1644
pb.update('Resolution pass', n+1, 10)
1645
conflicts = tt.find_conflicts()
1646
if len(conflicts) == 0:
1647
return new_conflicts
1648
new_conflicts.update(pass_func(tt, conflicts))
1649
raise MalformedTransform(conflicts=conflicts)
1654
def conflict_pass(tt, conflicts, path_tree=None):
1655
"""Resolve some classes of conflicts.
1657
:param tt: The transform to resolve conflicts in
1658
:param conflicts: The conflicts to resolve
1659
:param path_tree: A Tree to get supplemental paths from
1661
new_conflicts = set()
1662
for c_type, conflict in ((c[0], c) for c in conflicts):
1663
if c_type == 'duplicate id':
1664
tt.unversion_file(conflict[1])
1665
new_conflicts.add((c_type, 'Unversioned existing file',
1666
conflict[1], conflict[2], ))
1667
elif c_type == 'duplicate':
1668
# files that were renamed take precedence
1669
new_name = tt.final_name(conflict[1])+'.moved'
1670
final_parent = tt.final_parent(conflict[1])
1671
if tt.path_changed(conflict[1]):
1672
tt.adjust_path(new_name, final_parent, conflict[2])
1673
new_conflicts.add((c_type, 'Moved existing file to',
1674
conflict[2], conflict[1]))
1676
tt.adjust_path(new_name, final_parent, conflict[1])
1677
new_conflicts.add((c_type, 'Moved existing file to',
1678
conflict[1], conflict[2]))
1679
elif c_type == 'parent loop':
1680
# break the loop by undoing one of the ops that caused the loop
1682
while not tt.path_changed(cur):
1683
cur = tt.final_parent(cur)
1684
new_conflicts.add((c_type, 'Cancelled move', cur,
1685
tt.final_parent(cur),))
1686
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1688
elif c_type == 'missing parent':
1689
trans_id = conflict[1]
1691
tt.cancel_deletion(trans_id)
1692
new_conflicts.add(('deleting parent', 'Not deleting',
1695
tt.create_directory(trans_id)
1696
new_conflicts.add((c_type, 'Created directory', trans_id))
1698
tt.final_name(trans_id)
1700
file_id = tt.final_file_id(trans_id)
1701
entry = path_tree.inventory[file_id]
1702
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1703
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1704
elif c_type == 'unversioned parent':
1705
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1706
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1707
return new_conflicts
1710
def cook_conflicts(raw_conflicts, tt):
1711
"""Generate a list of cooked conflicts, sorted by file path"""
1712
from bzrlib.conflicts import Conflict
1713
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1714
return sorted(conflict_iter, key=Conflict.sort_key)
1717
def iter_cook_conflicts(raw_conflicts, tt):
1718
from bzrlib.conflicts import Conflict
1720
for conflict in raw_conflicts:
1721
c_type = conflict[0]
1722
action = conflict[1]
1723
modified_path = fp.get_path(conflict[2])
1724
modified_id = tt.final_file_id(conflict[2])
1725
if len(conflict) == 3:
1726
yield Conflict.factory(c_type, action=action, path=modified_path,
1727
file_id=modified_id)
1730
conflicting_path = fp.get_path(conflict[3])
1731
conflicting_id = tt.final_file_id(conflict[3])
1732
yield Conflict.factory(c_type, action=action, path=modified_path,
1733
file_id=modified_id,
1734
conflict_path=conflicting_path,
1735
conflict_file_id=conflicting_id)