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
if trans_id not in self._new_contents:
210
del self._limbo_files[trans_id]
212
old_path = self._limbo_files[trans_id]
213
new_path = self._limbo_name(trans_id, from_scratch=True)
214
os.rename(old_path, new_path)
216
def adjust_root_path(self, name, parent):
217
"""Emulate moving the root by moving all children, instead.
219
We do this by undoing the association of root's transaction id with the
220
current tree. This allows us to create a new directory with that
221
transaction id. We unversion the root directory and version the
222
physically new directory, and hope someone versions the tree root
225
old_root = self._new_root
226
old_root_file_id = self.final_file_id(old_root)
227
# force moving all children of root
228
for child_id in self.iter_tree_children(old_root):
229
if child_id != parent:
230
self.adjust_path(self.final_name(child_id),
231
self.final_parent(child_id), child_id)
232
file_id = self.final_file_id(child_id)
233
if file_id is not None:
234
self.unversion_file(child_id)
235
self.version_file(file_id, child_id)
237
# the physical root needs a new transaction id
238
self._tree_path_ids.pop("")
239
self._tree_id_paths.pop(old_root)
240
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
241
if parent == old_root:
242
parent = self._new_root
243
self.adjust_path(name, parent, old_root)
244
self.create_directory(old_root)
245
self.version_file(old_root_file_id, old_root)
246
self.unversion_file(self._new_root)
248
def trans_id_tree_file_id(self, inventory_id):
249
"""Determine the transaction id of a working tree file.
251
This reflects only files that already exist, not ones that will be
252
added by transactions.
254
path = self._tree.inventory.id2path(inventory_id)
255
return self.trans_id_tree_path(path)
257
def trans_id_file_id(self, file_id):
258
"""Determine or set the transaction id associated with a file ID.
259
A new id is only created for file_ids that were never present. If
260
a transaction has been unversioned, it is deliberately still returned.
261
(this will likely lead to an unversioned parent conflict.)
263
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
264
return self._r_new_id[file_id]
265
elif file_id in self._tree.inventory:
266
return self.trans_id_tree_file_id(file_id)
267
elif file_id in self._non_present_ids:
268
return self._non_present_ids[file_id]
270
trans_id = self._assign_id()
271
self._non_present_ids[file_id] = trans_id
274
def canonical_path(self, path):
275
"""Get the canonical tree-relative path"""
276
# don't follow final symlinks
277
abs = self._tree.abspath(path)
278
if abs in self._relpaths:
279
return self._relpaths[abs]
280
dirname, basename = os.path.split(abs)
281
if dirname not in self._realpaths:
282
self._realpaths[dirname] = os.path.realpath(dirname)
283
dirname = self._realpaths[dirname]
284
abs = pathjoin(dirname, basename)
285
if dirname in self._relpaths:
286
relpath = pathjoin(self._relpaths[dirname], basename)
287
relpath = relpath.rstrip('/\\')
289
relpath = self._tree.relpath(abs)
290
self._relpaths[abs] = relpath
293
def trans_id_tree_path(self, path):
294
"""Determine (and maybe set) the transaction ID for a tree path."""
295
path = self.canonical_path(path)
296
if path not in self._tree_path_ids:
297
self._tree_path_ids[path] = self._assign_id()
298
self._tree_id_paths[self._tree_path_ids[path]] = path
299
return self._tree_path_ids[path]
301
def get_tree_parent(self, trans_id):
302
"""Determine id of the parent in the tree."""
303
path = self._tree_id_paths[trans_id]
306
return self.trans_id_tree_path(os.path.dirname(path))
308
def create_file(self, contents, trans_id, mode_id=None):
309
"""Schedule creation of a new file.
313
Contents is an iterator of strings, all of which will be written
314
to the target destination.
316
New file takes the permissions of any existing file with that id,
317
unless mode_id is specified.
319
name = self._limbo_name(trans_id)
323
unique_add(self._new_contents, trans_id, 'file')
325
# Clean up the file, it never got registered so
326
# TreeTransform.finalize() won't clean it up.
331
f.writelines(contents)
334
self._set_mode(trans_id, mode_id, S_ISREG)
336
def _set_mode(self, trans_id, mode_id, typefunc):
337
"""Set the mode of new file contents.
338
The mode_id is the existing file to get the mode from (often the same
339
as trans_id). The operation is only performed if there's a mode match
340
according to typefunc.
345
old_path = self._tree_id_paths[mode_id]
349
mode = os.stat(self._tree.abspath(old_path)).st_mode
351
if e.errno == errno.ENOENT:
356
os.chmod(self._limbo_name(trans_id), mode)
358
def create_directory(self, trans_id):
359
"""Schedule creation of a new directory.
361
See also new_directory.
363
os.mkdir(self._limbo_name(trans_id))
364
unique_add(self._new_contents, trans_id, 'directory')
366
def create_symlink(self, target, trans_id):
367
"""Schedule creation of a new symbolic link.
369
target is a bytestring.
370
See also new_symlink.
372
os.symlink(target, self._limbo_name(trans_id))
373
unique_add(self._new_contents, trans_id, 'symlink')
375
def cancel_creation(self, trans_id):
376
"""Cancel the creation of new file contents."""
377
del self._new_contents[trans_id]
378
children = self._limbo_children.get(trans_id)
379
# if this is a limbo directory with children, move them before removing
381
if children is not None:
382
self._rename_in_limbo(children)
383
del self._limbo_children[trans_id]
384
del self._limbo_children_names[trans_id]
385
delete_any(self._limbo_name(trans_id))
387
def delete_contents(self, trans_id):
388
"""Schedule the contents of a path entry for deletion"""
389
self.tree_kind(trans_id)
390
self._removed_contents.add(trans_id)
392
def cancel_deletion(self, trans_id):
393
"""Cancel a scheduled deletion"""
394
self._removed_contents.remove(trans_id)
396
def unversion_file(self, trans_id):
397
"""Schedule a path entry to become unversioned"""
398
self._removed_id.add(trans_id)
400
def delete_versioned(self, trans_id):
401
"""Delete and unversion a versioned file"""
402
self.delete_contents(trans_id)
403
self.unversion_file(trans_id)
405
def set_executability(self, executability, trans_id):
406
"""Schedule setting of the 'execute' bit
407
To unschedule, set to None
409
if executability is None:
410
del self._new_executability[trans_id]
412
unique_add(self._new_executability, trans_id, executability)
414
def set_tree_reference(self, revision_id, trans_id):
415
"""Set the reference associated with a directory"""
416
unique_add(self._new_reference_revision, trans_id, revision_id)
418
def version_file(self, file_id, trans_id):
419
"""Schedule a file to become versioned."""
420
assert file_id is not None
421
unique_add(self._new_id, trans_id, file_id)
422
unique_add(self._r_new_id, file_id, trans_id)
424
def cancel_versioning(self, trans_id):
425
"""Undo a previous versioning of a file"""
426
file_id = self._new_id[trans_id]
427
del self._new_id[trans_id]
428
del self._r_new_id[file_id]
431
"""Determine the paths of all new and changed files"""
433
fp = FinalPaths(self)
434
for id_set in (self._new_name, self._new_parent, self._new_contents,
435
self._new_id, self._new_executability):
436
new_ids.update(id_set)
437
new_paths = [(fp.get_path(t), t) for t in new_ids]
441
def tree_kind(self, trans_id):
442
"""Determine the file kind in the working tree.
444
Raises NoSuchFile if the file does not exist
446
path = self._tree_id_paths.get(trans_id)
448
raise NoSuchFile(None)
450
return file_kind(self._tree.abspath(path))
452
if e.errno != errno.ENOENT:
455
raise NoSuchFile(path)
457
def final_kind(self, trans_id):
458
"""Determine the final file kind, after any changes applied.
460
Raises NoSuchFile if the file does not exist/has no contents.
461
(It is conceivable that a path would be created without the
462
corresponding contents insertion command)
464
if trans_id in self._new_contents:
465
return self._new_contents[trans_id]
466
elif trans_id in self._removed_contents:
467
raise NoSuchFile(None)
469
return self.tree_kind(trans_id)
471
def tree_file_id(self, trans_id):
472
"""Determine the file id associated with the trans_id in the tree"""
474
path = self._tree_id_paths[trans_id]
476
# the file is a new, unversioned file, or invalid trans_id
478
# the file is old; the old id is still valid
479
if self._new_root == trans_id:
480
return self._tree.inventory.root.file_id
481
return self._tree.inventory.path2id(path)
483
def final_file_id(self, trans_id):
484
"""Determine the file id after any changes are applied, or None.
486
None indicates that the file will not be versioned after changes are
490
# there is a new id for this file
491
assert self._new_id[trans_id] is not None
492
return self._new_id[trans_id]
494
if trans_id in self._removed_id:
496
return self.tree_file_id(trans_id)
498
def inactive_file_id(self, trans_id):
499
"""Return the inactive file_id associated with a transaction id.
500
That is, the one in the tree or in non_present_ids.
501
The file_id may actually be active, too.
503
file_id = self.tree_file_id(trans_id)
504
if file_id is not None:
506
for key, value in self._non_present_ids.iteritems():
507
if value == trans_id:
510
def final_parent(self, trans_id):
511
"""Determine the parent file_id, after any changes are applied.
513
ROOT_PARENT is returned for the tree root.
516
return self._new_parent[trans_id]
518
return self.get_tree_parent(trans_id)
520
def final_name(self, trans_id):
521
"""Determine the final filename, after all changes are applied."""
523
return self._new_name[trans_id]
526
return os.path.basename(self._tree_id_paths[trans_id])
528
raise NoFinalPath(trans_id, self)
531
"""Return a map of parent: children for known parents.
533
Only new paths and parents of tree files with assigned ids are used.
536
items = list(self._new_parent.iteritems())
537
items.extend((t, self.final_parent(t)) for t in
538
self._tree_id_paths.keys())
539
for trans_id, parent_id in items:
540
if parent_id not in by_parent:
541
by_parent[parent_id] = set()
542
by_parent[parent_id].add(trans_id)
545
def path_changed(self, trans_id):
546
"""Return True if a trans_id's path has changed."""
547
return (trans_id in self._new_name) or (trans_id in self._new_parent)
549
def new_contents(self, trans_id):
550
return (trans_id in self._new_contents)
552
def find_conflicts(self):
553
"""Find any violations of inventory or filesystem invariants"""
554
if self.__done is True:
555
raise ReusingTransform()
557
# ensure all children of all existent parents are known
558
# all children of non-existent parents are known, by definition.
559
self._add_tree_children()
560
by_parent = self.by_parent()
561
conflicts.extend(self._unversioned_parents(by_parent))
562
conflicts.extend(self._parent_loops())
563
conflicts.extend(self._duplicate_entries(by_parent))
564
conflicts.extend(self._duplicate_ids())
565
conflicts.extend(self._parent_type_conflicts(by_parent))
566
conflicts.extend(self._improper_versioning())
567
conflicts.extend(self._executability_conflicts())
568
conflicts.extend(self._overwrite_conflicts())
571
def _add_tree_children(self):
572
"""Add all the children of all active parents to the known paths.
574
Active parents are those which gain children, and those which are
575
removed. This is a necessary first step in detecting conflicts.
577
parents = self.by_parent().keys()
578
parents.extend([t for t in self._removed_contents if
579
self.tree_kind(t) == 'directory'])
580
for trans_id in self._removed_id:
581
file_id = self.tree_file_id(trans_id)
582
if self._tree.inventory[file_id].kind == 'directory':
583
parents.append(trans_id)
585
for parent_id in parents:
586
# ensure that all children are registered with the transaction
587
list(self.iter_tree_children(parent_id))
589
def iter_tree_children(self, parent_id):
590
"""Iterate through the entry's tree children, if any"""
592
path = self._tree_id_paths[parent_id]
596
children = os.listdir(self._tree.abspath(path))
598
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
602
for child in children:
603
childpath = joinpath(path, child)
604
if self._tree.is_control_filename(childpath):
606
yield self.trans_id_tree_path(childpath)
608
def has_named_child(self, by_parent, parent_id, name):
610
children = by_parent[parent_id]
613
for child in children:
614
if self.final_name(child) == name:
617
path = self._tree_id_paths[parent_id]
620
childpath = joinpath(path, name)
621
child_id = self._tree_path_ids.get(childpath)
623
return lexists(self._tree.abspath(childpath))
625
if self.final_parent(child_id) != parent_id:
627
if child_id in self._removed_contents:
628
# XXX What about dangling file-ids?
633
def _parent_loops(self):
634
"""No entry should be its own ancestor"""
636
for trans_id in self._new_parent:
639
while parent_id is not ROOT_PARENT:
642
parent_id = self.final_parent(parent_id)
645
if parent_id == trans_id:
646
conflicts.append(('parent loop', trans_id))
647
if parent_id in seen:
651
def _unversioned_parents(self, by_parent):
652
"""If parent directories are versioned, children must be versioned."""
654
for parent_id, children in by_parent.iteritems():
655
if parent_id is ROOT_PARENT:
657
if self.final_file_id(parent_id) is not None:
659
for child_id in children:
660
if self.final_file_id(child_id) is not None:
661
conflicts.append(('unversioned parent', parent_id))
665
def _improper_versioning(self):
666
"""Cannot version a file with no contents, or a bad type.
668
However, existing entries with no contents are okay.
671
for trans_id in self._new_id.iterkeys():
673
kind = self.final_kind(trans_id)
675
conflicts.append(('versioning no contents', trans_id))
677
if not InventoryEntry.versionable_kind(kind):
678
conflicts.append(('versioning bad kind', trans_id, kind))
681
def _executability_conflicts(self):
682
"""Check for bad executability changes.
684
Only versioned files may have their executability set, because
685
1. only versioned entries can have executability under windows
686
2. only files can be executable. (The execute bit on a directory
687
does not indicate searchability)
690
for trans_id in self._new_executability:
691
if self.final_file_id(trans_id) is None:
692
conflicts.append(('unversioned executability', trans_id))
695
non_file = self.final_kind(trans_id) != "file"
699
conflicts.append(('non-file executability', trans_id))
702
def _overwrite_conflicts(self):
703
"""Check for overwrites (not permitted on Win32)"""
705
for trans_id in self._new_contents:
707
self.tree_kind(trans_id)
710
if trans_id not in self._removed_contents:
711
conflicts.append(('overwrite', trans_id,
712
self.final_name(trans_id)))
715
def _duplicate_entries(self, by_parent):
716
"""No directory may have two entries with the same name."""
718
for children in by_parent.itervalues():
719
name_ids = [(self.final_name(t), t) for t in children]
723
for name, trans_id in name_ids:
725
kind = self.final_kind(trans_id)
728
file_id = self.final_file_id(trans_id)
729
if kind is None and file_id is None:
731
if name == last_name:
732
conflicts.append(('duplicate', last_trans_id, trans_id,
735
last_trans_id = trans_id
738
def _duplicate_ids(self):
739
"""Each inventory id may only be used once"""
741
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
743
active_tree_ids = set((f for f in self._tree.inventory if
744
f not in removed_tree_ids))
745
for trans_id, file_id in self._new_id.iteritems():
746
if file_id in active_tree_ids:
747
old_trans_id = self.trans_id_tree_file_id(file_id)
748
conflicts.append(('duplicate id', old_trans_id, trans_id))
751
def _parent_type_conflicts(self, by_parent):
752
"""parents must have directory 'contents'."""
754
for parent_id, children in by_parent.iteritems():
755
if parent_id is ROOT_PARENT:
757
if not self._any_contents(children):
759
for child in children:
761
self.final_kind(child)
765
kind = self.final_kind(parent_id)
769
conflicts.append(('missing parent', parent_id))
770
elif kind != "directory":
771
conflicts.append(('non-directory parent', parent_id))
774
def _any_contents(self, trans_ids):
775
"""Return true if any of the trans_ids, will have contents."""
776
for trans_id in trans_ids:
778
kind = self.final_kind(trans_id)
785
"""Apply all changes to the inventory and filesystem.
787
If filesystem or inventory conflicts are present, MalformedTransform
790
If apply succeeds, finalize is not necessary.
792
conflicts = self.find_conflicts()
793
if len(conflicts) != 0:
794
raise MalformedTransform(conflicts=conflicts)
795
inv = self._tree.inventory
797
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
799
child_pb.update('Apply phase', 0, 2)
800
self._apply_removals(inv, inventory_delta)
801
child_pb.update('Apply phase', 1, 2)
802
modified_paths = self._apply_insertions(inv, inventory_delta)
805
self._tree.apply_inventory_delta(inventory_delta)
808
return _TransformResults(modified_paths, self.rename_count)
810
def _limbo_name(self, trans_id, from_scratch=False):
811
"""Generate the limbo name of a file"""
813
limbo_name = self._limbo_files.get(trans_id)
814
if limbo_name is not None:
816
parent = self._new_parent.get(trans_id)
817
# if the parent directory is already in limbo (e.g. when building a
818
# tree), choose a limbo name inside the parent, to reduce further
820
use_direct_path = False
821
if self._new_contents.get(parent) == 'directory':
822
filename = self._new_name.get(trans_id)
823
if filename is not None:
824
if parent not in self._limbo_children:
825
self._limbo_children[parent] = set()
826
self._limbo_children_names[parent] = {}
827
use_direct_path = True
828
# the direct path can only be used if no other file has
829
# already taken this pathname, i.e. if the name is unused, or
830
# if it is already associated with this trans_id.
831
elif (self._limbo_children_names[parent].get(filename)
832
in (trans_id, None)):
833
use_direct_path = True
835
limbo_name = pathjoin(self._limbo_files[parent], filename)
836
self._limbo_children[parent].add(trans_id)
837
self._limbo_children_names[parent][filename] = trans_id
839
limbo_name = pathjoin(self._limbodir, trans_id)
840
self._needs_rename.add(trans_id)
841
self._limbo_files[trans_id] = limbo_name
844
def _apply_removals(self, inv, inventory_delta):
845
"""Perform tree operations that remove directory/inventory names.
847
That is, delete files that are to be deleted, and put any files that
848
need renaming into limbo. This must be done in strict child-to-parent
851
tree_paths = list(self._tree_path_ids.iteritems())
852
tree_paths.sort(reverse=True)
853
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
855
for num, data in enumerate(tree_paths):
856
path, trans_id = data
857
child_pb.update('removing file', num, len(tree_paths))
858
full_path = self._tree.abspath(path)
859
if trans_id in self._removed_contents:
860
delete_any(full_path)
861
elif trans_id in self._new_name or trans_id in \
864
os.rename(full_path, self._limbo_name(trans_id))
866
if e.errno != errno.ENOENT:
869
self.rename_count += 1
870
if trans_id in self._removed_id:
871
if trans_id == self._new_root:
872
file_id = self._tree.inventory.root.file_id
874
file_id = self.tree_file_id(trans_id)
875
assert file_id is not None
876
inventory_delta.append((path, None, file_id, None))
880
def _apply_insertions(self, inv, inventory_delta):
881
"""Perform tree operations that insert directory/inventory names.
883
That is, create any files that need to be created, and restore from
884
limbo any files that needed renaming. This must be done in strict
885
parent-to-child order.
887
new_paths = self.new_paths()
889
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
891
for num, (path, trans_id) in enumerate(new_paths):
893
child_pb.update('adding file', num, len(new_paths))
895
kind = self._new_contents[trans_id]
897
kind = contents = None
898
if trans_id in self._new_contents or \
899
self.path_changed(trans_id):
900
full_path = self._tree.abspath(path)
901
if trans_id in self._needs_rename:
903
os.rename(self._limbo_name(trans_id), full_path)
905
# We may be renaming a dangling inventory id
906
if e.errno != errno.ENOENT:
909
self.rename_count += 1
910
if trans_id in self._new_contents:
911
modified_paths.append(full_path)
912
del self._new_contents[trans_id]
914
if trans_id in self._new_id:
916
kind = file_kind(self._tree.abspath(path))
917
if trans_id in self._new_reference_revision:
918
new_entry = inventory.TreeReference(
919
self._new_id[trans_id],
920
self._new_name[trans_id],
921
self.final_file_id(self._new_parent[trans_id]),
922
None, self._new_reference_revision[trans_id])
924
new_entry = inventory.make_entry(kind,
925
self.final_name(trans_id),
926
self.final_file_id(self.final_parent(trans_id)),
927
self._new_id[trans_id])
929
if trans_id in self._new_name or trans_id in\
931
trans_id in self._new_executability:
932
file_id = self.final_file_id(trans_id)
933
if file_id is not None:
935
new_entry = entry.copy()
937
if trans_id in self._new_name or trans_id in\
939
if new_entry is not None:
940
new_entry.name = self.final_name(trans_id)
941
parent = self.final_parent(trans_id)
942
parent_id = self.final_file_id(parent)
943
new_entry.parent_id = parent_id
945
if trans_id in self._new_executability:
946
self._set_executability(path, new_entry, trans_id)
947
if new_entry is not None:
948
if new_entry.file_id in inv:
949
old_path = inv.id2path(new_entry.file_id)
952
inventory_delta.append((old_path, path,
957
return modified_paths
959
def _set_executability(self, path, entry, trans_id):
960
"""Set the executability of versioned files """
961
new_executability = self._new_executability[trans_id]
962
entry.executable = new_executability
963
if supports_executable():
964
abspath = self._tree.abspath(path)
965
current_mode = os.stat(abspath).st_mode
966
if new_executability:
969
to_mode = current_mode | (0100 & ~umask)
970
# Enable x-bit for others only if they can read it.
971
if current_mode & 0004:
972
to_mode |= 0001 & ~umask
973
if current_mode & 0040:
974
to_mode |= 0010 & ~umask
976
to_mode = current_mode & ~0111
977
os.chmod(abspath, to_mode)
979
def _new_entry(self, name, parent_id, file_id):
980
"""Helper function to create a new filesystem entry."""
981
trans_id = self.create_path(name, parent_id)
982
if file_id is not None:
983
self.version_file(file_id, trans_id)
986
def new_file(self, name, parent_id, contents, file_id=None,
988
"""Convenience method to create files.
990
name is the name of the file to create.
991
parent_id is the transaction id of the parent directory of the file.
992
contents is an iterator of bytestrings, which will be used to produce
994
:param file_id: The inventory ID of the file, if it is to be versioned.
995
:param executable: Only valid when a file_id has been supplied.
997
trans_id = self._new_entry(name, parent_id, file_id)
998
# TODO: rather than scheduling a set_executable call,
999
# have create_file create the file with the right mode.
1000
self.create_file(contents, trans_id)
1001
if executable is not None:
1002
self.set_executability(executable, trans_id)
1005
def new_directory(self, name, parent_id, file_id=None):
1006
"""Convenience method to create directories.
1008
name is the name of the directory to create.
1009
parent_id is the transaction id of the parent directory of the
1011
file_id is the inventory ID of the directory, if it is to be versioned.
1013
trans_id = self._new_entry(name, parent_id, file_id)
1014
self.create_directory(trans_id)
1017
def new_symlink(self, name, parent_id, target, file_id=None):
1018
"""Convenience method to create symbolic link.
1020
name is the name of the symlink to create.
1021
parent_id is the transaction id of the parent directory of the symlink.
1022
target is a bytestring of the target of the symlink.
1023
file_id is the inventory ID of the file, if it is to be versioned.
1025
trans_id = self._new_entry(name, parent_id, file_id)
1026
self.create_symlink(target, trans_id)
1029
def _affected_ids(self):
1030
"""Return the set of transform ids affected by the transform"""
1031
trans_ids = set(self._removed_id)
1032
trans_ids.update(self._new_id.keys())
1033
trans_ids.update(self._removed_contents)
1034
trans_ids.update(self._new_contents.keys())
1035
trans_ids.update(self._new_executability.keys())
1036
trans_ids.update(self._new_name.keys())
1037
trans_ids.update(self._new_parent.keys())
1040
def _get_file_id_maps(self):
1041
"""Return mapping of file_ids to trans_ids in the to and from states"""
1042
trans_ids = self._affected_ids()
1045
# Build up two dicts: trans_ids associated with file ids in the
1046
# FROM state, vs the TO state.
1047
for trans_id in trans_ids:
1048
from_file_id = self.tree_file_id(trans_id)
1049
if from_file_id is not None:
1050
from_trans_ids[from_file_id] = trans_id
1051
to_file_id = self.final_file_id(trans_id)
1052
if to_file_id is not None:
1053
to_trans_ids[to_file_id] = trans_id
1054
return from_trans_ids, to_trans_ids
1056
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1057
"""Get data about a file in the from (tree) state
1059
Return a (name, parent, kind, executable) tuple
1061
from_path = self._tree_id_paths.get(from_trans_id)
1063
# get data from working tree if versioned
1064
from_entry = self._tree.inventory[file_id]
1065
from_name = from_entry.name
1066
from_parent = from_entry.parent_id
1069
if from_path is None:
1070
# File does not exist in FROM state
1074
# File exists, but is not versioned. Have to use path-
1076
from_name = os.path.basename(from_path)
1077
tree_parent = self.get_tree_parent(from_trans_id)
1078
from_parent = self.tree_file_id(tree_parent)
1079
if from_path is not None:
1080
from_kind, from_executable, from_stats = \
1081
self._tree._comparison_data(from_entry, from_path)
1084
from_executable = False
1085
return from_name, from_parent, from_kind, from_executable
1087
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1088
"""Get data about a file in the to (target) state
1090
Return a (name, parent, kind, executable) tuple
1092
to_name = self.final_name(to_trans_id)
1094
to_kind = self.final_kind(to_trans_id)
1097
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1098
if to_trans_id in self._new_executability:
1099
to_executable = self._new_executability[to_trans_id]
1100
elif to_trans_id == from_trans_id:
1101
to_executable = from_executable
1103
to_executable = False
1104
return to_name, to_parent, to_kind, to_executable
1106
def _iter_changes(self):
1107
"""Produce output in the same format as Tree._iter_changes.
1109
Will produce nonsensical results if invoked while inventory/filesystem
1110
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1112
This reads the Transform, but only reproduces changes involving a
1113
file_id. Files that are not versioned in either of the FROM or TO
1114
states are not reflected.
1116
final_paths = FinalPaths(self)
1117
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1119
# Now iterate through all active file_ids
1120
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1122
from_trans_id = from_trans_ids.get(file_id)
1123
# find file ids, and determine versioning state
1124
if from_trans_id is None:
1125
from_versioned = False
1126
from_trans_id = to_trans_ids[file_id]
1128
from_versioned = True
1129
to_trans_id = to_trans_ids.get(file_id)
1130
if to_trans_id is None:
1131
to_versioned = False
1132
to_trans_id = from_trans_id
1136
from_name, from_parent, from_kind, from_executable = \
1137
self._from_file_data(from_trans_id, from_versioned, file_id)
1139
to_name, to_parent, to_kind, to_executable = \
1140
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1142
if not from_versioned:
1145
from_path = self._tree_id_paths.get(from_trans_id)
1146
if not to_versioned:
1149
to_path = final_paths.get_path(to_trans_id)
1150
if from_kind != to_kind:
1152
elif to_kind in ('file', 'symlink') and (
1153
to_trans_id != from_trans_id or
1154
to_trans_id in self._new_contents):
1156
if (not modified and from_versioned == to_versioned and
1157
from_parent==to_parent and from_name == to_name and
1158
from_executable == to_executable):
1160
results.append((file_id, (from_path, to_path), modified,
1161
(from_versioned, to_versioned),
1162
(from_parent, to_parent),
1163
(from_name, to_name),
1164
(from_kind, to_kind),
1165
(from_executable, to_executable)))
1166
return iter(sorted(results, key=lambda x:x[1]))
1169
def joinpath(parent, child):
1170
"""Join tree-relative paths, handling the tree root specially"""
1171
if parent is None or parent == "":
1174
return pathjoin(parent, child)
1177
class FinalPaths(object):
1178
"""Make path calculation cheap by memoizing paths.
1180
The underlying tree must not be manipulated between calls, or else
1181
the results will likely be incorrect.
1183
def __init__(self, transform):
1184
object.__init__(self)
1185
self._known_paths = {}
1186
self.transform = transform
1188
def _determine_path(self, trans_id):
1189
if trans_id == self.transform.root:
1191
name = self.transform.final_name(trans_id)
1192
parent_id = self.transform.final_parent(trans_id)
1193
if parent_id == self.transform.root:
1196
return pathjoin(self.get_path(parent_id), name)
1198
def get_path(self, trans_id):
1199
"""Find the final path associated with a trans_id"""
1200
if trans_id not in self._known_paths:
1201
self._known_paths[trans_id] = self._determine_path(trans_id)
1202
return self._known_paths[trans_id]
1204
def topology_sorted_ids(tree):
1205
"""Determine the topological order of the ids in a tree"""
1206
file_ids = list(tree)
1207
file_ids.sort(key=tree.id2path)
1211
def build_tree(tree, wt):
1212
"""Create working tree for a branch, using a TreeTransform.
1214
This function should be used on empty trees, having a tree root at most.
1215
(see merge and revert functionality for working with existing trees)
1217
Existing files are handled like so:
1219
- Existing bzrdirs take precedence over creating new items. They are
1220
created as '%s.diverted' % name.
1221
- Otherwise, if the content on disk matches the content we are building,
1222
it is silently replaced.
1223
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1225
wt.lock_tree_write()
1229
return _build_tree(tree, wt)
1235
def _build_tree(tree, wt):
1236
"""See build_tree."""
1237
if len(wt.inventory) > 1: # more than just a root
1238
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1240
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1241
pp = ProgressPhase("Build phase", 2, top_pb)
1242
if tree.inventory.root is not None:
1243
# This is kind of a hack: we should be altering the root
1244
# as part of the regular tree shape diff logic.
1245
# The conditional test here is to avoid doing an
1246
# expensive operation (flush) every time the root id
1247
# is set within the tree, nor setting the root and thus
1248
# marking the tree as dirty, because we use two different
1249
# idioms here: tree interfaces and inventory interfaces.
1250
if wt.path2id('') != tree.inventory.root.file_id:
1251
wt.set_root_id(tree.inventory.root.file_id)
1253
tt = TreeTransform(wt)
1257
file_trans_id[wt.get_root_id()] = \
1258
tt.trans_id_tree_file_id(wt.get_root_id())
1259
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1261
for num, (tree_path, entry) in \
1262
enumerate(tree.inventory.iter_entries_by_dir()):
1263
pb.update("Building tree", num, len(tree.inventory))
1264
if entry.parent_id is None:
1267
file_id = entry.file_id
1268
target_path = wt.abspath(tree_path)
1270
kind = file_kind(target_path)
1274
if kind == "directory":
1276
bzrdir.BzrDir.open(target_path)
1277
except errors.NotBranchError:
1281
if (file_id not in divert and
1282
_content_match(tree, entry, file_id, kind,
1284
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1285
if kind == 'directory':
1287
if entry.parent_id not in file_trans_id:
1288
raise AssertionError(
1289
'entry %s parent id %r is not in file_trans_id %r'
1290
% (entry, entry.parent_id, file_trans_id))
1291
parent_id = file_trans_id[entry.parent_id]
1292
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1295
new_trans_id = file_trans_id[file_id]
1296
old_parent = tt.trans_id_tree_path(tree_path)
1297
_reparent_children(tt, old_parent, new_trans_id)
1301
divert_trans = set(file_trans_id[f] for f in divert)
1302
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1303
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1304
conflicts = cook_conflicts(raw_conflicts, tt)
1305
for conflict in conflicts:
1308
wt.add_conflicts(conflicts)
1309
except errors.UnsupportedOperation:
1318
def _reparent_children(tt, old_parent, new_parent):
1319
for child in tt.iter_tree_children(old_parent):
1320
tt.adjust_path(tt.final_name(child), new_parent, child)
1323
def _content_match(tree, entry, file_id, kind, target_path):
1324
if entry.kind != kind:
1326
if entry.kind == "directory":
1328
if entry.kind == "file":
1329
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1331
elif entry.kind == "symlink":
1332
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1337
def resolve_checkout(tt, conflicts, divert):
1338
new_conflicts = set()
1339
for c_type, conflict in ((c[0], c) for c in conflicts):
1340
# Anything but a 'duplicate' would indicate programmer error
1341
assert c_type == 'duplicate', c_type
1342
# Now figure out which is new and which is old
1343
if tt.new_contents(conflict[1]):
1344
new_file = conflict[1]
1345
old_file = conflict[2]
1347
new_file = conflict[2]
1348
old_file = conflict[1]
1350
# We should only get here if the conflict wasn't completely
1352
final_parent = tt.final_parent(old_file)
1353
if new_file in divert:
1354
new_name = tt.final_name(old_file)+'.diverted'
1355
tt.adjust_path(new_name, final_parent, new_file)
1356
new_conflicts.add((c_type, 'Diverted to',
1357
new_file, old_file))
1359
new_name = tt.final_name(old_file)+'.moved'
1360
tt.adjust_path(new_name, final_parent, old_file)
1361
new_conflicts.add((c_type, 'Moved existing file to',
1362
old_file, new_file))
1363
return new_conflicts
1366
def new_by_entry(tt, entry, parent_id, tree):
1367
"""Create a new file according to its inventory entry"""
1371
contents = tree.get_file(entry.file_id).readlines()
1372
executable = tree.is_executable(entry.file_id)
1373
return tt.new_file(name, parent_id, contents, entry.file_id,
1375
elif kind in ('directory', 'tree-reference'):
1376
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1377
if kind == 'tree-reference':
1378
tt.set_tree_reference(entry.reference_revision, trans_id)
1380
elif kind == 'symlink':
1381
target = tree.get_symlink_target(entry.file_id)
1382
return tt.new_symlink(name, parent_id, target, entry.file_id)
1384
raise errors.BadFileKindError(name, kind)
1386
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1387
"""Create new file contents according to an inventory entry."""
1388
if entry.kind == "file":
1390
lines = tree.get_file(entry.file_id).readlines()
1391
tt.create_file(lines, trans_id, mode_id=mode_id)
1392
elif entry.kind == "symlink":
1393
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1394
elif entry.kind == "directory":
1395
tt.create_directory(trans_id)
1397
def create_entry_executability(tt, entry, trans_id):
1398
"""Set the executability of a trans_id according to an inventory entry"""
1399
if entry.kind == "file":
1400
tt.set_executability(entry.executable, trans_id)
1403
@deprecated_function(zero_fifteen)
1404
def find_interesting(working_tree, target_tree, filenames):
1405
"""Find the ids corresponding to specified filenames.
1407
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1409
working_tree.lock_read()
1411
target_tree.lock_read()
1413
return working_tree.paths2ids(filenames, [target_tree])
1415
target_tree.unlock()
1417
working_tree.unlock()
1420
def change_entry(tt, file_id, working_tree, target_tree,
1421
trans_id_file_id, backups, trans_id, by_parent):
1422
"""Replace a file_id's contents with those from a target tree."""
1423
e_trans_id = trans_id_file_id(file_id)
1424
entry = target_tree.inventory[file_id]
1425
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1428
mode_id = e_trans_id
1431
tt.delete_contents(e_trans_id)
1433
parent_trans_id = trans_id_file_id(entry.parent_id)
1434
backup_name = get_backup_name(entry, by_parent,
1435
parent_trans_id, tt)
1436
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1437
tt.unversion_file(e_trans_id)
1438
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1439
tt.version_file(file_id, e_trans_id)
1440
trans_id[file_id] = e_trans_id
1441
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1442
create_entry_executability(tt, entry, e_trans_id)
1445
tt.set_executability(entry.executable, e_trans_id)
1446
if tt.final_name(e_trans_id) != entry.name:
1449
parent_id = tt.final_parent(e_trans_id)
1450
parent_file_id = tt.final_file_id(parent_id)
1451
if parent_file_id != entry.parent_id:
1456
parent_trans_id = trans_id_file_id(entry.parent_id)
1457
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1460
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1461
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1464
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1465
"""Produce a backup-style name that appears to be available"""
1469
yield "%s.~%d~" % (name, counter)
1471
for new_name in name_gen():
1472
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1476
def _entry_changes(file_id, entry, working_tree):
1477
"""Determine in which ways the inventory entry has changed.
1479
Returns booleans: has_contents, content_mod, meta_mod
1480
has_contents means there are currently contents, but they differ
1481
contents_mod means contents need to be modified
1482
meta_mod means the metadata needs to be modified
1484
cur_entry = working_tree.inventory[file_id]
1486
working_kind = working_tree.kind(file_id)
1489
has_contents = False
1492
if has_contents is True:
1493
if entry.kind != working_kind:
1494
contents_mod, meta_mod = True, False
1496
cur_entry._read_tree_state(working_tree.id2path(file_id),
1498
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1499
cur_entry._forget_tree_state()
1500
return has_contents, contents_mod, meta_mod
1503
def revert(working_tree, target_tree, filenames, backups=False,
1504
pb=DummyProgress(), change_reporter=None):
1505
"""Revert a working tree's contents to those of a target tree."""
1506
target_tree.lock_read()
1507
tt = TreeTransform(working_tree, pb)
1509
pp = ProgressPhase("Revert phase", 3, pb)
1511
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1513
merge_modified = _alter_files(working_tree, target_tree, tt,
1514
child_pb, filenames, backups)
1518
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1520
raw_conflicts = resolve_conflicts(tt, child_pb)
1523
conflicts = cook_conflicts(raw_conflicts, tt)
1525
change_reporter = delta._ChangeReporter(
1526
unversioned_filter=working_tree.is_ignored)
1527
delta.report_changes(tt._iter_changes(), change_reporter)
1528
for conflict in conflicts:
1532
working_tree.set_merge_modified(merge_modified)
1534
target_tree.unlock()
1540
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1542
merge_modified = working_tree.merge_modified()
1543
change_list = target_tree._iter_changes(working_tree,
1544
specific_files=specific_files, pb=pb)
1545
if target_tree.inventory.root is None:
1551
for id_num, (file_id, path, changed_content, versioned, parent, name,
1552
kind, executable) in enumerate(change_list):
1553
if skip_root and file_id[0] is not None and parent[0] is None:
1555
trans_id = tt.trans_id_file_id(file_id)
1558
keep_content = False
1559
if kind[0] == 'file' and (backups or kind[1] is None):
1560
wt_sha1 = working_tree.get_file_sha1(file_id)
1561
if merge_modified.get(file_id) != wt_sha1:
1562
# acquire the basis tree lazily to prevent the
1563
# expense of accessing it when it's not needed ?
1564
# (Guessing, RBC, 200702)
1565
if basis_tree is None:
1566
basis_tree = working_tree.basis_tree()
1567
basis_tree.lock_read()
1568
if file_id in basis_tree:
1569
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1571
elif kind[1] is None and not versioned[1]:
1573
if kind[0] is not None:
1574
if not keep_content:
1575
tt.delete_contents(trans_id)
1576
elif kind[1] is not None:
1577
parent_trans_id = tt.trans_id_file_id(parent[0])
1578
by_parent = tt.by_parent()
1579
backup_name = _get_backup_name(name[0], by_parent,
1580
parent_trans_id, tt)
1581
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1582
new_trans_id = tt.create_path(name[0], parent_trans_id)
1583
if versioned == (True, True):
1584
tt.unversion_file(trans_id)
1585
tt.version_file(file_id, new_trans_id)
1586
# New contents should have the same unix perms as old
1589
trans_id = new_trans_id
1590
if kind[1] == 'directory':
1591
tt.create_directory(trans_id)
1592
elif kind[1] == 'symlink':
1593
tt.create_symlink(target_tree.get_symlink_target(file_id),
1595
elif kind[1] == 'file':
1596
tt.create_file(target_tree.get_file_lines(file_id),
1598
if basis_tree is None:
1599
basis_tree = working_tree.basis_tree()
1600
basis_tree.lock_read()
1601
new_sha1 = target_tree.get_file_sha1(file_id)
1602
if (file_id in basis_tree and new_sha1 ==
1603
basis_tree.get_file_sha1(file_id)):
1604
if file_id in merge_modified:
1605
del merge_modified[file_id]
1607
merge_modified[file_id] = new_sha1
1609
# preserve the execute bit when backing up
1610
if keep_content and executable[0] == executable[1]:
1611
tt.set_executability(executable[1], trans_id)
1613
assert kind[1] is None
1614
if versioned == (False, True):
1615
tt.version_file(file_id, trans_id)
1616
if versioned == (True, False):
1617
tt.unversion_file(trans_id)
1618
if (name[1] is not None and
1619
(name[0] != name[1] or parent[0] != parent[1])):
1621
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1622
if executable[0] != executable[1] and kind[1] == "file":
1623
tt.set_executability(executable[1], trans_id)
1625
if basis_tree is not None:
1627
return merge_modified
1630
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1631
"""Make many conflict-resolution attempts, but die if they fail"""
1632
if pass_func is None:
1633
pass_func = conflict_pass
1634
new_conflicts = set()
1637
pb.update('Resolution pass', n+1, 10)
1638
conflicts = tt.find_conflicts()
1639
if len(conflicts) == 0:
1640
return new_conflicts
1641
new_conflicts.update(pass_func(tt, conflicts))
1642
raise MalformedTransform(conflicts=conflicts)
1647
def conflict_pass(tt, conflicts):
1648
"""Resolve some classes of conflicts."""
1649
new_conflicts = set()
1650
for c_type, conflict in ((c[0], c) for c in conflicts):
1651
if c_type == 'duplicate id':
1652
tt.unversion_file(conflict[1])
1653
new_conflicts.add((c_type, 'Unversioned existing file',
1654
conflict[1], conflict[2], ))
1655
elif c_type == 'duplicate':
1656
# files that were renamed take precedence
1657
new_name = tt.final_name(conflict[1])+'.moved'
1658
final_parent = tt.final_parent(conflict[1])
1659
if tt.path_changed(conflict[1]):
1660
tt.adjust_path(new_name, final_parent, conflict[2])
1661
new_conflicts.add((c_type, 'Moved existing file to',
1662
conflict[2], conflict[1]))
1664
tt.adjust_path(new_name, final_parent, conflict[1])
1665
new_conflicts.add((c_type, 'Moved existing file to',
1666
conflict[1], conflict[2]))
1667
elif c_type == 'parent loop':
1668
# break the loop by undoing one of the ops that caused the loop
1670
while not tt.path_changed(cur):
1671
cur = tt.final_parent(cur)
1672
new_conflicts.add((c_type, 'Cancelled move', cur,
1673
tt.final_parent(cur),))
1674
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1676
elif c_type == 'missing parent':
1677
trans_id = conflict[1]
1679
tt.cancel_deletion(trans_id)
1680
new_conflicts.add(('deleting parent', 'Not deleting',
1683
tt.create_directory(trans_id)
1684
new_conflicts.add((c_type, 'Created directory', trans_id))
1685
elif c_type == 'unversioned parent':
1686
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1687
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1688
return new_conflicts
1691
def cook_conflicts(raw_conflicts, tt):
1692
"""Generate a list of cooked conflicts, sorted by file path"""
1693
from bzrlib.conflicts import Conflict
1694
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1695
return sorted(conflict_iter, key=Conflict.sort_key)
1698
def iter_cook_conflicts(raw_conflicts, tt):
1699
from bzrlib.conflicts import Conflict
1701
for conflict in raw_conflicts:
1702
c_type = conflict[0]
1703
action = conflict[1]
1704
modified_path = fp.get_path(conflict[2])
1705
modified_id = tt.final_file_id(conflict[2])
1706
if len(conflict) == 3:
1707
yield Conflict.factory(c_type, action=action, path=modified_path,
1708
file_id=modified_id)
1711
conflicting_path = fp.get_path(conflict[3])
1712
conflicting_id = tt.final_file_id(conflict[3])
1713
yield Conflict.factory(c_type, action=action, path=modified_path,
1714
file_id=modified_id,
1715
conflict_path=conflicting_path,
1716
conflict_file_id=conflicting_id)