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, \
39
from bzrlib.trace import mutter, warning
40
from bzrlib import tree
42
import bzrlib.urlutils as urlutils
45
ROOT_PARENT = "root-parent"
48
def unique_add(map, key, value):
50
raise DuplicateKey(key=key)
54
class _TransformResults(object):
55
def __init__(self, modified_paths, rename_count):
57
self.modified_paths = modified_paths
58
self.rename_count = rename_count
61
class TreeTransform(object):
62
"""Represent a tree transformation.
64
This object is designed to support incremental generation of the transform,
67
However, it gives optimum performance when parent directories are created
68
before their contents. The transform is then able to put child files
69
directly in their parent directory, avoiding later renames.
71
It is easy to produce malformed transforms, but they are generally
72
harmless. Attempting to apply a malformed transform will cause an
73
exception to be raised before any modifications are made to the tree.
75
Many kinds of malformed transforms can be corrected with the
76
resolve_conflicts function. The remaining ones indicate programming error,
77
such as trying to create a file with no path.
79
Two sets of file creation methods are supplied. Convenience methods are:
84
These are composed of the low-level methods:
86
* create_file or create_directory or create_symlink
90
def __init__(self, tree, pb=DummyProgress()):
91
"""Note: a tree_write lock is taken on the tree.
93
Use TreeTransform.finalize() to release the lock (can be omitted if
94
TreeTransform.apply() called).
98
self._tree.lock_tree_write()
100
control_files = self._tree._control_files
101
self._limbodir = urlutils.local_path_from_url(
102
control_files.controlfilename('limbo'))
104
os.mkdir(self._limbodir)
106
if e.errno == errno.EEXIST:
107
raise ExistingLimbo(self._limbodir)
114
self._new_parent = {}
115
self._new_contents = {}
116
# A mapping of transform ids to their limbo filename
117
self._limbo_files = {}
118
# A mapping of transform ids to a set of the transform ids of children
119
# that their limbo directory has
120
self._limbo_children = {}
121
# Map transform ids to maps of child filename to child transform id
122
self._limbo_children_names = {}
123
# List of transform ids that need to be renamed from limbo into place
124
self._needs_rename = set()
125
self._removed_contents = set()
126
self._new_executability = {}
127
self._new_reference_revision = {}
129
self._non_present_ids = {}
131
self._removed_id = set()
132
self._tree_path_ids = {}
133
self._tree_id_paths = {}
134
# Cache of realpath results, to speed up canonical_path
136
# Cache of relpath results, to speed up canonical_path
138
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
141
self.rename_count = 0
143
def __get_root(self):
144
return self._new_root
146
root = property(__get_root)
149
"""Release the working tree lock, if held, clean up limbo dir.
151
This is required if apply has not been invoked, but can be invoked
154
if self._tree is None:
157
entries = [(self._limbo_name(t), t, k) for t, k in
158
self._new_contents.iteritems()]
159
entries.sort(reverse=True)
160
for path, trans_id, kind in entries:
161
if kind == "directory":
166
os.rmdir(self._limbodir)
168
# We don't especially care *why* the dir is immortal.
169
raise ImmortalLimbo(self._limbodir)
174
def _assign_id(self):
175
"""Produce a new tranform id"""
176
new_id = "new-%s" % self._id_number
180
def create_path(self, name, parent):
181
"""Assign a transaction id to a new path"""
182
trans_id = self._assign_id()
183
unique_add(self._new_name, trans_id, name)
184
unique_add(self._new_parent, trans_id, parent)
187
def adjust_path(self, name, parent, trans_id):
188
"""Change the path that is assigned to a transaction id."""
189
if trans_id == self._new_root:
191
previous_parent = self._new_parent.get(trans_id)
192
previous_name = self._new_name.get(trans_id)
193
self._new_name[trans_id] = name
194
self._new_parent[trans_id] = parent
195
if (trans_id in self._limbo_files and
196
trans_id not in self._needs_rename):
197
self._rename_in_limbo([trans_id])
198
self._limbo_children[previous_parent].remove(trans_id)
199
del self._limbo_children_names[previous_parent][previous_name]
201
def _rename_in_limbo(self, trans_ids):
202
"""Fix limbo names so that the right final path is produced.
204
This means we outsmarted ourselves-- we tried to avoid renaming
205
these files later by creating them with their final names in their
206
final parents. But now the previous name or parent is no longer
207
suitable, so we have to rename them.
209
Even for trans_ids that have no new contents, we must remove their
210
entries from _limbo_files, because they are now stale.
212
for trans_id in trans_ids:
213
old_path = self._limbo_files.pop(trans_id)
214
if trans_id not in self._new_contents:
216
new_path = self._limbo_name(trans_id)
217
os.rename(old_path, new_path)
219
def adjust_root_path(self, name, parent):
220
"""Emulate moving the root by moving all children, instead.
222
We do this by undoing the association of root's transaction id with the
223
current tree. This allows us to create a new directory with that
224
transaction id. We unversion the root directory and version the
225
physically new directory, and hope someone versions the tree root
228
old_root = self._new_root
229
old_root_file_id = self.final_file_id(old_root)
230
# force moving all children of root
231
for child_id in self.iter_tree_children(old_root):
232
if child_id != parent:
233
self.adjust_path(self.final_name(child_id),
234
self.final_parent(child_id), child_id)
235
file_id = self.final_file_id(child_id)
236
if file_id is not None:
237
self.unversion_file(child_id)
238
self.version_file(file_id, child_id)
240
# the physical root needs a new transaction id
241
self._tree_path_ids.pop("")
242
self._tree_id_paths.pop(old_root)
243
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
244
if parent == old_root:
245
parent = self._new_root
246
self.adjust_path(name, parent, old_root)
247
self.create_directory(old_root)
248
self.version_file(old_root_file_id, old_root)
249
self.unversion_file(self._new_root)
251
def trans_id_tree_file_id(self, inventory_id):
252
"""Determine the transaction id of a working tree file.
254
This reflects only files that already exist, not ones that will be
255
added by transactions.
257
path = self._tree.inventory.id2path(inventory_id)
258
return self.trans_id_tree_path(path)
260
def trans_id_file_id(self, file_id):
261
"""Determine or set the transaction id associated with a file ID.
262
A new id is only created for file_ids that were never present. If
263
a transaction has been unversioned, it is deliberately still returned.
264
(this will likely lead to an unversioned parent conflict.)
266
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
267
return self._r_new_id[file_id]
268
elif file_id in self._tree.inventory:
269
return self.trans_id_tree_file_id(file_id)
270
elif file_id in self._non_present_ids:
271
return self._non_present_ids[file_id]
273
trans_id = self._assign_id()
274
self._non_present_ids[file_id] = trans_id
277
def canonical_path(self, path):
278
"""Get the canonical tree-relative path"""
279
# don't follow final symlinks
280
abs = self._tree.abspath(path)
281
if abs in self._relpaths:
282
return self._relpaths[abs]
283
dirname, basename = os.path.split(abs)
284
if dirname not in self._realpaths:
285
self._realpaths[dirname] = os.path.realpath(dirname)
286
dirname = self._realpaths[dirname]
287
abs = pathjoin(dirname, basename)
288
if dirname in self._relpaths:
289
relpath = pathjoin(self._relpaths[dirname], basename)
290
relpath = relpath.rstrip('/\\')
292
relpath = self._tree.relpath(abs)
293
self._relpaths[abs] = relpath
296
def trans_id_tree_path(self, path):
297
"""Determine (and maybe set) the transaction ID for a tree path."""
298
path = self.canonical_path(path)
299
if path not in self._tree_path_ids:
300
self._tree_path_ids[path] = self._assign_id()
301
self._tree_id_paths[self._tree_path_ids[path]] = path
302
return self._tree_path_ids[path]
304
def get_tree_parent(self, trans_id):
305
"""Determine id of the parent in the tree."""
306
path = self._tree_id_paths[trans_id]
309
return self.trans_id_tree_path(os.path.dirname(path))
311
def create_file(self, contents, trans_id, mode_id=None):
312
"""Schedule creation of a new file.
316
Contents is an iterator of strings, all of which will be written
317
to the target destination.
319
New file takes the permissions of any existing file with that id,
320
unless mode_id is specified.
322
name = self._limbo_name(trans_id)
326
unique_add(self._new_contents, trans_id, 'file')
328
# Clean up the file, it never got registered so
329
# TreeTransform.finalize() won't clean it up.
334
f.writelines(contents)
337
self._set_mode(trans_id, mode_id, S_ISREG)
339
def _set_mode(self, trans_id, mode_id, typefunc):
340
"""Set the mode of new file contents.
341
The mode_id is the existing file to get the mode from (often the same
342
as trans_id). The operation is only performed if there's a mode match
343
according to typefunc.
348
old_path = self._tree_id_paths[mode_id]
352
mode = os.stat(self._tree.abspath(old_path)).st_mode
354
if e.errno == errno.ENOENT:
359
os.chmod(self._limbo_name(trans_id), mode)
361
def create_directory(self, trans_id):
362
"""Schedule creation of a new directory.
364
See also new_directory.
366
os.mkdir(self._limbo_name(trans_id))
367
unique_add(self._new_contents, trans_id, 'directory')
369
def create_symlink(self, target, trans_id):
370
"""Schedule creation of a new symbolic link.
372
target is a bytestring.
373
See also new_symlink.
375
os.symlink(target, self._limbo_name(trans_id))
376
unique_add(self._new_contents, trans_id, 'symlink')
378
def cancel_creation(self, trans_id):
379
"""Cancel the creation of new file contents."""
380
del self._new_contents[trans_id]
381
children = self._limbo_children.get(trans_id)
382
# if this is a limbo directory with children, move them before removing
384
if children is not None:
385
self._rename_in_limbo(children)
386
del self._limbo_children[trans_id]
387
del self._limbo_children_names[trans_id]
388
delete_any(self._limbo_name(trans_id))
390
def delete_contents(self, trans_id):
391
"""Schedule the contents of a path entry for deletion"""
392
self.tree_kind(trans_id)
393
self._removed_contents.add(trans_id)
395
def cancel_deletion(self, trans_id):
396
"""Cancel a scheduled deletion"""
397
self._removed_contents.remove(trans_id)
399
def unversion_file(self, trans_id):
400
"""Schedule a path entry to become unversioned"""
401
self._removed_id.add(trans_id)
403
def delete_versioned(self, trans_id):
404
"""Delete and unversion a versioned file"""
405
self.delete_contents(trans_id)
406
self.unversion_file(trans_id)
408
def set_executability(self, executability, trans_id):
409
"""Schedule setting of the 'execute' bit
410
To unschedule, set to None
412
if executability is None:
413
del self._new_executability[trans_id]
415
unique_add(self._new_executability, trans_id, executability)
417
def set_tree_reference(self, revision_id, trans_id):
418
"""Set the reference associated with a directory"""
419
unique_add(self._new_reference_revision, trans_id, revision_id)
421
def version_file(self, file_id, trans_id):
422
"""Schedule a file to become versioned."""
423
assert file_id is not None
424
unique_add(self._new_id, trans_id, file_id)
425
unique_add(self._r_new_id, file_id, trans_id)
427
def cancel_versioning(self, trans_id):
428
"""Undo a previous versioning of a file"""
429
file_id = self._new_id[trans_id]
430
del self._new_id[trans_id]
431
del self._r_new_id[file_id]
434
"""Determine the paths of all new and changed files"""
436
fp = FinalPaths(self)
437
for id_set in (self._new_name, self._new_parent, self._new_contents,
438
self._new_id, self._new_executability):
439
new_ids.update(id_set)
440
new_paths = [(fp.get_path(t), t) for t in new_ids]
444
def tree_kind(self, trans_id):
445
"""Determine the file kind in the working tree.
447
Raises NoSuchFile if the file does not exist
449
path = self._tree_id_paths.get(trans_id)
451
raise NoSuchFile(None)
453
return file_kind(self._tree.abspath(path))
455
if e.errno != errno.ENOENT:
458
raise NoSuchFile(path)
460
def final_kind(self, trans_id):
461
"""Determine the final file kind, after any changes applied.
463
Raises NoSuchFile if the file does not exist/has no contents.
464
(It is conceivable that a path would be created without the
465
corresponding contents insertion command)
467
if trans_id in self._new_contents:
468
return self._new_contents[trans_id]
469
elif trans_id in self._removed_contents:
470
raise NoSuchFile(None)
472
return self.tree_kind(trans_id)
474
def tree_file_id(self, trans_id):
475
"""Determine the file id associated with the trans_id in the tree"""
477
path = self._tree_id_paths[trans_id]
479
# the file is a new, unversioned file, or invalid trans_id
481
# the file is old; the old id is still valid
482
if self._new_root == trans_id:
483
return self._tree.inventory.root.file_id
484
return self._tree.inventory.path2id(path)
486
def final_file_id(self, trans_id):
487
"""Determine the file id after any changes are applied, or None.
489
None indicates that the file will not be versioned after changes are
493
# there is a new id for this file
494
assert self._new_id[trans_id] is not None
495
return self._new_id[trans_id]
497
if trans_id in self._removed_id:
499
return self.tree_file_id(trans_id)
501
def inactive_file_id(self, trans_id):
502
"""Return the inactive file_id associated with a transaction id.
503
That is, the one in the tree or in non_present_ids.
504
The file_id may actually be active, too.
506
file_id = self.tree_file_id(trans_id)
507
if file_id is not None:
509
for key, value in self._non_present_ids.iteritems():
510
if value == trans_id:
513
def final_parent(self, trans_id):
514
"""Determine the parent file_id, after any changes are applied.
516
ROOT_PARENT is returned for the tree root.
519
return self._new_parent[trans_id]
521
return self.get_tree_parent(trans_id)
523
def final_name(self, trans_id):
524
"""Determine the final filename, after all changes are applied."""
526
return self._new_name[trans_id]
529
return os.path.basename(self._tree_id_paths[trans_id])
531
raise NoFinalPath(trans_id, self)
534
"""Return a map of parent: children for known parents.
536
Only new paths and parents of tree files with assigned ids are used.
539
items = list(self._new_parent.iteritems())
540
items.extend((t, self.final_parent(t)) for t in
541
self._tree_id_paths.keys())
542
for trans_id, parent_id in items:
543
if parent_id not in by_parent:
544
by_parent[parent_id] = set()
545
by_parent[parent_id].add(trans_id)
548
def path_changed(self, trans_id):
549
"""Return True if a trans_id's path has changed."""
550
return (trans_id in self._new_name) or (trans_id in self._new_parent)
552
def new_contents(self, trans_id):
553
return (trans_id in self._new_contents)
555
def find_conflicts(self):
556
"""Find any violations of inventory or filesystem invariants"""
557
if self.__done is True:
558
raise ReusingTransform()
560
# ensure all children of all existent parents are known
561
# all children of non-existent parents are known, by definition.
562
self._add_tree_children()
563
by_parent = self.by_parent()
564
conflicts.extend(self._unversioned_parents(by_parent))
565
conflicts.extend(self._parent_loops())
566
conflicts.extend(self._duplicate_entries(by_parent))
567
conflicts.extend(self._duplicate_ids())
568
conflicts.extend(self._parent_type_conflicts(by_parent))
569
conflicts.extend(self._improper_versioning())
570
conflicts.extend(self._executability_conflicts())
571
conflicts.extend(self._overwrite_conflicts())
574
def _add_tree_children(self):
575
"""Add all the children of all active parents to the known paths.
577
Active parents are those which gain children, and those which are
578
removed. This is a necessary first step in detecting conflicts.
580
parents = self.by_parent().keys()
581
parents.extend([t for t in self._removed_contents if
582
self.tree_kind(t) == 'directory'])
583
for trans_id in self._removed_id:
584
file_id = self.tree_file_id(trans_id)
585
if self._tree.inventory[file_id].kind == 'directory':
586
parents.append(trans_id)
588
for parent_id in parents:
589
# ensure that all children are registered with the transaction
590
list(self.iter_tree_children(parent_id))
592
def iter_tree_children(self, parent_id):
593
"""Iterate through the entry's tree children, if any"""
595
path = self._tree_id_paths[parent_id]
599
children = os.listdir(self._tree.abspath(path))
601
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
605
for child in children:
606
childpath = joinpath(path, child)
607
if self._tree.is_control_filename(childpath):
609
yield self.trans_id_tree_path(childpath)
611
def has_named_child(self, by_parent, parent_id, name):
613
children = by_parent[parent_id]
616
for child in children:
617
if self.final_name(child) == name:
620
path = self._tree_id_paths[parent_id]
623
childpath = joinpath(path, name)
624
child_id = self._tree_path_ids.get(childpath)
626
return lexists(self._tree.abspath(childpath))
628
if self.final_parent(child_id) != parent_id:
630
if child_id in self._removed_contents:
631
# XXX What about dangling file-ids?
636
def _parent_loops(self):
637
"""No entry should be its own ancestor"""
639
for trans_id in self._new_parent:
642
while parent_id is not ROOT_PARENT:
645
parent_id = self.final_parent(parent_id)
648
if parent_id == trans_id:
649
conflicts.append(('parent loop', trans_id))
650
if parent_id in seen:
654
def _unversioned_parents(self, by_parent):
655
"""If parent directories are versioned, children must be versioned."""
657
for parent_id, children in by_parent.iteritems():
658
if parent_id is ROOT_PARENT:
660
if self.final_file_id(parent_id) is not None:
662
for child_id in children:
663
if self.final_file_id(child_id) is not None:
664
conflicts.append(('unversioned parent', parent_id))
668
def _improper_versioning(self):
669
"""Cannot version a file with no contents, or a bad type.
671
However, existing entries with no contents are okay.
674
for trans_id in self._new_id.iterkeys():
676
kind = self.final_kind(trans_id)
678
conflicts.append(('versioning no contents', trans_id))
680
if not InventoryEntry.versionable_kind(kind):
681
conflicts.append(('versioning bad kind', trans_id, kind))
684
def _executability_conflicts(self):
685
"""Check for bad executability changes.
687
Only versioned files may have their executability set, because
688
1. only versioned entries can have executability under windows
689
2. only files can be executable. (The execute bit on a directory
690
does not indicate searchability)
693
for trans_id in self._new_executability:
694
if self.final_file_id(trans_id) is None:
695
conflicts.append(('unversioned executability', trans_id))
698
non_file = self.final_kind(trans_id) != "file"
702
conflicts.append(('non-file executability', trans_id))
705
def _overwrite_conflicts(self):
706
"""Check for overwrites (not permitted on Win32)"""
708
for trans_id in self._new_contents:
710
self.tree_kind(trans_id)
713
if trans_id not in self._removed_contents:
714
conflicts.append(('overwrite', trans_id,
715
self.final_name(trans_id)))
718
def _duplicate_entries(self, by_parent):
719
"""No directory may have two entries with the same name."""
721
if (self._new_name, self._new_parent) == ({}, {}):
723
for children in by_parent.itervalues():
724
name_ids = [(self.final_name(t), t) for t in children]
728
for name, trans_id in name_ids:
730
kind = self.final_kind(trans_id)
733
file_id = self.final_file_id(trans_id)
734
if kind is None and file_id is None:
736
if name == last_name:
737
conflicts.append(('duplicate', last_trans_id, trans_id,
740
last_trans_id = trans_id
743
def _duplicate_ids(self):
744
"""Each inventory id may only be used once"""
746
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
748
active_tree_ids = set((f for f in self._tree.inventory if
749
f not in removed_tree_ids))
750
for trans_id, file_id in self._new_id.iteritems():
751
if file_id in active_tree_ids:
752
old_trans_id = self.trans_id_tree_file_id(file_id)
753
conflicts.append(('duplicate id', old_trans_id, trans_id))
756
def _parent_type_conflicts(self, by_parent):
757
"""parents must have directory 'contents'."""
759
for parent_id, children in by_parent.iteritems():
760
if parent_id is ROOT_PARENT:
762
if not self._any_contents(children):
764
for child in children:
766
self.final_kind(child)
770
kind = self.final_kind(parent_id)
774
conflicts.append(('missing parent', parent_id))
775
elif kind != "directory":
776
conflicts.append(('non-directory parent', parent_id))
779
def _any_contents(self, trans_ids):
780
"""Return true if any of the trans_ids, will have contents."""
781
for trans_id in trans_ids:
783
kind = self.final_kind(trans_id)
789
def apply(self, no_conflicts=False):
790
"""Apply all changes to the inventory and filesystem.
792
If filesystem or inventory conflicts are present, MalformedTransform
795
If apply succeeds, finalize is not necessary.
797
:param no_conflicts: if True, the caller guarantees there are no
798
conflicts, so no check is made.
801
conflicts = self.find_conflicts()
802
if len(conflicts) != 0:
803
raise MalformedTransform(conflicts=conflicts)
804
inv = self._tree.inventory
806
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
808
child_pb.update('Apply phase', 0, 2)
809
self._apply_removals(inv, inventory_delta)
810
child_pb.update('Apply phase', 1, 2)
811
modified_paths = self._apply_insertions(inv, inventory_delta)
814
self._tree.apply_inventory_delta(inventory_delta)
817
return _TransformResults(modified_paths, self.rename_count)
819
def _limbo_name(self, trans_id):
820
"""Generate the limbo name of a file"""
821
limbo_name = self._limbo_files.get(trans_id)
822
if limbo_name is not None:
824
parent = self._new_parent.get(trans_id)
825
# if the parent directory is already in limbo (e.g. when building a
826
# tree), choose a limbo name inside the parent, to reduce further
828
use_direct_path = False
829
if self._new_contents.get(parent) == 'directory':
830
filename = self._new_name.get(trans_id)
831
if filename is not None:
832
if parent not in self._limbo_children:
833
self._limbo_children[parent] = set()
834
self._limbo_children_names[parent] = {}
835
use_direct_path = True
836
# the direct path can only be used if no other file has
837
# already taken this pathname, i.e. if the name is unused, or
838
# if it is already associated with this trans_id.
839
elif (self._limbo_children_names[parent].get(filename)
840
in (trans_id, None)):
841
use_direct_path = True
843
limbo_name = pathjoin(self._limbo_files[parent], filename)
844
self._limbo_children[parent].add(trans_id)
845
self._limbo_children_names[parent][filename] = trans_id
847
limbo_name = pathjoin(self._limbodir, trans_id)
848
self._needs_rename.add(trans_id)
849
self._limbo_files[trans_id] = limbo_name
852
def _apply_removals(self, inv, inventory_delta):
853
"""Perform tree operations that remove directory/inventory names.
855
That is, delete files that are to be deleted, and put any files that
856
need renaming into limbo. This must be done in strict child-to-parent
859
tree_paths = list(self._tree_path_ids.iteritems())
860
tree_paths.sort(reverse=True)
861
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
863
for num, data in enumerate(tree_paths):
864
path, trans_id = data
865
child_pb.update('removing file', num, len(tree_paths))
866
full_path = self._tree.abspath(path)
867
if trans_id in self._removed_contents:
868
delete_any(full_path)
869
elif trans_id in self._new_name or trans_id in \
872
os.rename(full_path, self._limbo_name(trans_id))
874
if e.errno != errno.ENOENT:
877
self.rename_count += 1
878
if trans_id in self._removed_id:
879
if trans_id == self._new_root:
880
file_id = self._tree.inventory.root.file_id
882
file_id = self.tree_file_id(trans_id)
883
assert file_id is not None
884
inventory_delta.append((path, None, file_id, None))
888
def _apply_insertions(self, inv, inventory_delta):
889
"""Perform tree operations that insert directory/inventory names.
891
That is, create any files that need to be created, and restore from
892
limbo any files that needed renaming. This must be done in strict
893
parent-to-child order.
895
new_paths = self.new_paths()
897
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
899
for num, (path, trans_id) in enumerate(new_paths):
901
child_pb.update('adding file', num, len(new_paths))
903
kind = self._new_contents[trans_id]
905
kind = contents = None
906
if trans_id in self._new_contents or \
907
self.path_changed(trans_id):
908
full_path = self._tree.abspath(path)
909
if trans_id in self._needs_rename:
911
os.rename(self._limbo_name(trans_id), full_path)
913
# We may be renaming a dangling inventory id
914
if e.errno != errno.ENOENT:
917
self.rename_count += 1
918
if trans_id in self._new_contents:
919
modified_paths.append(full_path)
920
del self._new_contents[trans_id]
922
if trans_id in self._new_id:
924
kind = file_kind(self._tree.abspath(path))
925
if trans_id in self._new_reference_revision:
926
new_entry = inventory.TreeReference(
927
self._new_id[trans_id],
928
self._new_name[trans_id],
929
self.final_file_id(self._new_parent[trans_id]),
930
None, self._new_reference_revision[trans_id])
932
new_entry = inventory.make_entry(kind,
933
self.final_name(trans_id),
934
self.final_file_id(self.final_parent(trans_id)),
935
self._new_id[trans_id])
937
if trans_id in self._new_name or trans_id in\
939
trans_id in self._new_executability:
940
file_id = self.final_file_id(trans_id)
941
if file_id is not None:
943
new_entry = entry.copy()
945
if trans_id in self._new_name or trans_id in\
947
if new_entry is not None:
948
new_entry.name = self.final_name(trans_id)
949
parent = self.final_parent(trans_id)
950
parent_id = self.final_file_id(parent)
951
new_entry.parent_id = parent_id
953
if trans_id in self._new_executability:
954
self._set_executability(path, new_entry, trans_id)
955
if new_entry is not None:
956
if new_entry.file_id in inv:
957
old_path = inv.id2path(new_entry.file_id)
960
inventory_delta.append((old_path, path,
965
return modified_paths
967
def _set_executability(self, path, entry, trans_id):
968
"""Set the executability of versioned files """
969
new_executability = self._new_executability[trans_id]
970
entry.executable = new_executability
971
if supports_executable():
972
abspath = self._tree.abspath(path)
973
current_mode = os.stat(abspath).st_mode
974
if new_executability:
977
to_mode = current_mode | (0100 & ~umask)
978
# Enable x-bit for others only if they can read it.
979
if current_mode & 0004:
980
to_mode |= 0001 & ~umask
981
if current_mode & 0040:
982
to_mode |= 0010 & ~umask
984
to_mode = current_mode & ~0111
985
os.chmod(abspath, to_mode)
987
def _new_entry(self, name, parent_id, file_id):
988
"""Helper function to create a new filesystem entry."""
989
trans_id = self.create_path(name, parent_id)
990
if file_id is not None:
991
self.version_file(file_id, trans_id)
994
def new_file(self, name, parent_id, contents, file_id=None,
996
"""Convenience method to create files.
998
name is the name of the file to create.
999
parent_id is the transaction id of the parent directory of the file.
1000
contents is an iterator of bytestrings, which will be used to produce
1002
:param file_id: The inventory ID of the file, if it is to be versioned.
1003
:param executable: Only valid when a file_id has been supplied.
1005
trans_id = self._new_entry(name, parent_id, file_id)
1006
# TODO: rather than scheduling a set_executable call,
1007
# have create_file create the file with the right mode.
1008
self.create_file(contents, trans_id)
1009
if executable is not None:
1010
self.set_executability(executable, trans_id)
1013
def new_directory(self, name, parent_id, file_id=None):
1014
"""Convenience method to create directories.
1016
name is the name of the directory to create.
1017
parent_id is the transaction id of the parent directory of the
1019
file_id is the inventory ID of the directory, if it is to be versioned.
1021
trans_id = self._new_entry(name, parent_id, file_id)
1022
self.create_directory(trans_id)
1025
def new_symlink(self, name, parent_id, target, file_id=None):
1026
"""Convenience method to create symbolic link.
1028
name is the name of the symlink to create.
1029
parent_id is the transaction id of the parent directory of the symlink.
1030
target is a bytestring of the target of the symlink.
1031
file_id is the inventory ID of the file, if it is to be versioned.
1033
trans_id = self._new_entry(name, parent_id, file_id)
1034
self.create_symlink(target, trans_id)
1037
def _affected_ids(self):
1038
"""Return the set of transform ids affected by the transform"""
1039
trans_ids = set(self._removed_id)
1040
trans_ids.update(self._new_id.keys())
1041
trans_ids.update(self._removed_contents)
1042
trans_ids.update(self._new_contents.keys())
1043
trans_ids.update(self._new_executability.keys())
1044
trans_ids.update(self._new_name.keys())
1045
trans_ids.update(self._new_parent.keys())
1048
def _get_file_id_maps(self):
1049
"""Return mapping of file_ids to trans_ids in the to and from states"""
1050
trans_ids = self._affected_ids()
1053
# Build up two dicts: trans_ids associated with file ids in the
1054
# FROM state, vs the TO state.
1055
for trans_id in trans_ids:
1056
from_file_id = self.tree_file_id(trans_id)
1057
if from_file_id is not None:
1058
from_trans_ids[from_file_id] = trans_id
1059
to_file_id = self.final_file_id(trans_id)
1060
if to_file_id is not None:
1061
to_trans_ids[to_file_id] = trans_id
1062
return from_trans_ids, to_trans_ids
1064
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1065
"""Get data about a file in the from (tree) state
1067
Return a (name, parent, kind, executable) tuple
1069
from_path = self._tree_id_paths.get(from_trans_id)
1071
# get data from working tree if versioned
1072
from_entry = self._tree.inventory[file_id]
1073
from_name = from_entry.name
1074
from_parent = from_entry.parent_id
1077
if from_path is None:
1078
# File does not exist in FROM state
1082
# File exists, but is not versioned. Have to use path-
1084
from_name = os.path.basename(from_path)
1085
tree_parent = self.get_tree_parent(from_trans_id)
1086
from_parent = self.tree_file_id(tree_parent)
1087
if from_path is not None:
1088
from_kind, from_executable, from_stats = \
1089
self._tree._comparison_data(from_entry, from_path)
1092
from_executable = False
1093
return from_name, from_parent, from_kind, from_executable
1095
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1096
"""Get data about a file in the to (target) state
1098
Return a (name, parent, kind, executable) tuple
1100
to_name = self.final_name(to_trans_id)
1102
to_kind = self.final_kind(to_trans_id)
1105
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1106
if to_trans_id in self._new_executability:
1107
to_executable = self._new_executability[to_trans_id]
1108
elif to_trans_id == from_trans_id:
1109
to_executable = from_executable
1111
to_executable = False
1112
return to_name, to_parent, to_kind, to_executable
1114
def _iter_changes(self):
1115
"""Produce output in the same format as Tree._iter_changes.
1117
Will produce nonsensical results if invoked while inventory/filesystem
1118
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1120
This reads the Transform, but only reproduces changes involving a
1121
file_id. Files that are not versioned in either of the FROM or TO
1122
states are not reflected.
1124
final_paths = FinalPaths(self)
1125
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1127
# Now iterate through all active file_ids
1128
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1130
from_trans_id = from_trans_ids.get(file_id)
1131
# find file ids, and determine versioning state
1132
if from_trans_id is None:
1133
from_versioned = False
1134
from_trans_id = to_trans_ids[file_id]
1136
from_versioned = True
1137
to_trans_id = to_trans_ids.get(file_id)
1138
if to_trans_id is None:
1139
to_versioned = False
1140
to_trans_id = from_trans_id
1144
from_name, from_parent, from_kind, from_executable = \
1145
self._from_file_data(from_trans_id, from_versioned, file_id)
1147
to_name, to_parent, to_kind, to_executable = \
1148
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1150
if not from_versioned:
1153
from_path = self._tree_id_paths.get(from_trans_id)
1154
if not to_versioned:
1157
to_path = final_paths.get_path(to_trans_id)
1158
if from_kind != to_kind:
1160
elif to_kind in ('file', 'symlink') and (
1161
to_trans_id != from_trans_id or
1162
to_trans_id in self._new_contents):
1164
if (not modified and from_versioned == to_versioned and
1165
from_parent==to_parent and from_name == to_name and
1166
from_executable == to_executable):
1168
results.append((file_id, (from_path, to_path), modified,
1169
(from_versioned, to_versioned),
1170
(from_parent, to_parent),
1171
(from_name, to_name),
1172
(from_kind, to_kind),
1173
(from_executable, to_executable)))
1174
return iter(sorted(results, key=lambda x:x[1]))
1177
def joinpath(parent, child):
1178
"""Join tree-relative paths, handling the tree root specially"""
1179
if parent is None or parent == "":
1182
return pathjoin(parent, child)
1185
class FinalPaths(object):
1186
"""Make path calculation cheap by memoizing paths.
1188
The underlying tree must not be manipulated between calls, or else
1189
the results will likely be incorrect.
1191
def __init__(self, transform):
1192
object.__init__(self)
1193
self._known_paths = {}
1194
self.transform = transform
1196
def _determine_path(self, trans_id):
1197
if trans_id == self.transform.root:
1199
name = self.transform.final_name(trans_id)
1200
parent_id = self.transform.final_parent(trans_id)
1201
if parent_id == self.transform.root:
1204
return pathjoin(self.get_path(parent_id), name)
1206
def get_path(self, trans_id):
1207
"""Find the final path associated with a trans_id"""
1208
if trans_id not in self._known_paths:
1209
self._known_paths[trans_id] = self._determine_path(trans_id)
1210
return self._known_paths[trans_id]
1212
def topology_sorted_ids(tree):
1213
"""Determine the topological order of the ids in a tree"""
1214
file_ids = list(tree)
1215
file_ids.sort(key=tree.id2path)
1219
def build_tree(tree, wt):
1220
"""Create working tree for a branch, using a TreeTransform.
1222
This function should be used on empty trees, having a tree root at most.
1223
(see merge and revert functionality for working with existing trees)
1225
Existing files are handled like so:
1227
- Existing bzrdirs take precedence over creating new items. They are
1228
created as '%s.diverted' % name.
1229
- Otherwise, if the content on disk matches the content we are building,
1230
it is silently replaced.
1231
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1233
wt.lock_tree_write()
1237
return _build_tree(tree, wt)
1243
def _build_tree(tree, wt):
1244
"""See build_tree."""
1245
if len(wt.inventory) > 1: # more than just a root
1246
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1248
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1249
pp = ProgressPhase("Build phase", 2, top_pb)
1250
if tree.inventory.root is not None:
1251
# This is kind of a hack: we should be altering the root
1252
# as part of the regular tree shape diff logic.
1253
# The conditional test here is to avoid doing an
1254
# expensive operation (flush) every time the root id
1255
# is set within the tree, nor setting the root and thus
1256
# marking the tree as dirty, because we use two different
1257
# idioms here: tree interfaces and inventory interfaces.
1258
if wt.path2id('') != tree.inventory.root.file_id:
1259
wt.set_root_id(tree.inventory.root.file_id)
1261
tt = TreeTransform(wt)
1265
file_trans_id[wt.get_root_id()] = \
1266
tt.trans_id_tree_file_id(wt.get_root_id())
1267
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1269
for num, (tree_path, entry) in \
1270
enumerate(tree.inventory.iter_entries_by_dir()):
1271
pb.update("Building tree", num, len(tree.inventory))
1272
if entry.parent_id is None:
1275
file_id = entry.file_id
1276
target_path = wt.abspath(tree_path)
1278
kind = file_kind(target_path)
1282
if kind == "directory":
1284
bzrdir.BzrDir.open(target_path)
1285
except errors.NotBranchError:
1289
if (file_id not in divert and
1290
_content_match(tree, entry, file_id, kind,
1292
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1293
if kind == 'directory':
1295
if entry.parent_id not in file_trans_id:
1296
raise AssertionError(
1297
'entry %s parent id %r is not in file_trans_id %r'
1298
% (entry, entry.parent_id, file_trans_id))
1299
parent_id = file_trans_id[entry.parent_id]
1300
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1303
new_trans_id = file_trans_id[file_id]
1304
old_parent = tt.trans_id_tree_path(tree_path)
1305
_reparent_children(tt, old_parent, new_trans_id)
1309
divert_trans = set(file_trans_id[f] for f in divert)
1310
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1311
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1312
conflicts = cook_conflicts(raw_conflicts, tt)
1313
for conflict in conflicts:
1316
wt.add_conflicts(conflicts)
1317
except errors.UnsupportedOperation:
1326
def _reparent_children(tt, old_parent, new_parent):
1327
for child in tt.iter_tree_children(old_parent):
1328
tt.adjust_path(tt.final_name(child), new_parent, child)
1331
def _content_match(tree, entry, file_id, kind, target_path):
1332
if entry.kind != kind:
1334
if entry.kind == "directory":
1336
if entry.kind == "file":
1337
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1339
elif entry.kind == "symlink":
1340
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1345
def resolve_checkout(tt, conflicts, divert):
1346
new_conflicts = set()
1347
for c_type, conflict in ((c[0], c) for c in conflicts):
1348
# Anything but a 'duplicate' would indicate programmer error
1349
assert c_type == 'duplicate', c_type
1350
# Now figure out which is new and which is old
1351
if tt.new_contents(conflict[1]):
1352
new_file = conflict[1]
1353
old_file = conflict[2]
1355
new_file = conflict[2]
1356
old_file = conflict[1]
1358
# We should only get here if the conflict wasn't completely
1360
final_parent = tt.final_parent(old_file)
1361
if new_file in divert:
1362
new_name = tt.final_name(old_file)+'.diverted'
1363
tt.adjust_path(new_name, final_parent, new_file)
1364
new_conflicts.add((c_type, 'Diverted to',
1365
new_file, old_file))
1367
new_name = tt.final_name(old_file)+'.moved'
1368
tt.adjust_path(new_name, final_parent, old_file)
1369
new_conflicts.add((c_type, 'Moved existing file to',
1370
old_file, new_file))
1371
return new_conflicts
1374
def new_by_entry(tt, entry, parent_id, tree):
1375
"""Create a new file according to its inventory entry"""
1379
contents = tree.get_file(entry.file_id).readlines()
1380
executable = tree.is_executable(entry.file_id)
1381
return tt.new_file(name, parent_id, contents, entry.file_id,
1383
elif kind in ('directory', 'tree-reference'):
1384
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1385
if kind == 'tree-reference':
1386
tt.set_tree_reference(entry.reference_revision, trans_id)
1388
elif kind == 'symlink':
1389
target = tree.get_symlink_target(entry.file_id)
1390
return tt.new_symlink(name, parent_id, target, entry.file_id)
1392
raise errors.BadFileKindError(name, kind)
1394
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1395
"""Create new file contents according to an inventory entry."""
1396
if entry.kind == "file":
1398
lines = tree.get_file(entry.file_id).readlines()
1399
tt.create_file(lines, trans_id, mode_id=mode_id)
1400
elif entry.kind == "symlink":
1401
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1402
elif entry.kind == "directory":
1403
tt.create_directory(trans_id)
1405
def create_entry_executability(tt, entry, trans_id):
1406
"""Set the executability of a trans_id according to an inventory entry"""
1407
if entry.kind == "file":
1408
tt.set_executability(entry.executable, trans_id)
1411
@deprecated_function(zero_fifteen)
1412
def find_interesting(working_tree, target_tree, filenames):
1413
"""Find the ids corresponding to specified filenames.
1415
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1417
working_tree.lock_read()
1419
target_tree.lock_read()
1421
return working_tree.paths2ids(filenames, [target_tree])
1423
target_tree.unlock()
1425
working_tree.unlock()
1428
@deprecated_function(zero_nineteen)
1429
def change_entry(tt, file_id, working_tree, target_tree,
1430
trans_id_file_id, backups, trans_id, by_parent):
1431
"""Replace a file_id's contents with those from a target tree."""
1432
if file_id is None and target_tree is None:
1433
# skip the logic altogether in the deprecation test
1435
e_trans_id = trans_id_file_id(file_id)
1436
entry = target_tree.inventory[file_id]
1437
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1440
mode_id = e_trans_id
1443
tt.delete_contents(e_trans_id)
1445
parent_trans_id = trans_id_file_id(entry.parent_id)
1446
backup_name = get_backup_name(entry, by_parent,
1447
parent_trans_id, tt)
1448
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1449
tt.unversion_file(e_trans_id)
1450
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1451
tt.version_file(file_id, e_trans_id)
1452
trans_id[file_id] = e_trans_id
1453
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1454
create_entry_executability(tt, entry, e_trans_id)
1457
tt.set_executability(entry.executable, e_trans_id)
1458
if tt.final_name(e_trans_id) != entry.name:
1461
parent_id = tt.final_parent(e_trans_id)
1462
parent_file_id = tt.final_file_id(parent_id)
1463
if parent_file_id != entry.parent_id:
1468
parent_trans_id = trans_id_file_id(entry.parent_id)
1469
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1472
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1473
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1476
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1477
"""Produce a backup-style name that appears to be available"""
1481
yield "%s.~%d~" % (name, counter)
1483
for new_name in name_gen():
1484
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1488
def _entry_changes(file_id, entry, working_tree):
1489
"""Determine in which ways the inventory entry has changed.
1491
Returns booleans: has_contents, content_mod, meta_mod
1492
has_contents means there are currently contents, but they differ
1493
contents_mod means contents need to be modified
1494
meta_mod means the metadata needs to be modified
1496
cur_entry = working_tree.inventory[file_id]
1498
working_kind = working_tree.kind(file_id)
1501
has_contents = False
1504
if has_contents is True:
1505
if entry.kind != working_kind:
1506
contents_mod, meta_mod = True, False
1508
cur_entry._read_tree_state(working_tree.id2path(file_id),
1510
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1511
cur_entry._forget_tree_state()
1512
return has_contents, contents_mod, meta_mod
1515
def revert(working_tree, target_tree, filenames, backups=False,
1516
pb=DummyProgress(), change_reporter=None):
1517
"""Revert a working tree's contents to those of a target tree."""
1518
target_tree.lock_read()
1519
tt = TreeTransform(working_tree, pb)
1521
pp = ProgressPhase("Revert phase", 3, pb)
1523
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1525
merge_modified = _alter_files(working_tree, target_tree, tt,
1526
child_pb, filenames, backups)
1530
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1532
raw_conflicts = resolve_conflicts(tt, child_pb)
1535
conflicts = cook_conflicts(raw_conflicts, tt)
1537
change_reporter = delta._ChangeReporter(
1538
unversioned_filter=working_tree.is_ignored)
1539
delta.report_changes(tt._iter_changes(), change_reporter)
1540
for conflict in conflicts:
1544
working_tree.set_merge_modified(merge_modified)
1546
target_tree.unlock()
1552
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1554
merge_modified = working_tree.merge_modified()
1555
change_list = target_tree._iter_changes(working_tree,
1556
specific_files=specific_files, pb=pb)
1557
if target_tree.inventory.root is None:
1563
for id_num, (file_id, path, changed_content, versioned, parent, name,
1564
kind, executable) in enumerate(change_list):
1565
if skip_root and file_id[0] is not None and parent[0] is None:
1567
trans_id = tt.trans_id_file_id(file_id)
1570
keep_content = False
1571
if kind[0] == 'file' and (backups or kind[1] is None):
1572
wt_sha1 = working_tree.get_file_sha1(file_id)
1573
if merge_modified.get(file_id) != wt_sha1:
1574
# acquire the basis tree lazily to prevent the
1575
# expense of accessing it when it's not needed ?
1576
# (Guessing, RBC, 200702)
1577
if basis_tree is None:
1578
basis_tree = working_tree.basis_tree()
1579
basis_tree.lock_read()
1580
if file_id in basis_tree:
1581
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1583
elif kind[1] is None and not versioned[1]:
1585
if kind[0] is not None:
1586
if not keep_content:
1587
tt.delete_contents(trans_id)
1588
elif kind[1] is not None:
1589
parent_trans_id = tt.trans_id_file_id(parent[0])
1590
by_parent = tt.by_parent()
1591
backup_name = _get_backup_name(name[0], by_parent,
1592
parent_trans_id, tt)
1593
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1594
new_trans_id = tt.create_path(name[0], parent_trans_id)
1595
if versioned == (True, True):
1596
tt.unversion_file(trans_id)
1597
tt.version_file(file_id, new_trans_id)
1598
# New contents should have the same unix perms as old
1601
trans_id = new_trans_id
1602
if kind[1] == 'directory':
1603
tt.create_directory(trans_id)
1604
elif kind[1] == 'symlink':
1605
tt.create_symlink(target_tree.get_symlink_target(file_id),
1607
elif kind[1] == 'file':
1608
tt.create_file(target_tree.get_file_lines(file_id),
1610
if basis_tree is None:
1611
basis_tree = working_tree.basis_tree()
1612
basis_tree.lock_read()
1613
new_sha1 = target_tree.get_file_sha1(file_id)
1614
if (file_id in basis_tree and new_sha1 ==
1615
basis_tree.get_file_sha1(file_id)):
1616
if file_id in merge_modified:
1617
del merge_modified[file_id]
1619
merge_modified[file_id] = new_sha1
1621
# preserve the execute bit when backing up
1622
if keep_content and executable[0] == executable[1]:
1623
tt.set_executability(executable[1], trans_id)
1625
assert kind[1] is None
1626
if versioned == (False, True):
1627
tt.version_file(file_id, trans_id)
1628
if versioned == (True, False):
1629
tt.unversion_file(trans_id)
1630
if (name[1] is not None and
1631
(name[0] != name[1] or parent[0] != parent[1])):
1633
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1634
if executable[0] != executable[1] and kind[1] == "file":
1635
tt.set_executability(executable[1], trans_id)
1637
if basis_tree is not None:
1639
return merge_modified
1642
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1643
"""Make many conflict-resolution attempts, but die if they fail"""
1644
if pass_func is None:
1645
pass_func = conflict_pass
1646
new_conflicts = set()
1649
pb.update('Resolution pass', n+1, 10)
1650
conflicts = tt.find_conflicts()
1651
if len(conflicts) == 0:
1652
return new_conflicts
1653
new_conflicts.update(pass_func(tt, conflicts))
1654
raise MalformedTransform(conflicts=conflicts)
1659
def conflict_pass(tt, conflicts, path_tree=None):
1660
"""Resolve some classes of conflicts.
1662
:param tt: The transform to resolve conflicts in
1663
:param conflicts: The conflicts to resolve
1664
:param path_tree: A Tree to get supplemental paths from
1666
new_conflicts = set()
1667
for c_type, conflict in ((c[0], c) for c in conflicts):
1668
if c_type == 'duplicate id':
1669
tt.unversion_file(conflict[1])
1670
new_conflicts.add((c_type, 'Unversioned existing file',
1671
conflict[1], conflict[2], ))
1672
elif c_type == 'duplicate':
1673
# files that were renamed take precedence
1674
new_name = tt.final_name(conflict[1])+'.moved'
1675
final_parent = tt.final_parent(conflict[1])
1676
if tt.path_changed(conflict[1]):
1677
tt.adjust_path(new_name, final_parent, conflict[2])
1678
new_conflicts.add((c_type, 'Moved existing file to',
1679
conflict[2], conflict[1]))
1681
tt.adjust_path(new_name, final_parent, conflict[1])
1682
new_conflicts.add((c_type, 'Moved existing file to',
1683
conflict[1], conflict[2]))
1684
elif c_type == 'parent loop':
1685
# break the loop by undoing one of the ops that caused the loop
1687
while not tt.path_changed(cur):
1688
cur = tt.final_parent(cur)
1689
new_conflicts.add((c_type, 'Cancelled move', cur,
1690
tt.final_parent(cur),))
1691
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1693
elif c_type == 'missing parent':
1694
trans_id = conflict[1]
1696
tt.cancel_deletion(trans_id)
1697
new_conflicts.add(('deleting parent', 'Not deleting',
1700
tt.create_directory(trans_id)
1701
new_conflicts.add((c_type, 'Created directory', trans_id))
1703
tt.final_name(trans_id)
1705
file_id = tt.final_file_id(trans_id)
1706
entry = path_tree.inventory[file_id]
1707
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1708
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1709
elif c_type == 'unversioned parent':
1710
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1711
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1712
return new_conflicts
1715
def cook_conflicts(raw_conflicts, tt):
1716
"""Generate a list of cooked conflicts, sorted by file path"""
1717
from bzrlib.conflicts import Conflict
1718
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1719
return sorted(conflict_iter, key=Conflict.sort_key)
1722
def iter_cook_conflicts(raw_conflicts, tt):
1723
from bzrlib.conflicts import Conflict
1725
for conflict in raw_conflicts:
1726
c_type = conflict[0]
1727
action = conflict[1]
1728
modified_path = fp.get_path(conflict[2])
1729
modified_id = tt.final_file_id(conflict[2])
1730
if len(conflict) == 3:
1731
yield Conflict.factory(c_type, action=action, path=modified_path,
1732
file_id=modified_id)
1735
conflicting_path = fp.get_path(conflict[3])
1736
conflicting_id = tt.final_file_id(conflict[3])
1737
yield Conflict.factory(c_type, action=action, path=modified_path,
1738
file_id=modified_id,
1739
conflict_path=conflicting_path,
1740
conflict_file_id=conflicting_id)