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 (
42
from bzrlib.trace import mutter, warning
43
from bzrlib import tree
45
import bzrlib.urlutils as urlutils
48
ROOT_PARENT = "root-parent"
51
def unique_add(map, key, value):
53
raise DuplicateKey(key=key)
57
class _TransformResults(object):
58
def __init__(self, modified_paths, rename_count):
60
self.modified_paths = modified_paths
61
self.rename_count = rename_count
64
class TreeTransform(object):
65
"""Represent a tree transformation.
67
This object is designed to support incremental generation of the transform,
70
However, it gives optimum performance when parent directories are created
71
before their contents. The transform is then able to put child files
72
directly in their parent directory, avoiding later renames.
74
It is easy to produce malformed transforms, but they are generally
75
harmless. Attempting to apply a malformed transform will cause an
76
exception to be raised before any modifications are made to the tree.
78
Many kinds of malformed transforms can be corrected with the
79
resolve_conflicts function. The remaining ones indicate programming error,
80
such as trying to create a file with no path.
82
Two sets of file creation methods are supplied. Convenience methods are:
87
These are composed of the low-level methods:
89
* create_file or create_directory or create_symlink
93
def __init__(self, tree, pb=DummyProgress()):
94
"""Note: a tree_write lock is taken on the tree.
96
Use TreeTransform.finalize() to release the lock (can be omitted if
97
TreeTransform.apply() called).
101
self._tree.lock_tree_write()
103
control_files = self._tree._control_files
104
self._limbodir = urlutils.local_path_from_url(
105
control_files.controlfilename('limbo'))
107
os.mkdir(self._limbodir)
109
if e.errno == errno.EEXIST:
110
raise ExistingLimbo(self._limbodir)
111
self._deletiondir = urlutils.local_path_from_url(
112
control_files.controlfilename('pending-deletion'))
114
os.mkdir(self._deletiondir)
116
if e.errno == errno.EEXIST:
117
raise errors.ExistingPendingDeletion(self._deletiondir)
125
self._new_parent = {}
126
self._new_contents = {}
127
# A mapping of transform ids to their limbo filename
128
self._limbo_files = {}
129
# A mapping of transform ids to a set of the transform ids of children
130
# that their limbo directory has
131
self._limbo_children = {}
132
# Map transform ids to maps of child filename to child transform id
133
self._limbo_children_names = {}
134
# List of transform ids that need to be renamed from limbo into place
135
self._needs_rename = set()
136
self._removed_contents = set()
137
self._new_executability = {}
138
self._new_reference_revision = {}
140
self._non_present_ids = {}
142
self._removed_id = set()
143
self._tree_path_ids = {}
144
self._tree_id_paths = {}
145
# Cache of realpath results, to speed up canonical_path
147
# Cache of relpath results, to speed up canonical_path
149
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
152
self.rename_count = 0
154
def __get_root(self):
155
return self._new_root
157
root = property(__get_root)
160
"""Release the working tree lock, if held, clean up limbo dir.
162
This is required if apply has not been invoked, but can be invoked
165
if self._tree is None:
168
entries = [(self._limbo_name(t), t, k) for t, k in
169
self._new_contents.iteritems()]
170
entries.sort(reverse=True)
171
for path, trans_id, kind in entries:
172
if kind == "directory":
177
os.rmdir(self._limbodir)
179
# We don't especially care *why* the dir is immortal.
180
raise ImmortalLimbo(self._limbodir)
182
os.rmdir(self._deletiondir)
184
raise errors.ImmortalPendingDeletion(self._deletiondir)
189
def _assign_id(self):
190
"""Produce a new tranform id"""
191
new_id = "new-%s" % self._id_number
195
def create_path(self, name, parent):
196
"""Assign a transaction id to a new path"""
197
trans_id = self._assign_id()
198
unique_add(self._new_name, trans_id, name)
199
unique_add(self._new_parent, trans_id, parent)
202
def adjust_path(self, name, parent, trans_id):
203
"""Change the path that is assigned to a transaction id."""
204
if trans_id == self._new_root:
206
previous_parent = self._new_parent.get(trans_id)
207
previous_name = self._new_name.get(trans_id)
208
self._new_name[trans_id] = name
209
self._new_parent[trans_id] = parent
210
if (trans_id in self._limbo_files and
211
trans_id not in self._needs_rename):
212
self._rename_in_limbo([trans_id])
213
self._limbo_children[previous_parent].remove(trans_id)
214
del self._limbo_children_names[previous_parent][previous_name]
216
def _rename_in_limbo(self, trans_ids):
217
"""Fix limbo names so that the right final path is produced.
219
This means we outsmarted ourselves-- we tried to avoid renaming
220
these files later by creating them with their final names in their
221
final parents. But now the previous name or parent is no longer
222
suitable, so we have to rename them.
224
Even for trans_ids that have no new contents, we must remove their
225
entries from _limbo_files, because they are now stale.
227
for trans_id in trans_ids:
228
old_path = self._limbo_files.pop(trans_id)
229
if trans_id not in self._new_contents:
231
new_path = self._limbo_name(trans_id)
232
os.rename(old_path, new_path)
234
def adjust_root_path(self, name, parent):
235
"""Emulate moving the root by moving all children, instead.
237
We do this by undoing the association of root's transaction id with the
238
current tree. This allows us to create a new directory with that
239
transaction id. We unversion the root directory and version the
240
physically new directory, and hope someone versions the tree root
243
old_root = self._new_root
244
old_root_file_id = self.final_file_id(old_root)
245
# force moving all children of root
246
for child_id in self.iter_tree_children(old_root):
247
if child_id != parent:
248
self.adjust_path(self.final_name(child_id),
249
self.final_parent(child_id), child_id)
250
file_id = self.final_file_id(child_id)
251
if file_id is not None:
252
self.unversion_file(child_id)
253
self.version_file(file_id, child_id)
255
# the physical root needs a new transaction id
256
self._tree_path_ids.pop("")
257
self._tree_id_paths.pop(old_root)
258
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
259
if parent == old_root:
260
parent = self._new_root
261
self.adjust_path(name, parent, old_root)
262
self.create_directory(old_root)
263
self.version_file(old_root_file_id, old_root)
264
self.unversion_file(self._new_root)
266
def trans_id_tree_file_id(self, inventory_id):
267
"""Determine the transaction id of a working tree file.
269
This reflects only files that already exist, not ones that will be
270
added by transactions.
272
path = self._tree.inventory.id2path(inventory_id)
273
return self.trans_id_tree_path(path)
275
def trans_id_file_id(self, file_id):
276
"""Determine or set the transaction id associated with a file ID.
277
A new id is only created for file_ids that were never present. If
278
a transaction has been unversioned, it is deliberately still returned.
279
(this will likely lead to an unversioned parent conflict.)
281
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
282
return self._r_new_id[file_id]
283
elif file_id in self._tree.inventory:
284
return self.trans_id_tree_file_id(file_id)
285
elif file_id in self._non_present_ids:
286
return self._non_present_ids[file_id]
288
trans_id = self._assign_id()
289
self._non_present_ids[file_id] = trans_id
292
def canonical_path(self, path):
293
"""Get the canonical tree-relative path"""
294
# don't follow final symlinks
295
abs = self._tree.abspath(path)
296
if abs in self._relpaths:
297
return self._relpaths[abs]
298
dirname, basename = os.path.split(abs)
299
if dirname not in self._realpaths:
300
self._realpaths[dirname] = os.path.realpath(dirname)
301
dirname = self._realpaths[dirname]
302
abs = pathjoin(dirname, basename)
303
if dirname in self._relpaths:
304
relpath = pathjoin(self._relpaths[dirname], basename)
305
relpath = relpath.rstrip('/\\')
307
relpath = self._tree.relpath(abs)
308
self._relpaths[abs] = relpath
311
def trans_id_tree_path(self, path):
312
"""Determine (and maybe set) the transaction ID for a tree path."""
313
path = self.canonical_path(path)
314
if path not in self._tree_path_ids:
315
self._tree_path_ids[path] = self._assign_id()
316
self._tree_id_paths[self._tree_path_ids[path]] = path
317
return self._tree_path_ids[path]
319
def get_tree_parent(self, trans_id):
320
"""Determine id of the parent in the tree."""
321
path = self._tree_id_paths[trans_id]
324
return self.trans_id_tree_path(os.path.dirname(path))
326
def create_file(self, contents, trans_id, mode_id=None):
327
"""Schedule creation of a new file.
331
Contents is an iterator of strings, all of which will be written
332
to the target destination.
334
New file takes the permissions of any existing file with that id,
335
unless mode_id is specified.
337
name = self._limbo_name(trans_id)
341
unique_add(self._new_contents, trans_id, 'file')
343
# Clean up the file, it never got registered so
344
# TreeTransform.finalize() won't clean it up.
349
f.writelines(contents)
352
self._set_mode(trans_id, mode_id, S_ISREG)
354
def _set_mode(self, trans_id, mode_id, typefunc):
355
"""Set the mode of new file contents.
356
The mode_id is the existing file to get the mode from (often the same
357
as trans_id). The operation is only performed if there's a mode match
358
according to typefunc.
363
old_path = self._tree_id_paths[mode_id]
367
mode = os.stat(self._tree.abspath(old_path)).st_mode
369
if e.errno == errno.ENOENT:
374
os.chmod(self._limbo_name(trans_id), mode)
376
def create_directory(self, trans_id):
377
"""Schedule creation of a new directory.
379
See also new_directory.
381
os.mkdir(self._limbo_name(trans_id))
382
unique_add(self._new_contents, trans_id, 'directory')
384
def create_symlink(self, target, trans_id):
385
"""Schedule creation of a new symbolic link.
387
target is a bytestring.
388
See also new_symlink.
390
os.symlink(target, self._limbo_name(trans_id))
391
unique_add(self._new_contents, trans_id, 'symlink')
393
def cancel_creation(self, trans_id):
394
"""Cancel the creation of new file contents."""
395
del self._new_contents[trans_id]
396
children = self._limbo_children.get(trans_id)
397
# if this is a limbo directory with children, move them before removing
399
if children is not None:
400
self._rename_in_limbo(children)
401
del self._limbo_children[trans_id]
402
del self._limbo_children_names[trans_id]
403
delete_any(self._limbo_name(trans_id))
405
def delete_contents(self, trans_id):
406
"""Schedule the contents of a path entry for deletion"""
407
self.tree_kind(trans_id)
408
self._removed_contents.add(trans_id)
410
def cancel_deletion(self, trans_id):
411
"""Cancel a scheduled deletion"""
412
self._removed_contents.remove(trans_id)
414
def unversion_file(self, trans_id):
415
"""Schedule a path entry to become unversioned"""
416
self._removed_id.add(trans_id)
418
def delete_versioned(self, trans_id):
419
"""Delete and unversion a versioned file"""
420
self.delete_contents(trans_id)
421
self.unversion_file(trans_id)
423
def set_executability(self, executability, trans_id):
424
"""Schedule setting of the 'execute' bit
425
To unschedule, set to None
427
if executability is None:
428
del self._new_executability[trans_id]
430
unique_add(self._new_executability, trans_id, executability)
432
def set_tree_reference(self, revision_id, trans_id):
433
"""Set the reference associated with a directory"""
434
unique_add(self._new_reference_revision, trans_id, revision_id)
436
def version_file(self, file_id, trans_id):
437
"""Schedule a file to become versioned."""
438
assert file_id is not None
439
unique_add(self._new_id, trans_id, file_id)
440
unique_add(self._r_new_id, file_id, trans_id)
442
def cancel_versioning(self, trans_id):
443
"""Undo a previous versioning of a file"""
444
file_id = self._new_id[trans_id]
445
del self._new_id[trans_id]
446
del self._r_new_id[file_id]
449
"""Determine the paths of all new and changed files"""
451
fp = FinalPaths(self)
452
for id_set in (self._new_name, self._new_parent, self._new_contents,
453
self._new_id, self._new_executability):
454
new_ids.update(id_set)
455
new_paths = [(fp.get_path(t), t) for t in new_ids]
459
def tree_kind(self, trans_id):
460
"""Determine the file kind in the working tree.
462
Raises NoSuchFile if the file does not exist
464
path = self._tree_id_paths.get(trans_id)
466
raise NoSuchFile(None)
468
return file_kind(self._tree.abspath(path))
470
if e.errno != errno.ENOENT:
473
raise NoSuchFile(path)
475
def final_kind(self, trans_id):
476
"""Determine the final file kind, after any changes applied.
478
Raises NoSuchFile if the file does not exist/has no contents.
479
(It is conceivable that a path would be created without the
480
corresponding contents insertion command)
482
if trans_id in self._new_contents:
483
return self._new_contents[trans_id]
484
elif trans_id in self._removed_contents:
485
raise NoSuchFile(None)
487
return self.tree_kind(trans_id)
489
def tree_file_id(self, trans_id):
490
"""Determine the file id associated with the trans_id in the tree"""
492
path = self._tree_id_paths[trans_id]
494
# the file is a new, unversioned file, or invalid trans_id
496
# the file is old; the old id is still valid
497
if self._new_root == trans_id:
498
return self._tree.inventory.root.file_id
499
return self._tree.inventory.path2id(path)
501
def final_file_id(self, trans_id):
502
"""Determine the file id after any changes are applied, or None.
504
None indicates that the file will not be versioned after changes are
508
# there is a new id for this file
509
assert self._new_id[trans_id] is not None
510
return self._new_id[trans_id]
512
if trans_id in self._removed_id:
514
return self.tree_file_id(trans_id)
516
def inactive_file_id(self, trans_id):
517
"""Return the inactive file_id associated with a transaction id.
518
That is, the one in the tree or in non_present_ids.
519
The file_id may actually be active, too.
521
file_id = self.tree_file_id(trans_id)
522
if file_id is not None:
524
for key, value in self._non_present_ids.iteritems():
525
if value == trans_id:
528
def final_parent(self, trans_id):
529
"""Determine the parent file_id, after any changes are applied.
531
ROOT_PARENT is returned for the tree root.
534
return self._new_parent[trans_id]
536
return self.get_tree_parent(trans_id)
538
def final_name(self, trans_id):
539
"""Determine the final filename, after all changes are applied."""
541
return self._new_name[trans_id]
544
return os.path.basename(self._tree_id_paths[trans_id])
546
raise NoFinalPath(trans_id, self)
549
"""Return a map of parent: children for known parents.
551
Only new paths and parents of tree files with assigned ids are used.
554
items = list(self._new_parent.iteritems())
555
items.extend((t, self.final_parent(t)) for t in
556
self._tree_id_paths.keys())
557
for trans_id, parent_id in items:
558
if parent_id not in by_parent:
559
by_parent[parent_id] = set()
560
by_parent[parent_id].add(trans_id)
563
def path_changed(self, trans_id):
564
"""Return True if a trans_id's path has changed."""
565
return (trans_id in self._new_name) or (trans_id in self._new_parent)
567
def new_contents(self, trans_id):
568
return (trans_id in self._new_contents)
570
def find_conflicts(self):
571
"""Find any violations of inventory or filesystem invariants"""
572
if self.__done is True:
573
raise ReusingTransform()
575
# ensure all children of all existent parents are known
576
# all children of non-existent parents are known, by definition.
577
self._add_tree_children()
578
by_parent = self.by_parent()
579
conflicts.extend(self._unversioned_parents(by_parent))
580
conflicts.extend(self._parent_loops())
581
conflicts.extend(self._duplicate_entries(by_parent))
582
conflicts.extend(self._duplicate_ids())
583
conflicts.extend(self._parent_type_conflicts(by_parent))
584
conflicts.extend(self._improper_versioning())
585
conflicts.extend(self._executability_conflicts())
586
conflicts.extend(self._overwrite_conflicts())
589
def _add_tree_children(self):
590
"""Add all the children of all active parents to the known paths.
592
Active parents are those which gain children, and those which are
593
removed. This is a necessary first step in detecting conflicts.
595
parents = self.by_parent().keys()
596
parents.extend([t for t in self._removed_contents if
597
self.tree_kind(t) == 'directory'])
598
for trans_id in self._removed_id:
599
file_id = self.tree_file_id(trans_id)
600
if self._tree.inventory[file_id].kind == 'directory':
601
parents.append(trans_id)
603
for parent_id in parents:
604
# ensure that all children are registered with the transaction
605
list(self.iter_tree_children(parent_id))
607
def iter_tree_children(self, parent_id):
608
"""Iterate through the entry's tree children, if any"""
610
path = self._tree_id_paths[parent_id]
614
children = os.listdir(self._tree.abspath(path))
616
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
620
for child in children:
621
childpath = joinpath(path, child)
622
if self._tree.is_control_filename(childpath):
624
yield self.trans_id_tree_path(childpath)
626
def has_named_child(self, by_parent, parent_id, name):
628
children = by_parent[parent_id]
631
for child in children:
632
if self.final_name(child) == name:
635
path = self._tree_id_paths[parent_id]
638
childpath = joinpath(path, name)
639
child_id = self._tree_path_ids.get(childpath)
641
return lexists(self._tree.abspath(childpath))
643
if self.final_parent(child_id) != parent_id:
645
if child_id in self._removed_contents:
646
# XXX What about dangling file-ids?
651
def _parent_loops(self):
652
"""No entry should be its own ancestor"""
654
for trans_id in self._new_parent:
657
while parent_id is not ROOT_PARENT:
660
parent_id = self.final_parent(parent_id)
663
if parent_id == trans_id:
664
conflicts.append(('parent loop', trans_id))
665
if parent_id in seen:
669
def _unversioned_parents(self, by_parent):
670
"""If parent directories are versioned, children must be versioned."""
672
for parent_id, children in by_parent.iteritems():
673
if parent_id is ROOT_PARENT:
675
if self.final_file_id(parent_id) is not None:
677
for child_id in children:
678
if self.final_file_id(child_id) is not None:
679
conflicts.append(('unversioned parent', parent_id))
683
def _improper_versioning(self):
684
"""Cannot version a file with no contents, or a bad type.
686
However, existing entries with no contents are okay.
689
for trans_id in self._new_id.iterkeys():
691
kind = self.final_kind(trans_id)
693
conflicts.append(('versioning no contents', trans_id))
695
if not InventoryEntry.versionable_kind(kind):
696
conflicts.append(('versioning bad kind', trans_id, kind))
699
def _executability_conflicts(self):
700
"""Check for bad executability changes.
702
Only versioned files may have their executability set, because
703
1. only versioned entries can have executability under windows
704
2. only files can be executable. (The execute bit on a directory
705
does not indicate searchability)
708
for trans_id in self._new_executability:
709
if self.final_file_id(trans_id) is None:
710
conflicts.append(('unversioned executability', trans_id))
713
non_file = self.final_kind(trans_id) != "file"
717
conflicts.append(('non-file executability', trans_id))
720
def _overwrite_conflicts(self):
721
"""Check for overwrites (not permitted on Win32)"""
723
for trans_id in self._new_contents:
725
self.tree_kind(trans_id)
728
if trans_id not in self._removed_contents:
729
conflicts.append(('overwrite', trans_id,
730
self.final_name(trans_id)))
733
def _duplicate_entries(self, by_parent):
734
"""No directory may have two entries with the same name."""
736
if (self._new_name, self._new_parent) == ({}, {}):
738
for children in by_parent.itervalues():
739
name_ids = [(self.final_name(t), t) for t in children]
743
for name, trans_id in name_ids:
745
kind = self.final_kind(trans_id)
748
file_id = self.final_file_id(trans_id)
749
if kind is None and file_id is None:
751
if name == last_name:
752
conflicts.append(('duplicate', last_trans_id, trans_id,
755
last_trans_id = trans_id
758
def _duplicate_ids(self):
759
"""Each inventory id may only be used once"""
761
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
763
active_tree_ids = set((f for f in self._tree.inventory if
764
f not in removed_tree_ids))
765
for trans_id, file_id in self._new_id.iteritems():
766
if file_id in active_tree_ids:
767
old_trans_id = self.trans_id_tree_file_id(file_id)
768
conflicts.append(('duplicate id', old_trans_id, trans_id))
771
def _parent_type_conflicts(self, by_parent):
772
"""parents must have directory 'contents'."""
774
for parent_id, children in by_parent.iteritems():
775
if parent_id is ROOT_PARENT:
777
if not self._any_contents(children):
779
for child in children:
781
self.final_kind(child)
785
kind = self.final_kind(parent_id)
789
conflicts.append(('missing parent', parent_id))
790
elif kind != "directory":
791
conflicts.append(('non-directory parent', parent_id))
794
def _any_contents(self, trans_ids):
795
"""Return true if any of the trans_ids, will have contents."""
796
for trans_id in trans_ids:
798
kind = self.final_kind(trans_id)
804
def apply(self, no_conflicts=False, _mover=None):
805
"""Apply all changes to the inventory and filesystem.
807
If filesystem or inventory conflicts are present, MalformedTransform
810
If apply succeeds, finalize is not necessary.
812
:param no_conflicts: if True, the caller guarantees there are no
813
conflicts, so no check is made.
814
:param _mover: Supply an alternate FileMover, for testing
817
conflicts = self.find_conflicts()
818
if len(conflicts) != 0:
819
raise MalformedTransform(conflicts=conflicts)
820
inv = self._tree.inventory
822
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
829
child_pb.update('Apply phase', 0, 2)
830
self._apply_removals(inv, inventory_delta, mover)
831
child_pb.update('Apply phase', 1, 2)
832
modified_paths = self._apply_insertions(inv, inventory_delta,
838
mover.apply_deletions()
841
self._tree.apply_inventory_delta(inventory_delta)
844
return _TransformResults(modified_paths, self.rename_count)
846
def _limbo_name(self, trans_id):
847
"""Generate the limbo name of a file"""
848
limbo_name = self._limbo_files.get(trans_id)
849
if limbo_name is not None:
851
parent = self._new_parent.get(trans_id)
852
# if the parent directory is already in limbo (e.g. when building a
853
# tree), choose a limbo name inside the parent, to reduce further
855
use_direct_path = False
856
if self._new_contents.get(parent) == 'directory':
857
filename = self._new_name.get(trans_id)
858
if filename is not None:
859
if parent not in self._limbo_children:
860
self._limbo_children[parent] = set()
861
self._limbo_children_names[parent] = {}
862
use_direct_path = True
863
# the direct path can only be used if no other file has
864
# already taken this pathname, i.e. if the name is unused, or
865
# if it is already associated with this trans_id.
866
elif (self._limbo_children_names[parent].get(filename)
867
in (trans_id, None)):
868
use_direct_path = True
870
limbo_name = pathjoin(self._limbo_files[parent], filename)
871
self._limbo_children[parent].add(trans_id)
872
self._limbo_children_names[parent][filename] = trans_id
874
limbo_name = pathjoin(self._limbodir, trans_id)
875
self._needs_rename.add(trans_id)
876
self._limbo_files[trans_id] = limbo_name
879
def _apply_removals(self, inv, inventory_delta, mover):
880
"""Perform tree operations that remove directory/inventory names.
882
That is, delete files that are to be deleted, and put any files that
883
need renaming into limbo. This must be done in strict child-to-parent
886
tree_paths = list(self._tree_path_ids.iteritems())
887
tree_paths.sort(reverse=True)
888
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
890
for num, data in enumerate(tree_paths):
891
path, trans_id = data
892
child_pb.update('removing file', num, len(tree_paths))
893
full_path = self._tree.abspath(path)
894
if trans_id in self._removed_contents:
895
mover.pre_delete(full_path, os.path.join(self._deletiondir,
897
elif trans_id in self._new_name or trans_id in \
900
mover.rename(full_path, self._limbo_name(trans_id))
902
if e.errno != errno.ENOENT:
905
self.rename_count += 1
906
if trans_id in self._removed_id:
907
if trans_id == self._new_root:
908
file_id = self._tree.inventory.root.file_id
910
file_id = self.tree_file_id(trans_id)
911
assert file_id is not None
912
inventory_delta.append((path, None, file_id, None))
916
def _apply_insertions(self, inv, inventory_delta, mover):
917
"""Perform tree operations that insert directory/inventory names.
919
That is, create any files that need to be created, and restore from
920
limbo any files that needed renaming. This must be done in strict
921
parent-to-child order.
923
new_paths = self.new_paths()
925
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
927
for num, (path, trans_id) in enumerate(new_paths):
929
child_pb.update('adding file', num, len(new_paths))
931
kind = self._new_contents[trans_id]
933
kind = contents = None
934
if trans_id in self._new_contents or \
935
self.path_changed(trans_id):
936
full_path = self._tree.abspath(path)
937
if trans_id in self._needs_rename:
939
mover.rename(self._limbo_name(trans_id), full_path)
941
# We may be renaming a dangling inventory id
942
if e.errno != errno.ENOENT:
945
self.rename_count += 1
946
if trans_id in self._new_contents:
947
modified_paths.append(full_path)
948
del self._new_contents[trans_id]
950
if trans_id in self._new_id:
952
kind = file_kind(self._tree.abspath(path))
953
if trans_id in self._new_reference_revision:
954
new_entry = inventory.TreeReference(
955
self._new_id[trans_id],
956
self._new_name[trans_id],
957
self.final_file_id(self._new_parent[trans_id]),
958
None, self._new_reference_revision[trans_id])
960
new_entry = inventory.make_entry(kind,
961
self.final_name(trans_id),
962
self.final_file_id(self.final_parent(trans_id)),
963
self._new_id[trans_id])
965
if trans_id in self._new_name or trans_id in\
967
trans_id in self._new_executability:
968
file_id = self.final_file_id(trans_id)
969
if file_id is not None:
971
new_entry = entry.copy()
973
if trans_id in self._new_name or trans_id in\
975
if new_entry is not None:
976
new_entry.name = self.final_name(trans_id)
977
parent = self.final_parent(trans_id)
978
parent_id = self.final_file_id(parent)
979
new_entry.parent_id = parent_id
981
if trans_id in self._new_executability:
982
self._set_executability(path, new_entry, trans_id)
983
if new_entry is not None:
984
if new_entry.file_id in inv:
985
old_path = inv.id2path(new_entry.file_id)
988
inventory_delta.append((old_path, path,
993
return modified_paths
995
def _set_executability(self, path, entry, trans_id):
996
"""Set the executability of versioned files """
997
new_executability = self._new_executability[trans_id]
998
entry.executable = new_executability
999
if supports_executable():
1000
abspath = self._tree.abspath(path)
1001
current_mode = os.stat(abspath).st_mode
1002
if new_executability:
1005
to_mode = current_mode | (0100 & ~umask)
1006
# Enable x-bit for others only if they can read it.
1007
if current_mode & 0004:
1008
to_mode |= 0001 & ~umask
1009
if current_mode & 0040:
1010
to_mode |= 0010 & ~umask
1012
to_mode = current_mode & ~0111
1013
os.chmod(abspath, to_mode)
1015
def _new_entry(self, name, parent_id, file_id):
1016
"""Helper function to create a new filesystem entry."""
1017
trans_id = self.create_path(name, parent_id)
1018
if file_id is not None:
1019
self.version_file(file_id, trans_id)
1022
def new_file(self, name, parent_id, contents, file_id=None,
1024
"""Convenience method to create files.
1026
name is the name of the file to create.
1027
parent_id is the transaction id of the parent directory of the file.
1028
contents is an iterator of bytestrings, which will be used to produce
1030
:param file_id: The inventory ID of the file, if it is to be versioned.
1031
:param executable: Only valid when a file_id has been supplied.
1033
trans_id = self._new_entry(name, parent_id, file_id)
1034
# TODO: rather than scheduling a set_executable call,
1035
# have create_file create the file with the right mode.
1036
self.create_file(contents, trans_id)
1037
if executable is not None:
1038
self.set_executability(executable, trans_id)
1041
def new_directory(self, name, parent_id, file_id=None):
1042
"""Convenience method to create directories.
1044
name is the name of the directory to create.
1045
parent_id is the transaction id of the parent directory of the
1047
file_id is the inventory ID of the directory, if it is to be versioned.
1049
trans_id = self._new_entry(name, parent_id, file_id)
1050
self.create_directory(trans_id)
1053
def new_symlink(self, name, parent_id, target, file_id=None):
1054
"""Convenience method to create symbolic link.
1056
name is the name of the symlink to create.
1057
parent_id is the transaction id of the parent directory of the symlink.
1058
target is a bytestring of the target of the symlink.
1059
file_id is the inventory ID of the file, if it is to be versioned.
1061
trans_id = self._new_entry(name, parent_id, file_id)
1062
self.create_symlink(target, trans_id)
1065
def _affected_ids(self):
1066
"""Return the set of transform ids affected by the transform"""
1067
trans_ids = set(self._removed_id)
1068
trans_ids.update(self._new_id.keys())
1069
trans_ids.update(self._removed_contents)
1070
trans_ids.update(self._new_contents.keys())
1071
trans_ids.update(self._new_executability.keys())
1072
trans_ids.update(self._new_name.keys())
1073
trans_ids.update(self._new_parent.keys())
1076
def _get_file_id_maps(self):
1077
"""Return mapping of file_ids to trans_ids in the to and from states"""
1078
trans_ids = self._affected_ids()
1081
# Build up two dicts: trans_ids associated with file ids in the
1082
# FROM state, vs the TO state.
1083
for trans_id in trans_ids:
1084
from_file_id = self.tree_file_id(trans_id)
1085
if from_file_id is not None:
1086
from_trans_ids[from_file_id] = trans_id
1087
to_file_id = self.final_file_id(trans_id)
1088
if to_file_id is not None:
1089
to_trans_ids[to_file_id] = trans_id
1090
return from_trans_ids, to_trans_ids
1092
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1093
"""Get data about a file in the from (tree) state
1095
Return a (name, parent, kind, executable) tuple
1097
from_path = self._tree_id_paths.get(from_trans_id)
1099
# get data from working tree if versioned
1100
from_entry = self._tree.inventory[file_id]
1101
from_name = from_entry.name
1102
from_parent = from_entry.parent_id
1105
if from_path is None:
1106
# File does not exist in FROM state
1110
# File exists, but is not versioned. Have to use path-
1112
from_name = os.path.basename(from_path)
1113
tree_parent = self.get_tree_parent(from_trans_id)
1114
from_parent = self.tree_file_id(tree_parent)
1115
if from_path is not None:
1116
from_kind, from_executable, from_stats = \
1117
self._tree._comparison_data(from_entry, from_path)
1120
from_executable = False
1121
return from_name, from_parent, from_kind, from_executable
1123
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1124
"""Get data about a file in the to (target) state
1126
Return a (name, parent, kind, executable) tuple
1128
to_name = self.final_name(to_trans_id)
1130
to_kind = self.final_kind(to_trans_id)
1133
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1134
if to_trans_id in self._new_executability:
1135
to_executable = self._new_executability[to_trans_id]
1136
elif to_trans_id == from_trans_id:
1137
to_executable = from_executable
1139
to_executable = False
1140
return to_name, to_parent, to_kind, to_executable
1142
def _iter_changes(self):
1143
"""Produce output in the same format as Tree._iter_changes.
1145
Will produce nonsensical results if invoked while inventory/filesystem
1146
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1148
This reads the Transform, but only reproduces changes involving a
1149
file_id. Files that are not versioned in either of the FROM or TO
1150
states are not reflected.
1152
final_paths = FinalPaths(self)
1153
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1155
# Now iterate through all active file_ids
1156
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1158
from_trans_id = from_trans_ids.get(file_id)
1159
# find file ids, and determine versioning state
1160
if from_trans_id is None:
1161
from_versioned = False
1162
from_trans_id = to_trans_ids[file_id]
1164
from_versioned = True
1165
to_trans_id = to_trans_ids.get(file_id)
1166
if to_trans_id is None:
1167
to_versioned = False
1168
to_trans_id = from_trans_id
1172
from_name, from_parent, from_kind, from_executable = \
1173
self._from_file_data(from_trans_id, from_versioned, file_id)
1175
to_name, to_parent, to_kind, to_executable = \
1176
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1178
if not from_versioned:
1181
from_path = self._tree_id_paths.get(from_trans_id)
1182
if not to_versioned:
1185
to_path = final_paths.get_path(to_trans_id)
1186
if from_kind != to_kind:
1188
elif to_kind in ('file', 'symlink') and (
1189
to_trans_id != from_trans_id or
1190
to_trans_id in self._new_contents):
1192
if (not modified and from_versioned == to_versioned and
1193
from_parent==to_parent and from_name == to_name and
1194
from_executable == to_executable):
1196
results.append((file_id, (from_path, to_path), modified,
1197
(from_versioned, to_versioned),
1198
(from_parent, to_parent),
1199
(from_name, to_name),
1200
(from_kind, to_kind),
1201
(from_executable, to_executable)))
1202
return iter(sorted(results, key=lambda x:x[1]))
1205
def joinpath(parent, child):
1206
"""Join tree-relative paths, handling the tree root specially"""
1207
if parent is None or parent == "":
1210
return pathjoin(parent, child)
1213
class FinalPaths(object):
1214
"""Make path calculation cheap by memoizing paths.
1216
The underlying tree must not be manipulated between calls, or else
1217
the results will likely be incorrect.
1219
def __init__(self, transform):
1220
object.__init__(self)
1221
self._known_paths = {}
1222
self.transform = transform
1224
def _determine_path(self, trans_id):
1225
if trans_id == self.transform.root:
1227
name = self.transform.final_name(trans_id)
1228
parent_id = self.transform.final_parent(trans_id)
1229
if parent_id == self.transform.root:
1232
return pathjoin(self.get_path(parent_id), name)
1234
def get_path(self, trans_id):
1235
"""Find the final path associated with a trans_id"""
1236
if trans_id not in self._known_paths:
1237
self._known_paths[trans_id] = self._determine_path(trans_id)
1238
return self._known_paths[trans_id]
1240
def topology_sorted_ids(tree):
1241
"""Determine the topological order of the ids in a tree"""
1242
file_ids = list(tree)
1243
file_ids.sort(key=tree.id2path)
1247
def build_tree(tree, wt):
1248
"""Create working tree for a branch, using a TreeTransform.
1250
This function should be used on empty trees, having a tree root at most.
1251
(see merge and revert functionality for working with existing trees)
1253
Existing files are handled like so:
1255
- Existing bzrdirs take precedence over creating new items. They are
1256
created as '%s.diverted' % name.
1257
- Otherwise, if the content on disk matches the content we are building,
1258
it is silently replaced.
1259
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1261
wt.lock_tree_write()
1265
return _build_tree(tree, wt)
1271
def _build_tree(tree, wt):
1272
"""See build_tree."""
1273
if len(wt.inventory) > 1: # more than just a root
1274
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1276
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1277
pp = ProgressPhase("Build phase", 2, top_pb)
1278
if tree.inventory.root is not None:
1279
# This is kind of a hack: we should be altering the root
1280
# as part of the regular tree shape diff logic.
1281
# The conditional test here is to avoid doing an
1282
# expensive operation (flush) every time the root id
1283
# is set within the tree, nor setting the root and thus
1284
# marking the tree as dirty, because we use two different
1285
# idioms here: tree interfaces and inventory interfaces.
1286
if wt.path2id('') != tree.inventory.root.file_id:
1287
wt.set_root_id(tree.inventory.root.file_id)
1289
tt = TreeTransform(wt)
1293
file_trans_id[wt.get_root_id()] = \
1294
tt.trans_id_tree_file_id(wt.get_root_id())
1295
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1297
deferred_contents = []
1298
for num, (tree_path, entry) in \
1299
enumerate(tree.inventory.iter_entries_by_dir()):
1300
pb.update("Building tree", num - len(deferred_contents),
1301
len(tree.inventory))
1302
if entry.parent_id is None:
1305
file_id = entry.file_id
1306
target_path = wt.abspath(tree_path)
1308
kind = file_kind(target_path)
1312
if kind == "directory":
1314
bzrdir.BzrDir.open(target_path)
1315
except errors.NotBranchError:
1319
if (file_id not in divert and
1320
_content_match(tree, entry, file_id, kind,
1322
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1323
if kind == 'directory':
1325
if entry.parent_id not in file_trans_id:
1326
raise AssertionError(
1327
'entry %s parent id %r is not in file_trans_id %r'
1328
% (entry, entry.parent_id, file_trans_id))
1329
parent_id = file_trans_id[entry.parent_id]
1330
if entry.kind == 'file':
1331
# We *almost* replicate new_by_entry, so that we can defer
1332
# getting the file text, and get them all at once.
1333
trans_id = tt.create_path(entry.name, parent_id)
1334
file_trans_id[file_id] = trans_id
1335
tt.version_file(entry.file_id, trans_id)
1336
executable = tree.is_executable(entry.file_id, tree_path)
1337
if executable is not None:
1338
tt.set_executability(executable, trans_id)
1339
deferred_contents.append((entry.file_id, trans_id))
1341
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1344
new_trans_id = file_trans_id[file_id]
1345
old_parent = tt.trans_id_tree_path(tree_path)
1346
_reparent_children(tt, old_parent, new_trans_id)
1347
for num, (trans_id, bytes) in enumerate(
1348
tree.iter_files_bytes(deferred_contents)):
1349
tt.create_file(bytes, trans_id)
1350
pb.update('Adding file contents',
1351
(num + len(tree.inventory) - len(deferred_contents)),
1352
len(tree.inventory))
1356
divert_trans = set(file_trans_id[f] for f in divert)
1357
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1358
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1359
conflicts = cook_conflicts(raw_conflicts, tt)
1360
for conflict in conflicts:
1363
wt.add_conflicts(conflicts)
1364
except errors.UnsupportedOperation:
1373
def _reparent_children(tt, old_parent, new_parent):
1374
for child in tt.iter_tree_children(old_parent):
1375
tt.adjust_path(tt.final_name(child), new_parent, child)
1378
def _content_match(tree, entry, file_id, kind, target_path):
1379
if entry.kind != kind:
1381
if entry.kind == "directory":
1383
if entry.kind == "file":
1384
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1386
elif entry.kind == "symlink":
1387
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1392
def resolve_checkout(tt, conflicts, divert):
1393
new_conflicts = set()
1394
for c_type, conflict in ((c[0], c) for c in conflicts):
1395
# Anything but a 'duplicate' would indicate programmer error
1396
assert c_type == 'duplicate', c_type
1397
# Now figure out which is new and which is old
1398
if tt.new_contents(conflict[1]):
1399
new_file = conflict[1]
1400
old_file = conflict[2]
1402
new_file = conflict[2]
1403
old_file = conflict[1]
1405
# We should only get here if the conflict wasn't completely
1407
final_parent = tt.final_parent(old_file)
1408
if new_file in divert:
1409
new_name = tt.final_name(old_file)+'.diverted'
1410
tt.adjust_path(new_name, final_parent, new_file)
1411
new_conflicts.add((c_type, 'Diverted to',
1412
new_file, old_file))
1414
new_name = tt.final_name(old_file)+'.moved'
1415
tt.adjust_path(new_name, final_parent, old_file)
1416
new_conflicts.add((c_type, 'Moved existing file to',
1417
old_file, new_file))
1418
return new_conflicts
1421
def new_by_entry(tt, entry, parent_id, tree):
1422
"""Create a new file according to its inventory entry"""
1426
contents = tree.get_file(entry.file_id).readlines()
1427
executable = tree.is_executable(entry.file_id)
1428
return tt.new_file(name, parent_id, contents, entry.file_id,
1430
elif kind in ('directory', 'tree-reference'):
1431
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1432
if kind == 'tree-reference':
1433
tt.set_tree_reference(entry.reference_revision, trans_id)
1435
elif kind == 'symlink':
1436
target = tree.get_symlink_target(entry.file_id)
1437
return tt.new_symlink(name, parent_id, target, entry.file_id)
1439
raise errors.BadFileKindError(name, kind)
1441
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1442
"""Create new file contents according to an inventory entry."""
1443
if entry.kind == "file":
1445
lines = tree.get_file(entry.file_id).readlines()
1446
tt.create_file(lines, trans_id, mode_id=mode_id)
1447
elif entry.kind == "symlink":
1448
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1449
elif entry.kind == "directory":
1450
tt.create_directory(trans_id)
1452
def create_entry_executability(tt, entry, trans_id):
1453
"""Set the executability of a trans_id according to an inventory entry"""
1454
if entry.kind == "file":
1455
tt.set_executability(entry.executable, trans_id)
1458
@deprecated_function(zero_fifteen)
1459
def find_interesting(working_tree, target_tree, filenames):
1460
"""Find the ids corresponding to specified filenames.
1462
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1464
working_tree.lock_read()
1466
target_tree.lock_read()
1468
return working_tree.paths2ids(filenames, [target_tree])
1470
target_tree.unlock()
1472
working_tree.unlock()
1475
@deprecated_function(zero_ninety)
1476
def change_entry(tt, file_id, working_tree, target_tree,
1477
trans_id_file_id, backups, trans_id, by_parent):
1478
"""Replace a file_id's contents with those from a target tree."""
1479
if file_id is None and target_tree is None:
1480
# skip the logic altogether in the deprecation test
1482
e_trans_id = trans_id_file_id(file_id)
1483
entry = target_tree.inventory[file_id]
1484
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1487
mode_id = e_trans_id
1490
tt.delete_contents(e_trans_id)
1492
parent_trans_id = trans_id_file_id(entry.parent_id)
1493
backup_name = get_backup_name(entry, by_parent,
1494
parent_trans_id, tt)
1495
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1496
tt.unversion_file(e_trans_id)
1497
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1498
tt.version_file(file_id, e_trans_id)
1499
trans_id[file_id] = e_trans_id
1500
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1501
create_entry_executability(tt, entry, e_trans_id)
1504
tt.set_executability(entry.executable, e_trans_id)
1505
if tt.final_name(e_trans_id) != entry.name:
1508
parent_id = tt.final_parent(e_trans_id)
1509
parent_file_id = tt.final_file_id(parent_id)
1510
if parent_file_id != entry.parent_id:
1515
parent_trans_id = trans_id_file_id(entry.parent_id)
1516
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1519
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1520
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1523
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1524
"""Produce a backup-style name that appears to be available"""
1528
yield "%s.~%d~" % (name, counter)
1530
for new_name in name_gen():
1531
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1535
def _entry_changes(file_id, entry, working_tree):
1536
"""Determine in which ways the inventory entry has changed.
1538
Returns booleans: has_contents, content_mod, meta_mod
1539
has_contents means there are currently contents, but they differ
1540
contents_mod means contents need to be modified
1541
meta_mod means the metadata needs to be modified
1543
cur_entry = working_tree.inventory[file_id]
1545
working_kind = working_tree.kind(file_id)
1548
has_contents = False
1551
if has_contents is True:
1552
if entry.kind != working_kind:
1553
contents_mod, meta_mod = True, False
1555
cur_entry._read_tree_state(working_tree.id2path(file_id),
1557
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1558
cur_entry._forget_tree_state()
1559
return has_contents, contents_mod, meta_mod
1562
def revert(working_tree, target_tree, filenames, backups=False,
1563
pb=DummyProgress(), change_reporter=None):
1564
"""Revert a working tree's contents to those of a target tree."""
1565
target_tree.lock_read()
1566
tt = TreeTransform(working_tree, pb)
1568
pp = ProgressPhase("Revert phase", 3, pb)
1570
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1572
merge_modified = _alter_files(working_tree, target_tree, tt,
1573
child_pb, filenames, backups)
1577
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1579
raw_conflicts = resolve_conflicts(tt, child_pb)
1582
conflicts = cook_conflicts(raw_conflicts, tt)
1584
change_reporter = delta._ChangeReporter(
1585
unversioned_filter=working_tree.is_ignored)
1586
delta.report_changes(tt._iter_changes(), change_reporter)
1587
for conflict in conflicts:
1591
working_tree.set_merge_modified(merge_modified)
1593
target_tree.unlock()
1599
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1601
merge_modified = working_tree.merge_modified()
1602
change_list = target_tree._iter_changes(working_tree,
1603
specific_files=specific_files, pb=pb)
1604
if target_tree.inventory.root is None:
1611
for id_num, (file_id, path, changed_content, versioned, parent, name,
1612
kind, executable) in enumerate(change_list):
1613
if skip_root and file_id[0] is not None and parent[0] is None:
1615
trans_id = tt.trans_id_file_id(file_id)
1618
keep_content = False
1619
if kind[0] == 'file' and (backups or kind[1] is None):
1620
wt_sha1 = working_tree.get_file_sha1(file_id)
1621
if merge_modified.get(file_id) != wt_sha1:
1622
# acquire the basis tree lazily to prevent the
1623
# expense of accessing it when it's not needed ?
1624
# (Guessing, RBC, 200702)
1625
if basis_tree is None:
1626
basis_tree = working_tree.basis_tree()
1627
basis_tree.lock_read()
1628
if file_id in basis_tree:
1629
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1631
elif kind[1] is None and not versioned[1]:
1633
if kind[0] is not None:
1634
if not keep_content:
1635
tt.delete_contents(trans_id)
1636
elif kind[1] is not None:
1637
parent_trans_id = tt.trans_id_file_id(parent[0])
1638
by_parent = tt.by_parent()
1639
backup_name = _get_backup_name(name[0], by_parent,
1640
parent_trans_id, tt)
1641
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1642
new_trans_id = tt.create_path(name[0], parent_trans_id)
1643
if versioned == (True, True):
1644
tt.unversion_file(trans_id)
1645
tt.version_file(file_id, new_trans_id)
1646
# New contents should have the same unix perms as old
1649
trans_id = new_trans_id
1650
if kind[1] == 'directory':
1651
tt.create_directory(trans_id)
1652
elif kind[1] == 'symlink':
1653
tt.create_symlink(target_tree.get_symlink_target(file_id),
1655
elif kind[1] == 'file':
1656
deferred_files.append((file_id, (trans_id, mode_id)))
1657
if basis_tree is None:
1658
basis_tree = working_tree.basis_tree()
1659
basis_tree.lock_read()
1660
new_sha1 = target_tree.get_file_sha1(file_id)
1661
if (file_id in basis_tree and new_sha1 ==
1662
basis_tree.get_file_sha1(file_id)):
1663
if file_id in merge_modified:
1664
del merge_modified[file_id]
1666
merge_modified[file_id] = new_sha1
1668
# preserve the execute bit when backing up
1669
if keep_content and executable[0] == executable[1]:
1670
tt.set_executability(executable[1], trans_id)
1672
assert kind[1] is None
1673
if versioned == (False, True):
1674
tt.version_file(file_id, trans_id)
1675
if versioned == (True, False):
1676
tt.unversion_file(trans_id)
1677
if (name[1] is not None and
1678
(name[0] != name[1] or parent[0] != parent[1])):
1680
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1681
if executable[0] != executable[1] and kind[1] == "file":
1682
tt.set_executability(executable[1], trans_id)
1683
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
1685
tt.create_file(bytes, trans_id, mode_id)
1687
if basis_tree is not None:
1689
return merge_modified
1692
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1693
"""Make many conflict-resolution attempts, but die if they fail"""
1694
if pass_func is None:
1695
pass_func = conflict_pass
1696
new_conflicts = set()
1699
pb.update('Resolution pass', n+1, 10)
1700
conflicts = tt.find_conflicts()
1701
if len(conflicts) == 0:
1702
return new_conflicts
1703
new_conflicts.update(pass_func(tt, conflicts))
1704
raise MalformedTransform(conflicts=conflicts)
1709
def conflict_pass(tt, conflicts, path_tree=None):
1710
"""Resolve some classes of conflicts.
1712
:param tt: The transform to resolve conflicts in
1713
:param conflicts: The conflicts to resolve
1714
:param path_tree: A Tree to get supplemental paths from
1716
new_conflicts = set()
1717
for c_type, conflict in ((c[0], c) for c in conflicts):
1718
if c_type == 'duplicate id':
1719
tt.unversion_file(conflict[1])
1720
new_conflicts.add((c_type, 'Unversioned existing file',
1721
conflict[1], conflict[2], ))
1722
elif c_type == 'duplicate':
1723
# files that were renamed take precedence
1724
new_name = tt.final_name(conflict[1])+'.moved'
1725
final_parent = tt.final_parent(conflict[1])
1726
if tt.path_changed(conflict[1]):
1727
tt.adjust_path(new_name, final_parent, conflict[2])
1728
new_conflicts.add((c_type, 'Moved existing file to',
1729
conflict[2], conflict[1]))
1731
tt.adjust_path(new_name, final_parent, conflict[1])
1732
new_conflicts.add((c_type, 'Moved existing file to',
1733
conflict[1], conflict[2]))
1734
elif c_type == 'parent loop':
1735
# break the loop by undoing one of the ops that caused the loop
1737
while not tt.path_changed(cur):
1738
cur = tt.final_parent(cur)
1739
new_conflicts.add((c_type, 'Cancelled move', cur,
1740
tt.final_parent(cur),))
1741
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1743
elif c_type == 'missing parent':
1744
trans_id = conflict[1]
1746
tt.cancel_deletion(trans_id)
1747
new_conflicts.add(('deleting parent', 'Not deleting',
1750
tt.create_directory(trans_id)
1751
new_conflicts.add((c_type, 'Created directory', trans_id))
1753
tt.final_name(trans_id)
1755
file_id = tt.final_file_id(trans_id)
1756
entry = path_tree.inventory[file_id]
1757
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1758
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1759
elif c_type == 'unversioned parent':
1760
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1761
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1762
return new_conflicts
1765
def cook_conflicts(raw_conflicts, tt):
1766
"""Generate a list of cooked conflicts, sorted by file path"""
1767
from bzrlib.conflicts import Conflict
1768
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1769
return sorted(conflict_iter, key=Conflict.sort_key)
1772
def iter_cook_conflicts(raw_conflicts, tt):
1773
from bzrlib.conflicts import Conflict
1775
for conflict in raw_conflicts:
1776
c_type = conflict[0]
1777
action = conflict[1]
1778
modified_path = fp.get_path(conflict[2])
1779
modified_id = tt.final_file_id(conflict[2])
1780
if len(conflict) == 3:
1781
yield Conflict.factory(c_type, action=action, path=modified_path,
1782
file_id=modified_id)
1785
conflicting_path = fp.get_path(conflict[3])
1786
conflicting_id = tt.final_file_id(conflict[3])
1787
yield Conflict.factory(c_type, action=action, path=modified_path,
1788
file_id=modified_id,
1789
conflict_path=conflicting_path,
1790
conflict_file_id=conflicting_id)
1793
class _FileMover(object):
1794
"""Moves and deletes files for TreeTransform, tracking operations"""
1797
self.past_renames = []
1798
self.pending_deletions = []
1800
def rename(self, from_, to):
1801
"""Rename a file from one path to another. Functions like os.rename"""
1802
os.rename(from_, to)
1803
self.past_renames.append((from_, to))
1805
def pre_delete(self, from_, to):
1806
"""Rename a file out of the way and mark it for deletion.
1808
Unlike os.unlink, this works equally well for files and directories.
1809
:param from_: The current file path
1810
:param to: A temporary path for the file
1812
self.rename(from_, to)
1813
self.pending_deletions.append(to)
1816
"""Reverse all renames that have been performed"""
1817
for from_, to in reversed(self.past_renames):
1818
os.rename(to, from_)
1819
# after rollback, don't reuse _FileMover
1821
pending_deletions = None
1823
def apply_deletions(self):
1824
"""Apply all marked deletions"""
1825
for path in self.pending_deletions:
1827
# after apply_deletions, don't reuse _FileMover
1829
pending_deletions = None