1
# Copyright (C) 2006-2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
from stat import S_ISREG, S_IEXEC
29
lazy_import.lazy_import(globals(), """
40
revision as _mod_revision,
45
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
46
ReusingTransform, CantMoveRoot,
47
ExistingLimbo, ImmortalLimbo, NoFinalPath,
49
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
50
from bzrlib.osutils import (
59
from bzrlib.progress import ProgressPhase
60
from bzrlib.symbol_versioning import (
67
ROOT_PARENT = "root-parent"
69
def unique_add(map, key, value):
71
raise DuplicateKey(key=key)
76
class _TransformResults(object):
77
def __init__(self, modified_paths, rename_count):
79
self.modified_paths = modified_paths
80
self.rename_count = rename_count
83
class TreeTransformBase(object):
84
"""The base class for TreeTransform and its kin."""
86
def __init__(self, tree, pb=None,
90
:param tree: The tree that will be transformed, but not necessarily
93
:param case_sensitive: If True, the target of the transform is
94
case sensitive, not just case preserving.
99
# mapping of trans_id -> new basename
101
# mapping of trans_id -> new parent trans_id
102
self._new_parent = {}
103
# mapping of trans_id with new contents -> new file_kind
104
self._new_contents = {}
105
# Set of trans_ids whose contents will be removed
106
self._removed_contents = set()
107
# Mapping of trans_id -> new execute-bit value
108
self._new_executability = {}
109
# Mapping of trans_id -> new tree-reference value
110
self._new_reference_revision = {}
111
# Mapping of trans_id -> new file_id
113
# Mapping of old file-id -> trans_id
114
self._non_present_ids = {}
115
# Mapping of new file_id -> trans_id
117
# Set of trans_ids that will be removed
118
self._removed_id = set()
119
# Mapping of path in old tree -> trans_id
120
self._tree_path_ids = {}
121
# Mapping trans_id -> path in old tree
122
self._tree_id_paths = {}
123
# The trans_id that will be used as the tree root
124
root_id = tree.get_root_id()
125
if root_id is not None:
126
self._new_root = self.trans_id_tree_file_id(root_id)
128
self._new_root = None
129
# Indicator of whether the transform has been applied
133
# Whether the target is case sensitive
134
self._case_sensitive_target = case_sensitive
135
# A counter of how many files have been renamed
136
self.rename_count = 0
139
"""Release the working tree lock, if held.
141
This is required if apply has not been invoked, but can be invoked
144
if self._tree is None:
149
def __get_root(self):
150
return self._new_root
152
root = property(__get_root)
154
def _assign_id(self):
155
"""Produce a new tranform id"""
156
new_id = "new-%s" % self._id_number
160
def create_path(self, name, parent):
161
"""Assign a transaction id to a new path"""
162
trans_id = self._assign_id()
163
unique_add(self._new_name, trans_id, name)
164
unique_add(self._new_parent, trans_id, parent)
167
def adjust_path(self, name, parent, trans_id):
168
"""Change the path that is assigned to a transaction id."""
170
raise ValueError("Parent trans-id may not be None")
171
if trans_id == self._new_root:
173
self._new_name[trans_id] = name
174
self._new_parent[trans_id] = parent
176
def adjust_root_path(self, name, parent):
177
"""Emulate moving the root by moving all children, instead.
179
We do this by undoing the association of root's transaction id with the
180
current tree. This allows us to create a new directory with that
181
transaction id. We unversion the root directory and version the
182
physically new directory, and hope someone versions the tree root
185
old_root = self._new_root
186
old_root_file_id = self.final_file_id(old_root)
187
# force moving all children of root
188
for child_id in self.iter_tree_children(old_root):
189
if child_id != parent:
190
self.adjust_path(self.final_name(child_id),
191
self.final_parent(child_id), child_id)
192
file_id = self.final_file_id(child_id)
193
if file_id is not None:
194
self.unversion_file(child_id)
195
self.version_file(file_id, child_id)
197
# the physical root needs a new transaction id
198
self._tree_path_ids.pop("")
199
self._tree_id_paths.pop(old_root)
200
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
201
if parent == old_root:
202
parent = self._new_root
203
self.adjust_path(name, parent, old_root)
204
self.create_directory(old_root)
205
self.version_file(old_root_file_id, old_root)
206
self.unversion_file(self._new_root)
208
def fixup_new_roots(self):
209
"""Reinterpret requests to change the root directory
211
Instead of creating a root directory, or moving an existing directory,
212
all the attributes and children of the new root are applied to the
213
existing root directory.
215
This means that the old root trans-id becomes obsolete, so it is
216
recommended only to invoke this after the root trans-id has become
219
new_roots = [k for k, v in self._new_parent.iteritems() if v is
221
if len(new_roots) < 1:
223
if len(new_roots) != 1:
224
raise ValueError('A tree cannot have two roots!')
225
if self._new_root is None:
226
self._new_root = new_roots[0]
228
old_new_root = new_roots[0]
229
# TODO: What to do if a old_new_root is present, but self._new_root is
230
# not listed as being removed? This code explicitly unversions
231
# the old root and versions it with the new file_id. Though that
232
# seems like an incomplete delta
234
# unversion the new root's directory.
235
file_id = self.final_file_id(old_new_root)
236
if old_new_root in self._new_id:
237
self.cancel_versioning(old_new_root)
239
self.unversion_file(old_new_root)
240
# if, at this stage, root still has an old file_id, zap it so we can
241
# stick a new one in.
242
if (self.tree_file_id(self._new_root) is not None and
243
self._new_root not in self._removed_id):
244
self.unversion_file(self._new_root)
245
self.version_file(file_id, self._new_root)
247
# Now move children of new root into old root directory.
248
# Ensure all children are registered with the transaction, but don't
249
# use directly-- some tree children have new parents
250
list(self.iter_tree_children(old_new_root))
251
# Move all children of new root into old root directory.
252
for child in self.by_parent().get(old_new_root, []):
253
self.adjust_path(self.final_name(child), self._new_root, child)
255
# Ensure old_new_root has no directory.
256
if old_new_root in self._new_contents:
257
self.cancel_creation(old_new_root)
259
self.delete_contents(old_new_root)
261
# prevent deletion of root directory.
262
if self._new_root in self._removed_contents:
263
self.cancel_deletion(self._new_root)
265
# destroy path info for old_new_root.
266
del self._new_parent[old_new_root]
267
del self._new_name[old_new_root]
269
def trans_id_tree_file_id(self, inventory_id):
270
"""Determine the transaction id of a working tree file.
272
This reflects only files that already exist, not ones that will be
273
added by transactions.
275
if inventory_id is None:
276
raise ValueError('None is not a valid file id')
277
path = self._tree.id2path(inventory_id)
278
return self.trans_id_tree_path(path)
280
def trans_id_file_id(self, file_id):
281
"""Determine or set the transaction id associated with a file ID.
282
A new id is only created for file_ids that were never present. If
283
a transaction has been unversioned, it is deliberately still returned.
284
(this will likely lead to an unversioned parent conflict.)
287
raise ValueError('None is not a valid file id')
288
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
289
return self._r_new_id[file_id]
292
self._tree.iter_entries_by_dir([file_id]).next()
293
except StopIteration:
294
if file_id in self._non_present_ids:
295
return self._non_present_ids[file_id]
297
trans_id = self._assign_id()
298
self._non_present_ids[file_id] = trans_id
301
return self.trans_id_tree_file_id(file_id)
303
def trans_id_tree_path(self, path):
304
"""Determine (and maybe set) the transaction ID for a tree path."""
305
path = self.canonical_path(path)
306
if path not in self._tree_path_ids:
307
self._tree_path_ids[path] = self._assign_id()
308
self._tree_id_paths[self._tree_path_ids[path]] = path
309
return self._tree_path_ids[path]
311
def get_tree_parent(self, trans_id):
312
"""Determine id of the parent in the tree."""
313
path = self._tree_id_paths[trans_id]
316
return self.trans_id_tree_path(os.path.dirname(path))
318
def delete_contents(self, trans_id):
319
"""Schedule the contents of a path entry for deletion"""
320
kind = self.tree_kind(trans_id)
322
self._removed_contents.add(trans_id)
324
def cancel_deletion(self, trans_id):
325
"""Cancel a scheduled deletion"""
326
self._removed_contents.remove(trans_id)
328
def unversion_file(self, trans_id):
329
"""Schedule a path entry to become unversioned"""
330
self._removed_id.add(trans_id)
332
def delete_versioned(self, trans_id):
333
"""Delete and unversion a versioned file"""
334
self.delete_contents(trans_id)
335
self.unversion_file(trans_id)
337
def set_executability(self, executability, trans_id):
338
"""Schedule setting of the 'execute' bit
339
To unschedule, set to None
341
if executability is None:
342
del self._new_executability[trans_id]
344
unique_add(self._new_executability, trans_id, executability)
346
def set_tree_reference(self, revision_id, trans_id):
347
"""Set the reference associated with a directory"""
348
unique_add(self._new_reference_revision, trans_id, revision_id)
350
def version_file(self, file_id, trans_id):
351
"""Schedule a file to become versioned."""
354
unique_add(self._new_id, trans_id, file_id)
355
unique_add(self._r_new_id, file_id, trans_id)
357
def cancel_versioning(self, trans_id):
358
"""Undo a previous versioning of a file"""
359
file_id = self._new_id[trans_id]
360
del self._new_id[trans_id]
361
del self._r_new_id[file_id]
363
def new_paths(self, filesystem_only=False):
364
"""Determine the paths of all new and changed files.
366
:param filesystem_only: if True, only calculate values for files
367
that require renames or execute bit changes.
371
stale_ids = self._needs_rename.difference(self._new_name)
372
stale_ids.difference_update(self._new_parent)
373
stale_ids.difference_update(self._new_contents)
374
stale_ids.difference_update(self._new_id)
375
needs_rename = self._needs_rename.difference(stale_ids)
376
id_sets = (needs_rename, self._new_executability)
378
id_sets = (self._new_name, self._new_parent, self._new_contents,
379
self._new_id, self._new_executability)
380
for id_set in id_sets:
381
new_ids.update(id_set)
382
return sorted(FinalPaths(self).get_paths(new_ids))
384
def _inventory_altered(self):
385
"""Get the trans_ids and paths of files needing new inv entries."""
387
for id_set in [self._new_name, self._new_parent, self._new_id,
388
self._new_executability]:
389
new_ids.update(id_set)
390
changed_kind = set(self._removed_contents)
391
changed_kind.intersection_update(self._new_contents)
392
changed_kind.difference_update(new_ids)
393
changed_kind = (t for t in changed_kind
394
if self.tree_kind(t) != self.final_kind(t))
395
new_ids.update(changed_kind)
396
return sorted(FinalPaths(self).get_paths(new_ids))
398
def final_kind(self, trans_id):
399
"""Determine the final file kind, after any changes applied.
401
:return: None if the file does not exist/has no contents. (It is
402
conceivable that a path would be created without the corresponding
403
contents insertion command)
405
if trans_id in self._new_contents:
406
return self._new_contents[trans_id]
407
elif trans_id in self._removed_contents:
410
return self.tree_kind(trans_id)
412
def tree_file_id(self, trans_id):
413
"""Determine the file id associated with the trans_id in the tree"""
415
path = self._tree_id_paths[trans_id]
417
# the file is a new, unversioned file, or invalid trans_id
419
# the file is old; the old id is still valid
420
if self._new_root == trans_id:
421
return self._tree.get_root_id()
422
return self._tree.path2id(path)
424
def final_file_id(self, trans_id):
425
"""Determine the file id after any changes are applied, or None.
427
None indicates that the file will not be versioned after changes are
431
return self._new_id[trans_id]
433
if trans_id in self._removed_id:
435
return self.tree_file_id(trans_id)
437
def inactive_file_id(self, trans_id):
438
"""Return the inactive file_id associated with a transaction id.
439
That is, the one in the tree or in non_present_ids.
440
The file_id may actually be active, too.
442
file_id = self.tree_file_id(trans_id)
443
if file_id is not None:
445
for key, value in self._non_present_ids.iteritems():
446
if value == trans_id:
449
def final_parent(self, trans_id):
450
"""Determine the parent file_id, after any changes are applied.
452
ROOT_PARENT is returned for the tree root.
455
return self._new_parent[trans_id]
457
return self.get_tree_parent(trans_id)
459
def final_name(self, trans_id):
460
"""Determine the final filename, after all changes are applied."""
462
return self._new_name[trans_id]
465
return os.path.basename(self._tree_id_paths[trans_id])
467
raise NoFinalPath(trans_id, self)
470
"""Return a map of parent: children for known parents.
472
Only new paths and parents of tree files with assigned ids are used.
475
items = list(self._new_parent.iteritems())
476
items.extend((t, self.final_parent(t)) for t in
477
self._tree_id_paths.keys())
478
for trans_id, parent_id in items:
479
if parent_id not in by_parent:
480
by_parent[parent_id] = set()
481
by_parent[parent_id].add(trans_id)
484
def path_changed(self, trans_id):
485
"""Return True if a trans_id's path has changed."""
486
return (trans_id in self._new_name) or (trans_id in self._new_parent)
488
def new_contents(self, trans_id):
489
return (trans_id in self._new_contents)
491
def find_conflicts(self):
492
"""Find any violations of inventory or filesystem invariants"""
493
if self._done is True:
494
raise ReusingTransform()
496
# ensure all children of all existent parents are known
497
# all children of non-existent parents are known, by definition.
498
self._add_tree_children()
499
by_parent = self.by_parent()
500
conflicts.extend(self._unversioned_parents(by_parent))
501
conflicts.extend(self._parent_loops())
502
conflicts.extend(self._duplicate_entries(by_parent))
503
conflicts.extend(self._duplicate_ids())
504
conflicts.extend(self._parent_type_conflicts(by_parent))
505
conflicts.extend(self._improper_versioning())
506
conflicts.extend(self._executability_conflicts())
507
conflicts.extend(self._overwrite_conflicts())
510
def _check_malformed(self):
511
conflicts = self.find_conflicts()
512
if len(conflicts) != 0:
513
raise MalformedTransform(conflicts=conflicts)
515
def _add_tree_children(self):
516
"""Add all the children of all active parents to the known paths.
518
Active parents are those which gain children, and those which are
519
removed. This is a necessary first step in detecting conflicts.
521
parents = self.by_parent().keys()
522
parents.extend([t for t in self._removed_contents if
523
self.tree_kind(t) == 'directory'])
524
for trans_id in self._removed_id:
525
file_id = self.tree_file_id(trans_id)
526
if file_id is not None:
527
# XXX: This seems like something that should go via a different
529
if self._tree.inventory[file_id].kind == 'directory':
530
parents.append(trans_id)
531
elif self.tree_kind(trans_id) == 'directory':
532
parents.append(trans_id)
534
for parent_id in parents:
535
# ensure that all children are registered with the transaction
536
list(self.iter_tree_children(parent_id))
538
@deprecated_method(deprecated_in((2, 3, 0)))
539
def has_named_child(self, by_parent, parent_id, name):
540
return self._has_named_child(
541
name, parent_id, known_children=by_parent.get(parent_id, []))
543
def _has_named_child(self, name, parent_id, known_children):
544
"""Does a parent already have a name child.
546
:param name: The searched for name.
548
:param parent_id: The parent for which the check is made.
550
:param known_children: The already known children. This should have
551
been recently obtained from `self.by_parent.get(parent_id)`
552
(or will be if None is passed).
554
if known_children is None:
555
known_children = self.by_parent().get(parent_id, [])
556
for child in known_children:
557
if self.final_name(child) == name:
559
parent_path = self._tree_id_paths.get(parent_id, None)
560
if parent_path is None:
561
# No parent... no children
563
child_path = joinpath(parent_path, name)
564
child_id = self._tree_path_ids.get(child_path, None)
566
# Not known by the tree transform yet, check the filesystem
567
return osutils.lexists(self._tree.abspath(child_path))
569
raise AssertionError('child_id is missing: %s, %s, %s'
570
% (name, parent_id, child_id))
572
def _available_backup_name(self, name, target_id):
573
"""Find an available backup name.
575
:param name: The basename of the file.
577
:param target_id: The directory trans_id where the backup should
580
known_children = self.by_parent().get(target_id, [])
581
return osutils.available_backup_name(
583
lambda base: self._has_named_child(
584
base, target_id, known_children))
586
def _parent_loops(self):
587
"""No entry should be its own ancestor"""
589
for trans_id in self._new_parent:
592
while parent_id is not ROOT_PARENT:
595
parent_id = self.final_parent(parent_id)
598
if parent_id == trans_id:
599
conflicts.append(('parent loop', trans_id))
600
if parent_id in seen:
604
def _unversioned_parents(self, by_parent):
605
"""If parent directories are versioned, children must be versioned."""
607
for parent_id, children in by_parent.iteritems():
608
if parent_id is ROOT_PARENT:
610
if self.final_file_id(parent_id) is not None:
612
for child_id in children:
613
if self.final_file_id(child_id) is not None:
614
conflicts.append(('unversioned parent', parent_id))
618
def _improper_versioning(self):
619
"""Cannot version a file with no contents, or a bad type.
621
However, existing entries with no contents are okay.
624
for trans_id in self._new_id.iterkeys():
625
kind = self.final_kind(trans_id)
627
conflicts.append(('versioning no contents', trans_id))
629
if not inventory.InventoryEntry.versionable_kind(kind):
630
conflicts.append(('versioning bad kind', trans_id, kind))
633
def _executability_conflicts(self):
634
"""Check for bad executability changes.
636
Only versioned files may have their executability set, because
637
1. only versioned entries can have executability under windows
638
2. only files can be executable. (The execute bit on a directory
639
does not indicate searchability)
642
for trans_id in self._new_executability:
643
if self.final_file_id(trans_id) is None:
644
conflicts.append(('unversioned executability', trans_id))
646
if self.final_kind(trans_id) != "file":
647
conflicts.append(('non-file executability', trans_id))
650
def _overwrite_conflicts(self):
651
"""Check for overwrites (not permitted on Win32)"""
653
for trans_id in self._new_contents:
654
if self.tree_kind(trans_id) is None:
656
if trans_id not in self._removed_contents:
657
conflicts.append(('overwrite', trans_id,
658
self.final_name(trans_id)))
661
def _duplicate_entries(self, by_parent):
662
"""No directory may have two entries with the same name."""
664
if (self._new_name, self._new_parent) == ({}, {}):
666
for children in by_parent.itervalues():
668
for child_tid in children:
669
name = self.final_name(child_tid)
671
# Keep children only if they still exist in the end
672
if not self._case_sensitive_target:
674
name_ids.append((name, child_tid))
678
for name, trans_id in name_ids:
679
kind = self.final_kind(trans_id)
680
file_id = self.final_file_id(trans_id)
681
if kind is None and file_id is None:
683
if name == last_name:
684
conflicts.append(('duplicate', last_trans_id, trans_id,
687
last_trans_id = trans_id
690
def _duplicate_ids(self):
691
"""Each inventory id may only be used once"""
693
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
695
all_ids = self._tree.all_file_ids()
696
active_tree_ids = all_ids.difference(removed_tree_ids)
697
for trans_id, file_id in self._new_id.iteritems():
698
if file_id in active_tree_ids:
699
old_trans_id = self.trans_id_tree_file_id(file_id)
700
conflicts.append(('duplicate id', old_trans_id, trans_id))
703
def _parent_type_conflicts(self, by_parent):
704
"""Children must have a directory parent"""
706
for parent_id, children in by_parent.iteritems():
707
if parent_id is ROOT_PARENT:
710
for child_id in children:
711
if self.final_kind(child_id) is not None:
716
# There is at least a child, so we need an existing directory to
718
kind = self.final_kind(parent_id)
720
# The directory will be deleted
721
conflicts.append(('missing parent', parent_id))
722
elif kind != "directory":
723
# Meh, we need a *directory* to put something in it
724
conflicts.append(('non-directory parent', parent_id))
727
def _set_executability(self, path, trans_id):
728
"""Set the executability of versioned files """
729
if supports_executable():
730
new_executability = self._new_executability[trans_id]
731
abspath = self._tree.abspath(path)
732
current_mode = os.stat(abspath).st_mode
733
if new_executability:
736
to_mode = current_mode | (0100 & ~umask)
737
# Enable x-bit for others only if they can read it.
738
if current_mode & 0004:
739
to_mode |= 0001 & ~umask
740
if current_mode & 0040:
741
to_mode |= 0010 & ~umask
743
to_mode = current_mode & ~0111
744
os.chmod(abspath, to_mode)
746
def _new_entry(self, name, parent_id, file_id):
747
"""Helper function to create a new filesystem entry."""
748
trans_id = self.create_path(name, parent_id)
749
if file_id is not None:
750
self.version_file(file_id, trans_id)
753
def new_file(self, name, parent_id, contents, file_id=None,
755
"""Convenience method to create files.
757
name is the name of the file to create.
758
parent_id is the transaction id of the parent directory of the file.
759
contents is an iterator of bytestrings, which will be used to produce
761
:param file_id: The inventory ID of the file, if it is to be versioned.
762
:param executable: Only valid when a file_id has been supplied.
764
trans_id = self._new_entry(name, parent_id, file_id)
765
# TODO: rather than scheduling a set_executable call,
766
# have create_file create the file with the right mode.
767
self.create_file(contents, trans_id)
768
if executable is not None:
769
self.set_executability(executable, trans_id)
772
def new_directory(self, name, parent_id, file_id=None):
773
"""Convenience method to create directories.
775
name is the name of the directory to create.
776
parent_id is the transaction id of the parent directory of the
778
file_id is the inventory ID of the directory, if it is to be versioned.
780
trans_id = self._new_entry(name, parent_id, file_id)
781
self.create_directory(trans_id)
784
def new_symlink(self, name, parent_id, target, file_id=None):
785
"""Convenience method to create symbolic link.
787
name is the name of the symlink to create.
788
parent_id is the transaction id of the parent directory of the symlink.
789
target is a bytestring of the target of the symlink.
790
file_id is the inventory ID of the file, if it is to be versioned.
792
trans_id = self._new_entry(name, parent_id, file_id)
793
self.create_symlink(target, trans_id)
796
def new_orphan(self, trans_id, parent_id):
797
"""Schedule an item to be orphaned.
799
When a directory is about to be removed, its children, if they are not
800
versioned are moved out of the way: they don't have a parent anymore.
802
:param trans_id: The trans_id of the existing item.
803
:param parent_id: The parent trans_id of the item.
805
raise NotImplementedError(self.new_orphan)
807
def _get_potential_orphans(self, dir_id):
808
"""Find the potential orphans in a directory.
810
A directory can't be safely deleted if there are versioned files in it.
811
If all the contained files are unversioned then they can be orphaned.
813
The 'None' return value means that the directory contains at least one
814
versioned file and should not be deleted.
816
:param dir_id: The directory trans id.
818
:return: A list of the orphan trans ids or None if at least one
819
versioned file is present.
822
# Find the potential orphans, stop if one item should be kept
823
for child_tid in self.by_parent()[dir_id]:
824
if child_tid in self._removed_contents:
825
# The child is removed as part of the transform. Since it was
826
# versioned before, it's not an orphan
828
elif self.final_file_id(child_tid) is None:
829
# The child is not versioned
830
orphans.append(child_tid)
832
# We have a versioned file here, searching for orphans is
838
def _affected_ids(self):
839
"""Return the set of transform ids affected by the transform"""
840
trans_ids = set(self._removed_id)
841
trans_ids.update(self._new_id.keys())
842
trans_ids.update(self._removed_contents)
843
trans_ids.update(self._new_contents.keys())
844
trans_ids.update(self._new_executability.keys())
845
trans_ids.update(self._new_name.keys())
846
trans_ids.update(self._new_parent.keys())
849
def _get_file_id_maps(self):
850
"""Return mapping of file_ids to trans_ids in the to and from states"""
851
trans_ids = self._affected_ids()
854
# Build up two dicts: trans_ids associated with file ids in the
855
# FROM state, vs the TO state.
856
for trans_id in trans_ids:
857
from_file_id = self.tree_file_id(trans_id)
858
if from_file_id is not None:
859
from_trans_ids[from_file_id] = trans_id
860
to_file_id = self.final_file_id(trans_id)
861
if to_file_id is not None:
862
to_trans_ids[to_file_id] = trans_id
863
return from_trans_ids, to_trans_ids
865
def _from_file_data(self, from_trans_id, from_versioned, file_id):
866
"""Get data about a file in the from (tree) state
868
Return a (name, parent, kind, executable) tuple
870
from_path = self._tree_id_paths.get(from_trans_id)
872
# get data from working tree if versioned
873
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
874
from_name = from_entry.name
875
from_parent = from_entry.parent_id
878
if from_path is None:
879
# File does not exist in FROM state
883
# File exists, but is not versioned. Have to use path-
885
from_name = os.path.basename(from_path)
886
tree_parent = self.get_tree_parent(from_trans_id)
887
from_parent = self.tree_file_id(tree_parent)
888
if from_path is not None:
889
from_kind, from_executable, from_stats = \
890
self._tree._comparison_data(from_entry, from_path)
893
from_executable = False
894
return from_name, from_parent, from_kind, from_executable
896
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
897
"""Get data about a file in the to (target) state
899
Return a (name, parent, kind, executable) tuple
901
to_name = self.final_name(to_trans_id)
902
to_kind = self.final_kind(to_trans_id)
903
to_parent = self.final_file_id(self.final_parent(to_trans_id))
904
if to_trans_id in self._new_executability:
905
to_executable = self._new_executability[to_trans_id]
906
elif to_trans_id == from_trans_id:
907
to_executable = from_executable
909
to_executable = False
910
return to_name, to_parent, to_kind, to_executable
912
def iter_changes(self):
913
"""Produce output in the same format as Tree.iter_changes.
915
Will produce nonsensical results if invoked while inventory/filesystem
916
conflicts (as reported by TreeTransform.find_conflicts()) are present.
918
This reads the Transform, but only reproduces changes involving a
919
file_id. Files that are not versioned in either of the FROM or TO
920
states are not reflected.
922
final_paths = FinalPaths(self)
923
from_trans_ids, to_trans_ids = self._get_file_id_maps()
925
# Now iterate through all active file_ids
926
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
928
from_trans_id = from_trans_ids.get(file_id)
929
# find file ids, and determine versioning state
930
if from_trans_id is None:
931
from_versioned = False
932
from_trans_id = to_trans_ids[file_id]
934
from_versioned = True
935
to_trans_id = to_trans_ids.get(file_id)
936
if to_trans_id is None:
938
to_trans_id = from_trans_id
942
from_name, from_parent, from_kind, from_executable = \
943
self._from_file_data(from_trans_id, from_versioned, file_id)
945
to_name, to_parent, to_kind, to_executable = \
946
self._to_file_data(to_trans_id, from_trans_id, from_executable)
948
if not from_versioned:
951
from_path = self._tree_id_paths.get(from_trans_id)
955
to_path = final_paths.get_path(to_trans_id)
956
if from_kind != to_kind:
958
elif to_kind in ('file', 'symlink') and (
959
to_trans_id != from_trans_id or
960
to_trans_id in self._new_contents):
962
if (not modified and from_versioned == to_versioned and
963
from_parent==to_parent and from_name == to_name and
964
from_executable == to_executable):
966
results.append((file_id, (from_path, to_path), modified,
967
(from_versioned, to_versioned),
968
(from_parent, to_parent),
969
(from_name, to_name),
970
(from_kind, to_kind),
971
(from_executable, to_executable)))
972
return iter(sorted(results, key=lambda x:x[1]))
974
def get_preview_tree(self):
975
"""Return a tree representing the result of the transform.
977
The tree is a snapshot, and altering the TreeTransform will invalidate
980
return _PreviewTree(self)
982
def commit(self, branch, message, merge_parents=None, strict=False,
983
timestamp=None, timezone=None, committer=None, authors=None,
984
revprops=None, revision_id=None):
985
"""Commit the result of this TreeTransform to a branch.
987
:param branch: The branch to commit to.
988
:param message: The message to attach to the commit.
989
:param merge_parents: Additional parent revision-ids specified by
991
:param strict: If True, abort the commit if there are unversioned
993
:param timestamp: if not None, seconds-since-epoch for the time and
994
date. (May be a float.)
995
:param timezone: Optional timezone for timestamp, as an offset in
997
:param committer: Optional committer in email-id format.
998
(e.g. "J Random Hacker <jrandom@example.com>")
999
:param authors: Optional list of authors in email-id format.
1000
:param revprops: Optional dictionary of revision properties.
1001
:param revision_id: Optional revision id. (Specifying a revision-id
1002
may reduce performance for some non-native formats.)
1003
:return: The revision_id of the revision committed.
1005
self._check_malformed()
1007
unversioned = set(self._new_contents).difference(set(self._new_id))
1008
for trans_id in unversioned:
1009
if self.final_file_id(trans_id) is None:
1010
raise errors.StrictCommitFailed()
1012
revno, last_rev_id = branch.last_revision_info()
1013
if last_rev_id == _mod_revision.NULL_REVISION:
1014
if merge_parents is not None:
1015
raise ValueError('Cannot supply merge parents for first'
1019
parent_ids = [last_rev_id]
1020
if merge_parents is not None:
1021
parent_ids.extend(merge_parents)
1022
if self._tree.get_revision_id() != last_rev_id:
1023
raise ValueError('TreeTransform not based on branch basis: %s' %
1024
self._tree.get_revision_id())
1025
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1026
builder = branch.get_commit_builder(parent_ids,
1027
timestamp=timestamp,
1029
committer=committer,
1031
revision_id=revision_id)
1032
preview = self.get_preview_tree()
1033
list(builder.record_iter_changes(preview, last_rev_id,
1034
self.iter_changes()))
1035
builder.finish_inventory()
1036
revision_id = builder.commit(message)
1037
branch.set_last_revision_info(revno + 1, revision_id)
1040
def _text_parent(self, trans_id):
1041
file_id = self.tree_file_id(trans_id)
1043
if file_id is None or self._tree.kind(file_id) != 'file':
1045
except errors.NoSuchFile:
1049
def _get_parents_texts(self, trans_id):
1050
"""Get texts for compression parents of this file."""
1051
file_id = self._text_parent(trans_id)
1054
return (self._tree.get_file_text(file_id),)
1056
def _get_parents_lines(self, trans_id):
1057
"""Get lines for compression parents of this file."""
1058
file_id = self._text_parent(trans_id)
1061
return (self._tree.get_file_lines(file_id),)
1063
def serialize(self, serializer):
1064
"""Serialize this TreeTransform.
1066
:param serializer: A Serialiser like pack.ContainerSerializer.
1068
new_name = dict((k, v.encode('utf-8')) for k, v in
1069
self._new_name.items())
1070
new_executability = dict((k, int(v)) for k, v in
1071
self._new_executability.items())
1072
tree_path_ids = dict((k.encode('utf-8'), v)
1073
for k, v in self._tree_path_ids.items())
1075
'_id_number': self._id_number,
1076
'_new_name': new_name,
1077
'_new_parent': self._new_parent,
1078
'_new_executability': new_executability,
1079
'_new_id': self._new_id,
1080
'_tree_path_ids': tree_path_ids,
1081
'_removed_id': list(self._removed_id),
1082
'_removed_contents': list(self._removed_contents),
1083
'_non_present_ids': self._non_present_ids,
1085
yield serializer.bytes_record(bencode.bencode(attribs),
1087
for trans_id, kind in self._new_contents.items():
1089
lines = osutils.chunks_to_lines(
1090
self._read_file_chunks(trans_id))
1091
parents = self._get_parents_lines(trans_id)
1092
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1093
content = ''.join(mpdiff.to_patch())
1094
if kind == 'directory':
1096
if kind == 'symlink':
1097
content = self._read_symlink_target(trans_id)
1098
yield serializer.bytes_record(content, ((trans_id, kind),))
1100
def deserialize(self, records):
1101
"""Deserialize a stored TreeTransform.
1103
:param records: An iterable of (names, content) tuples, as per
1104
pack.ContainerPushParser.
1106
names, content = records.next()
1107
attribs = bencode.bdecode(content)
1108
self._id_number = attribs['_id_number']
1109
self._new_name = dict((k, v.decode('utf-8'))
1110
for k, v in attribs['_new_name'].items())
1111
self._new_parent = attribs['_new_parent']
1112
self._new_executability = dict((k, bool(v)) for k, v in
1113
attribs['_new_executability'].items())
1114
self._new_id = attribs['_new_id']
1115
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1116
self._tree_path_ids = {}
1117
self._tree_id_paths = {}
1118
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1119
path = bytepath.decode('utf-8')
1120
self._tree_path_ids[path] = trans_id
1121
self._tree_id_paths[trans_id] = path
1122
self._removed_id = set(attribs['_removed_id'])
1123
self._removed_contents = set(attribs['_removed_contents'])
1124
self._non_present_ids = attribs['_non_present_ids']
1125
for ((trans_id, kind),), content in records:
1127
mpdiff = multiparent.MultiParent.from_patch(content)
1128
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1129
self.create_file(lines, trans_id)
1130
if kind == 'directory':
1131
self.create_directory(trans_id)
1132
if kind == 'symlink':
1133
self.create_symlink(content.decode('utf-8'), trans_id)
1136
class DiskTreeTransform(TreeTransformBase):
1137
"""Tree transform storing its contents on disk."""
1139
def __init__(self, tree, limbodir, pb=None,
1140
case_sensitive=True):
1142
:param tree: The tree that will be transformed, but not necessarily
1144
:param limbodir: A directory where new files can be stored until
1145
they are installed in their proper places
1147
:param case_sensitive: If True, the target of the transform is
1148
case sensitive, not just case preserving.
1150
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1151
self._limbodir = limbodir
1152
self._deletiondir = None
1153
# A mapping of transform ids to their limbo filename
1154
self._limbo_files = {}
1155
# A mapping of transform ids to a set of the transform ids of children
1156
# that their limbo directory has
1157
self._limbo_children = {}
1158
# Map transform ids to maps of child filename to child transform id
1159
self._limbo_children_names = {}
1160
# List of transform ids that need to be renamed from limbo into place
1161
self._needs_rename = set()
1162
self._creation_mtime = None
1165
"""Release the working tree lock, if held, clean up limbo dir.
1167
This is required if apply has not been invoked, but can be invoked
1170
if self._tree is None:
1173
entries = [(self._limbo_name(t), t, k) for t, k in
1174
self._new_contents.iteritems()]
1175
entries.sort(reverse=True)
1176
for path, trans_id, kind in entries:
1179
delete_any(self._limbodir)
1181
# We don't especially care *why* the dir is immortal.
1182
raise ImmortalLimbo(self._limbodir)
1184
if self._deletiondir is not None:
1185
delete_any(self._deletiondir)
1187
raise errors.ImmortalPendingDeletion(self._deletiondir)
1189
TreeTransformBase.finalize(self)
1191
def _limbo_name(self, trans_id):
1192
"""Generate the limbo name of a file"""
1193
limbo_name = self._limbo_files.get(trans_id)
1194
if limbo_name is None:
1195
limbo_name = self._generate_limbo_path(trans_id)
1196
self._limbo_files[trans_id] = limbo_name
1199
def _generate_limbo_path(self, trans_id):
1200
"""Generate a limbo path using the trans_id as the relative path.
1202
This is suitable as a fallback, and when the transform should not be
1203
sensitive to the path encoding of the limbo directory.
1205
self._needs_rename.add(trans_id)
1206
return pathjoin(self._limbodir, trans_id)
1208
def adjust_path(self, name, parent, trans_id):
1209
previous_parent = self._new_parent.get(trans_id)
1210
previous_name = self._new_name.get(trans_id)
1211
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1212
if (trans_id in self._limbo_files and
1213
trans_id not in self._needs_rename):
1214
self._rename_in_limbo([trans_id])
1215
if previous_parent != parent:
1216
self._limbo_children[previous_parent].remove(trans_id)
1217
if previous_parent != parent or previous_name != name:
1218
del self._limbo_children_names[previous_parent][previous_name]
1220
def _rename_in_limbo(self, trans_ids):
1221
"""Fix limbo names so that the right final path is produced.
1223
This means we outsmarted ourselves-- we tried to avoid renaming
1224
these files later by creating them with their final names in their
1225
final parents. But now the previous name or parent is no longer
1226
suitable, so we have to rename them.
1228
Even for trans_ids that have no new contents, we must remove their
1229
entries from _limbo_files, because they are now stale.
1231
for trans_id in trans_ids:
1232
old_path = self._limbo_files.pop(trans_id)
1233
if trans_id not in self._new_contents:
1235
new_path = self._limbo_name(trans_id)
1236
os.rename(old_path, new_path)
1237
for descendant in self._limbo_descendants(trans_id):
1238
desc_path = self._limbo_files[descendant]
1239
desc_path = new_path + desc_path[len(old_path):]
1240
self._limbo_files[descendant] = desc_path
1242
def _limbo_descendants(self, trans_id):
1243
"""Return the set of trans_ids whose limbo paths descend from this."""
1244
descendants = set(self._limbo_children.get(trans_id, []))
1245
for descendant in list(descendants):
1246
descendants.update(self._limbo_descendants(descendant))
1249
def create_file(self, contents, trans_id, mode_id=None):
1250
"""Schedule creation of a new file.
1254
Contents is an iterator of strings, all of which will be written
1255
to the target destination.
1257
New file takes the permissions of any existing file with that id,
1258
unless mode_id is specified.
1260
name = self._limbo_name(trans_id)
1261
f = open(name, 'wb')
1264
unique_add(self._new_contents, trans_id, 'file')
1266
# Clean up the file, it never got registered so
1267
# TreeTransform.finalize() won't clean it up.
1272
f.writelines(contents)
1275
self._set_mtime(name)
1276
self._set_mode(trans_id, mode_id, S_ISREG)
1278
def _read_file_chunks(self, trans_id):
1279
cur_file = open(self._limbo_name(trans_id), 'rb')
1281
return cur_file.readlines()
1285
def _read_symlink_target(self, trans_id):
1286
return os.readlink(self._limbo_name(trans_id))
1288
def _set_mtime(self, path):
1289
"""All files that are created get the same mtime.
1291
This time is set by the first object to be created.
1293
if self._creation_mtime is None:
1294
self._creation_mtime = time.time()
1295
os.utime(path, (self._creation_mtime, self._creation_mtime))
1297
def create_hardlink(self, path, trans_id):
1298
"""Schedule creation of a hard link"""
1299
name = self._limbo_name(trans_id)
1303
if e.errno != errno.EPERM:
1305
raise errors.HardLinkNotSupported(path)
1307
unique_add(self._new_contents, trans_id, 'file')
1309
# Clean up the file, it never got registered so
1310
# TreeTransform.finalize() won't clean it up.
1314
def create_directory(self, trans_id):
1315
"""Schedule creation of a new directory.
1317
See also new_directory.
1319
os.mkdir(self._limbo_name(trans_id))
1320
unique_add(self._new_contents, trans_id, 'directory')
1322
def create_symlink(self, target, trans_id):
1323
"""Schedule creation of a new symbolic link.
1325
target is a bytestring.
1326
See also new_symlink.
1329
os.symlink(target, self._limbo_name(trans_id))
1330
unique_add(self._new_contents, trans_id, 'symlink')
1333
path = FinalPaths(self).get_path(trans_id)
1336
raise UnableCreateSymlink(path=path)
1338
def cancel_creation(self, trans_id):
1339
"""Cancel the creation of new file contents."""
1340
del self._new_contents[trans_id]
1341
children = self._limbo_children.get(trans_id)
1342
# if this is a limbo directory with children, move them before removing
1344
if children is not None:
1345
self._rename_in_limbo(children)
1346
del self._limbo_children[trans_id]
1347
del self._limbo_children_names[trans_id]
1348
delete_any(self._limbo_name(trans_id))
1350
def new_orphan(self, trans_id, parent_id):
1351
# FIXME: There is no tree config, so we use the branch one (it's weird
1352
# to define it this way as orphaning can only occur in a working tree,
1353
# but that's all we have (for now). It will find the option in
1354
# locations.conf or bazaar.conf though) -- vila 20100916
1355
conf = self._tree.branch.get_config()
1356
conf_var_name = 'bzr.transform.orphan_policy'
1357
orphan_policy = conf.get_user_option(conf_var_name)
1358
default_policy = orphaning_registry.default_key
1359
if orphan_policy is None:
1360
orphan_policy = default_policy
1361
if orphan_policy not in orphaning_registry:
1363
'%s (from %s) is not a known policy, defaulting to %s'
1364
% (orphan_policy, conf_var_name, default_policy))
1365
orphan_policy = default_policy
1366
handle_orphan = orphaning_registry.get(orphan_policy)
1367
handle_orphan(self, trans_id, parent_id)
1370
class OrphaningError(errors.BzrError):
1372
# Only bugs could lead to such exception being seen by the user
1373
internal_error = True
1374
_fmt = "Error while orphaning %s in %s directory"
1376
def __init__(self, orphan, parent):
1377
errors.BzrError.__init__(self)
1378
self.orphan = orphan
1379
self.parent = parent
1382
class OrphaningForbidden(OrphaningError):
1384
_fmt = "Policy: %s doesn't allow creating orphans."
1386
def __init__(self, policy):
1387
errors.BzrError.__init__(self)
1388
self.policy = policy
1391
def move_orphan(tt, orphan_id, parent_id):
1392
"""See TreeTransformBase.new_orphan.
1394
This creates a new orphan in the `bzr-orphans` dir at the root of the
1397
:param tt: The TreeTransform orphaning `trans_id`.
1399
:param orphan_id: The trans id that should be orphaned.
1401
:param parent_id: The orphan parent trans id.
1403
# Add the orphan dir if it doesn't exist
1404
orphan_dir_basename = 'bzr-orphans'
1405
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1406
if tt.final_kind(od_id) is None:
1407
tt.create_directory(od_id)
1408
parent_path = tt._tree_id_paths[parent_id]
1409
# Find a name that doesn't exist yet in the orphan dir
1410
actual_name = tt.final_name(orphan_id)
1411
new_name = tt._available_backup_name(actual_name, od_id)
1412
tt.adjust_path(new_name, od_id, orphan_id)
1413
trace.warning('%s has been orphaned in %s'
1414
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1417
def refuse_orphan(tt, orphan_id, parent_id):
1418
"""See TreeTransformBase.new_orphan.
1420
This refuses to create orphan, letting the caller handle the conflict.
1422
raise OrphaningForbidden('never')
1425
orphaning_registry = registry.Registry()
1426
orphaning_registry.register(
1427
'conflict', refuse_orphan,
1428
'Leave orphans in place and create a conflict on the directory.')
1429
orphaning_registry.register(
1430
'move', move_orphan,
1431
'Move orphans into the bzr-orphans directory.')
1432
orphaning_registry._set_default_key('conflict')
1435
class TreeTransform(DiskTreeTransform):
1436
"""Represent a tree transformation.
1438
This object is designed to support incremental generation of the transform,
1441
However, it gives optimum performance when parent directories are created
1442
before their contents. The transform is then able to put child files
1443
directly in their parent directory, avoiding later renames.
1445
It is easy to produce malformed transforms, but they are generally
1446
harmless. Attempting to apply a malformed transform will cause an
1447
exception to be raised before any modifications are made to the tree.
1449
Many kinds of malformed transforms can be corrected with the
1450
resolve_conflicts function. The remaining ones indicate programming error,
1451
such as trying to create a file with no path.
1453
Two sets of file creation methods are supplied. Convenience methods are:
1458
These are composed of the low-level methods:
1460
* create_file or create_directory or create_symlink
1464
Transform/Transaction ids
1465
-------------------------
1466
trans_ids are temporary ids assigned to all files involved in a transform.
1467
It's possible, even common, that not all files in the Tree have trans_ids.
1469
trans_ids are used because filenames and file_ids are not good enough
1470
identifiers; filenames change, and not all files have file_ids. File-ids
1471
are also associated with trans-ids, so that moving a file moves its
1474
trans_ids are only valid for the TreeTransform that generated them.
1478
Limbo is a temporary directory use to hold new versions of files.
1479
Files are added to limbo by create_file, create_directory, create_symlink,
1480
and their convenience variants (new_*). Files may be removed from limbo
1481
using cancel_creation. Files are renamed from limbo into their final
1482
location as part of TreeTransform.apply
1484
Limbo must be cleaned up, by either calling TreeTransform.apply or
1485
calling TreeTransform.finalize.
1487
Files are placed into limbo inside their parent directories, where
1488
possible. This reduces subsequent renames, and makes operations involving
1489
lots of files faster. This optimization is only possible if the parent
1490
directory is created *before* creating any of its children, so avoid
1491
creating children before parents, where possible.
1495
This temporary directory is used by _FileMover for storing files that are
1496
about to be deleted. In case of rollback, the files will be restored.
1497
FileMover does not delete files until it is sure that a rollback will not
1500
def __init__(self, tree, pb=None):
1501
"""Note: a tree_write lock is taken on the tree.
1503
Use TreeTransform.finalize() to release the lock (can be omitted if
1504
TreeTransform.apply() called).
1506
tree.lock_tree_write()
1509
limbodir = urlutils.local_path_from_url(
1510
tree._transport.abspath('limbo'))
1514
if e.errno == errno.EEXIST:
1515
raise ExistingLimbo(limbodir)
1516
deletiondir = urlutils.local_path_from_url(
1517
tree._transport.abspath('pending-deletion'))
1519
os.mkdir(deletiondir)
1521
if e.errno == errno.EEXIST:
1522
raise errors.ExistingPendingDeletion(deletiondir)
1527
# Cache of realpath results, to speed up canonical_path
1528
self._realpaths = {}
1529
# Cache of relpath results, to speed up canonical_path
1531
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1532
tree.case_sensitive)
1533
self._deletiondir = deletiondir
1535
def canonical_path(self, path):
1536
"""Get the canonical tree-relative path"""
1537
# don't follow final symlinks
1538
abs = self._tree.abspath(path)
1539
if abs in self._relpaths:
1540
return self._relpaths[abs]
1541
dirname, basename = os.path.split(abs)
1542
if dirname not in self._realpaths:
1543
self._realpaths[dirname] = os.path.realpath(dirname)
1544
dirname = self._realpaths[dirname]
1545
abs = pathjoin(dirname, basename)
1546
if dirname in self._relpaths:
1547
relpath = pathjoin(self._relpaths[dirname], basename)
1548
relpath = relpath.rstrip('/\\')
1550
relpath = self._tree.relpath(abs)
1551
self._relpaths[abs] = relpath
1554
def tree_kind(self, trans_id):
1555
"""Determine the file kind in the working tree.
1557
:returns: The file kind or None if the file does not exist
1559
path = self._tree_id_paths.get(trans_id)
1563
return file_kind(self._tree.abspath(path))
1564
except errors.NoSuchFile:
1567
def _set_mode(self, trans_id, mode_id, typefunc):
1568
"""Set the mode of new file contents.
1569
The mode_id is the existing file to get the mode from (often the same
1570
as trans_id). The operation is only performed if there's a mode match
1571
according to typefunc.
1576
old_path = self._tree_id_paths[mode_id]
1580
mode = os.stat(self._tree.abspath(old_path)).st_mode
1582
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1583
# Either old_path doesn't exist, or the parent of the
1584
# target is not a directory (but will be one eventually)
1585
# Either way, we know it doesn't exist *right now*
1586
# See also bug #248448
1591
os.chmod(self._limbo_name(trans_id), mode)
1593
def iter_tree_children(self, parent_id):
1594
"""Iterate through the entry's tree children, if any"""
1596
path = self._tree_id_paths[parent_id]
1600
children = os.listdir(self._tree.abspath(path))
1602
if not (osutils._is_error_enotdir(e)
1603
or e.errno in (errno.ENOENT, errno.ESRCH)):
1607
for child in children:
1608
childpath = joinpath(path, child)
1609
if self._tree.is_control_filename(childpath):
1611
yield self.trans_id_tree_path(childpath)
1613
def _generate_limbo_path(self, trans_id):
1614
"""Generate a limbo path using the final path if possible.
1616
This optimizes the performance of applying the tree transform by
1617
avoiding renames. These renames can be avoided only when the parent
1618
directory is already scheduled for creation.
1620
If the final path cannot be used, falls back to using the trans_id as
1623
parent = self._new_parent.get(trans_id)
1624
# if the parent directory is already in limbo (e.g. when building a
1625
# tree), choose a limbo name inside the parent, to reduce further
1627
use_direct_path = False
1628
if self._new_contents.get(parent) == 'directory':
1629
filename = self._new_name.get(trans_id)
1630
if filename is not None:
1631
if parent not in self._limbo_children:
1632
self._limbo_children[parent] = set()
1633
self._limbo_children_names[parent] = {}
1634
use_direct_path = True
1635
# the direct path can only be used if no other file has
1636
# already taken this pathname, i.e. if the name is unused, or
1637
# if it is already associated with this trans_id.
1638
elif self._case_sensitive_target:
1639
if (self._limbo_children_names[parent].get(filename)
1640
in (trans_id, None)):
1641
use_direct_path = True
1643
for l_filename, l_trans_id in\
1644
self._limbo_children_names[parent].iteritems():
1645
if l_trans_id == trans_id:
1647
if l_filename.lower() == filename.lower():
1650
use_direct_path = True
1652
if not use_direct_path:
1653
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1655
limbo_name = pathjoin(self._limbo_files[parent], filename)
1656
self._limbo_children[parent].add(trans_id)
1657
self._limbo_children_names[parent][filename] = trans_id
1661
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1662
"""Apply all changes to the inventory and filesystem.
1664
If filesystem or inventory conflicts are present, MalformedTransform
1667
If apply succeeds, finalize is not necessary.
1669
:param no_conflicts: if True, the caller guarantees there are no
1670
conflicts, so no check is made.
1671
:param precomputed_delta: An inventory delta to use instead of
1673
:param _mover: Supply an alternate FileMover, for testing
1675
if not no_conflicts:
1676
self._check_malformed()
1677
child_pb = ui.ui_factory.nested_progress_bar()
1679
if precomputed_delta is None:
1680
child_pb.update('Apply phase', 0, 2)
1681
inventory_delta = self._generate_inventory_delta()
1684
inventory_delta = precomputed_delta
1687
mover = _FileMover()
1691
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1692
self._apply_removals(mover)
1693
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1694
modified_paths = self._apply_insertions(mover)
1699
mover.apply_deletions()
1702
self._tree.apply_inventory_delta(inventory_delta)
1705
return _TransformResults(modified_paths, self.rename_count)
1707
def _generate_inventory_delta(self):
1708
"""Generate an inventory delta for the current transform."""
1709
inventory_delta = []
1710
child_pb = ui.ui_factory.nested_progress_bar()
1711
new_paths = self._inventory_altered()
1712
total_entries = len(new_paths) + len(self._removed_id)
1714
for num, trans_id in enumerate(self._removed_id):
1716
child_pb.update('removing file', num, total_entries)
1717
if trans_id == self._new_root:
1718
file_id = self._tree.get_root_id()
1720
file_id = self.tree_file_id(trans_id)
1721
# File-id isn't really being deleted, just moved
1722
if file_id in self._r_new_id:
1724
path = self._tree_id_paths[trans_id]
1725
inventory_delta.append((path, None, file_id, None))
1726
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1728
entries = self._tree.iter_entries_by_dir(
1729
new_path_file_ids.values())
1730
old_paths = dict((e.file_id, p) for p, e in entries)
1732
for num, (path, trans_id) in enumerate(new_paths):
1734
child_pb.update('adding file',
1735
num + len(self._removed_id), total_entries)
1736
file_id = new_path_file_ids[trans_id]
1740
kind = self.final_kind(trans_id)
1742
kind = self._tree.stored_kind(file_id)
1743
parent_trans_id = self.final_parent(trans_id)
1744
parent_file_id = new_path_file_ids.get(parent_trans_id)
1745
if parent_file_id is None:
1746
parent_file_id = self.final_file_id(parent_trans_id)
1747
if trans_id in self._new_reference_revision:
1748
new_entry = inventory.TreeReference(
1750
self._new_name[trans_id],
1751
self.final_file_id(self._new_parent[trans_id]),
1752
None, self._new_reference_revision[trans_id])
1754
new_entry = inventory.make_entry(kind,
1755
self.final_name(trans_id),
1756
parent_file_id, file_id)
1757
old_path = old_paths.get(new_entry.file_id)
1758
new_executability = self._new_executability.get(trans_id)
1759
if new_executability is not None:
1760
new_entry.executable = new_executability
1761
inventory_delta.append(
1762
(old_path, path, new_entry.file_id, new_entry))
1765
return inventory_delta
1767
def _apply_removals(self, mover):
1768
"""Perform tree operations that remove directory/inventory names.
1770
That is, delete files that are to be deleted, and put any files that
1771
need renaming into limbo. This must be done in strict child-to-parent
1774
If inventory_delta is None, no inventory delta generation is performed.
1776
tree_paths = list(self._tree_path_ids.iteritems())
1777
tree_paths.sort(reverse=True)
1778
child_pb = ui.ui_factory.nested_progress_bar()
1780
for num, data in enumerate(tree_paths):
1781
path, trans_id = data
1782
child_pb.update('removing file', num, len(tree_paths))
1783
full_path = self._tree.abspath(path)
1784
if trans_id in self._removed_contents:
1785
delete_path = os.path.join(self._deletiondir, trans_id)
1786
mover.pre_delete(full_path, delete_path)
1787
elif (trans_id in self._new_name
1788
or trans_id in self._new_parent):
1790
mover.rename(full_path, self._limbo_name(trans_id))
1791
except errors.TransformRenameFailed, e:
1792
if e.errno != errno.ENOENT:
1795
self.rename_count += 1
1799
def _apply_insertions(self, mover):
1800
"""Perform tree operations that insert directory/inventory names.
1802
That is, create any files that need to be created, and restore from
1803
limbo any files that needed renaming. This must be done in strict
1804
parent-to-child order.
1806
If inventory_delta is None, no inventory delta is calculated, and
1807
no list of modified paths is returned.
1809
new_paths = self.new_paths(filesystem_only=True)
1811
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1813
child_pb = ui.ui_factory.nested_progress_bar()
1815
for num, (path, trans_id) in enumerate(new_paths):
1817
child_pb.update('adding file', num, len(new_paths))
1818
full_path = self._tree.abspath(path)
1819
if trans_id in self._needs_rename:
1821
mover.rename(self._limbo_name(trans_id), full_path)
1822
except errors.TransformRenameFailed, e:
1823
# We may be renaming a dangling inventory id
1824
if e.errno != errno.ENOENT:
1827
self.rename_count += 1
1828
if (trans_id in self._new_contents or
1829
self.path_changed(trans_id)):
1830
if trans_id in self._new_contents:
1831
modified_paths.append(full_path)
1832
if trans_id in self._new_executability:
1833
self._set_executability(path, trans_id)
1836
self._new_contents.clear()
1837
return modified_paths
1840
class TransformPreview(DiskTreeTransform):
1841
"""A TreeTransform for generating preview trees.
1843
Unlike TreeTransform, this version works when the input tree is a
1844
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1845
unversioned files in the input tree.
1848
def __init__(self, tree, pb=None, case_sensitive=True):
1850
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1851
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1853
def canonical_path(self, path):
1856
def tree_kind(self, trans_id):
1857
path = self._tree_id_paths.get(trans_id)
1860
file_id = self._tree.path2id(path)
1862
return self._tree.kind(file_id)
1863
except errors.NoSuchFile:
1866
def _set_mode(self, trans_id, mode_id, typefunc):
1867
"""Set the mode of new file contents.
1868
The mode_id is the existing file to get the mode from (often the same
1869
as trans_id). The operation is only performed if there's a mode match
1870
according to typefunc.
1872
# is it ok to ignore this? probably
1875
def iter_tree_children(self, parent_id):
1876
"""Iterate through the entry's tree children, if any"""
1878
path = self._tree_id_paths[parent_id]
1881
file_id = self.tree_file_id(parent_id)
1884
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1885
children = getattr(entry, 'children', {})
1886
for child in children:
1887
childpath = joinpath(path, child)
1888
yield self.trans_id_tree_path(childpath)
1890
def new_orphan(self, trans_id, parent_id):
1891
raise NotImplementedError(self.new_orphan)
1894
class _PreviewTree(tree.Tree):
1895
"""Partial implementation of Tree to support show_diff_trees"""
1897
def __init__(self, transform):
1898
self._transform = transform
1899
self._final_paths = FinalPaths(transform)
1900
self.__by_parent = None
1901
self._parent_ids = []
1902
self._all_children_cache = {}
1903
self._path2trans_id_cache = {}
1904
self._final_name_cache = {}
1905
self._iter_changes_cache = dict((c[0], c) for c in
1906
self._transform.iter_changes())
1908
def _content_change(self, file_id):
1909
"""Return True if the content of this file changed"""
1910
changes = self._iter_changes_cache.get(file_id)
1911
# changes[2] is true if the file content changed. See
1912
# InterTree.iter_changes.
1913
return (changes is not None and changes[2])
1915
def _get_repository(self):
1916
repo = getattr(self._transform._tree, '_repository', None)
1918
repo = self._transform._tree.branch.repository
1921
def _iter_parent_trees(self):
1922
for revision_id in self.get_parent_ids():
1924
yield self.revision_tree(revision_id)
1925
except errors.NoSuchRevisionInTree:
1926
yield self._get_repository().revision_tree(revision_id)
1928
def _get_file_revision(self, file_id, vf, tree_revision):
1929
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1930
self._iter_parent_trees()]
1931
vf.add_lines((file_id, tree_revision), parent_keys,
1932
self.get_file_lines(file_id))
1933
repo = self._get_repository()
1934
base_vf = repo.texts
1935
if base_vf not in vf.fallback_versionedfiles:
1936
vf.fallback_versionedfiles.append(base_vf)
1937
return tree_revision
1939
def _stat_limbo_file(self, file_id):
1940
trans_id = self._transform.trans_id_file_id(file_id)
1941
name = self._transform._limbo_name(trans_id)
1942
return os.lstat(name)
1945
def _by_parent(self):
1946
if self.__by_parent is None:
1947
self.__by_parent = self._transform.by_parent()
1948
return self.__by_parent
1950
def _comparison_data(self, entry, path):
1951
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1952
if kind == 'missing':
1956
file_id = self._transform.final_file_id(self._path2trans_id(path))
1957
executable = self.is_executable(file_id, path)
1958
return kind, executable, None
1960
def is_locked(self):
1963
def lock_read(self):
1964
# Perhaps in theory, this should lock the TreeTransform?
1971
def inventory(self):
1972
"""This Tree does not use inventory as its backing data."""
1973
raise NotImplementedError(_PreviewTree.inventory)
1975
def get_root_id(self):
1976
return self._transform.final_file_id(self._transform.root)
1978
def all_file_ids(self):
1979
tree_ids = set(self._transform._tree.all_file_ids())
1980
tree_ids.difference_update(self._transform.tree_file_id(t)
1981
for t in self._transform._removed_id)
1982
tree_ids.update(self._transform._new_id.values())
1986
return iter(self.all_file_ids())
1988
def _has_id(self, file_id, fallback_check):
1989
if file_id in self._transform._r_new_id:
1991
elif file_id in set([self._transform.tree_file_id(trans_id) for
1992
trans_id in self._transform._removed_id]):
1995
return fallback_check(file_id)
1997
def has_id(self, file_id):
1998
return self._has_id(file_id, self._transform._tree.has_id)
2000
def has_or_had_id(self, file_id):
2001
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2003
def _path2trans_id(self, path):
2004
# We must not use None here, because that is a valid value to store.
2005
trans_id = self._path2trans_id_cache.get(path, object)
2006
if trans_id is not object:
2008
segments = splitpath(path)
2009
cur_parent = self._transform.root
2010
for cur_segment in segments:
2011
for child in self._all_children(cur_parent):
2012
final_name = self._final_name_cache.get(child)
2013
if final_name is None:
2014
final_name = self._transform.final_name(child)
2015
self._final_name_cache[child] = final_name
2016
if final_name == cur_segment:
2020
self._path2trans_id_cache[path] = None
2022
self._path2trans_id_cache[path] = cur_parent
2025
def path2id(self, path):
2026
return self._transform.final_file_id(self._path2trans_id(path))
2028
def id2path(self, file_id):
2029
trans_id = self._transform.trans_id_file_id(file_id)
2031
return self._final_paths._determine_path(trans_id)
2033
raise errors.NoSuchId(self, file_id)
2035
def _all_children(self, trans_id):
2036
children = self._all_children_cache.get(trans_id)
2037
if children is not None:
2039
children = set(self._transform.iter_tree_children(trans_id))
2040
# children in the _new_parent set are provided by _by_parent.
2041
children.difference_update(self._transform._new_parent.keys())
2042
children.update(self._by_parent.get(trans_id, []))
2043
self._all_children_cache[trans_id] = children
2046
def iter_children(self, file_id):
2047
trans_id = self._transform.trans_id_file_id(file_id)
2048
for child_trans_id in self._all_children(trans_id):
2049
yield self._transform.final_file_id(child_trans_id)
2052
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2053
in self._transform._tree.extras())
2054
possible_extras.update(self._transform._new_contents)
2055
possible_extras.update(self._transform._removed_id)
2056
for trans_id in possible_extras:
2057
if self._transform.final_file_id(trans_id) is None:
2058
yield self._final_paths._determine_path(trans_id)
2060
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
2061
yield_parents=False):
2062
for trans_id, parent_file_id in ordered_entries:
2063
file_id = self._transform.final_file_id(trans_id)
2066
if (specific_file_ids is not None
2067
and file_id not in specific_file_ids):
2069
kind = self._transform.final_kind(trans_id)
2071
kind = self._transform._tree.stored_kind(file_id)
2072
new_entry = inventory.make_entry(
2074
self._transform.final_name(trans_id),
2075
parent_file_id, file_id)
2076
yield new_entry, trans_id
2078
def _list_files_by_dir(self):
2079
todo = [ROOT_PARENT]
2081
while len(todo) > 0:
2083
parent_file_id = self._transform.final_file_id(parent)
2084
children = list(self._all_children(parent))
2085
paths = dict(zip(children, self._final_paths.get_paths(children)))
2086
children.sort(key=paths.get)
2087
todo.extend(reversed(children))
2088
for trans_id in children:
2089
ordered_ids.append((trans_id, parent_file_id))
2092
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
2093
# This may not be a maximally efficient implementation, but it is
2094
# reasonably straightforward. An implementation that grafts the
2095
# TreeTransform changes onto the tree's iter_entries_by_dir results
2096
# might be more efficient, but requires tricky inferences about stack
2098
ordered_ids = self._list_files_by_dir()
2099
for entry, trans_id in self._make_inv_entries(ordered_ids,
2100
specific_file_ids, yield_parents=yield_parents):
2101
yield unicode(self._final_paths.get_path(trans_id)), entry
2103
def _iter_entries_for_dir(self, dir_path):
2104
"""Return path, entry for items in a directory without recursing down."""
2105
dir_file_id = self.path2id(dir_path)
2107
for file_id in self.iter_children(dir_file_id):
2108
trans_id = self._transform.trans_id_file_id(file_id)
2109
ordered_ids.append((trans_id, file_id))
2110
for entry, trans_id in self._make_inv_entries(ordered_ids):
2111
yield unicode(self._final_paths.get_path(trans_id)), entry
2113
def list_files(self, include_root=False, from_dir=None, recursive=True):
2114
"""See WorkingTree.list_files."""
2115
# XXX This should behave like WorkingTree.list_files, but is really
2116
# more like RevisionTree.list_files.
2120
prefix = from_dir + '/'
2121
entries = self.iter_entries_by_dir()
2122
for path, entry in entries:
2123
if entry.name == '' and not include_root:
2126
if not path.startswith(prefix):
2128
path = path[len(prefix):]
2129
yield path, 'V', entry.kind, entry.file_id, entry
2131
if from_dir is None and include_root is True:
2132
root_entry = inventory.make_entry('directory', '',
2133
ROOT_PARENT, self.get_root_id())
2134
yield '', 'V', 'directory', root_entry.file_id, root_entry
2135
entries = self._iter_entries_for_dir(from_dir or '')
2136
for path, entry in entries:
2137
yield path, 'V', entry.kind, entry.file_id, entry
2139
def kind(self, file_id):
2140
trans_id = self._transform.trans_id_file_id(file_id)
2141
return self._transform.final_kind(trans_id)
2143
def stored_kind(self, file_id):
2144
trans_id = self._transform.trans_id_file_id(file_id)
2146
return self._transform._new_contents[trans_id]
2148
return self._transform._tree.stored_kind(file_id)
2150
def get_file_mtime(self, file_id, path=None):
2151
"""See Tree.get_file_mtime"""
2152
if not self._content_change(file_id):
2153
return self._transform._tree.get_file_mtime(file_id)
2154
return self._stat_limbo_file(file_id).st_mtime
2156
def _file_size(self, entry, stat_value):
2157
return self.get_file_size(entry.file_id)
2159
def get_file_size(self, file_id):
2160
"""See Tree.get_file_size"""
2161
if self.kind(file_id) == 'file':
2162
return self._transform._tree.get_file_size(file_id)
2166
def get_file_sha1(self, file_id, path=None, stat_value=None):
2167
trans_id = self._transform.trans_id_file_id(file_id)
2168
kind = self._transform._new_contents.get(trans_id)
2170
return self._transform._tree.get_file_sha1(file_id)
2172
fileobj = self.get_file(file_id)
2174
return sha_file(fileobj)
2178
def is_executable(self, file_id, path=None):
2181
trans_id = self._transform.trans_id_file_id(file_id)
2183
return self._transform._new_executability[trans_id]
2186
return self._transform._tree.is_executable(file_id, path)
2188
if e.errno == errno.ENOENT:
2191
except errors.NoSuchId:
2194
def path_content_summary(self, path):
2195
trans_id = self._path2trans_id(path)
2196
tt = self._transform
2197
tree_path = tt._tree_id_paths.get(trans_id)
2198
kind = tt._new_contents.get(trans_id)
2200
if tree_path is None or trans_id in tt._removed_contents:
2201
return 'missing', None, None, None
2202
summary = tt._tree.path_content_summary(tree_path)
2203
kind, size, executable, link_or_sha1 = summary
2206
limbo_name = tt._limbo_name(trans_id)
2207
if trans_id in tt._new_reference_revision:
2208
kind = 'tree-reference'
2210
statval = os.lstat(limbo_name)
2211
size = statval.st_size
2212
if not supports_executable():
2215
executable = statval.st_mode & S_IEXEC
2219
if kind == 'symlink':
2220
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2221
executable = tt._new_executability.get(trans_id, executable)
2222
return kind, size, executable, link_or_sha1
2224
def iter_changes(self, from_tree, include_unchanged=False,
2225
specific_files=None, pb=None, extra_trees=None,
2226
require_versioned=True, want_unversioned=False):
2227
"""See InterTree.iter_changes.
2229
This has a fast path that is only used when the from_tree matches
2230
the transform tree, and no fancy options are supplied.
2232
if (from_tree is not self._transform._tree or include_unchanged or
2233
specific_files or want_unversioned):
2234
return tree.InterTree(from_tree, self).iter_changes(
2235
include_unchanged=include_unchanged,
2236
specific_files=specific_files,
2238
extra_trees=extra_trees,
2239
require_versioned=require_versioned,
2240
want_unversioned=want_unversioned)
2241
if want_unversioned:
2242
raise ValueError('want_unversioned is not supported')
2243
return self._transform.iter_changes()
2245
def get_file(self, file_id, path=None):
2246
"""See Tree.get_file"""
2247
if not self._content_change(file_id):
2248
return self._transform._tree.get_file(file_id, path)
2249
trans_id = self._transform.trans_id_file_id(file_id)
2250
name = self._transform._limbo_name(trans_id)
2251
return open(name, 'rb')
2253
def get_file_with_stat(self, file_id, path=None):
2254
return self.get_file(file_id, path), None
2256
def annotate_iter(self, file_id,
2257
default_revision=_mod_revision.CURRENT_REVISION):
2258
changes = self._iter_changes_cache.get(file_id)
2262
changed_content, versioned, kind = (changes[2], changes[3],
2266
get_old = (kind[0] == 'file' and versioned[0])
2268
old_annotation = self._transform._tree.annotate_iter(file_id,
2269
default_revision=default_revision)
2273
return old_annotation
2274
if not changed_content:
2275
return old_annotation
2276
# TODO: This is doing something similar to what WT.annotate_iter is
2277
# doing, however it fails slightly because it doesn't know what
2278
# the *other* revision_id is, so it doesn't know how to give the
2279
# other as the origin for some lines, they all get
2280
# 'default_revision'
2281
# It would be nice to be able to use the new Annotator based
2282
# approach, as well.
2283
return annotate.reannotate([old_annotation],
2284
self.get_file(file_id).readlines(),
2287
def get_symlink_target(self, file_id):
2288
"""See Tree.get_symlink_target"""
2289
if not self._content_change(file_id):
2290
return self._transform._tree.get_symlink_target(file_id)
2291
trans_id = self._transform.trans_id_file_id(file_id)
2292
name = self._transform._limbo_name(trans_id)
2293
return osutils.readlink(name)
2295
def walkdirs(self, prefix=''):
2296
pending = [self._transform.root]
2297
while len(pending) > 0:
2298
parent_id = pending.pop()
2301
prefix = prefix.rstrip('/')
2302
parent_path = self._final_paths.get_path(parent_id)
2303
parent_file_id = self._transform.final_file_id(parent_id)
2304
for child_id in self._all_children(parent_id):
2305
path_from_root = self._final_paths.get_path(child_id)
2306
basename = self._transform.final_name(child_id)
2307
file_id = self._transform.final_file_id(child_id)
2308
kind = self._transform.final_kind(child_id)
2309
if kind is not None:
2310
versioned_kind = kind
2313
versioned_kind = self._transform._tree.stored_kind(file_id)
2314
if versioned_kind == 'directory':
2315
subdirs.append(child_id)
2316
children.append((path_from_root, basename, kind, None,
2317
file_id, versioned_kind))
2319
if parent_path.startswith(prefix):
2320
yield (parent_path, parent_file_id), children
2321
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2324
def get_parent_ids(self):
2325
return self._parent_ids
2327
def set_parent_ids(self, parent_ids):
2328
self._parent_ids = parent_ids
2330
def get_revision_tree(self, revision_id):
2331
return self._transform._tree.get_revision_tree(revision_id)
2334
def joinpath(parent, child):
2335
"""Join tree-relative paths, handling the tree root specially"""
2336
if parent is None or parent == "":
2339
return pathjoin(parent, child)
2342
class FinalPaths(object):
2343
"""Make path calculation cheap by memoizing paths.
2345
The underlying tree must not be manipulated between calls, or else
2346
the results will likely be incorrect.
2348
def __init__(self, transform):
2349
object.__init__(self)
2350
self._known_paths = {}
2351
self.transform = transform
2353
def _determine_path(self, trans_id):
2354
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2356
name = self.transform.final_name(trans_id)
2357
parent_id = self.transform.final_parent(trans_id)
2358
if parent_id == self.transform.root:
2361
return pathjoin(self.get_path(parent_id), name)
2363
def get_path(self, trans_id):
2364
"""Find the final path associated with a trans_id"""
2365
if trans_id not in self._known_paths:
2366
self._known_paths[trans_id] = self._determine_path(trans_id)
2367
return self._known_paths[trans_id]
2369
def get_paths(self, trans_ids):
2370
return [(self.get_path(t), t) for t in trans_ids]
2374
def topology_sorted_ids(tree):
2375
"""Determine the topological order of the ids in a tree"""
2376
file_ids = list(tree)
2377
file_ids.sort(key=tree.id2path)
2381
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2382
delta_from_tree=False):
2383
"""Create working tree for a branch, using a TreeTransform.
2385
This function should be used on empty trees, having a tree root at most.
2386
(see merge and revert functionality for working with existing trees)
2388
Existing files are handled like so:
2390
- Existing bzrdirs take precedence over creating new items. They are
2391
created as '%s.diverted' % name.
2392
- Otherwise, if the content on disk matches the content we are building,
2393
it is silently replaced.
2394
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2396
:param tree: The tree to convert wt into a copy of
2397
:param wt: The working tree that files will be placed into
2398
:param accelerator_tree: A tree which can be used for retrieving file
2399
contents more quickly than tree itself, i.e. a workingtree. tree
2400
will be used for cases where accelerator_tree's content is different.
2401
:param hardlink: If true, hard-link files to accelerator_tree, where
2402
possible. accelerator_tree must implement abspath, i.e. be a
2404
:param delta_from_tree: If true, build_tree may use the input Tree to
2405
generate the inventory delta.
2407
wt.lock_tree_write()
2411
if accelerator_tree is not None:
2412
accelerator_tree.lock_read()
2414
return _build_tree(tree, wt, accelerator_tree, hardlink,
2417
if accelerator_tree is not None:
2418
accelerator_tree.unlock()
2425
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2426
"""See build_tree."""
2427
for num, _unused in enumerate(wt.all_file_ids()):
2428
if num > 0: # more than just a root
2429
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2431
top_pb = ui.ui_factory.nested_progress_bar()
2432
pp = ProgressPhase("Build phase", 2, top_pb)
2433
if tree.inventory.root is not None:
2434
# This is kind of a hack: we should be altering the root
2435
# as part of the regular tree shape diff logic.
2436
# The conditional test here is to avoid doing an
2437
# expensive operation (flush) every time the root id
2438
# is set within the tree, nor setting the root and thus
2439
# marking the tree as dirty, because we use two different
2440
# idioms here: tree interfaces and inventory interfaces.
2441
if wt.get_root_id() != tree.get_root_id():
2442
wt.set_root_id(tree.get_root_id())
2444
tt = TreeTransform(wt)
2448
file_trans_id[wt.get_root_id()] = \
2449
tt.trans_id_tree_file_id(wt.get_root_id())
2450
pb = ui.ui_factory.nested_progress_bar()
2452
deferred_contents = []
2454
total = len(tree.inventory)
2456
precomputed_delta = []
2458
precomputed_delta = None
2459
# Check if tree inventory has content. If so, we populate
2460
# existing_files with the directory content. If there are no
2461
# entries we skip populating existing_files as its not used.
2462
# This improves performance and unncessary work on large
2463
# directory trees. (#501307)
2465
existing_files = set()
2466
for dir, files in wt.walkdirs():
2467
existing_files.update(f[0] for f in files)
2468
for num, (tree_path, entry) in \
2469
enumerate(tree.inventory.iter_entries_by_dir()):
2470
pb.update("Building tree", num - len(deferred_contents), total)
2471
if entry.parent_id is None:
2474
file_id = entry.file_id
2476
precomputed_delta.append((None, tree_path, file_id, entry))
2477
if tree_path in existing_files:
2478
target_path = wt.abspath(tree_path)
2479
kind = file_kind(target_path)
2480
if kind == "directory":
2482
bzrdir.BzrDir.open(target_path)
2483
except errors.NotBranchError:
2487
if (file_id not in divert and
2488
_content_match(tree, entry, file_id, kind,
2490
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2491
if kind == 'directory':
2493
parent_id = file_trans_id[entry.parent_id]
2494
if entry.kind == 'file':
2495
# We *almost* replicate new_by_entry, so that we can defer
2496
# getting the file text, and get them all at once.
2497
trans_id = tt.create_path(entry.name, parent_id)
2498
file_trans_id[file_id] = trans_id
2499
tt.version_file(file_id, trans_id)
2500
executable = tree.is_executable(file_id, tree_path)
2502
tt.set_executability(executable, trans_id)
2503
trans_data = (trans_id, tree_path)
2504
deferred_contents.append((file_id, trans_data))
2506
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2509
new_trans_id = file_trans_id[file_id]
2510
old_parent = tt.trans_id_tree_path(tree_path)
2511
_reparent_children(tt, old_parent, new_trans_id)
2512
offset = num + 1 - len(deferred_contents)
2513
_create_files(tt, tree, deferred_contents, pb, offset,
2514
accelerator_tree, hardlink)
2518
divert_trans = set(file_trans_id[f] for f in divert)
2519
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2520
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2521
if len(raw_conflicts) > 0:
2522
precomputed_delta = None
2523
conflicts = cook_conflicts(raw_conflicts, tt)
2524
for conflict in conflicts:
2525
trace.warning(conflict)
2527
wt.add_conflicts(conflicts)
2528
except errors.UnsupportedOperation:
2530
result = tt.apply(no_conflicts=True,
2531
precomputed_delta=precomputed_delta)
2538
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2540
total = len(desired_files) + offset
2542
if accelerator_tree is None:
2543
new_desired_files = desired_files
2545
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2546
unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
2547
in iter if not (c or e[0] != e[1])]
2548
if accelerator_tree.supports_content_filtering():
2549
unchanged = [(f, p) for (f, p) in unchanged
2550
if not accelerator_tree.iter_search_rules([p]).next()]
2551
unchanged = dict(unchanged)
2552
new_desired_files = []
2554
for file_id, (trans_id, tree_path) in desired_files:
2555
accelerator_path = unchanged.get(file_id)
2556
if accelerator_path is None:
2557
new_desired_files.append((file_id, (trans_id, tree_path)))
2559
pb.update('Adding file contents', count + offset, total)
2561
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2564
contents = accelerator_tree.get_file(file_id, accelerator_path)
2565
if wt.supports_content_filtering():
2566
filters = wt._content_filter_stack(tree_path)
2567
contents = filtered_output_bytes(contents, filters,
2568
ContentFilterContext(tree_path, tree))
2570
tt.create_file(contents, trans_id)
2574
except AttributeError:
2575
# after filtering, contents may no longer be file-like
2579
for count, ((trans_id, tree_path), contents) in enumerate(
2580
tree.iter_files_bytes(new_desired_files)):
2581
if wt.supports_content_filtering():
2582
filters = wt._content_filter_stack(tree_path)
2583
contents = filtered_output_bytes(contents, filters,
2584
ContentFilterContext(tree_path, tree))
2585
tt.create_file(contents, trans_id)
2586
pb.update('Adding file contents', count + offset, total)
2589
def _reparent_children(tt, old_parent, new_parent):
2590
for child in tt.iter_tree_children(old_parent):
2591
tt.adjust_path(tt.final_name(child), new_parent, child)
2594
def _reparent_transform_children(tt, old_parent, new_parent):
2595
by_parent = tt.by_parent()
2596
for child in by_parent[old_parent]:
2597
tt.adjust_path(tt.final_name(child), new_parent, child)
2598
return by_parent[old_parent]
2601
def _content_match(tree, entry, file_id, kind, target_path):
2602
if entry.kind != kind:
2604
if entry.kind == "directory":
2606
if entry.kind == "file":
2607
f = file(target_path, 'rb')
2609
if tree.get_file_text(file_id) == f.read():
2613
elif entry.kind == "symlink":
2614
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2619
def resolve_checkout(tt, conflicts, divert):
2620
new_conflicts = set()
2621
for c_type, conflict in ((c[0], c) for c in conflicts):
2622
# Anything but a 'duplicate' would indicate programmer error
2623
if c_type != 'duplicate':
2624
raise AssertionError(c_type)
2625
# Now figure out which is new and which is old
2626
if tt.new_contents(conflict[1]):
2627
new_file = conflict[1]
2628
old_file = conflict[2]
2630
new_file = conflict[2]
2631
old_file = conflict[1]
2633
# We should only get here if the conflict wasn't completely
2635
final_parent = tt.final_parent(old_file)
2636
if new_file in divert:
2637
new_name = tt.final_name(old_file)+'.diverted'
2638
tt.adjust_path(new_name, final_parent, new_file)
2639
new_conflicts.add((c_type, 'Diverted to',
2640
new_file, old_file))
2642
new_name = tt.final_name(old_file)+'.moved'
2643
tt.adjust_path(new_name, final_parent, old_file)
2644
new_conflicts.add((c_type, 'Moved existing file to',
2645
old_file, new_file))
2646
return new_conflicts
2649
def new_by_entry(tt, entry, parent_id, tree):
2650
"""Create a new file according to its inventory entry"""
2654
contents = tree.get_file(entry.file_id).readlines()
2655
executable = tree.is_executable(entry.file_id)
2656
return tt.new_file(name, parent_id, contents, entry.file_id,
2658
elif kind in ('directory', 'tree-reference'):
2659
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2660
if kind == 'tree-reference':
2661
tt.set_tree_reference(entry.reference_revision, trans_id)
2663
elif kind == 'symlink':
2664
target = tree.get_symlink_target(entry.file_id)
2665
return tt.new_symlink(name, parent_id, target, entry.file_id)
2667
raise errors.BadFileKindError(name, kind)
2670
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2671
filter_tree_path=None):
2672
"""Create new file contents according to tree contents.
2674
:param filter_tree_path: the tree path to use to lookup
2675
content filters to apply to the bytes output in the working tree.
2676
This only applies if the working tree supports content filtering.
2678
kind = tree.kind(file_id)
2679
if kind == 'directory':
2680
tt.create_directory(trans_id)
2681
elif kind == "file":
2683
tree_file = tree.get_file(file_id)
2685
bytes = tree_file.readlines()
2689
if wt.supports_content_filtering() and filter_tree_path is not None:
2690
filters = wt._content_filter_stack(filter_tree_path)
2691
bytes = filtered_output_bytes(bytes, filters,
2692
ContentFilterContext(filter_tree_path, tree))
2693
tt.create_file(bytes, trans_id)
2694
elif kind == "symlink":
2695
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2697
raise AssertionError('Unknown kind %r' % kind)
2700
def create_entry_executability(tt, entry, trans_id):
2701
"""Set the executability of a trans_id according to an inventory entry"""
2702
if entry.kind == "file":
2703
tt.set_executability(entry.executable, trans_id)
2706
@deprecated_function(deprecated_in((2, 3, 0)))
2707
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2708
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2711
@deprecated_function(deprecated_in((2, 3, 0)))
2712
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2713
"""Produce a backup-style name that appears to be available"""
2717
yield "%s.~%d~" % (name, counter)
2719
for new_name in name_gen():
2720
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2724
def _entry_changes(file_id, entry, working_tree):
2725
"""Determine in which ways the inventory entry has changed.
2727
Returns booleans: has_contents, content_mod, meta_mod
2728
has_contents means there are currently contents, but they differ
2729
contents_mod means contents need to be modified
2730
meta_mod means the metadata needs to be modified
2732
cur_entry = working_tree.inventory[file_id]
2734
working_kind = working_tree.kind(file_id)
2737
has_contents = False
2740
if has_contents is True:
2741
if entry.kind != working_kind:
2742
contents_mod, meta_mod = True, False
2744
cur_entry._read_tree_state(working_tree.id2path(file_id),
2746
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2747
cur_entry._forget_tree_state()
2748
return has_contents, contents_mod, meta_mod
2751
def revert(working_tree, target_tree, filenames, backups=False,
2752
pb=None, change_reporter=None):
2753
"""Revert a working tree's contents to those of a target tree."""
2754
target_tree.lock_read()
2755
pb = ui.ui_factory.nested_progress_bar()
2756
tt = TreeTransform(working_tree, pb)
2758
pp = ProgressPhase("Revert phase", 3, pb)
2759
conflicts, merge_modified = _prepare_revert_transform(
2760
working_tree, target_tree, tt, filenames, backups, pp)
2762
change_reporter = delta._ChangeReporter(
2763
unversioned_filter=working_tree.is_ignored)
2764
delta.report_changes(tt.iter_changes(), change_reporter)
2765
for conflict in conflicts:
2766
trace.warning(conflict)
2769
working_tree.set_merge_modified(merge_modified)
2771
target_tree.unlock()
2777
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2778
backups, pp, basis_tree=None,
2779
merge_modified=None):
2780
child_pb = ui.ui_factory.nested_progress_bar()
2782
if merge_modified is None:
2783
merge_modified = working_tree.merge_modified()
2784
merge_modified = _alter_files(working_tree, target_tree, tt,
2785
child_pb, filenames, backups,
2786
merge_modified, basis_tree)
2789
child_pb = ui.ui_factory.nested_progress_bar()
2791
raw_conflicts = resolve_conflicts(tt, child_pb,
2792
lambda t, c: conflict_pass(t, c, target_tree))
2795
conflicts = cook_conflicts(raw_conflicts, tt)
2796
return conflicts, merge_modified
2799
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2800
backups, merge_modified, basis_tree=None):
2801
if basis_tree is not None:
2802
basis_tree.lock_read()
2803
change_list = target_tree.iter_changes(working_tree,
2804
specific_files=specific_files, pb=pb)
2805
if target_tree.get_root_id() is None:
2811
for id_num, (file_id, path, changed_content, versioned, parent, name,
2812
kind, executable) in enumerate(change_list):
2813
if skip_root and file_id[0] is not None and parent[0] is None:
2815
trans_id = tt.trans_id_file_id(file_id)
2818
keep_content = False
2819
if kind[0] == 'file' and (backups or kind[1] is None):
2820
wt_sha1 = working_tree.get_file_sha1(file_id)
2821
if merge_modified.get(file_id) != wt_sha1:
2822
# acquire the basis tree lazily to prevent the
2823
# expense of accessing it when it's not needed ?
2824
# (Guessing, RBC, 200702)
2825
if basis_tree is None:
2826
basis_tree = working_tree.basis_tree()
2827
basis_tree.lock_read()
2828
if file_id in basis_tree:
2829
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2831
elif kind[1] is None and not versioned[1]:
2833
if kind[0] is not None:
2834
if not keep_content:
2835
tt.delete_contents(trans_id)
2836
elif kind[1] is not None:
2837
parent_trans_id = tt.trans_id_file_id(parent[0])
2838
backup_name = tt._available_backup_name(
2839
name[0], parent_trans_id)
2840
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2841
new_trans_id = tt.create_path(name[0], parent_trans_id)
2842
if versioned == (True, True):
2843
tt.unversion_file(trans_id)
2844
tt.version_file(file_id, new_trans_id)
2845
# New contents should have the same unix perms as old
2848
trans_id = new_trans_id
2849
if kind[1] in ('directory', 'tree-reference'):
2850
tt.create_directory(trans_id)
2851
if kind[1] == 'tree-reference':
2852
revision = target_tree.get_reference_revision(file_id,
2854
tt.set_tree_reference(revision, trans_id)
2855
elif kind[1] == 'symlink':
2856
tt.create_symlink(target_tree.get_symlink_target(file_id),
2858
elif kind[1] == 'file':
2859
deferred_files.append((file_id, (trans_id, mode_id)))
2860
if basis_tree is None:
2861
basis_tree = working_tree.basis_tree()
2862
basis_tree.lock_read()
2863
new_sha1 = target_tree.get_file_sha1(file_id)
2864
if (file_id in basis_tree and new_sha1 ==
2865
basis_tree.get_file_sha1(file_id)):
2866
if file_id in merge_modified:
2867
del merge_modified[file_id]
2869
merge_modified[file_id] = new_sha1
2871
# preserve the execute bit when backing up
2872
if keep_content and executable[0] == executable[1]:
2873
tt.set_executability(executable[1], trans_id)
2874
elif kind[1] is not None:
2875
raise AssertionError(kind[1])
2876
if versioned == (False, True):
2877
tt.version_file(file_id, trans_id)
2878
if versioned == (True, False):
2879
tt.unversion_file(trans_id)
2880
if (name[1] is not None and
2881
(name[0] != name[1] or parent[0] != parent[1])):
2882
if name[1] == '' and parent[1] is None:
2883
parent_trans = ROOT_PARENT
2885
parent_trans = tt.trans_id_file_id(parent[1])
2886
if parent[0] is None and versioned[0]:
2887
tt.adjust_root_path(name[1], parent_trans)
2889
tt.adjust_path(name[1], parent_trans, trans_id)
2890
if executable[0] != executable[1] and kind[1] == "file":
2891
tt.set_executability(executable[1], trans_id)
2892
if working_tree.supports_content_filtering():
2893
for index, ((trans_id, mode_id), bytes) in enumerate(
2894
target_tree.iter_files_bytes(deferred_files)):
2895
file_id = deferred_files[index][0]
2896
# We're reverting a tree to the target tree so using the
2897
# target tree to find the file path seems the best choice
2898
# here IMO - Ian C 27/Oct/2009
2899
filter_tree_path = target_tree.id2path(file_id)
2900
filters = working_tree._content_filter_stack(filter_tree_path)
2901
bytes = filtered_output_bytes(bytes, filters,
2902
ContentFilterContext(filter_tree_path, working_tree))
2903
tt.create_file(bytes, trans_id, mode_id)
2905
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2907
tt.create_file(bytes, trans_id, mode_id)
2908
tt.fixup_new_roots()
2910
if basis_tree is not None:
2912
return merge_modified
2915
def resolve_conflicts(tt, pb=None, pass_func=None):
2916
"""Make many conflict-resolution attempts, but die if they fail"""
2917
if pass_func is None:
2918
pass_func = conflict_pass
2919
new_conflicts = set()
2920
pb = ui.ui_factory.nested_progress_bar()
2923
pb.update('Resolution pass', n+1, 10)
2924
conflicts = tt.find_conflicts()
2925
if len(conflicts) == 0:
2926
return new_conflicts
2927
new_conflicts.update(pass_func(tt, conflicts))
2928
raise MalformedTransform(conflicts=conflicts)
2933
def conflict_pass(tt, conflicts, path_tree=None):
2934
"""Resolve some classes of conflicts.
2936
:param tt: The transform to resolve conflicts in
2937
:param conflicts: The conflicts to resolve
2938
:param path_tree: A Tree to get supplemental paths from
2940
new_conflicts = set()
2941
for c_type, conflict in ((c[0], c) for c in conflicts):
2942
if c_type == 'duplicate id':
2943
tt.unversion_file(conflict[1])
2944
new_conflicts.add((c_type, 'Unversioned existing file',
2945
conflict[1], conflict[2], ))
2946
elif c_type == 'duplicate':
2947
# files that were renamed take precedence
2948
final_parent = tt.final_parent(conflict[1])
2949
if tt.path_changed(conflict[1]):
2950
existing_file, new_file = conflict[2], conflict[1]
2952
existing_file, new_file = conflict[1], conflict[2]
2953
new_name = tt.final_name(existing_file)+'.moved'
2954
tt.adjust_path(new_name, final_parent, existing_file)
2955
new_conflicts.add((c_type, 'Moved existing file to',
2956
existing_file, new_file))
2957
elif c_type == 'parent loop':
2958
# break the loop by undoing one of the ops that caused the loop
2960
while not tt.path_changed(cur):
2961
cur = tt.final_parent(cur)
2962
new_conflicts.add((c_type, 'Cancelled move', cur,
2963
tt.final_parent(cur),))
2964
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2966
elif c_type == 'missing parent':
2967
trans_id = conflict[1]
2968
if trans_id in tt._removed_contents:
2969
cancel_deletion = True
2970
orphans = tt._get_potential_orphans(trans_id)
2972
cancel_deletion = False
2973
# All children are orphans
2976
tt.new_orphan(o, trans_id)
2977
except OrphaningError:
2978
# Something bad happened so we cancel the directory
2979
# deletion which will leave it in place with a
2980
# conflict. The user can deal with it from there.
2981
# Note that this also catch the case where we don't
2982
# want to create orphans and leave the directory in
2984
cancel_deletion = True
2987
# Cancel the directory deletion
2988
tt.cancel_deletion(trans_id)
2989
new_conflicts.add(('deleting parent', 'Not deleting',
2994
tt.final_name(trans_id)
2996
if path_tree is not None:
2997
file_id = tt.final_file_id(trans_id)
2999
file_id = tt.inactive_file_id(trans_id)
3000
entry = path_tree.inventory[file_id]
3001
# special-case the other tree root (move its
3002
# children to current root)
3003
if entry.parent_id is None:
3005
moved = _reparent_transform_children(
3006
tt, trans_id, tt.root)
3008
new_conflicts.add((c_type, 'Moved to root',
3011
parent_trans_id = tt.trans_id_file_id(
3013
tt.adjust_path(entry.name, parent_trans_id,
3016
tt.create_directory(trans_id)
3017
new_conflicts.add((c_type, 'Created directory', trans_id))
3018
elif c_type == 'unversioned parent':
3019
file_id = tt.inactive_file_id(conflict[1])
3020
# special-case the other tree root (move its children instead)
3021
if path_tree and file_id in path_tree:
3022
if path_tree.path2id('') == file_id:
3023
# This is the root entry, skip it
3025
tt.version_file(file_id, conflict[1])
3026
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3027
elif c_type == 'non-directory parent':
3028
parent_id = conflict[1]
3029
parent_parent = tt.final_parent(parent_id)
3030
parent_name = tt.final_name(parent_id)
3031
parent_file_id = tt.final_file_id(parent_id)
3032
new_parent_id = tt.new_directory(parent_name + '.new',
3033
parent_parent, parent_file_id)
3034
_reparent_transform_children(tt, parent_id, new_parent_id)
3035
if parent_file_id is not None:
3036
tt.unversion_file(parent_id)
3037
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3038
elif c_type == 'versioning no contents':
3039
tt.cancel_versioning(conflict[1])
3040
return new_conflicts
3043
def cook_conflicts(raw_conflicts, tt):
3044
"""Generate a list of cooked conflicts, sorted by file path"""
3045
from bzrlib.conflicts import Conflict
3046
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3047
return sorted(conflict_iter, key=Conflict.sort_key)
3050
def iter_cook_conflicts(raw_conflicts, tt):
3051
from bzrlib.conflicts import Conflict
3053
for conflict in raw_conflicts:
3054
c_type = conflict[0]
3055
action = conflict[1]
3056
modified_path = fp.get_path(conflict[2])
3057
modified_id = tt.final_file_id(conflict[2])
3058
if len(conflict) == 3:
3059
yield Conflict.factory(c_type, action=action, path=modified_path,
3060
file_id=modified_id)
3063
conflicting_path = fp.get_path(conflict[3])
3064
conflicting_id = tt.final_file_id(conflict[3])
3065
yield Conflict.factory(c_type, action=action, path=modified_path,
3066
file_id=modified_id,
3067
conflict_path=conflicting_path,
3068
conflict_file_id=conflicting_id)
3071
class _FileMover(object):
3072
"""Moves and deletes files for TreeTransform, tracking operations"""
3075
self.past_renames = []
3076
self.pending_deletions = []
3078
def rename(self, from_, to):
3079
"""Rename a file from one path to another."""
3081
os.rename(from_, to)
3083
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3084
raise errors.FileExists(to, str(e))
3085
# normal OSError doesn't include filenames so it's hard to see where
3086
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3087
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3088
self.past_renames.append((from_, to))
3090
def pre_delete(self, from_, to):
3091
"""Rename a file out of the way and mark it for deletion.
3093
Unlike os.unlink, this works equally well for files and directories.
3094
:param from_: The current file path
3095
:param to: A temporary path for the file
3097
self.rename(from_, to)
3098
self.pending_deletions.append(to)
3101
"""Reverse all renames that have been performed"""
3102
for from_, to in reversed(self.past_renames):
3104
os.rename(to, from_)
3106
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3107
# after rollback, don't reuse _FileMover
3109
pending_deletions = None
3111
def apply_deletions(self):
3112
"""Apply all marked deletions"""
3113
for path in self.pending_deletions:
3115
# after apply_deletions, don't reuse _FileMover
3117
pending_deletions = None