1
# Copyright (C) 2006, 2007, 2008 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, S_IEXEC
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
31
revision as _mod_revision,
34
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
35
ReusingTransform, NotVersionedError, CantMoveRoot,
36
ExistingLimbo, ImmortalLimbo, NoFinalPath,
38
from bzrlib.inventory import InventoryEntry
39
from bzrlib.osutils import (
48
from bzrlib.progress import DummyProgress, ProgressPhase
49
from bzrlib.symbol_versioning import (
52
from bzrlib.trace import mutter, warning
53
from bzrlib import tree
55
import bzrlib.urlutils as urlutils
58
ROOT_PARENT = "root-parent"
61
def unique_add(map, key, value):
63
raise DuplicateKey(key=key)
67
class _TransformResults(object):
68
def __init__(self, modified_paths, rename_count):
70
self.modified_paths = modified_paths
71
self.rename_count = rename_count
74
class TreeTransformBase(object):
75
"""The base class for TreeTransform and TreeTransformBase"""
77
def __init__(self, tree, limbodir, pb=DummyProgress(),
81
:param tree: The tree that will be transformed, but not necessarily
83
:param limbodir: A directory where new files can be stored until
84
they are installed in their proper places
85
:param pb: A ProgressBar indicating how much progress is being made
86
:param case_sensitive: If True, the target of the transform is
87
case sensitive, not just case preserving.
91
self._limbodir = limbodir
92
self._deletiondir = None
94
# mapping of trans_id -> new basename
96
# mapping of trans_id -> new parent trans_id
98
# mapping of trans_id with new contents -> new file_kind
99
self._new_contents = {}
100
# A mapping of transform ids to their limbo filename
101
self._limbo_files = {}
102
# A mapping of transform ids to a set of the transform ids of children
103
# that their limbo directory has
104
self._limbo_children = {}
105
# Map transform ids to maps of child filename to child transform id
106
self._limbo_children_names = {}
107
# List of transform ids that need to be renamed from limbo into place
108
self._needs_rename = set()
109
# Set of trans_ids whose contents will be removed
110
self._removed_contents = set()
111
# Mapping of trans_id -> new execute-bit value
112
self._new_executability = {}
113
# Mapping of trans_id -> new tree-reference value
114
self._new_reference_revision = {}
115
# Mapping of trans_id -> new file_id
117
# Mapping of old file-id -> trans_id
118
self._non_present_ids = {}
119
# Mapping of new file_id -> trans_id
121
# Set of file_ids that will be removed
122
self._removed_id = set()
123
# Mapping of path in old tree -> trans_id
124
self._tree_path_ids = {}
125
# Mapping trans_id -> path in old tree
126
self._tree_id_paths = {}
127
# Cache of realpath results, to speed up canonical_path
129
# Cache of relpath results, to speed up canonical_path
131
# The trans_id that will be used as the tree root
132
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
133
# Indictor of whether the transform has been applied
137
# Whether the target is case sensitive
138
self._case_sensitive_target = case_sensitive
139
# A counter of how many files have been renamed
140
self.rename_count = 0
142
def __get_root(self):
143
return self._new_root
145
root = property(__get_root)
148
"""Release the working tree lock, if held, clean up limbo dir.
150
This is required if apply has not been invoked, but can be invoked
153
if self._tree is None:
156
entries = [(self._limbo_name(t), t, k) for t, k in
157
self._new_contents.iteritems()]
158
entries.sort(reverse=True)
159
for path, trans_id, kind in entries:
160
if kind == "directory":
165
os.rmdir(self._limbodir)
167
# We don't especially care *why* the dir is immortal.
168
raise ImmortalLimbo(self._limbodir)
170
if self._deletiondir is not None:
171
os.rmdir(self._deletiondir)
173
raise errors.ImmortalPendingDeletion(self._deletiondir)
178
def _assign_id(self):
179
"""Produce a new tranform id"""
180
new_id = "new-%s" % self._id_number
184
def create_path(self, name, parent):
185
"""Assign a transaction id to a new path"""
186
trans_id = self._assign_id()
187
unique_add(self._new_name, trans_id, name)
188
unique_add(self._new_parent, trans_id, parent)
191
def adjust_path(self, name, parent, trans_id):
192
"""Change the path that is assigned to a transaction id."""
193
if trans_id == self._new_root:
195
previous_parent = self._new_parent.get(trans_id)
196
previous_name = self._new_name.get(trans_id)
197
self._new_name[trans_id] = name
198
self._new_parent[trans_id] = parent
199
if (trans_id in self._limbo_files and
200
trans_id not in self._needs_rename):
201
self._rename_in_limbo([trans_id])
202
self._limbo_children[previous_parent].remove(trans_id)
203
del self._limbo_children_names[previous_parent][previous_name]
205
def _rename_in_limbo(self, trans_ids):
206
"""Fix limbo names so that the right final path is produced.
208
This means we outsmarted ourselves-- we tried to avoid renaming
209
these files later by creating them with their final names in their
210
final parents. But now the previous name or parent is no longer
211
suitable, so we have to rename them.
213
Even for trans_ids that have no new contents, we must remove their
214
entries from _limbo_files, because they are now stale.
216
for trans_id in trans_ids:
217
old_path = self._limbo_files.pop(trans_id)
218
if trans_id not in self._new_contents:
220
new_path = self._limbo_name(trans_id)
221
os.rename(old_path, new_path)
223
def adjust_root_path(self, name, parent):
224
"""Emulate moving the root by moving all children, instead.
226
We do this by undoing the association of root's transaction id with the
227
current tree. This allows us to create a new directory with that
228
transaction id. We unversion the root directory and version the
229
physically new directory, and hope someone versions the tree root
232
old_root = self._new_root
233
old_root_file_id = self.final_file_id(old_root)
234
# force moving all children of root
235
for child_id in self.iter_tree_children(old_root):
236
if child_id != parent:
237
self.adjust_path(self.final_name(child_id),
238
self.final_parent(child_id), child_id)
239
file_id = self.final_file_id(child_id)
240
if file_id is not None:
241
self.unversion_file(child_id)
242
self.version_file(file_id, child_id)
244
# the physical root needs a new transaction id
245
self._tree_path_ids.pop("")
246
self._tree_id_paths.pop(old_root)
247
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
248
if parent == old_root:
249
parent = self._new_root
250
self.adjust_path(name, parent, old_root)
251
self.create_directory(old_root)
252
self.version_file(old_root_file_id, old_root)
253
self.unversion_file(self._new_root)
255
def trans_id_tree_file_id(self, inventory_id):
256
"""Determine the transaction id of a working tree file.
258
This reflects only files that already exist, not ones that will be
259
added by transactions.
261
path = self._tree.id2path(inventory_id)
262
return self.trans_id_tree_path(path)
264
def trans_id_file_id(self, file_id):
265
"""Determine or set the transaction id associated with a file ID.
266
A new id is only created for file_ids that were never present. If
267
a transaction has been unversioned, it is deliberately still returned.
268
(this will likely lead to an unversioned parent conflict.)
270
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
271
return self._r_new_id[file_id]
272
elif file_id in self._tree.inventory:
273
return self.trans_id_tree_file_id(file_id)
274
elif file_id in self._non_present_ids:
275
return self._non_present_ids[file_id]
277
trans_id = self._assign_id()
278
self._non_present_ids[file_id] = trans_id
281
def canonical_path(self, path):
282
"""Get the canonical tree-relative path"""
283
# don't follow final symlinks
284
abs = self._tree.abspath(path)
285
if abs in self._relpaths:
286
return self._relpaths[abs]
287
dirname, basename = os.path.split(abs)
288
if dirname not in self._realpaths:
289
self._realpaths[dirname] = os.path.realpath(dirname)
290
dirname = self._realpaths[dirname]
291
abs = pathjoin(dirname, basename)
292
if dirname in self._relpaths:
293
relpath = pathjoin(self._relpaths[dirname], basename)
294
relpath = relpath.rstrip('/\\')
296
relpath = self._tree.relpath(abs)
297
self._relpaths[abs] = relpath
300
def trans_id_tree_path(self, path):
301
"""Determine (and maybe set) the transaction ID for a tree path."""
302
path = self.canonical_path(path)
303
if path not in self._tree_path_ids:
304
self._tree_path_ids[path] = self._assign_id()
305
self._tree_id_paths[self._tree_path_ids[path]] = path
306
return self._tree_path_ids[path]
308
def get_tree_parent(self, trans_id):
309
"""Determine id of the parent in the tree."""
310
path = self._tree_id_paths[trans_id]
313
return self.trans_id_tree_path(os.path.dirname(path))
315
def create_file(self, contents, trans_id, mode_id=None):
316
"""Schedule creation of a new file.
320
Contents is an iterator of strings, all of which will be written
321
to the target destination.
323
New file takes the permissions of any existing file with that id,
324
unless mode_id is specified.
326
name = self._limbo_name(trans_id)
330
unique_add(self._new_contents, trans_id, 'file')
332
# Clean up the file, it never got registered so
333
# TreeTransform.finalize() won't clean it up.
338
f.writelines(contents)
341
self._set_mode(trans_id, mode_id, S_ISREG)
343
def _set_mode(self, trans_id, mode_id, typefunc):
344
"""Set the mode of new file contents.
345
The mode_id is the existing file to get the mode from (often the same
346
as trans_id). The operation is only performed if there's a mode match
347
according to typefunc.
352
old_path = self._tree_id_paths[mode_id]
356
mode = os.stat(self._tree.abspath(old_path)).st_mode
358
if e.errno in (errno.ENOENT, errno.ENOTDIR):
359
# Either old_path doesn't exist, or the parent of the
360
# target is not a directory (but will be one eventually)
361
# Either way, we know it doesn't exist *right now*
362
# See also bug #248448
367
os.chmod(self._limbo_name(trans_id), mode)
369
def create_hardlink(self, path, trans_id):
370
"""Schedule creation of a hard link"""
371
name = self._limbo_name(trans_id)
375
if e.errno != errno.EPERM:
377
raise errors.HardLinkNotSupported(path)
379
unique_add(self._new_contents, trans_id, 'file')
381
# Clean up the file, it never got registered so
382
# TreeTransform.finalize() won't clean it up.
386
def create_directory(self, trans_id):
387
"""Schedule creation of a new directory.
389
See also new_directory.
391
os.mkdir(self._limbo_name(trans_id))
392
unique_add(self._new_contents, trans_id, 'directory')
394
def create_symlink(self, target, trans_id):
395
"""Schedule creation of a new symbolic link.
397
target is a bytestring.
398
See also new_symlink.
401
os.symlink(target, self._limbo_name(trans_id))
402
unique_add(self._new_contents, trans_id, 'symlink')
405
path = FinalPaths(self).get_path(trans_id)
408
raise UnableCreateSymlink(path=path)
410
def cancel_creation(self, trans_id):
411
"""Cancel the creation of new file contents."""
412
del self._new_contents[trans_id]
413
children = self._limbo_children.get(trans_id)
414
# if this is a limbo directory with children, move them before removing
416
if children is not None:
417
self._rename_in_limbo(children)
418
del self._limbo_children[trans_id]
419
del self._limbo_children_names[trans_id]
420
delete_any(self._limbo_name(trans_id))
422
def delete_contents(self, trans_id):
423
"""Schedule the contents of a path entry for deletion"""
424
self.tree_kind(trans_id)
425
self._removed_contents.add(trans_id)
427
def cancel_deletion(self, trans_id):
428
"""Cancel a scheduled deletion"""
429
self._removed_contents.remove(trans_id)
431
def unversion_file(self, trans_id):
432
"""Schedule a path entry to become unversioned"""
433
self._removed_id.add(trans_id)
435
def delete_versioned(self, trans_id):
436
"""Delete and unversion a versioned file"""
437
self.delete_contents(trans_id)
438
self.unversion_file(trans_id)
440
def set_executability(self, executability, trans_id):
441
"""Schedule setting of the 'execute' bit
442
To unschedule, set to None
444
if executability is None:
445
del self._new_executability[trans_id]
447
unique_add(self._new_executability, trans_id, executability)
449
def set_tree_reference(self, revision_id, trans_id):
450
"""Set the reference associated with a directory"""
451
unique_add(self._new_reference_revision, trans_id, revision_id)
453
def version_file(self, file_id, trans_id):
454
"""Schedule a file to become versioned."""
457
unique_add(self._new_id, trans_id, file_id)
458
unique_add(self._r_new_id, file_id, trans_id)
460
def cancel_versioning(self, trans_id):
461
"""Undo a previous versioning of a file"""
462
file_id = self._new_id[trans_id]
463
del self._new_id[trans_id]
464
del self._r_new_id[file_id]
466
def new_paths(self, filesystem_only=False):
467
"""Determine the paths of all new and changed files.
469
:param filesystem_only: if True, only calculate values for files
470
that require renames or execute bit changes.
474
stale_ids = self._needs_rename.difference(self._new_name)
475
stale_ids.difference_update(self._new_parent)
476
stale_ids.difference_update(self._new_contents)
477
stale_ids.difference_update(self._new_id)
478
needs_rename = self._needs_rename.difference(stale_ids)
479
id_sets = (needs_rename, self._new_executability)
481
id_sets = (self._new_name, self._new_parent, self._new_contents,
482
self._new_id, self._new_executability)
483
for id_set in id_sets:
484
new_ids.update(id_set)
485
return sorted(FinalPaths(self).get_paths(new_ids))
487
def _inventory_altered(self):
488
"""Get the trans_ids and paths of files needing new inv entries."""
490
for id_set in [self._new_name, self._new_parent, self._new_id,
491
self._new_executability]:
492
new_ids.update(id_set)
493
changed_kind = set(self._removed_contents)
494
changed_kind.intersection_update(self._new_contents)
495
changed_kind.difference_update(new_ids)
496
changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
498
new_ids.update(changed_kind)
499
return sorted(FinalPaths(self).get_paths(new_ids))
501
def tree_kind(self, trans_id):
502
"""Determine the file kind in the working tree.
504
Raises NoSuchFile if the file does not exist
506
path = self._tree_id_paths.get(trans_id)
508
raise NoSuchFile(None)
510
return file_kind(self._tree.abspath(path))
512
if e.errno != errno.ENOENT:
515
raise NoSuchFile(path)
517
def final_kind(self, trans_id):
518
"""Determine the final file kind, after any changes applied.
520
Raises NoSuchFile if the file does not exist/has no contents.
521
(It is conceivable that a path would be created without the
522
corresponding contents insertion command)
524
if trans_id in self._new_contents:
525
return self._new_contents[trans_id]
526
elif trans_id in self._removed_contents:
527
raise NoSuchFile(None)
529
return self.tree_kind(trans_id)
531
def tree_file_id(self, trans_id):
532
"""Determine the file id associated with the trans_id in the tree"""
534
path = self._tree_id_paths[trans_id]
536
# the file is a new, unversioned file, or invalid trans_id
538
# the file is old; the old id is still valid
539
if self._new_root == trans_id:
540
return self._tree.get_root_id()
541
return self._tree.inventory.path2id(path)
543
def final_file_id(self, trans_id):
544
"""Determine the file id after any changes are applied, or None.
546
None indicates that the file will not be versioned after changes are
550
return self._new_id[trans_id]
552
if trans_id in self._removed_id:
554
return self.tree_file_id(trans_id)
556
def inactive_file_id(self, trans_id):
557
"""Return the inactive file_id associated with a transaction id.
558
That is, the one in the tree or in non_present_ids.
559
The file_id may actually be active, too.
561
file_id = self.tree_file_id(trans_id)
562
if file_id is not None:
564
for key, value in self._non_present_ids.iteritems():
565
if value == trans_id:
568
def final_parent(self, trans_id):
569
"""Determine the parent file_id, after any changes are applied.
571
ROOT_PARENT is returned for the tree root.
574
return self._new_parent[trans_id]
576
return self.get_tree_parent(trans_id)
578
def final_name(self, trans_id):
579
"""Determine the final filename, after all changes are applied."""
581
return self._new_name[trans_id]
584
return os.path.basename(self._tree_id_paths[trans_id])
586
raise NoFinalPath(trans_id, self)
589
"""Return a map of parent: children for known parents.
591
Only new paths and parents of tree files with assigned ids are used.
594
items = list(self._new_parent.iteritems())
595
items.extend((t, self.final_parent(t)) for t in
596
self._tree_id_paths.keys())
597
for trans_id, parent_id in items:
598
if parent_id not in by_parent:
599
by_parent[parent_id] = set()
600
by_parent[parent_id].add(trans_id)
603
def path_changed(self, trans_id):
604
"""Return True if a trans_id's path has changed."""
605
return (trans_id in self._new_name) or (trans_id in self._new_parent)
607
def new_contents(self, trans_id):
608
return (trans_id in self._new_contents)
610
def find_conflicts(self):
611
"""Find any violations of inventory or filesystem invariants"""
612
if self._done is True:
613
raise ReusingTransform()
615
# ensure all children of all existent parents are known
616
# all children of non-existent parents are known, by definition.
617
self._add_tree_children()
618
by_parent = self.by_parent()
619
conflicts.extend(self._unversioned_parents(by_parent))
620
conflicts.extend(self._parent_loops())
621
conflicts.extend(self._duplicate_entries(by_parent))
622
conflicts.extend(self._duplicate_ids())
623
conflicts.extend(self._parent_type_conflicts(by_parent))
624
conflicts.extend(self._improper_versioning())
625
conflicts.extend(self._executability_conflicts())
626
conflicts.extend(self._overwrite_conflicts())
629
def _add_tree_children(self):
630
"""Add all the children of all active parents to the known paths.
632
Active parents are those which gain children, and those which are
633
removed. This is a necessary first step in detecting conflicts.
635
parents = self.by_parent().keys()
636
parents.extend([t for t in self._removed_contents if
637
self.tree_kind(t) == 'directory'])
638
for trans_id in self._removed_id:
639
file_id = self.tree_file_id(trans_id)
640
if file_id is not None:
641
if self._tree.inventory[file_id].kind == 'directory':
642
parents.append(trans_id)
643
elif self.tree_kind(trans_id) == 'directory':
644
parents.append(trans_id)
646
for parent_id in parents:
647
# ensure that all children are registered with the transaction
648
list(self.iter_tree_children(parent_id))
650
def iter_tree_children(self, parent_id):
651
"""Iterate through the entry's tree children, if any"""
653
path = self._tree_id_paths[parent_id]
657
children = os.listdir(self._tree.abspath(path))
659
if not (osutils._is_error_enotdir(e)
660
or e.errno in (errno.ENOENT, errno.ESRCH)):
664
for child in children:
665
childpath = joinpath(path, child)
666
if self._tree.is_control_filename(childpath):
668
yield self.trans_id_tree_path(childpath)
670
def has_named_child(self, by_parent, parent_id, name):
672
children = by_parent[parent_id]
675
for child in children:
676
if self.final_name(child) == name:
679
path = self._tree_id_paths[parent_id]
682
childpath = joinpath(path, name)
683
child_id = self._tree_path_ids.get(childpath)
685
return lexists(self._tree.abspath(childpath))
687
if self.final_parent(child_id) != parent_id:
689
if child_id in self._removed_contents:
690
# XXX What about dangling file-ids?
695
def _parent_loops(self):
696
"""No entry should be its own ancestor"""
698
for trans_id in self._new_parent:
701
while parent_id is not ROOT_PARENT:
704
parent_id = self.final_parent(parent_id)
707
if parent_id == trans_id:
708
conflicts.append(('parent loop', trans_id))
709
if parent_id in seen:
713
def _unversioned_parents(self, by_parent):
714
"""If parent directories are versioned, children must be versioned."""
716
for parent_id, children in by_parent.iteritems():
717
if parent_id is ROOT_PARENT:
719
if self.final_file_id(parent_id) is not None:
721
for child_id in children:
722
if self.final_file_id(child_id) is not None:
723
conflicts.append(('unversioned parent', parent_id))
727
def _improper_versioning(self):
728
"""Cannot version a file with no contents, or a bad type.
730
However, existing entries with no contents are okay.
733
for trans_id in self._new_id.iterkeys():
735
kind = self.final_kind(trans_id)
737
conflicts.append(('versioning no contents', trans_id))
739
if not InventoryEntry.versionable_kind(kind):
740
conflicts.append(('versioning bad kind', trans_id, kind))
743
def _executability_conflicts(self):
744
"""Check for bad executability changes.
746
Only versioned files may have their executability set, because
747
1. only versioned entries can have executability under windows
748
2. only files can be executable. (The execute bit on a directory
749
does not indicate searchability)
752
for trans_id in self._new_executability:
753
if self.final_file_id(trans_id) is None:
754
conflicts.append(('unversioned executability', trans_id))
757
non_file = self.final_kind(trans_id) != "file"
761
conflicts.append(('non-file executability', trans_id))
764
def _overwrite_conflicts(self):
765
"""Check for overwrites (not permitted on Win32)"""
767
for trans_id in self._new_contents:
769
self.tree_kind(trans_id)
772
if trans_id not in self._removed_contents:
773
conflicts.append(('overwrite', trans_id,
774
self.final_name(trans_id)))
777
def _duplicate_entries(self, by_parent):
778
"""No directory may have two entries with the same name."""
780
if (self._new_name, self._new_parent) == ({}, {}):
782
for children in by_parent.itervalues():
783
name_ids = [(self.final_name(t), t) for t in children]
784
if not self._case_sensitive_target:
785
name_ids = [(n.lower(), t) for n, t in name_ids]
789
for name, trans_id in name_ids:
791
kind = self.final_kind(trans_id)
794
file_id = self.final_file_id(trans_id)
795
if kind is None and file_id is None:
797
if name == last_name:
798
conflicts.append(('duplicate', last_trans_id, trans_id,
801
last_trans_id = trans_id
804
def _duplicate_ids(self):
805
"""Each inventory id may only be used once"""
807
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
809
all_ids = self._tree.all_file_ids()
810
active_tree_ids = all_ids.difference(removed_tree_ids)
811
for trans_id, file_id in self._new_id.iteritems():
812
if file_id in active_tree_ids:
813
old_trans_id = self.trans_id_tree_file_id(file_id)
814
conflicts.append(('duplicate id', old_trans_id, trans_id))
817
def _parent_type_conflicts(self, by_parent):
818
"""parents must have directory 'contents'."""
820
for parent_id, children in by_parent.iteritems():
821
if parent_id is ROOT_PARENT:
823
if not self._any_contents(children):
825
for child in children:
827
self.final_kind(child)
831
kind = self.final_kind(parent_id)
835
conflicts.append(('missing parent', parent_id))
836
elif kind != "directory":
837
conflicts.append(('non-directory parent', parent_id))
840
def _any_contents(self, trans_ids):
841
"""Return true if any of the trans_ids, will have contents."""
842
for trans_id in trans_ids:
844
kind = self.final_kind(trans_id)
850
def _limbo_name(self, trans_id):
851
"""Generate the limbo name of a file"""
852
limbo_name = self._limbo_files.get(trans_id)
853
if limbo_name is not None:
855
parent = self._new_parent.get(trans_id)
856
# if the parent directory is already in limbo (e.g. when building a
857
# tree), choose a limbo name inside the parent, to reduce further
859
use_direct_path = False
860
if self._new_contents.get(parent) == 'directory':
861
filename = self._new_name.get(trans_id)
862
if filename is not None:
863
if parent not in self._limbo_children:
864
self._limbo_children[parent] = set()
865
self._limbo_children_names[parent] = {}
866
use_direct_path = True
867
# the direct path can only be used if no other file has
868
# already taken this pathname, i.e. if the name is unused, or
869
# if it is already associated with this trans_id.
870
elif self._case_sensitive_target:
871
if (self._limbo_children_names[parent].get(filename)
872
in (trans_id, None)):
873
use_direct_path = True
875
for l_filename, l_trans_id in\
876
self._limbo_children_names[parent].iteritems():
877
if l_trans_id == trans_id:
879
if l_filename.lower() == filename.lower():
882
use_direct_path = True
885
limbo_name = pathjoin(self._limbo_files[parent], filename)
886
self._limbo_children[parent].add(trans_id)
887
self._limbo_children_names[parent][filename] = trans_id
889
limbo_name = pathjoin(self._limbodir, trans_id)
890
self._needs_rename.add(trans_id)
891
self._limbo_files[trans_id] = limbo_name
894
def _set_executability(self, path, trans_id):
895
"""Set the executability of versioned files """
896
if supports_executable():
897
new_executability = self._new_executability[trans_id]
898
abspath = self._tree.abspath(path)
899
current_mode = os.stat(abspath).st_mode
900
if new_executability:
903
to_mode = current_mode | (0100 & ~umask)
904
# Enable x-bit for others only if they can read it.
905
if current_mode & 0004:
906
to_mode |= 0001 & ~umask
907
if current_mode & 0040:
908
to_mode |= 0010 & ~umask
910
to_mode = current_mode & ~0111
911
os.chmod(abspath, to_mode)
913
def _new_entry(self, name, parent_id, file_id):
914
"""Helper function to create a new filesystem entry."""
915
trans_id = self.create_path(name, parent_id)
916
if file_id is not None:
917
self.version_file(file_id, trans_id)
920
def new_file(self, name, parent_id, contents, file_id=None,
922
"""Convenience method to create files.
924
name is the name of the file to create.
925
parent_id is the transaction id of the parent directory of the file.
926
contents is an iterator of bytestrings, which will be used to produce
928
:param file_id: The inventory ID of the file, if it is to be versioned.
929
:param executable: Only valid when a file_id has been supplied.
931
trans_id = self._new_entry(name, parent_id, file_id)
932
# TODO: rather than scheduling a set_executable call,
933
# have create_file create the file with the right mode.
934
self.create_file(contents, trans_id)
935
if executable is not None:
936
self.set_executability(executable, trans_id)
939
def new_directory(self, name, parent_id, file_id=None):
940
"""Convenience method to create directories.
942
name is the name of the directory to create.
943
parent_id is the transaction id of the parent directory of the
945
file_id is the inventory ID of the directory, if it is to be versioned.
947
trans_id = self._new_entry(name, parent_id, file_id)
948
self.create_directory(trans_id)
951
def new_symlink(self, name, parent_id, target, file_id=None):
952
"""Convenience method to create symbolic link.
954
name is the name of the symlink to create.
955
parent_id is the transaction id of the parent directory of the symlink.
956
target is a bytestring of the target of the symlink.
957
file_id is the inventory ID of the file, if it is to be versioned.
959
trans_id = self._new_entry(name, parent_id, file_id)
960
self.create_symlink(target, trans_id)
963
def _affected_ids(self):
964
"""Return the set of transform ids affected by the transform"""
965
trans_ids = set(self._removed_id)
966
trans_ids.update(self._new_id.keys())
967
trans_ids.update(self._removed_contents)
968
trans_ids.update(self._new_contents.keys())
969
trans_ids.update(self._new_executability.keys())
970
trans_ids.update(self._new_name.keys())
971
trans_ids.update(self._new_parent.keys())
974
def _get_file_id_maps(self):
975
"""Return mapping of file_ids to trans_ids in the to and from states"""
976
trans_ids = self._affected_ids()
979
# Build up two dicts: trans_ids associated with file ids in the
980
# FROM state, vs the TO state.
981
for trans_id in trans_ids:
982
from_file_id = self.tree_file_id(trans_id)
983
if from_file_id is not None:
984
from_trans_ids[from_file_id] = trans_id
985
to_file_id = self.final_file_id(trans_id)
986
if to_file_id is not None:
987
to_trans_ids[to_file_id] = trans_id
988
return from_trans_ids, to_trans_ids
990
def _from_file_data(self, from_trans_id, from_versioned, file_id):
991
"""Get data about a file in the from (tree) state
993
Return a (name, parent, kind, executable) tuple
995
from_path = self._tree_id_paths.get(from_trans_id)
997
# get data from working tree if versioned
998
from_entry = self._tree.inventory[file_id]
999
from_name = from_entry.name
1000
from_parent = from_entry.parent_id
1003
if from_path is None:
1004
# File does not exist in FROM state
1008
# File exists, but is not versioned. Have to use path-
1010
from_name = os.path.basename(from_path)
1011
tree_parent = self.get_tree_parent(from_trans_id)
1012
from_parent = self.tree_file_id(tree_parent)
1013
if from_path is not None:
1014
from_kind, from_executable, from_stats = \
1015
self._tree._comparison_data(from_entry, from_path)
1018
from_executable = False
1019
return from_name, from_parent, from_kind, from_executable
1021
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1022
"""Get data about a file in the to (target) state
1024
Return a (name, parent, kind, executable) tuple
1026
to_name = self.final_name(to_trans_id)
1028
to_kind = self.final_kind(to_trans_id)
1031
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1032
if to_trans_id in self._new_executability:
1033
to_executable = self._new_executability[to_trans_id]
1034
elif to_trans_id == from_trans_id:
1035
to_executable = from_executable
1037
to_executable = False
1038
return to_name, to_parent, to_kind, to_executable
1040
def iter_changes(self):
1041
"""Produce output in the same format as Tree.iter_changes.
1043
Will produce nonsensical results if invoked while inventory/filesystem
1044
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1046
This reads the Transform, but only reproduces changes involving a
1047
file_id. Files that are not versioned in either of the FROM or TO
1048
states are not reflected.
1050
final_paths = FinalPaths(self)
1051
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1053
# Now iterate through all active file_ids
1054
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1056
from_trans_id = from_trans_ids.get(file_id)
1057
# find file ids, and determine versioning state
1058
if from_trans_id is None:
1059
from_versioned = False
1060
from_trans_id = to_trans_ids[file_id]
1062
from_versioned = True
1063
to_trans_id = to_trans_ids.get(file_id)
1064
if to_trans_id is None:
1065
to_versioned = False
1066
to_trans_id = from_trans_id
1070
from_name, from_parent, from_kind, from_executable = \
1071
self._from_file_data(from_trans_id, from_versioned, file_id)
1073
to_name, to_parent, to_kind, to_executable = \
1074
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1076
if not from_versioned:
1079
from_path = self._tree_id_paths.get(from_trans_id)
1080
if not to_versioned:
1083
to_path = final_paths.get_path(to_trans_id)
1084
if from_kind != to_kind:
1086
elif to_kind in ('file', 'symlink') and (
1087
to_trans_id != from_trans_id or
1088
to_trans_id in self._new_contents):
1090
if (not modified and from_versioned == to_versioned and
1091
from_parent==to_parent and from_name == to_name and
1092
from_executable == to_executable):
1094
results.append((file_id, (from_path, to_path), modified,
1095
(from_versioned, to_versioned),
1096
(from_parent, to_parent),
1097
(from_name, to_name),
1098
(from_kind, to_kind),
1099
(from_executable, to_executable)))
1100
return iter(sorted(results, key=lambda x:x[1]))
1102
def get_preview_tree(self):
1103
"""Return a tree representing the result of the transform.
1105
This tree only supports the subset of Tree functionality required
1106
by show_diff_trees. It must only be compared to tt._tree.
1108
return _PreviewTree(self)
1111
class TreeTransform(TreeTransformBase):
1112
"""Represent a tree transformation.
1114
This object is designed to support incremental generation of the transform,
1117
However, it gives optimum performance when parent directories are created
1118
before their contents. The transform is then able to put child files
1119
directly in their parent directory, avoiding later renames.
1121
It is easy to produce malformed transforms, but they are generally
1122
harmless. Attempting to apply a malformed transform will cause an
1123
exception to be raised before any modifications are made to the tree.
1125
Many kinds of malformed transforms can be corrected with the
1126
resolve_conflicts function. The remaining ones indicate programming error,
1127
such as trying to create a file with no path.
1129
Two sets of file creation methods are supplied. Convenience methods are:
1134
These are composed of the low-level methods:
1136
* create_file or create_directory or create_symlink
1140
Transform/Transaction ids
1141
-------------------------
1142
trans_ids are temporary ids assigned to all files involved in a transform.
1143
It's possible, even common, that not all files in the Tree have trans_ids.
1145
trans_ids are used because filenames and file_ids are not good enough
1146
identifiers; filenames change, and not all files have file_ids. File-ids
1147
are also associated with trans-ids, so that moving a file moves its
1150
trans_ids are only valid for the TreeTransform that generated them.
1154
Limbo is a temporary directory use to hold new versions of files.
1155
Files are added to limbo by create_file, create_directory, create_symlink,
1156
and their convenience variants (new_*). Files may be removed from limbo
1157
using cancel_creation. Files are renamed from limbo into their final
1158
location as part of TreeTransform.apply
1160
Limbo must be cleaned up, by either calling TreeTransform.apply or
1161
calling TreeTransform.finalize.
1163
Files are placed into limbo inside their parent directories, where
1164
possible. This reduces subsequent renames, and makes operations involving
1165
lots of files faster. This optimization is only possible if the parent
1166
directory is created *before* creating any of its children, so avoid
1167
creating children before parents, where possible.
1171
This temporary directory is used by _FileMover for storing files that are
1172
about to be deleted. In case of rollback, the files will be restored.
1173
FileMover does not delete files until it is sure that a rollback will not
1176
def __init__(self, tree, pb=DummyProgress()):
1177
"""Note: a tree_write lock is taken on the tree.
1179
Use TreeTransform.finalize() to release the lock (can be omitted if
1180
TreeTransform.apply() called).
1182
tree.lock_tree_write()
1185
limbodir = urlutils.local_path_from_url(
1186
tree._transport.abspath('limbo'))
1190
if e.errno == errno.EEXIST:
1191
raise ExistingLimbo(limbodir)
1192
deletiondir = urlutils.local_path_from_url(
1193
tree._transport.abspath('pending-deletion'))
1195
os.mkdir(deletiondir)
1197
if e.errno == errno.EEXIST:
1198
raise errors.ExistingPendingDeletion(deletiondir)
1203
TreeTransformBase.__init__(self, tree, limbodir, pb,
1204
tree.case_sensitive)
1205
self._deletiondir = deletiondir
1207
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1208
"""Apply all changes to the inventory and filesystem.
1210
If filesystem or inventory conflicts are present, MalformedTransform
1213
If apply succeeds, finalize is not necessary.
1215
:param no_conflicts: if True, the caller guarantees there are no
1216
conflicts, so no check is made.
1217
:param precomputed_delta: An inventory delta to use instead of
1219
:param _mover: Supply an alternate FileMover, for testing
1221
if not no_conflicts:
1222
conflicts = self.find_conflicts()
1223
if len(conflicts) != 0:
1224
raise MalformedTransform(conflicts=conflicts)
1225
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1227
if precomputed_delta is None:
1228
child_pb.update('Apply phase', 0, 2)
1229
inventory_delta = self._generate_inventory_delta()
1232
inventory_delta = precomputed_delta
1235
mover = _FileMover()
1239
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1240
self._apply_removals(mover)
1241
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1242
modified_paths = self._apply_insertions(mover)
1247
mover.apply_deletions()
1250
self._tree.apply_inventory_delta(inventory_delta)
1253
return _TransformResults(modified_paths, self.rename_count)
1255
def _generate_inventory_delta(self):
1256
"""Generate an inventory delta for the current transform."""
1257
inventory_delta = []
1258
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1259
new_paths = self._inventory_altered()
1260
total_entries = len(new_paths) + len(self._removed_id)
1262
for num, trans_id in enumerate(self._removed_id):
1264
child_pb.update('removing file', num, total_entries)
1265
if trans_id == self._new_root:
1266
file_id = self._tree.get_root_id()
1268
file_id = self.tree_file_id(trans_id)
1269
# File-id isn't really being deleted, just moved
1270
if file_id in self._r_new_id:
1272
path = self._tree_id_paths[trans_id]
1273
inventory_delta.append((path, None, file_id, None))
1274
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1276
entries = self._tree.iter_entries_by_dir(
1277
new_path_file_ids.values())
1278
old_paths = dict((e.file_id, p) for p, e in entries)
1280
for num, (path, trans_id) in enumerate(new_paths):
1282
child_pb.update('adding file',
1283
num + len(self._removed_id), total_entries)
1284
file_id = new_path_file_ids[trans_id]
1289
kind = self.final_kind(trans_id)
1291
kind = self._tree.stored_kind(file_id)
1292
parent_trans_id = self.final_parent(trans_id)
1293
parent_file_id = new_path_file_ids.get(parent_trans_id)
1294
if parent_file_id is None:
1295
parent_file_id = self.final_file_id(parent_trans_id)
1296
if trans_id in self._new_reference_revision:
1297
new_entry = inventory.TreeReference(
1299
self._new_name[trans_id],
1300
self.final_file_id(self._new_parent[trans_id]),
1301
None, self._new_reference_revision[trans_id])
1303
new_entry = inventory.make_entry(kind,
1304
self.final_name(trans_id),
1305
parent_file_id, file_id)
1306
old_path = old_paths.get(new_entry.file_id)
1307
new_executability = self._new_executability.get(trans_id)
1308
if new_executability is not None:
1309
new_entry.executable = new_executability
1310
inventory_delta.append(
1311
(old_path, path, new_entry.file_id, new_entry))
1314
return inventory_delta
1316
def _apply_removals(self, mover):
1317
"""Perform tree operations that remove directory/inventory names.
1319
That is, delete files that are to be deleted, and put any files that
1320
need renaming into limbo. This must be done in strict child-to-parent
1323
If inventory_delta is None, no inventory delta generation is performed.
1325
tree_paths = list(self._tree_path_ids.iteritems())
1326
tree_paths.sort(reverse=True)
1327
kind_changes = set()
1328
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1330
for num, data in enumerate(tree_paths):
1331
path, trans_id = data
1332
child_pb.update('removing file', num, len(tree_paths))
1333
full_path = self._tree.abspath(path)
1334
if trans_id in self._removed_contents:
1335
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1337
elif trans_id in self._new_name or trans_id in \
1340
mover.rename(full_path, self._limbo_name(trans_id))
1342
if e.errno != errno.ENOENT:
1345
self.rename_count += 1
1350
def _apply_insertions(self, mover):
1351
"""Perform tree operations that insert directory/inventory names.
1353
That is, create any files that need to be created, and restore from
1354
limbo any files that needed renaming. This must be done in strict
1355
parent-to-child order.
1357
If inventory_delta is None, no inventory delta is calculated, and
1358
no list of modified paths is returned.
1360
kind_changes is a set of trans ids where the entry has changed
1361
kind, and so an inventory delta entry should be created for them.
1363
new_paths = self.new_paths(filesystem_only=True)
1365
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1367
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1369
for num, (path, trans_id) in enumerate(new_paths):
1371
child_pb.update('adding file', num, len(new_paths))
1372
full_path = self._tree.abspath(path)
1373
if trans_id in self._needs_rename:
1375
mover.rename(self._limbo_name(trans_id), full_path)
1377
# We may be renaming a dangling inventory id
1378
if e.errno != errno.ENOENT:
1381
self.rename_count += 1
1382
if (trans_id in self._new_contents or
1383
self.path_changed(trans_id)):
1384
if trans_id in self._new_contents:
1385
modified_paths.append(full_path)
1386
if trans_id in self._new_executability:
1387
self._set_executability(path, trans_id)
1390
self._new_contents.clear()
1391
return modified_paths
1394
class TransformPreview(TreeTransformBase):
1395
"""A TreeTransform for generating preview trees.
1397
Unlike TreeTransform, this version works when the input tree is a
1398
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1399
unversioned files in the input tree.
1402
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1404
limbodir = tempfile.mkdtemp(prefix='bzr-limbo-')
1405
TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1407
def canonical_path(self, path):
1410
def tree_kind(self, trans_id):
1411
path = self._tree_id_paths.get(trans_id)
1413
raise NoSuchFile(None)
1414
file_id = self._tree.path2id(path)
1415
return self._tree.kind(file_id)
1417
def _set_mode(self, trans_id, mode_id, typefunc):
1418
"""Set the mode of new file contents.
1419
The mode_id is the existing file to get the mode from (often the same
1420
as trans_id). The operation is only performed if there's a mode match
1421
according to typefunc.
1423
# is it ok to ignore this? probably
1426
def iter_tree_children(self, parent_id):
1427
"""Iterate through the entry's tree children, if any"""
1429
path = self._tree_id_paths[parent_id]
1432
file_id = self.tree_file_id(parent_id)
1435
children = getattr(self._tree.inventory[file_id], 'children', {})
1436
for child in children:
1437
childpath = joinpath(path, child)
1438
yield self.trans_id_tree_path(childpath)
1441
class _PreviewTree(tree.Tree):
1442
"""Partial implementation of Tree to support show_diff_trees"""
1444
def __init__(self, transform):
1445
self._transform = transform
1446
self._final_paths = FinalPaths(transform)
1447
self.__by_parent = None
1448
self._parent_ids = []
1450
def _changes(self, file_id):
1451
for changes in self._transform.iter_changes():
1452
if changes[0] == file_id:
1455
def _content_change(self, file_id):
1456
"""Return True if the content of this file changed"""
1457
changes = self._changes(file_id)
1458
# changes[2] is true if the file content changed. See
1459
# InterTree.iter_changes.
1460
return (changes is not None and changes[2])
1462
def _get_repository(self):
1463
repo = getattr(self._transform._tree, '_repository', None)
1465
repo = self._transform._tree.branch.repository
1468
def _iter_parent_trees(self):
1469
for revision_id in self.get_parent_ids():
1471
yield self.revision_tree(revision_id)
1472
except errors.NoSuchRevisionInTree:
1473
yield self._get_repository().revision_tree(revision_id)
1475
def _get_file_revision(self, file_id, vf, tree_revision):
1476
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1477
self._iter_parent_trees()]
1478
vf.add_lines((file_id, tree_revision), parent_keys,
1479
self.get_file(file_id).readlines())
1480
repo = self._get_repository()
1481
base_vf = repo.texts
1482
if base_vf not in vf.fallback_versionedfiles:
1483
vf.fallback_versionedfiles.append(base_vf)
1484
return tree_revision
1486
def _stat_limbo_file(self, file_id):
1487
trans_id = self._transform.trans_id_file_id(file_id)
1488
name = self._transform._limbo_name(trans_id)
1489
return os.lstat(name)
1492
def _by_parent(self):
1493
if self.__by_parent is None:
1494
self.__by_parent = self._transform.by_parent()
1495
return self.__by_parent
1497
def lock_read(self):
1498
# Perhaps in theory, this should lock the TreeTransform?
1505
def inventory(self):
1506
"""This Tree does not use inventory as its backing data."""
1507
raise NotImplementedError(_PreviewTree.inventory)
1509
def get_root_id(self):
1510
return self._transform.final_file_id(self._transform.root)
1512
def all_file_ids(self):
1513
tree_ids = set(self._transform._tree.all_file_ids())
1514
tree_ids.difference_update(self._transform.tree_file_id(t)
1515
for t in self._transform._removed_id)
1516
tree_ids.update(self._transform._new_id.values())
1520
return iter(self.all_file_ids())
1522
def paths2ids(self, specific_files, trees=None, require_versioned=False):
1523
"""See Tree.paths2ids"""
1524
to_find = set(specific_files)
1526
for (file_id, paths, changed, versioned, parent, name, kind,
1527
executable) in self._transform.iter_changes():
1528
if paths[1] in to_find:
1529
result.append(file_id)
1530
to_find.remove(paths[1])
1531
result.update(self._transform._tree.paths2ids(to_find,
1532
trees=[], require_versioned=require_versioned))
1535
def _path2trans_id(self, path):
1536
segments = splitpath(path)
1537
cur_parent = self._transform.root
1538
for cur_segment in segments:
1539
for child in self._all_children(cur_parent):
1540
if self._transform.final_name(child) == cur_segment:
1547
def path2id(self, path):
1548
return self._transform.final_file_id(self._path2trans_id(path))
1550
def id2path(self, file_id):
1551
trans_id = self._transform.trans_id_file_id(file_id)
1553
return self._final_paths._determine_path(trans_id)
1555
raise errors.NoSuchId(self, file_id)
1557
def _all_children(self, trans_id):
1558
children = set(self._transform.iter_tree_children(trans_id))
1559
# children in the _new_parent set are provided by _by_parent.
1560
children.difference_update(self._transform._new_parent.keys())
1561
children.update(self._by_parent.get(trans_id, []))
1564
def _make_inv_entries(self, ordered_entries, specific_file_ids):
1565
for trans_id, parent_file_id in ordered_entries:
1566
file_id = self._transform.final_file_id(trans_id)
1569
if (specific_file_ids is not None
1570
and file_id not in specific_file_ids):
1573
kind = self._transform.final_kind(trans_id)
1575
kind = self._transform._tree.stored_kind(file_id)
1576
new_entry = inventory.make_entry(
1578
self._transform.final_name(trans_id),
1579
parent_file_id, file_id)
1580
yield new_entry, trans_id
1582
def iter_entries_by_dir(self, specific_file_ids=None):
1583
# This may not be a maximally efficient implementation, but it is
1584
# reasonably straightforward. An implementation that grafts the
1585
# TreeTransform changes onto the tree's iter_entries_by_dir results
1586
# might be more efficient, but requires tricky inferences about stack
1588
todo = [ROOT_PARENT]
1590
while len(todo) > 0:
1592
parent_file_id = self._transform.final_file_id(parent)
1593
children = list(self._all_children(parent))
1594
paths = dict(zip(children, self._final_paths.get_paths(children)))
1595
children.sort(key=paths.get)
1596
todo.extend(reversed(children))
1597
for trans_id in children:
1598
ordered_ids.append((trans_id, parent_file_id))
1599
for entry, trans_id in self._make_inv_entries(ordered_ids,
1601
yield unicode(self._final_paths.get_path(trans_id)), entry
1603
def kind(self, file_id):
1604
trans_id = self._transform.trans_id_file_id(file_id)
1605
return self._transform.final_kind(trans_id)
1607
def stored_kind(self, file_id):
1608
trans_id = self._transform.trans_id_file_id(file_id)
1610
return self._transform._new_contents[trans_id]
1612
return self._transform._tree.stored_kind(file_id)
1614
def get_file_mtime(self, file_id, path=None):
1615
"""See Tree.get_file_mtime"""
1616
if not self._content_change(file_id):
1617
return self._transform._tree.get_file_mtime(file_id, path)
1618
return self._stat_limbo_file(file_id).st_mtime
1620
def get_file_size(self, file_id):
1621
"""See Tree.get_file_size"""
1622
if self.kind(file_id) == 'file':
1623
return self._transform._tree.get_file_size(file_id)
1627
def get_file_sha1(self, file_id, path=None, stat_value=None):
1628
return self._transform._tree.get_file_sha1(file_id)
1630
def is_executable(self, file_id, path=None):
1631
trans_id = self._transform.trans_id_file_id(file_id)
1633
return self._transform._new_executability[trans_id]
1635
return self._transform._tree.is_executable(file_id, path)
1637
def path_content_summary(self, path):
1638
trans_id = self._path2trans_id(path)
1639
tt = self._transform
1640
tree_path = tt._tree_id_paths.get(trans_id)
1641
kind = tt._new_contents.get(trans_id)
1643
if tree_path is None or trans_id in tt._removed_contents:
1644
return 'missing', None, None, None
1645
summary = tt._tree.path_content_summary(tree_path)
1646
kind, size, executable, link_or_sha1 = summary
1649
limbo_name = tt._limbo_name(trans_id)
1650
if trans_id in tt._new_reference_revision:
1651
kind = 'tree-reference'
1653
statval = os.lstat(limbo_name)
1654
size = statval.st_size
1655
if not supports_executable():
1658
executable = statval.st_mode & S_IEXEC
1662
if kind == 'symlink':
1663
link_or_sha1 = os.readlink(limbo_name)
1664
if supports_executable():
1665
executable = tt._new_executability.get(trans_id, executable)
1666
return kind, size, executable, link_or_sha1
1668
def iter_changes(self, from_tree, include_unchanged=False,
1669
specific_files=None, pb=None, extra_trees=None,
1670
require_versioned=True, want_unversioned=False):
1671
"""See InterTree.iter_changes.
1673
This implementation does not support include_unchanged, specific_files,
1674
or want_unversioned. extra_trees, require_versioned, and pb are
1677
if from_tree is not self._transform._tree:
1678
raise ValueError('from_tree must be transform source tree.')
1679
if include_unchanged:
1680
raise ValueError('include_unchanged is not supported')
1681
if specific_files is not None:
1682
raise ValueError('specific_files is not supported')
1683
if want_unversioned:
1684
raise ValueError('want_unversioned is not supported')
1685
return self._transform.iter_changes()
1687
def get_file(self, file_id, path=None):
1688
"""See Tree.get_file"""
1689
if not self._content_change(file_id):
1690
return self._transform._tree.get_file(file_id, path)
1691
trans_id = self._transform.trans_id_file_id(file_id)
1692
name = self._transform._limbo_name(trans_id)
1693
return open(name, 'rb')
1695
def get_file_text(self, file_id):
1696
text_file = self.get_file(file_id)
1698
return text_file.read()
1702
def annotate_iter(self, file_id,
1703
default_revision=_mod_revision.CURRENT_REVISION):
1704
changes = self._changes(file_id)
1708
changed_content, versioned, kind = (changes[2], changes[3],
1712
get_old = (kind[0] == 'file' and versioned[0])
1714
old_annotation = self._transform._tree.annotate_iter(file_id,
1715
default_revision=default_revision)
1719
return old_annotation
1720
if not changed_content:
1721
return old_annotation
1722
return annotate.reannotate([old_annotation],
1723
self.get_file(file_id).readlines(),
1726
def get_symlink_target(self, file_id):
1727
"""See Tree.get_symlink_target"""
1728
if not self._content_change(file_id):
1729
return self._transform._tree.get_symlink_target(file_id)
1730
trans_id = self._transform.trans_id_file_id(file_id)
1731
name = self._transform._limbo_name(trans_id)
1732
return os.readlink(name)
1734
def list_files(self, include_root=False):
1735
return self._transform._tree.list_files(include_root)
1737
def walkdirs(self, prefix=""):
1738
return self._transform._tree.walkdirs(prefix)
1740
def get_parent_ids(self):
1741
return self._parent_ids
1743
def set_parent_ids(self, parent_ids):
1744
self._parent_ids = parent_ids
1746
def get_revision_tree(self, revision_id):
1747
return self._transform._tree.get_revision_tree(revision_id)
1750
def joinpath(parent, child):
1751
"""Join tree-relative paths, handling the tree root specially"""
1752
if parent is None or parent == "":
1755
return pathjoin(parent, child)
1758
class FinalPaths(object):
1759
"""Make path calculation cheap by memoizing paths.
1761
The underlying tree must not be manipulated between calls, or else
1762
the results will likely be incorrect.
1764
def __init__(self, transform):
1765
object.__init__(self)
1766
self._known_paths = {}
1767
self.transform = transform
1769
def _determine_path(self, trans_id):
1770
if trans_id == self.transform.root:
1772
name = self.transform.final_name(trans_id)
1773
parent_id = self.transform.final_parent(trans_id)
1774
if parent_id == self.transform.root:
1777
return pathjoin(self.get_path(parent_id), name)
1779
def get_path(self, trans_id):
1780
"""Find the final path associated with a trans_id"""
1781
if trans_id not in self._known_paths:
1782
self._known_paths[trans_id] = self._determine_path(trans_id)
1783
return self._known_paths[trans_id]
1785
def get_paths(self, trans_ids):
1786
return [(self.get_path(t), t) for t in trans_ids]
1790
def topology_sorted_ids(tree):
1791
"""Determine the topological order of the ids in a tree"""
1792
file_ids = list(tree)
1793
file_ids.sort(key=tree.id2path)
1797
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1798
delta_from_tree=False):
1799
"""Create working tree for a branch, using a TreeTransform.
1801
This function should be used on empty trees, having a tree root at most.
1802
(see merge and revert functionality for working with existing trees)
1804
Existing files are handled like so:
1806
- Existing bzrdirs take precedence over creating new items. They are
1807
created as '%s.diverted' % name.
1808
- Otherwise, if the content on disk matches the content we are building,
1809
it is silently replaced.
1810
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1812
:param tree: The tree to convert wt into a copy of
1813
:param wt: The working tree that files will be placed into
1814
:param accelerator_tree: A tree which can be used for retrieving file
1815
contents more quickly than tree itself, i.e. a workingtree. tree
1816
will be used for cases where accelerator_tree's content is different.
1817
:param hardlink: If true, hard-link files to accelerator_tree, where
1818
possible. accelerator_tree must implement abspath, i.e. be a
1820
:param delta_from_tree: If true, build_tree may use the input Tree to
1821
generate the inventory delta.
1823
wt.lock_tree_write()
1827
if accelerator_tree is not None:
1828
accelerator_tree.lock_read()
1830
return _build_tree(tree, wt, accelerator_tree, hardlink,
1833
if accelerator_tree is not None:
1834
accelerator_tree.unlock()
1841
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1842
"""See build_tree."""
1843
for num, _unused in enumerate(wt.all_file_ids()):
1844
if num > 0: # more than just a root
1845
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1846
existing_files = set()
1847
for dir, files in wt.walkdirs():
1848
existing_files.update(f[0] for f in files)
1850
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1851
pp = ProgressPhase("Build phase", 2, top_pb)
1852
if tree.inventory.root is not None:
1853
# This is kind of a hack: we should be altering the root
1854
# as part of the regular tree shape diff logic.
1855
# The conditional test here is to avoid doing an
1856
# expensive operation (flush) every time the root id
1857
# is set within the tree, nor setting the root and thus
1858
# marking the tree as dirty, because we use two different
1859
# idioms here: tree interfaces and inventory interfaces.
1860
if wt.get_root_id() != tree.get_root_id():
1861
wt.set_root_id(tree.get_root_id())
1863
tt = TreeTransform(wt)
1867
file_trans_id[wt.get_root_id()] = \
1868
tt.trans_id_tree_file_id(wt.get_root_id())
1869
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1871
deferred_contents = []
1873
total = len(tree.inventory)
1875
precomputed_delta = []
1877
precomputed_delta = None
1878
for num, (tree_path, entry) in \
1879
enumerate(tree.inventory.iter_entries_by_dir()):
1880
pb.update("Building tree", num - len(deferred_contents), total)
1881
if entry.parent_id is None:
1884
file_id = entry.file_id
1886
precomputed_delta.append((None, tree_path, file_id, entry))
1887
if tree_path in existing_files:
1888
target_path = wt.abspath(tree_path)
1889
kind = file_kind(target_path)
1890
if kind == "directory":
1892
bzrdir.BzrDir.open(target_path)
1893
except errors.NotBranchError:
1897
if (file_id not in divert and
1898
_content_match(tree, entry, file_id, kind,
1900
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1901
if kind == 'directory':
1903
parent_id = file_trans_id[entry.parent_id]
1904
if entry.kind == 'file':
1905
# We *almost* replicate new_by_entry, so that we can defer
1906
# getting the file text, and get them all at once.
1907
trans_id = tt.create_path(entry.name, parent_id)
1908
file_trans_id[file_id] = trans_id
1909
tt.version_file(file_id, trans_id)
1910
executable = tree.is_executable(file_id, tree_path)
1912
tt.set_executability(executable, trans_id)
1913
deferred_contents.append((file_id, trans_id))
1915
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1918
new_trans_id = file_trans_id[file_id]
1919
old_parent = tt.trans_id_tree_path(tree_path)
1920
_reparent_children(tt, old_parent, new_trans_id)
1921
offset = num + 1 - len(deferred_contents)
1922
_create_files(tt, tree, deferred_contents, pb, offset,
1923
accelerator_tree, hardlink)
1927
divert_trans = set(file_trans_id[f] for f in divert)
1928
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1929
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1930
if len(raw_conflicts) > 0:
1931
precomputed_delta = None
1932
conflicts = cook_conflicts(raw_conflicts, tt)
1933
for conflict in conflicts:
1936
wt.add_conflicts(conflicts)
1937
except errors.UnsupportedOperation:
1939
result = tt.apply(no_conflicts=True,
1940
precomputed_delta=precomputed_delta)
1947
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
1949
total = len(desired_files) + offset
1950
if accelerator_tree is None:
1951
new_desired_files = desired_files
1953
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
1954
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
1955
in iter if not (c or e[0] != e[1]))
1956
new_desired_files = []
1958
for file_id, trans_id in desired_files:
1959
accelerator_path = unchanged.get(file_id)
1960
if accelerator_path is None:
1961
new_desired_files.append((file_id, trans_id))
1963
pb.update('Adding file contents', count + offset, total)
1965
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
1968
contents = accelerator_tree.get_file(file_id, accelerator_path)
1970
tt.create_file(contents, trans_id)
1975
for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
1976
new_desired_files)):
1977
tt.create_file(contents, trans_id)
1978
pb.update('Adding file contents', count + offset, total)
1981
def _reparent_children(tt, old_parent, new_parent):
1982
for child in tt.iter_tree_children(old_parent):
1983
tt.adjust_path(tt.final_name(child), new_parent, child)
1985
def _reparent_transform_children(tt, old_parent, new_parent):
1986
by_parent = tt.by_parent()
1987
for child in by_parent[old_parent]:
1988
tt.adjust_path(tt.final_name(child), new_parent, child)
1989
return by_parent[old_parent]
1991
def _content_match(tree, entry, file_id, kind, target_path):
1992
if entry.kind != kind:
1994
if entry.kind == "directory":
1996
if entry.kind == "file":
1997
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1999
elif entry.kind == "symlink":
2000
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2005
def resolve_checkout(tt, conflicts, divert):
2006
new_conflicts = set()
2007
for c_type, conflict in ((c[0], c) for c in conflicts):
2008
# Anything but a 'duplicate' would indicate programmer error
2009
if c_type != 'duplicate':
2010
raise AssertionError(c_type)
2011
# Now figure out which is new and which is old
2012
if tt.new_contents(conflict[1]):
2013
new_file = conflict[1]
2014
old_file = conflict[2]
2016
new_file = conflict[2]
2017
old_file = conflict[1]
2019
# We should only get here if the conflict wasn't completely
2021
final_parent = tt.final_parent(old_file)
2022
if new_file in divert:
2023
new_name = tt.final_name(old_file)+'.diverted'
2024
tt.adjust_path(new_name, final_parent, new_file)
2025
new_conflicts.add((c_type, 'Diverted to',
2026
new_file, old_file))
2028
new_name = tt.final_name(old_file)+'.moved'
2029
tt.adjust_path(new_name, final_parent, old_file)
2030
new_conflicts.add((c_type, 'Moved existing file to',
2031
old_file, new_file))
2032
return new_conflicts
2035
def new_by_entry(tt, entry, parent_id, tree):
2036
"""Create a new file according to its inventory entry"""
2040
contents = tree.get_file(entry.file_id).readlines()
2041
executable = tree.is_executable(entry.file_id)
2042
return tt.new_file(name, parent_id, contents, entry.file_id,
2044
elif kind in ('directory', 'tree-reference'):
2045
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2046
if kind == 'tree-reference':
2047
tt.set_tree_reference(entry.reference_revision, trans_id)
2049
elif kind == 'symlink':
2050
target = tree.get_symlink_target(entry.file_id)
2051
return tt.new_symlink(name, parent_id, target, entry.file_id)
2053
raise errors.BadFileKindError(name, kind)
2056
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2057
"""Create new file contents according to an inventory entry."""
2058
if entry.kind == "file":
2060
lines = tree.get_file(entry.file_id).readlines()
2061
tt.create_file(lines, trans_id, mode_id=mode_id)
2062
elif entry.kind == "symlink":
2063
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2064
elif entry.kind == "directory":
2065
tt.create_directory(trans_id)
2068
def create_entry_executability(tt, entry, trans_id):
2069
"""Set the executability of a trans_id according to an inventory entry"""
2070
if entry.kind == "file":
2071
tt.set_executability(entry.executable, trans_id)
2074
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2075
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2078
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2079
"""Produce a backup-style name that appears to be available"""
2083
yield "%s.~%d~" % (name, counter)
2085
for new_name in name_gen():
2086
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2090
def _entry_changes(file_id, entry, working_tree):
2091
"""Determine in which ways the inventory entry has changed.
2093
Returns booleans: has_contents, content_mod, meta_mod
2094
has_contents means there are currently contents, but they differ
2095
contents_mod means contents need to be modified
2096
meta_mod means the metadata needs to be modified
2098
cur_entry = working_tree.inventory[file_id]
2100
working_kind = working_tree.kind(file_id)
2103
has_contents = False
2106
if has_contents is True:
2107
if entry.kind != working_kind:
2108
contents_mod, meta_mod = True, False
2110
cur_entry._read_tree_state(working_tree.id2path(file_id),
2112
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2113
cur_entry._forget_tree_state()
2114
return has_contents, contents_mod, meta_mod
2117
def revert(working_tree, target_tree, filenames, backups=False,
2118
pb=DummyProgress(), change_reporter=None):
2119
"""Revert a working tree's contents to those of a target tree."""
2120
target_tree.lock_read()
2121
tt = TreeTransform(working_tree, pb)
2123
pp = ProgressPhase("Revert phase", 3, pb)
2125
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2127
merge_modified = _alter_files(working_tree, target_tree, tt,
2128
child_pb, filenames, backups)
2132
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2134
raw_conflicts = resolve_conflicts(tt, child_pb,
2135
lambda t, c: conflict_pass(t, c, target_tree))
2138
conflicts = cook_conflicts(raw_conflicts, tt)
2140
change_reporter = delta._ChangeReporter(
2141
unversioned_filter=working_tree.is_ignored)
2142
delta.report_changes(tt.iter_changes(), change_reporter)
2143
for conflict in conflicts:
2147
working_tree.set_merge_modified(merge_modified)
2149
target_tree.unlock()
2155
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2157
merge_modified = working_tree.merge_modified()
2158
change_list = target_tree.iter_changes(working_tree,
2159
specific_files=specific_files, pb=pb)
2160
if target_tree.inventory.root is None:
2167
for id_num, (file_id, path, changed_content, versioned, parent, name,
2168
kind, executable) in enumerate(change_list):
2169
if skip_root and file_id[0] is not None and parent[0] is None:
2171
trans_id = tt.trans_id_file_id(file_id)
2174
keep_content = False
2175
if kind[0] == 'file' and (backups or kind[1] is None):
2176
wt_sha1 = working_tree.get_file_sha1(file_id)
2177
if merge_modified.get(file_id) != wt_sha1:
2178
# acquire the basis tree lazily to prevent the
2179
# expense of accessing it when it's not needed ?
2180
# (Guessing, RBC, 200702)
2181
if basis_tree is None:
2182
basis_tree = working_tree.basis_tree()
2183
basis_tree.lock_read()
2184
if file_id in basis_tree:
2185
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2187
elif kind[1] is None and not versioned[1]:
2189
if kind[0] is not None:
2190
if not keep_content:
2191
tt.delete_contents(trans_id)
2192
elif kind[1] is not None:
2193
parent_trans_id = tt.trans_id_file_id(parent[0])
2194
by_parent = tt.by_parent()
2195
backup_name = _get_backup_name(name[0], by_parent,
2196
parent_trans_id, tt)
2197
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2198
new_trans_id = tt.create_path(name[0], parent_trans_id)
2199
if versioned == (True, True):
2200
tt.unversion_file(trans_id)
2201
tt.version_file(file_id, new_trans_id)
2202
# New contents should have the same unix perms as old
2205
trans_id = new_trans_id
2206
if kind[1] == 'directory':
2207
tt.create_directory(trans_id)
2208
elif kind[1] == 'symlink':
2209
tt.create_symlink(target_tree.get_symlink_target(file_id),
2211
elif kind[1] == 'file':
2212
deferred_files.append((file_id, (trans_id, mode_id)))
2213
if basis_tree is None:
2214
basis_tree = working_tree.basis_tree()
2215
basis_tree.lock_read()
2216
new_sha1 = target_tree.get_file_sha1(file_id)
2217
if (file_id in basis_tree and new_sha1 ==
2218
basis_tree.get_file_sha1(file_id)):
2219
if file_id in merge_modified:
2220
del merge_modified[file_id]
2222
merge_modified[file_id] = new_sha1
2224
# preserve the execute bit when backing up
2225
if keep_content and executable[0] == executable[1]:
2226
tt.set_executability(executable[1], trans_id)
2227
elif kind[1] is not None:
2228
raise AssertionError(kind[1])
2229
if versioned == (False, True):
2230
tt.version_file(file_id, trans_id)
2231
if versioned == (True, False):
2232
tt.unversion_file(trans_id)
2233
if (name[1] is not None and
2234
(name[0] != name[1] or parent[0] != parent[1])):
2236
name[1], tt.trans_id_file_id(parent[1]), trans_id)
2237
if executable[0] != executable[1] and kind[1] == "file":
2238
tt.set_executability(executable[1], trans_id)
2239
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2241
tt.create_file(bytes, trans_id, mode_id)
2243
if basis_tree is not None:
2245
return merge_modified
2248
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2249
"""Make many conflict-resolution attempts, but die if they fail"""
2250
if pass_func is None:
2251
pass_func = conflict_pass
2252
new_conflicts = set()
2255
pb.update('Resolution pass', n+1, 10)
2256
conflicts = tt.find_conflicts()
2257
if len(conflicts) == 0:
2258
return new_conflicts
2259
new_conflicts.update(pass_func(tt, conflicts))
2260
raise MalformedTransform(conflicts=conflicts)
2265
def conflict_pass(tt, conflicts, path_tree=None):
2266
"""Resolve some classes of conflicts.
2268
:param tt: The transform to resolve conflicts in
2269
:param conflicts: The conflicts to resolve
2270
:param path_tree: A Tree to get supplemental paths from
2272
new_conflicts = set()
2273
for c_type, conflict in ((c[0], c) for c in conflicts):
2274
if c_type == 'duplicate id':
2275
tt.unversion_file(conflict[1])
2276
new_conflicts.add((c_type, 'Unversioned existing file',
2277
conflict[1], conflict[2], ))
2278
elif c_type == 'duplicate':
2279
# files that were renamed take precedence
2280
final_parent = tt.final_parent(conflict[1])
2281
if tt.path_changed(conflict[1]):
2282
existing_file, new_file = conflict[2], conflict[1]
2284
existing_file, new_file = conflict[1], conflict[2]
2285
new_name = tt.final_name(existing_file)+'.moved'
2286
tt.adjust_path(new_name, final_parent, existing_file)
2287
new_conflicts.add((c_type, 'Moved existing file to',
2288
existing_file, new_file))
2289
elif c_type == 'parent loop':
2290
# break the loop by undoing one of the ops that caused the loop
2292
while not tt.path_changed(cur):
2293
cur = tt.final_parent(cur)
2294
new_conflicts.add((c_type, 'Cancelled move', cur,
2295
tt.final_parent(cur),))
2296
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2298
elif c_type == 'missing parent':
2299
trans_id = conflict[1]
2301
tt.cancel_deletion(trans_id)
2302
new_conflicts.add(('deleting parent', 'Not deleting',
2307
tt.final_name(trans_id)
2309
if path_tree is not None:
2310
file_id = tt.final_file_id(trans_id)
2312
file_id = tt.inactive_file_id(trans_id)
2313
entry = path_tree.inventory[file_id]
2314
# special-case the other tree root (move its
2315
# children to current root)
2316
if entry.parent_id is None:
2318
moved = _reparent_transform_children(
2319
tt, trans_id, tt.root)
2321
new_conflicts.add((c_type, 'Moved to root',
2324
parent_trans_id = tt.trans_id_file_id(
2326
tt.adjust_path(entry.name, parent_trans_id,
2329
tt.create_directory(trans_id)
2330
new_conflicts.add((c_type, 'Created directory', trans_id))
2331
elif c_type == 'unversioned parent':
2332
file_id = tt.inactive_file_id(conflict[1])
2333
# special-case the other tree root (move its children instead)
2334
if path_tree and file_id in path_tree:
2335
if path_tree.inventory[file_id].parent_id is None:
2337
tt.version_file(file_id, conflict[1])
2338
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2339
elif c_type == 'non-directory parent':
2340
parent_id = conflict[1]
2341
parent_parent = tt.final_parent(parent_id)
2342
parent_name = tt.final_name(parent_id)
2343
parent_file_id = tt.final_file_id(parent_id)
2344
new_parent_id = tt.new_directory(parent_name + '.new',
2345
parent_parent, parent_file_id)
2346
_reparent_transform_children(tt, parent_id, new_parent_id)
2347
if parent_file_id is not None:
2348
tt.unversion_file(parent_id)
2349
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2350
return new_conflicts
2353
def cook_conflicts(raw_conflicts, tt):
2354
"""Generate a list of cooked conflicts, sorted by file path"""
2355
from bzrlib.conflicts import Conflict
2356
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2357
return sorted(conflict_iter, key=Conflict.sort_key)
2360
def iter_cook_conflicts(raw_conflicts, tt):
2361
from bzrlib.conflicts import Conflict
2363
for conflict in raw_conflicts:
2364
c_type = conflict[0]
2365
action = conflict[1]
2366
modified_path = fp.get_path(conflict[2])
2367
modified_id = tt.final_file_id(conflict[2])
2368
if len(conflict) == 3:
2369
yield Conflict.factory(c_type, action=action, path=modified_path,
2370
file_id=modified_id)
2373
conflicting_path = fp.get_path(conflict[3])
2374
conflicting_id = tt.final_file_id(conflict[3])
2375
yield Conflict.factory(c_type, action=action, path=modified_path,
2376
file_id=modified_id,
2377
conflict_path=conflicting_path,
2378
conflict_file_id=conflicting_id)
2381
class _FileMover(object):
2382
"""Moves and deletes files for TreeTransform, tracking operations"""
2385
self.past_renames = []
2386
self.pending_deletions = []
2388
def rename(self, from_, to):
2389
"""Rename a file from one path to another. Functions like os.rename"""
2391
os.rename(from_, to)
2393
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2394
raise errors.FileExists(to, str(e))
2396
self.past_renames.append((from_, to))
2398
def pre_delete(self, from_, to):
2399
"""Rename a file out of the way and mark it for deletion.
2401
Unlike os.unlink, this works equally well for files and directories.
2402
:param from_: The current file path
2403
:param to: A temporary path for the file
2405
self.rename(from_, to)
2406
self.pending_deletions.append(to)
2409
"""Reverse all renames that have been performed"""
2410
for from_, to in reversed(self.past_renames):
2411
os.rename(to, from_)
2412
# after rollback, don't reuse _FileMover
2414
pending_deletions = None
2416
def apply_deletions(self):
2417
"""Apply all marked deletions"""
2418
for path in self.pending_deletions:
2420
# after apply_deletions, don't reuse _FileMover
2422
pending_deletions = None