1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from stat import S_ISREG, S_IEXEC
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
30
revision as _mod_revision,
33
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
34
ReusingTransform, NotVersionedError, CantMoveRoot,
35
ExistingLimbo, ImmortalLimbo, NoFinalPath,
37
from bzrlib.inventory import InventoryEntry
38
from bzrlib.osutils import (
47
from bzrlib.progress import DummyProgress, ProgressPhase
48
from bzrlib.symbol_versioning import (
51
from bzrlib.trace import mutter, warning
52
from bzrlib import tree
54
import bzrlib.urlutils as urlutils
57
ROOT_PARENT = "root-parent"
60
def unique_add(map, key, value):
62
raise DuplicateKey(key=key)
66
class _TransformResults(object):
67
def __init__(self, modified_paths, rename_count):
69
self.modified_paths = modified_paths
70
self.rename_count = rename_count
73
class TreeTransformBase(object):
74
"""The base class for TreeTransform and TreeTransformBase"""
76
def __init__(self, tree, limbodir, pb=DummyProgress(),
80
:param tree: The tree that will be transformed, but not necessarily
82
:param limbodir: A directory where new files can be stored until
83
they are installed in their proper places
84
:param pb: A ProgressBar indicating how much progress is being made
85
:param case_sensitive: If True, the target of the transform is
86
case sensitive, not just case preserving.
90
self._limbodir = limbodir
91
self._deletiondir = None
93
# mapping of trans_id -> new basename
95
# mapping of trans_id -> new parent trans_id
97
# mapping of trans_id with new contents -> new file_kind
98
self._new_contents = {}
99
# A mapping of transform ids to their limbo filename
100
self._limbo_files = {}
101
# A mapping of transform ids to a set of the transform ids of children
102
# that their limbo directory has
103
self._limbo_children = {}
104
# Map transform ids to maps of child filename to child transform id
105
self._limbo_children_names = {}
106
# List of transform ids that need to be renamed from limbo into place
107
self._needs_rename = set()
108
# Set of trans_ids whose contents will be removed
109
self._removed_contents = set()
110
# Mapping of trans_id -> new execute-bit value
111
self._new_executability = {}
112
# Mapping of trans_id -> new tree-reference value
113
self._new_reference_revision = {}
114
# Mapping of trans_id -> new file_id
116
# Mapping of old file-id -> trans_id
117
self._non_present_ids = {}
118
# Mapping of new file_id -> trans_id
120
# Set of file_ids that will be removed
121
self._removed_id = set()
122
# Mapping of path in old tree -> trans_id
123
self._tree_path_ids = {}
124
# Mapping trans_id -> path in old tree
125
self._tree_id_paths = {}
126
# Cache of realpath results, to speed up canonical_path
128
# Cache of relpath results, to speed up canonical_path
130
# The trans_id that will be used as the tree root
131
root_id = tree.get_root_id()
132
if root_id is not None:
133
self._new_root = self.trans_id_tree_file_id(root_id)
135
self._new_root = None
136
# Indictor of whether the transform has been applied
140
# Whether the target is case sensitive
141
self._case_sensitive_target = case_sensitive
142
# A counter of how many files have been renamed
143
self.rename_count = 0
145
def __get_root(self):
146
return self._new_root
148
root = property(__get_root)
151
"""Release the working tree lock, if held, clean up limbo dir.
153
This is required if apply has not been invoked, but can be invoked
156
if self._tree is None:
159
entries = [(self._limbo_name(t), t, k) for t, k in
160
self._new_contents.iteritems()]
161
entries.sort(reverse=True)
162
for path, trans_id, kind in entries:
163
if kind == "directory":
168
os.rmdir(self._limbodir)
170
# We don't especially care *why* the dir is immortal.
171
raise ImmortalLimbo(self._limbodir)
173
if self._deletiondir is not None:
174
os.rmdir(self._deletiondir)
176
raise errors.ImmortalPendingDeletion(self._deletiondir)
181
def _assign_id(self):
182
"""Produce a new tranform id"""
183
new_id = "new-%s" % self._id_number
187
def create_path(self, name, parent):
188
"""Assign a transaction id to a new path"""
189
trans_id = self._assign_id()
190
unique_add(self._new_name, trans_id, name)
191
unique_add(self._new_parent, trans_id, parent)
194
def adjust_path(self, name, parent, trans_id):
195
"""Change the path that is assigned to a transaction id."""
196
if trans_id == self._new_root:
198
previous_parent = self._new_parent.get(trans_id)
199
previous_name = self._new_name.get(trans_id)
200
self._new_name[trans_id] = name
201
self._new_parent[trans_id] = parent
202
if parent == ROOT_PARENT:
203
if self._new_root is not None:
204
raise ValueError("Cannot have multiple roots.")
205
self._new_root = trans_id
206
if (trans_id in self._limbo_files and
207
trans_id not in self._needs_rename):
208
self._rename_in_limbo([trans_id])
209
self._limbo_children[previous_parent].remove(trans_id)
210
del self._limbo_children_names[previous_parent][previous_name]
212
def _rename_in_limbo(self, trans_ids):
213
"""Fix limbo names so that the right final path is produced.
215
This means we outsmarted ourselves-- we tried to avoid renaming
216
these files later by creating them with their final names in their
217
final parents. But now the previous name or parent is no longer
218
suitable, so we have to rename them.
220
Even for trans_ids that have no new contents, we must remove their
221
entries from _limbo_files, because they are now stale.
223
for trans_id in trans_ids:
224
old_path = self._limbo_files.pop(trans_id)
225
if trans_id not in self._new_contents:
227
new_path = self._limbo_name(trans_id)
228
os.rename(old_path, new_path)
230
def adjust_root_path(self, name, parent):
231
"""Emulate moving the root by moving all children, instead.
233
We do this by undoing the association of root's transaction id with the
234
current tree. This allows us to create a new directory with that
235
transaction id. We unversion the root directory and version the
236
physically new directory, and hope someone versions the tree root
239
old_root = self._new_root
240
old_root_file_id = self.final_file_id(old_root)
241
# force moving all children of root
242
for child_id in self.iter_tree_children(old_root):
243
if child_id != parent:
244
self.adjust_path(self.final_name(child_id),
245
self.final_parent(child_id), child_id)
246
file_id = self.final_file_id(child_id)
247
if file_id is not None:
248
self.unversion_file(child_id)
249
self.version_file(file_id, child_id)
251
# the physical root needs a new transaction id
252
self._tree_path_ids.pop("")
253
self._tree_id_paths.pop(old_root)
254
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
255
if parent == old_root:
256
parent = self._new_root
257
self.adjust_path(name, parent, old_root)
258
self.create_directory(old_root)
259
self.version_file(old_root_file_id, old_root)
260
self.unversion_file(self._new_root)
262
def trans_id_tree_file_id(self, inventory_id):
263
"""Determine the transaction id of a working tree file.
265
This reflects only files that already exist, not ones that will be
266
added by transactions.
268
if inventory_id is None:
269
raise ValueError('None is not a valid file id')
270
path = self._tree.id2path(inventory_id)
271
return self.trans_id_tree_path(path)
273
def trans_id_file_id(self, file_id):
274
"""Determine or set the transaction id associated with a file ID.
275
A new id is only created for file_ids that were never present. If
276
a transaction has been unversioned, it is deliberately still returned.
277
(this will likely lead to an unversioned parent conflict.)
280
raise ValueError('None is not a valid file id')
281
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
282
return self._r_new_id[file_id]
283
elif file_id in self._tree.inventory:
284
return self.trans_id_tree_file_id(file_id)
285
elif file_id in self._non_present_ids:
286
return self._non_present_ids[file_id]
288
trans_id = self._assign_id()
289
self._non_present_ids[file_id] = trans_id
292
def canonical_path(self, path):
293
"""Get the canonical tree-relative path"""
294
# don't follow final symlinks
295
abs = self._tree.abspath(path)
296
if abs in self._relpaths:
297
return self._relpaths[abs]
298
dirname, basename = os.path.split(abs)
299
if dirname not in self._realpaths:
300
self._realpaths[dirname] = os.path.realpath(dirname)
301
dirname = self._realpaths[dirname]
302
abs = pathjoin(dirname, basename)
303
if dirname in self._relpaths:
304
relpath = pathjoin(self._relpaths[dirname], basename)
305
relpath = relpath.rstrip('/\\')
307
relpath = self._tree.relpath(abs)
308
self._relpaths[abs] = relpath
311
def trans_id_tree_path(self, path):
312
"""Determine (and maybe set) the transaction ID for a tree path."""
313
path = self.canonical_path(path)
314
if path not in self._tree_path_ids:
315
self._tree_path_ids[path] = self._assign_id()
316
self._tree_id_paths[self._tree_path_ids[path]] = path
317
return self._tree_path_ids[path]
319
def get_tree_parent(self, trans_id):
320
"""Determine id of the parent in the tree."""
321
path = self._tree_id_paths[trans_id]
324
return self.trans_id_tree_path(os.path.dirname(path))
326
def create_file(self, contents, trans_id, mode_id=None):
327
"""Schedule creation of a new file.
331
Contents is an iterator of strings, all of which will be written
332
to the target destination.
334
New file takes the permissions of any existing file with that id,
335
unless mode_id is specified.
337
name = self._limbo_name(trans_id)
341
unique_add(self._new_contents, trans_id, 'file')
343
# Clean up the file, it never got registered so
344
# TreeTransform.finalize() won't clean it up.
349
f.writelines(contents)
352
self._set_mode(trans_id, mode_id, S_ISREG)
354
def _set_mode(self, trans_id, mode_id, typefunc):
355
"""Set the mode of new file contents.
356
The mode_id is the existing file to get the mode from (often the same
357
as trans_id). The operation is only performed if there's a mode match
358
according to typefunc.
363
old_path = self._tree_id_paths[mode_id]
367
mode = os.stat(self._tree.abspath(old_path)).st_mode
369
if e.errno in (errno.ENOENT, errno.ENOTDIR):
370
# Either old_path doesn't exist, or the parent of the
371
# target is not a directory (but will be one eventually)
372
# Either way, we know it doesn't exist *right now*
373
# See also bug #248448
378
os.chmod(self._limbo_name(trans_id), mode)
380
def create_hardlink(self, path, trans_id):
381
"""Schedule creation of a hard link"""
382
name = self._limbo_name(trans_id)
386
if e.errno != errno.EPERM:
388
raise errors.HardLinkNotSupported(path)
390
unique_add(self._new_contents, trans_id, 'file')
392
# Clean up the file, it never got registered so
393
# TreeTransform.finalize() won't clean it up.
397
def create_directory(self, trans_id):
398
"""Schedule creation of a new directory.
400
See also new_directory.
402
os.mkdir(self._limbo_name(trans_id))
403
unique_add(self._new_contents, trans_id, 'directory')
405
def create_symlink(self, target, trans_id):
406
"""Schedule creation of a new symbolic link.
408
target is a bytestring.
409
See also new_symlink.
412
os.symlink(target, self._limbo_name(trans_id))
413
unique_add(self._new_contents, trans_id, 'symlink')
416
path = FinalPaths(self).get_path(trans_id)
419
raise UnableCreateSymlink(path=path)
421
def cancel_creation(self, trans_id):
422
"""Cancel the creation of new file contents."""
423
del self._new_contents[trans_id]
424
children = self._limbo_children.get(trans_id)
425
# if this is a limbo directory with children, move them before removing
427
if children is not None:
428
self._rename_in_limbo(children)
429
del self._limbo_children[trans_id]
430
del self._limbo_children_names[trans_id]
431
delete_any(self._limbo_name(trans_id))
433
def delete_contents(self, trans_id):
434
"""Schedule the contents of a path entry for deletion"""
435
self.tree_kind(trans_id)
436
self._removed_contents.add(trans_id)
438
def cancel_deletion(self, trans_id):
439
"""Cancel a scheduled deletion"""
440
self._removed_contents.remove(trans_id)
442
def unversion_file(self, trans_id):
443
"""Schedule a path entry to become unversioned"""
444
self._removed_id.add(trans_id)
446
def delete_versioned(self, trans_id):
447
"""Delete and unversion a versioned file"""
448
self.delete_contents(trans_id)
449
self.unversion_file(trans_id)
451
def set_executability(self, executability, trans_id):
452
"""Schedule setting of the 'execute' bit
453
To unschedule, set to None
455
if executability is None:
456
del self._new_executability[trans_id]
458
unique_add(self._new_executability, trans_id, executability)
460
def set_tree_reference(self, revision_id, trans_id):
461
"""Set the reference associated with a directory"""
462
unique_add(self._new_reference_revision, trans_id, revision_id)
464
def version_file(self, file_id, trans_id):
465
"""Schedule a file to become versioned."""
468
unique_add(self._new_id, trans_id, file_id)
469
unique_add(self._r_new_id, file_id, trans_id)
471
def cancel_versioning(self, trans_id):
472
"""Undo a previous versioning of a file"""
473
file_id = self._new_id[trans_id]
474
del self._new_id[trans_id]
475
del self._r_new_id[file_id]
477
def new_paths(self, filesystem_only=False):
478
"""Determine the paths of all new and changed files.
480
:param filesystem_only: if True, only calculate values for files
481
that require renames or execute bit changes.
485
id_sets = (self._needs_rename, self._new_executability)
487
id_sets = (self._new_name, self._new_parent, self._new_contents,
488
self._new_id, self._new_executability)
489
for id_set in id_sets:
490
new_ids.update(id_set)
491
return sorted(FinalPaths(self).get_paths(new_ids))
493
def tree_kind(self, trans_id):
494
"""Determine the file kind in the working tree.
496
Raises NoSuchFile if the file does not exist
498
path = self._tree_id_paths.get(trans_id)
500
raise NoSuchFile(None)
502
return file_kind(self._tree.abspath(path))
504
if e.errno != errno.ENOENT:
507
raise NoSuchFile(path)
509
def final_kind(self, trans_id):
510
"""Determine the final file kind, after any changes applied.
512
Raises NoSuchFile if the file does not exist/has no contents.
513
(It is conceivable that a path would be created without the
514
corresponding contents insertion command)
516
if trans_id in self._new_contents:
517
return self._new_contents[trans_id]
518
elif trans_id in self._removed_contents:
519
raise NoSuchFile(None)
521
return self.tree_kind(trans_id)
523
def tree_file_id(self, trans_id):
524
"""Determine the file id associated with the trans_id in the tree"""
526
path = self._tree_id_paths[trans_id]
528
# the file is a new, unversioned file, or invalid trans_id
530
# the file is old; the old id is still valid
531
if self._new_root == trans_id:
532
return self._tree.get_root_id()
533
return self._tree.inventory.path2id(path)
535
def final_file_id(self, trans_id):
536
"""Determine the file id after any changes are applied, or None.
538
None indicates that the file will not be versioned after changes are
542
return self._new_id[trans_id]
544
if trans_id in self._removed_id:
546
return self.tree_file_id(trans_id)
548
def inactive_file_id(self, trans_id):
549
"""Return the inactive file_id associated with a transaction id.
550
That is, the one in the tree or in non_present_ids.
551
The file_id may actually be active, too.
553
file_id = self.tree_file_id(trans_id)
554
if file_id is not None:
556
for key, value in self._non_present_ids.iteritems():
557
if value == trans_id:
560
def final_parent(self, trans_id):
561
"""Determine the parent file_id, after any changes are applied.
563
ROOT_PARENT is returned for the tree root.
566
return self._new_parent[trans_id]
568
return self.get_tree_parent(trans_id)
570
def final_name(self, trans_id):
571
"""Determine the final filename, after all changes are applied."""
573
return self._new_name[trans_id]
576
return os.path.basename(self._tree_id_paths[trans_id])
578
raise NoFinalPath(trans_id, self)
581
"""Return a map of parent: children for known parents.
583
Only new paths and parents of tree files with assigned ids are used.
586
items = list(self._new_parent.iteritems())
587
items.extend((t, self.final_parent(t)) for t in
588
self._tree_id_paths.keys())
589
for trans_id, parent_id in items:
590
if parent_id not in by_parent:
591
by_parent[parent_id] = set()
592
by_parent[parent_id].add(trans_id)
595
def path_changed(self, trans_id):
596
"""Return True if a trans_id's path has changed."""
597
return (trans_id in self._new_name) or (trans_id in self._new_parent)
599
def new_contents(self, trans_id):
600
return (trans_id in self._new_contents)
602
def find_conflicts(self):
603
"""Find any violations of inventory or filesystem invariants"""
604
if self._done is True:
605
raise ReusingTransform()
607
# ensure all children of all existent parents are known
608
# all children of non-existent parents are known, by definition.
609
self._add_tree_children()
610
by_parent = self.by_parent()
611
conflicts.extend(self._unversioned_parents(by_parent))
612
conflicts.extend(self._parent_loops())
613
conflicts.extend(self._duplicate_entries(by_parent))
614
conflicts.extend(self._duplicate_ids())
615
conflicts.extend(self._parent_type_conflicts(by_parent))
616
conflicts.extend(self._improper_versioning())
617
conflicts.extend(self._executability_conflicts())
618
conflicts.extend(self._overwrite_conflicts())
621
def _add_tree_children(self):
622
"""Add all the children of all active parents to the known paths.
624
Active parents are those which gain children, and those which are
625
removed. This is a necessary first step in detecting conflicts.
627
parents = self.by_parent().keys()
628
parents.extend([t for t in self._removed_contents if
629
self.tree_kind(t) == 'directory'])
630
for trans_id in self._removed_id:
631
file_id = self.tree_file_id(trans_id)
632
if file_id is not None:
633
if self._tree.inventory[file_id].kind == 'directory':
634
parents.append(trans_id)
635
elif self.tree_kind(trans_id) == 'directory':
636
parents.append(trans_id)
638
for parent_id in parents:
639
# ensure that all children are registered with the transaction
640
list(self.iter_tree_children(parent_id))
642
def iter_tree_children(self, parent_id):
643
"""Iterate through the entry's tree children, if any"""
645
path = self._tree_id_paths[parent_id]
649
children = os.listdir(self._tree.abspath(path))
651
if e.errno not in (errno.ENOENT, errno.ESRCH, errno.ENOTDIR):
655
for child in children:
656
childpath = joinpath(path, child)
657
if self._tree.is_control_filename(childpath):
659
yield self.trans_id_tree_path(childpath)
661
def has_named_child(self, by_parent, parent_id, name):
663
children = by_parent[parent_id]
666
for child in children:
667
if self.final_name(child) == name:
670
path = self._tree_id_paths[parent_id]
673
childpath = joinpath(path, name)
674
child_id = self._tree_path_ids.get(childpath)
676
return lexists(self._tree.abspath(childpath))
678
if self.final_parent(child_id) != parent_id:
680
if child_id in self._removed_contents:
681
# XXX What about dangling file-ids?
686
def _parent_loops(self):
687
"""No entry should be its own ancestor"""
689
for trans_id in self._new_parent:
692
while parent_id is not ROOT_PARENT:
695
parent_id = self.final_parent(parent_id)
698
if parent_id == trans_id:
699
conflicts.append(('parent loop', trans_id))
700
if parent_id in seen:
704
def _unversioned_parents(self, by_parent):
705
"""If parent directories are versioned, children must be versioned."""
707
for parent_id, children in by_parent.iteritems():
708
if parent_id is ROOT_PARENT:
710
if self.final_file_id(parent_id) is not None:
712
for child_id in children:
713
if self.final_file_id(child_id) is not None:
714
conflicts.append(('unversioned parent', parent_id))
718
def _improper_versioning(self):
719
"""Cannot version a file with no contents, or a bad type.
721
However, existing entries with no contents are okay.
724
for trans_id in self._new_id.iterkeys():
726
kind = self.final_kind(trans_id)
728
conflicts.append(('versioning no contents', trans_id))
730
if not InventoryEntry.versionable_kind(kind):
731
conflicts.append(('versioning bad kind', trans_id, kind))
734
def _executability_conflicts(self):
735
"""Check for bad executability changes.
737
Only versioned files may have their executability set, because
738
1. only versioned entries can have executability under windows
739
2. only files can be executable. (The execute bit on a directory
740
does not indicate searchability)
743
for trans_id in self._new_executability:
744
if self.final_file_id(trans_id) is None:
745
conflicts.append(('unversioned executability', trans_id))
748
non_file = self.final_kind(trans_id) != "file"
752
conflicts.append(('non-file executability', trans_id))
755
def _overwrite_conflicts(self):
756
"""Check for overwrites (not permitted on Win32)"""
758
for trans_id in self._new_contents:
760
self.tree_kind(trans_id)
763
if trans_id not in self._removed_contents:
764
conflicts.append(('overwrite', trans_id,
765
self.final_name(trans_id)))
768
def _duplicate_entries(self, by_parent):
769
"""No directory may have two entries with the same name."""
771
if (self._new_name, self._new_parent) == ({}, {}):
773
for children in by_parent.itervalues():
774
name_ids = [(self.final_name(t), t) for t in children]
775
if not self._case_sensitive_target:
776
name_ids = [(n.lower(), t) for n, t in name_ids]
780
for name, trans_id in name_ids:
782
kind = self.final_kind(trans_id)
785
file_id = self.final_file_id(trans_id)
786
if kind is None and file_id is None:
788
if name == last_name:
789
conflicts.append(('duplicate', last_trans_id, trans_id,
792
last_trans_id = trans_id
795
def _duplicate_ids(self):
796
"""Each inventory id may only be used once"""
798
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
800
all_ids = self._tree.all_file_ids()
801
active_tree_ids = all_ids.difference(removed_tree_ids)
802
for trans_id, file_id in self._new_id.iteritems():
803
if file_id in active_tree_ids:
804
old_trans_id = self.trans_id_tree_file_id(file_id)
805
conflicts.append(('duplicate id', old_trans_id, trans_id))
808
def _parent_type_conflicts(self, by_parent):
809
"""parents must have directory 'contents'."""
811
for parent_id, children in by_parent.iteritems():
812
if parent_id is ROOT_PARENT:
814
if not self._any_contents(children):
816
for child in children:
818
self.final_kind(child)
822
kind = self.final_kind(parent_id)
826
conflicts.append(('missing parent', parent_id))
827
elif kind != "directory":
828
conflicts.append(('non-directory parent', parent_id))
831
def _any_contents(self, trans_ids):
832
"""Return true if any of the trans_ids, will have contents."""
833
for trans_id in trans_ids:
835
kind = self.final_kind(trans_id)
841
def _limbo_name(self, trans_id):
842
"""Generate the limbo name of a file"""
843
limbo_name = self._limbo_files.get(trans_id)
844
if limbo_name is not None:
846
parent = self._new_parent.get(trans_id)
847
# if the parent directory is already in limbo (e.g. when building a
848
# tree), choose a limbo name inside the parent, to reduce further
850
use_direct_path = False
851
if self._new_contents.get(parent) == 'directory':
852
filename = self._new_name.get(trans_id)
853
if filename is not None:
854
if parent not in self._limbo_children:
855
self._limbo_children[parent] = set()
856
self._limbo_children_names[parent] = {}
857
use_direct_path = True
858
# the direct path can only be used if no other file has
859
# already taken this pathname, i.e. if the name is unused, or
860
# if it is already associated with this trans_id.
861
elif self._case_sensitive_target:
862
if (self._limbo_children_names[parent].get(filename)
863
in (trans_id, None)):
864
use_direct_path = True
866
for l_filename, l_trans_id in\
867
self._limbo_children_names[parent].iteritems():
868
if l_trans_id == trans_id:
870
if l_filename.lower() == filename.lower():
873
use_direct_path = True
876
limbo_name = pathjoin(self._limbo_files[parent], filename)
877
self._limbo_children[parent].add(trans_id)
878
self._limbo_children_names[parent][filename] = trans_id
880
limbo_name = pathjoin(self._limbodir, trans_id)
881
self._needs_rename.add(trans_id)
882
self._limbo_files[trans_id] = limbo_name
885
def _set_executability(self, path, entry, trans_id):
886
"""Set the executability of versioned files """
887
new_executability = self._new_executability[trans_id]
888
if entry is not None:
889
entry.executable = new_executability
890
if supports_executable():
891
abspath = self._tree.abspath(path)
892
current_mode = os.stat(abspath).st_mode
893
if new_executability:
896
to_mode = current_mode | (0100 & ~umask)
897
# Enable x-bit for others only if they can read it.
898
if current_mode & 0004:
899
to_mode |= 0001 & ~umask
900
if current_mode & 0040:
901
to_mode |= 0010 & ~umask
903
to_mode = current_mode & ~0111
904
os.chmod(abspath, to_mode)
906
def _new_entry(self, name, parent_id, file_id):
907
"""Helper function to create a new filesystem entry."""
908
trans_id = self.create_path(name, parent_id)
909
if file_id is not None:
910
self.version_file(file_id, trans_id)
913
def new_file(self, name, parent_id, contents, file_id=None,
915
"""Convenience method to create files.
917
name is the name of the file to create.
918
parent_id is the transaction id of the parent directory of the file.
919
contents is an iterator of bytestrings, which will be used to produce
921
:param file_id: The inventory ID of the file, if it is to be versioned.
922
:param executable: Only valid when a file_id has been supplied.
924
trans_id = self._new_entry(name, parent_id, file_id)
925
# TODO: rather than scheduling a set_executable call,
926
# have create_file create the file with the right mode.
927
self.create_file(contents, trans_id)
928
if executable is not None:
929
self.set_executability(executable, trans_id)
932
def new_directory(self, name, parent_id, file_id=None):
933
"""Convenience method to create directories.
935
name is the name of the directory to create.
936
parent_id is the transaction id of the parent directory of the
938
file_id is the inventory ID of the directory, if it is to be versioned.
940
trans_id = self._new_entry(name, parent_id, file_id)
941
self.create_directory(trans_id)
944
def new_symlink(self, name, parent_id, target, file_id=None):
945
"""Convenience method to create symbolic link.
947
name is the name of the symlink to create.
948
parent_id is the transaction id of the parent directory of the symlink.
949
target is a bytestring of the target of the symlink.
950
file_id is the inventory ID of the file, if it is to be versioned.
952
trans_id = self._new_entry(name, parent_id, file_id)
953
self.create_symlink(target, trans_id)
956
def _affected_ids(self):
957
"""Return the set of transform ids affected by the transform"""
958
trans_ids = set(self._removed_id)
959
trans_ids.update(self._new_id.keys())
960
trans_ids.update(self._removed_contents)
961
trans_ids.update(self._new_contents.keys())
962
trans_ids.update(self._new_executability.keys())
963
trans_ids.update(self._new_name.keys())
964
trans_ids.update(self._new_parent.keys())
967
def _get_file_id_maps(self):
968
"""Return mapping of file_ids to trans_ids in the to and from states"""
969
trans_ids = self._affected_ids()
972
# Build up two dicts: trans_ids associated with file ids in the
973
# FROM state, vs the TO state.
974
for trans_id in trans_ids:
975
from_file_id = self.tree_file_id(trans_id)
976
if from_file_id is not None:
977
from_trans_ids[from_file_id] = trans_id
978
to_file_id = self.final_file_id(trans_id)
979
if to_file_id is not None:
980
to_trans_ids[to_file_id] = trans_id
981
return from_trans_ids, to_trans_ids
983
def _from_file_data(self, from_trans_id, from_versioned, file_id):
984
"""Get data about a file in the from (tree) state
986
Return a (name, parent, kind, executable) tuple
988
from_path = self._tree_id_paths.get(from_trans_id)
990
# get data from working tree if versioned
991
from_entry = self._tree.inventory[file_id]
992
from_name = from_entry.name
993
from_parent = from_entry.parent_id
996
if from_path is None:
997
# File does not exist in FROM state
1001
# File exists, but is not versioned. Have to use path-
1003
from_name = os.path.basename(from_path)
1004
tree_parent = self.get_tree_parent(from_trans_id)
1005
from_parent = self.tree_file_id(tree_parent)
1006
if from_path is not None:
1007
from_kind, from_executable, from_stats = \
1008
self._tree._comparison_data(from_entry, from_path)
1011
from_executable = False
1012
return from_name, from_parent, from_kind, from_executable
1014
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1015
"""Get data about a file in the to (target) state
1017
Return a (name, parent, kind, executable) tuple
1019
to_name = self.final_name(to_trans_id)
1021
to_kind = self.final_kind(to_trans_id)
1024
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1025
if to_trans_id in self._new_executability:
1026
to_executable = self._new_executability[to_trans_id]
1027
elif to_trans_id == from_trans_id:
1028
to_executable = from_executable
1030
to_executable = False
1031
return to_name, to_parent, to_kind, to_executable
1033
def iter_changes(self):
1034
"""Produce output in the same format as Tree.iter_changes.
1036
Will produce nonsensical results if invoked while inventory/filesystem
1037
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1039
This reads the Transform, but only reproduces changes involving a
1040
file_id. Files that are not versioned in either of the FROM or TO
1041
states are not reflected.
1043
final_paths = FinalPaths(self)
1044
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1046
# Now iterate through all active file_ids
1047
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1049
from_trans_id = from_trans_ids.get(file_id)
1050
# find file ids, and determine versioning state
1051
if from_trans_id is None:
1052
from_versioned = False
1053
from_trans_id = to_trans_ids[file_id]
1055
from_versioned = True
1056
to_trans_id = to_trans_ids.get(file_id)
1057
if to_trans_id is None:
1058
to_versioned = False
1059
to_trans_id = from_trans_id
1063
from_name, from_parent, from_kind, from_executable = \
1064
self._from_file_data(from_trans_id, from_versioned, file_id)
1066
to_name, to_parent, to_kind, to_executable = \
1067
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1069
if not from_versioned:
1072
from_path = self._tree_id_paths.get(from_trans_id)
1073
if not to_versioned:
1076
to_path = final_paths.get_path(to_trans_id)
1077
if from_kind != to_kind:
1079
elif to_kind in ('file', 'symlink') and (
1080
to_trans_id != from_trans_id or
1081
to_trans_id in self._new_contents):
1083
if (not modified and from_versioned == to_versioned and
1084
from_parent==to_parent and from_name == to_name and
1085
from_executable == to_executable):
1087
results.append((file_id, (from_path, to_path), modified,
1088
(from_versioned, to_versioned),
1089
(from_parent, to_parent),
1090
(from_name, to_name),
1091
(from_kind, to_kind),
1092
(from_executable, to_executable)))
1093
return iter(sorted(results, key=lambda x:x[1]))
1095
def get_preview_tree(self):
1096
"""Return a tree representing the result of the transform.
1098
This tree only supports the subset of Tree functionality required
1099
by show_diff_trees. It must only be compared to tt._tree.
1101
return _PreviewTree(self)
1104
class TreeTransform(TreeTransformBase):
1105
"""Represent a tree transformation.
1107
This object is designed to support incremental generation of the transform,
1110
However, it gives optimum performance when parent directories are created
1111
before their contents. The transform is then able to put child files
1112
directly in their parent directory, avoiding later renames.
1114
It is easy to produce malformed transforms, but they are generally
1115
harmless. Attempting to apply a malformed transform will cause an
1116
exception to be raised before any modifications are made to the tree.
1118
Many kinds of malformed transforms can be corrected with the
1119
resolve_conflicts function. The remaining ones indicate programming error,
1120
such as trying to create a file with no path.
1122
Two sets of file creation methods are supplied. Convenience methods are:
1127
These are composed of the low-level methods:
1129
* create_file or create_directory or create_symlink
1133
Transform/Transaction ids
1134
-------------------------
1135
trans_ids are temporary ids assigned to all files involved in a transform.
1136
It's possible, even common, that not all files in the Tree have trans_ids.
1138
trans_ids are used because filenames and file_ids are not good enough
1139
identifiers; filenames change, and not all files have file_ids. File-ids
1140
are also associated with trans-ids, so that moving a file moves its
1143
trans_ids are only valid for the TreeTransform that generated them.
1147
Limbo is a temporary directory use to hold new versions of files.
1148
Files are added to limbo by create_file, create_directory, create_symlink,
1149
and their convenience variants (new_*). Files may be removed from limbo
1150
using cancel_creation. Files are renamed from limbo into their final
1151
location as part of TreeTransform.apply
1153
Limbo must be cleaned up, by either calling TreeTransform.apply or
1154
calling TreeTransform.finalize.
1156
Files are placed into limbo inside their parent directories, where
1157
possible. This reduces subsequent renames, and makes operations involving
1158
lots of files faster. This optimization is only possible if the parent
1159
directory is created *before* creating any of its children, so avoid
1160
creating children before parents, where possible.
1164
This temporary directory is used by _FileMover for storing files that are
1165
about to be deleted. In case of rollback, the files will be restored.
1166
FileMover does not delete files until it is sure that a rollback will not
1169
def __init__(self, tree, pb=DummyProgress()):
1170
"""Note: a tree_write lock is taken on the tree.
1172
Use TreeTransform.finalize() to release the lock (can be omitted if
1173
TreeTransform.apply() called).
1175
tree.lock_tree_write()
1178
limbodir = urlutils.local_path_from_url(
1179
tree._transport.abspath('limbo'))
1183
if e.errno == errno.EEXIST:
1184
raise ExistingLimbo(limbodir)
1185
deletiondir = urlutils.local_path_from_url(
1186
tree._transport.abspath('pending-deletion'))
1188
os.mkdir(deletiondir)
1190
if e.errno == errno.EEXIST:
1191
raise errors.ExistingPendingDeletion(deletiondir)
1196
TreeTransformBase.__init__(self, tree, limbodir, pb,
1197
tree.case_sensitive)
1198
self._deletiondir = deletiondir
1200
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1201
"""Apply all changes to the inventory and filesystem.
1203
If filesystem or inventory conflicts are present, MalformedTransform
1206
If apply succeeds, finalize is not necessary.
1208
:param no_conflicts: if True, the caller guarantees there are no
1209
conflicts, so no check is made.
1210
:param precomputed_delta: An inventory delta to use instead of
1212
:param _mover: Supply an alternate FileMover, for testing
1214
if not no_conflicts:
1215
conflicts = self.find_conflicts()
1216
if len(conflicts) != 0:
1217
raise MalformedTransform(conflicts=conflicts)
1218
if precomputed_delta is None:
1219
new_inventory_delta = []
1220
inventory_delta = new_inventory_delta
1222
new_inventory_delta = None
1223
inventory_delta = precomputed_delta
1224
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1227
mover = _FileMover()
1231
child_pb.update('Apply phase', 0, 2)
1232
self._apply_removals(new_inventory_delta, mover)
1233
child_pb.update('Apply phase', 1, 2)
1234
modified_paths = self._apply_insertions(new_inventory_delta,
1240
mover.apply_deletions()
1243
self._tree.apply_inventory_delta(inventory_delta)
1246
return _TransformResults(modified_paths, self.rename_count)
1248
def _apply_removals(self, inventory_delta, mover):
1249
"""Perform tree operations that remove directory/inventory names.
1251
That is, delete files that are to be deleted, and put any files that
1252
need renaming into limbo. This must be done in strict child-to-parent
1255
If inventory_delta is None, no inventory delta generation is performed.
1257
tree_paths = list(self._tree_path_ids.iteritems())
1258
tree_paths.sort(reverse=True)
1259
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1261
for num, data in enumerate(tree_paths):
1262
path, trans_id = data
1263
child_pb.update('removing file', num, len(tree_paths))
1264
full_path = self._tree.abspath(path)
1265
if trans_id in self._removed_contents:
1266
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1268
elif trans_id in self._new_name or trans_id in \
1271
mover.rename(full_path, self._limbo_name(trans_id))
1273
if e.errno != errno.ENOENT:
1276
self.rename_count += 1
1277
if (trans_id in self._removed_id
1278
and inventory_delta is not None):
1279
if trans_id == self._new_root:
1280
file_id = self._tree.get_root_id()
1282
file_id = self.tree_file_id(trans_id)
1283
# File-id isn't really being deleted, just moved
1284
if file_id in self._r_new_id:
1286
inventory_delta.append((path, None, file_id, None))
1290
def _apply_insertions(self, inventory_delta, mover):
1291
"""Perform tree operations that insert directory/inventory names.
1293
That is, create any files that need to be created, and restore from
1294
limbo any files that needed renaming. This must be done in strict
1295
parent-to-child order.
1297
If inventory_delta is None, no inventory delta is calculated, and
1298
no list of modified paths is returned.
1300
new_paths = self.new_paths(filesystem_only=(inventory_delta is None))
1303
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1305
if inventory_delta is not None:
1306
entries = self._tree.iter_entries_by_dir(
1307
new_path_file_ids.values())
1308
old_paths = dict((e.file_id, p) for p, e in entries)
1309
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1311
for num, (path, trans_id) in enumerate(new_paths):
1314
child_pb.update('adding file', num, len(new_paths))
1315
full_path = self._tree.abspath(path)
1316
if trans_id in self._needs_rename:
1318
mover.rename(self._limbo_name(trans_id), full_path)
1320
# We may be renaming a dangling inventory id
1321
if e.errno != errno.ENOENT:
1324
self.rename_count += 1
1325
if inventory_delta is not None:
1326
if (trans_id in self._new_contents or
1327
self.path_changed(trans_id)):
1328
if trans_id in self._new_contents:
1329
modified_paths.append(full_path)
1330
completed_new.append(trans_id)
1331
file_id = new_path_file_ids[trans_id]
1332
if file_id is not None and (trans_id in self._new_id or
1333
trans_id in self._new_name or
1334
trans_id in self._new_parent
1335
or trans_id in self._new_executability):
1337
kind = self.final_kind(trans_id)
1339
kind = self._tree.stored_kind(file_id)
1340
parent_trans_id = self.final_parent(trans_id)
1341
parent_file_id = new_path_file_ids.get(parent_trans_id)
1342
if parent_file_id is None:
1343
parent_file_id = self.final_file_id(
1345
if trans_id in self._new_reference_revision:
1346
new_entry = inventory.TreeReference(
1348
self._new_name[trans_id],
1349
self.final_file_id(self._new_parent[trans_id]),
1350
None, self._new_reference_revision[trans_id])
1352
new_entry = inventory.make_entry(kind,
1353
self.final_name(trans_id),
1354
parent_file_id, file_id)
1355
old_path = old_paths.get(new_entry.file_id)
1356
inventory_delta.append(
1357
(old_path, path, new_entry.file_id, new_entry))
1359
if trans_id in self._new_executability:
1360
self._set_executability(path, new_entry, trans_id)
1363
if inventory_delta is None:
1364
self._new_contents.clear()
1366
for trans_id in completed_new:
1367
del self._new_contents[trans_id]
1368
return modified_paths
1371
class TransformPreview(TreeTransformBase):
1372
"""A TreeTransform for generating preview trees.
1374
Unlike TreeTransform, this version works when the input tree is a
1375
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1376
unversioned files in the input tree.
1379
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1381
limbodir = tempfile.mkdtemp(prefix='bzr-limbo-')
1382
TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
1384
def canonical_path(self, path):
1387
def tree_kind(self, trans_id):
1388
path = self._tree_id_paths.get(trans_id)
1390
raise NoSuchFile(None)
1391
file_id = self._tree.path2id(path)
1392
return self._tree.kind(file_id)
1394
def _set_mode(self, trans_id, mode_id, typefunc):
1395
"""Set the mode of new file contents.
1396
The mode_id is the existing file to get the mode from (often the same
1397
as trans_id). The operation is only performed if there's a mode match
1398
according to typefunc.
1400
# is it ok to ignore this? probably
1403
def iter_tree_children(self, parent_id):
1404
"""Iterate through the entry's tree children, if any"""
1406
path = self._tree_id_paths[parent_id]
1409
file_id = self.tree_file_id(parent_id)
1412
children = getattr(self._tree.inventory[file_id], 'children', {})
1413
for child in children:
1414
childpath = joinpath(path, child)
1415
yield self.trans_id_tree_path(childpath)
1418
class _PreviewTree(tree.Tree):
1419
"""Partial implementation of Tree to support show_diff_trees"""
1421
def __init__(self, transform):
1422
self._transform = transform
1423
self._final_paths = FinalPaths(transform)
1424
self.__by_parent = None
1425
self._parent_ids = []
1427
def _changes(self, file_id):
1428
for changes in self._transform.iter_changes():
1429
if changes[0] == file_id:
1432
def _content_change(self, file_id):
1433
"""Return True if the content of this file changed"""
1434
changes = self._changes(file_id)
1435
# changes[2] is true if the file content changed. See
1436
# InterTree.iter_changes.
1437
return (changes is not None and changes[2])
1439
def _get_repository(self):
1440
repo = getattr(self._transform._tree, '_repository', None)
1442
repo = self._transform._tree.branch.repository
1445
def _iter_parent_trees(self):
1446
for revision_id in self.get_parent_ids():
1448
yield self.revision_tree(revision_id)
1449
except errors.NoSuchRevisionInTree:
1450
yield self._get_repository().revision_tree(revision_id)
1452
def _get_file_revision(self, file_id, vf, tree_revision):
1453
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1454
self._iter_parent_trees()]
1455
vf.add_lines((file_id, tree_revision), parent_keys,
1456
self.get_file(file_id).readlines())
1457
repo = self._get_repository()
1458
base_vf = repo.texts
1459
if base_vf not in vf.fallback_versionedfiles:
1460
vf.fallback_versionedfiles.append(base_vf)
1461
return tree_revision
1463
def _stat_limbo_file(self, file_id):
1464
trans_id = self._transform.trans_id_file_id(file_id)
1465
name = self._transform._limbo_name(trans_id)
1466
return os.lstat(name)
1469
def _by_parent(self):
1470
if self.__by_parent is None:
1471
self.__by_parent = self._transform.by_parent()
1472
return self.__by_parent
1474
def lock_read(self):
1475
# Perhaps in theory, this should lock the TreeTransform?
1482
def inventory(self):
1483
"""This Tree does not use inventory as its backing data."""
1484
raise NotImplementedError(_PreviewTree.inventory)
1486
def get_root_id(self):
1487
return self._transform.final_file_id(self._transform.root)
1489
def all_file_ids(self):
1490
tree_ids = set(self._transform._tree.all_file_ids())
1491
tree_ids.difference_update(self._transform.tree_file_id(t)
1492
for t in self._transform._removed_id)
1493
tree_ids.update(self._transform._new_id.values())
1497
return iter(self.all_file_ids())
1499
def has_id(self, file_id):
1500
if file_id in self._transform._r_new_id:
1502
elif file_id in self._transform._removed_id:
1505
return self._transform._tree.has_id(file_id)
1507
def _path2trans_id(self, path):
1508
segments = splitpath(path)
1509
cur_parent = self._transform.root
1510
for cur_segment in segments:
1511
for child in self._all_children(cur_parent):
1512
if self._transform.final_name(child) == cur_segment:
1519
def path2id(self, path):
1520
return self._transform.final_file_id(self._path2trans_id(path))
1522
def id2path(self, file_id):
1523
trans_id = self._transform.trans_id_file_id(file_id)
1525
return self._final_paths._determine_path(trans_id)
1527
raise errors.NoSuchId(self, file_id)
1529
def _all_children(self, trans_id):
1530
children = set(self._transform.iter_tree_children(trans_id))
1531
# children in the _new_parent set are provided by _by_parent.
1532
children.difference_update(self._transform._new_parent.keys())
1533
children.update(self._by_parent.get(trans_id, []))
1536
def iter_children(self, file_id):
1537
trans_id = self._transform.trans_id_file_id(file_id)
1538
for child_trans_id in self._all_children(trans_id):
1539
yield self._transform.final_file_id(child_trans_id)
1542
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1543
in self._transform._tree.extras())
1544
possible_extras.update(self._transform._new_contents)
1545
possible_extras.update(self._transform._removed_id)
1546
for trans_id in possible_extras:
1547
if self._transform.final_file_id(trans_id) is None:
1548
yield self._final_paths._determine_path(trans_id)
1550
def _make_inv_entries(self, ordered_entries, specific_file_ids):
1551
for trans_id, parent_file_id in ordered_entries:
1552
file_id = self._transform.final_file_id(trans_id)
1555
if (specific_file_ids is not None
1556
and file_id not in specific_file_ids):
1559
kind = self._transform.final_kind(trans_id)
1561
kind = self._transform._tree.stored_kind(file_id)
1562
new_entry = inventory.make_entry(
1564
self._transform.final_name(trans_id),
1565
parent_file_id, file_id)
1566
yield new_entry, trans_id
1568
def _list_files_by_dir(self):
1569
todo = [ROOT_PARENT]
1571
while len(todo) > 0:
1573
parent_file_id = self._transform.final_file_id(parent)
1574
children = list(self._all_children(parent))
1575
paths = dict(zip(children, self._final_paths.get_paths(children)))
1576
children.sort(key=paths.get)
1577
todo.extend(reversed(children))
1578
for trans_id in children:
1579
ordered_ids.append((trans_id, parent_file_id))
1582
def iter_entries_by_dir(self, specific_file_ids=None):
1583
# This may not be a maximally efficient implementation, but it is
1584
# reasonably straightforward. An implementation that grafts the
1585
# TreeTransform changes onto the tree's iter_entries_by_dir results
1586
# might be more efficient, but requires tricky inferences about stack
1588
ordered_ids = self._list_files_by_dir()
1589
for entry, trans_id in self._make_inv_entries(ordered_ids,
1591
yield unicode(self._final_paths.get_path(trans_id)), entry
1593
def list_files(self, include_root=False):
1594
"""See Tree.list_files."""
1595
# XXX This should behave like WorkingTree.list_files, but is really
1596
# more like RevisionTree.list_files.
1597
for path, entry in self.iter_entries_by_dir():
1598
if entry.name == '' and not include_root:
1600
yield path, 'V', entry.kind, entry.file_id, entry
1602
def kind(self, file_id):
1603
trans_id = self._transform.trans_id_file_id(file_id)
1604
return self._transform.final_kind(trans_id)
1606
def stored_kind(self, file_id):
1607
trans_id = self._transform.trans_id_file_id(file_id)
1609
return self._transform._new_contents[trans_id]
1611
return self._transform._tree.stored_kind(file_id)
1613
def get_file_mtime(self, file_id, path=None):
1614
"""See Tree.get_file_mtime"""
1615
if not self._content_change(file_id):
1616
return self._transform._tree.get_file_mtime(file_id, path)
1617
return self._stat_limbo_file(file_id).st_mtime
1619
def get_file_size(self, file_id):
1620
"""See Tree.get_file_size"""
1621
if self.kind(file_id) == 'file':
1622
return self._transform._tree.get_file_size(file_id)
1626
def get_file_sha1(self, file_id, path=None, stat_value=None):
1627
return self._transform._tree.get_file_sha1(file_id)
1629
def is_executable(self, file_id, path=None):
1630
trans_id = self._transform.trans_id_file_id(file_id)
1632
return self._transform._new_executability[trans_id]
1634
return self._transform._tree.is_executable(file_id, path)
1636
def path_content_summary(self, path):
1637
trans_id = self._path2trans_id(path)
1638
tt = self._transform
1639
tree_path = tt._tree_id_paths.get(trans_id)
1640
kind = tt._new_contents.get(trans_id)
1642
if tree_path is None or trans_id in tt._removed_contents:
1643
return 'missing', None, None, None
1644
summary = tt._tree.path_content_summary(tree_path)
1645
kind, size, executable, link_or_sha1 = summary
1648
limbo_name = tt._limbo_name(trans_id)
1649
if trans_id in tt._new_reference_revision:
1650
kind = 'tree-reference'
1652
statval = os.lstat(limbo_name)
1653
size = statval.st_size
1654
if not supports_executable():
1657
executable = statval.st_mode & S_IEXEC
1661
if kind == 'symlink':
1662
link_or_sha1 = os.readlink(limbo_name)
1663
if supports_executable():
1664
executable = tt._new_executability.get(trans_id, executable)
1665
return kind, size, executable, link_or_sha1
1667
def iter_changes(self, from_tree, include_unchanged=False,
1668
specific_files=None, pb=None, extra_trees=None,
1669
require_versioned=True, want_unversioned=False):
1670
"""See InterTree.iter_changes.
1672
This implementation does not support include_unchanged, specific_files,
1673
or want_unversioned. extra_trees, require_versioned, and pb are
1676
if from_tree is not self._transform._tree:
1677
raise ValueError('from_tree must be transform source tree.')
1678
if include_unchanged:
1679
raise ValueError('include_unchanged is not supported')
1680
if specific_files is not None:
1681
raise ValueError('specific_files is not supported')
1682
if want_unversioned:
1683
raise ValueError('want_unversioned is not supported')
1684
return self._transform.iter_changes()
1686
def get_file(self, file_id, path=None):
1687
"""See Tree.get_file"""
1688
if not self._content_change(file_id):
1689
return self._transform._tree.get_file(file_id, path)
1690
trans_id = self._transform.trans_id_file_id(file_id)
1691
name = self._transform._limbo_name(trans_id)
1692
return open(name, 'rb')
1694
def get_file_text(self, file_id):
1695
text_file = self.get_file(file_id)
1697
return text_file.read()
1701
def annotate_iter(self, file_id,
1702
default_revision=_mod_revision.CURRENT_REVISION):
1703
changes = self._changes(file_id)
1707
changed_content, versioned, kind = (changes[2], changes[3],
1711
get_old = (kind[0] == 'file' and versioned[0])
1713
old_annotation = self._transform._tree.annotate_iter(file_id,
1714
default_revision=default_revision)
1718
return old_annotation
1719
if not changed_content:
1720
return old_annotation
1721
return annotate.reannotate([old_annotation],
1722
self.get_file(file_id).readlines(),
1725
def get_symlink_target(self, file_id):
1726
"""See Tree.get_symlink_target"""
1727
if not self._content_change(file_id):
1728
return self._transform._tree.get_symlink_target(file_id)
1729
trans_id = self._transform.trans_id_file_id(file_id)
1730
name = self._transform._limbo_name(trans_id)
1731
return os.readlink(name)
1733
def walkdirs(self, prefix=''):
1734
pending = [self._transform.root]
1735
while len(pending) > 0:
1736
parent_id = pending.pop()
1739
prefix = prefix.rstrip('/')
1740
parent_path = self._final_paths.get_path(parent_id)
1741
parent_file_id = self._transform.final_file_id(parent_id)
1742
for child_id in self._all_children(parent_id):
1743
path_from_root = self._final_paths.get_path(child_id)
1744
basename = self._transform.final_name(child_id)
1745
file_id = self._transform.final_file_id(child_id)
1747
kind = self._transform.final_kind(child_id)
1748
versioned_kind = kind
1751
versioned_kind = self._transform._tree.stored_kind(file_id)
1752
if versioned_kind == 'directory':
1753
subdirs.append(child_id)
1754
children.append((path_from_root, basename, kind, None,
1755
file_id, versioned_kind))
1757
if parent_path.startswith(prefix):
1758
yield (parent_path, parent_file_id), children
1759
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1762
def get_parent_ids(self):
1763
return self._parent_ids
1765
def set_parent_ids(self, parent_ids):
1766
self._parent_ids = parent_ids
1768
def get_revision_tree(self, revision_id):
1769
return self._transform._tree.get_revision_tree(revision_id)
1772
def joinpath(parent, child):
1773
"""Join tree-relative paths, handling the tree root specially"""
1774
if parent is None or parent == "":
1777
return pathjoin(parent, child)
1780
class FinalPaths(object):
1781
"""Make path calculation cheap by memoizing paths.
1783
The underlying tree must not be manipulated between calls, or else
1784
the results will likely be incorrect.
1786
def __init__(self, transform):
1787
object.__init__(self)
1788
self._known_paths = {}
1789
self.transform = transform
1791
def _determine_path(self, trans_id):
1792
if trans_id == self.transform.root:
1794
name = self.transform.final_name(trans_id)
1795
parent_id = self.transform.final_parent(trans_id)
1796
if parent_id == self.transform.root:
1799
return pathjoin(self.get_path(parent_id), name)
1801
def get_path(self, trans_id):
1802
"""Find the final path associated with a trans_id"""
1803
if trans_id not in self._known_paths:
1804
self._known_paths[trans_id] = self._determine_path(trans_id)
1805
return self._known_paths[trans_id]
1807
def get_paths(self, trans_ids):
1808
return [(self.get_path(t), t) for t in trans_ids]
1812
def topology_sorted_ids(tree):
1813
"""Determine the topological order of the ids in a tree"""
1814
file_ids = list(tree)
1815
file_ids.sort(key=tree.id2path)
1819
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1820
delta_from_tree=False):
1821
"""Create working tree for a branch, using a TreeTransform.
1823
This function should be used on empty trees, having a tree root at most.
1824
(see merge and revert functionality for working with existing trees)
1826
Existing files are handled like so:
1828
- Existing bzrdirs take precedence over creating new items. They are
1829
created as '%s.diverted' % name.
1830
- Otherwise, if the content on disk matches the content we are building,
1831
it is silently replaced.
1832
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1834
:param tree: The tree to convert wt into a copy of
1835
:param wt: The working tree that files will be placed into
1836
:param accelerator_tree: A tree which can be used for retrieving file
1837
contents more quickly than tree itself, i.e. a workingtree. tree
1838
will be used for cases where accelerator_tree's content is different.
1839
:param hardlink: If true, hard-link files to accelerator_tree, where
1840
possible. accelerator_tree must implement abspath, i.e. be a
1842
:param delta_from_tree: If true, build_tree may use the input Tree to
1843
generate the inventory delta.
1845
wt.lock_tree_write()
1849
if accelerator_tree is not None:
1850
accelerator_tree.lock_read()
1852
return _build_tree(tree, wt, accelerator_tree, hardlink,
1855
if accelerator_tree is not None:
1856
accelerator_tree.unlock()
1863
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1864
"""See build_tree."""
1865
for num, _unused in enumerate(wt.all_file_ids()):
1866
if num > 0: # more than just a root
1867
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1868
existing_files = set()
1869
for dir, files in wt.walkdirs():
1870
existing_files.update(f[0] for f in files)
1872
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1873
pp = ProgressPhase("Build phase", 2, top_pb)
1874
if tree.inventory.root is not None:
1875
# This is kind of a hack: we should be altering the root
1876
# as part of the regular tree shape diff logic.
1877
# The conditional test here is to avoid doing an
1878
# expensive operation (flush) every time the root id
1879
# is set within the tree, nor setting the root and thus
1880
# marking the tree as dirty, because we use two different
1881
# idioms here: tree interfaces and inventory interfaces.
1882
if wt.get_root_id() != tree.get_root_id():
1883
wt.set_root_id(tree.get_root_id())
1885
tt = TreeTransform(wt)
1889
file_trans_id[wt.get_root_id()] = \
1890
tt.trans_id_tree_file_id(wt.get_root_id())
1891
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1893
deferred_contents = []
1895
total = len(tree.inventory)
1897
precomputed_delta = []
1899
precomputed_delta = None
1900
for num, (tree_path, entry) in \
1901
enumerate(tree.inventory.iter_entries_by_dir()):
1902
pb.update("Building tree", num - len(deferred_contents), total)
1903
if entry.parent_id is None:
1906
file_id = entry.file_id
1908
precomputed_delta.append((None, tree_path, file_id, entry))
1909
if tree_path in existing_files:
1910
target_path = wt.abspath(tree_path)
1911
kind = file_kind(target_path)
1912
if kind == "directory":
1914
bzrdir.BzrDir.open(target_path)
1915
except errors.NotBranchError:
1919
if (file_id not in divert and
1920
_content_match(tree, entry, file_id, kind,
1922
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1923
if kind == 'directory':
1925
parent_id = file_trans_id[entry.parent_id]
1926
if entry.kind == 'file':
1927
# We *almost* replicate new_by_entry, so that we can defer
1928
# getting the file text, and get them all at once.
1929
trans_id = tt.create_path(entry.name, parent_id)
1930
file_trans_id[file_id] = trans_id
1931
tt.version_file(file_id, trans_id)
1932
executable = tree.is_executable(file_id, tree_path)
1934
tt.set_executability(executable, trans_id)
1935
deferred_contents.append((file_id, trans_id))
1937
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1940
new_trans_id = file_trans_id[file_id]
1941
old_parent = tt.trans_id_tree_path(tree_path)
1942
_reparent_children(tt, old_parent, new_trans_id)
1943
offset = num + 1 - len(deferred_contents)
1944
_create_files(tt, tree, deferred_contents, pb, offset,
1945
accelerator_tree, hardlink)
1949
divert_trans = set(file_trans_id[f] for f in divert)
1950
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1951
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1952
if len(raw_conflicts) > 0:
1953
precomputed_delta = None
1954
conflicts = cook_conflicts(raw_conflicts, tt)
1955
for conflict in conflicts:
1958
wt.add_conflicts(conflicts)
1959
except errors.UnsupportedOperation:
1961
result = tt.apply(no_conflicts=True,
1962
precomputed_delta=precomputed_delta)
1969
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
1971
total = len(desired_files) + offset
1972
if accelerator_tree is None:
1973
new_desired_files = desired_files
1975
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
1976
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
1977
in iter if not (c or e[0] != e[1]))
1978
new_desired_files = []
1980
for file_id, trans_id in desired_files:
1981
accelerator_path = unchanged.get(file_id)
1982
if accelerator_path is None:
1983
new_desired_files.append((file_id, trans_id))
1985
pb.update('Adding file contents', count + offset, total)
1987
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
1990
contents = accelerator_tree.get_file(file_id, accelerator_path)
1992
tt.create_file(contents, trans_id)
1997
for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
1998
new_desired_files)):
1999
tt.create_file(contents, trans_id)
2000
pb.update('Adding file contents', count + offset, total)
2003
def _reparent_children(tt, old_parent, new_parent):
2004
for child in tt.iter_tree_children(old_parent):
2005
tt.adjust_path(tt.final_name(child), new_parent, child)
2007
def _reparent_transform_children(tt, old_parent, new_parent):
2008
by_parent = tt.by_parent()
2009
for child in by_parent[old_parent]:
2010
tt.adjust_path(tt.final_name(child), new_parent, child)
2011
return by_parent[old_parent]
2013
def _content_match(tree, entry, file_id, kind, target_path):
2014
if entry.kind != kind:
2016
if entry.kind == "directory":
2018
if entry.kind == "file":
2019
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2021
elif entry.kind == "symlink":
2022
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2027
def resolve_checkout(tt, conflicts, divert):
2028
new_conflicts = set()
2029
for c_type, conflict in ((c[0], c) for c in conflicts):
2030
# Anything but a 'duplicate' would indicate programmer error
2031
if c_type != 'duplicate':
2032
raise AssertionError(c_type)
2033
# Now figure out which is new and which is old
2034
if tt.new_contents(conflict[1]):
2035
new_file = conflict[1]
2036
old_file = conflict[2]
2038
new_file = conflict[2]
2039
old_file = conflict[1]
2041
# We should only get here if the conflict wasn't completely
2043
final_parent = tt.final_parent(old_file)
2044
if new_file in divert:
2045
new_name = tt.final_name(old_file)+'.diverted'
2046
tt.adjust_path(new_name, final_parent, new_file)
2047
new_conflicts.add((c_type, 'Diverted to',
2048
new_file, old_file))
2050
new_name = tt.final_name(old_file)+'.moved'
2051
tt.adjust_path(new_name, final_parent, old_file)
2052
new_conflicts.add((c_type, 'Moved existing file to',
2053
old_file, new_file))
2054
return new_conflicts
2057
def new_by_entry(tt, entry, parent_id, tree):
2058
"""Create a new file according to its inventory entry"""
2062
contents = tree.get_file(entry.file_id).readlines()
2063
executable = tree.is_executable(entry.file_id)
2064
return tt.new_file(name, parent_id, contents, entry.file_id,
2066
elif kind in ('directory', 'tree-reference'):
2067
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2068
if kind == 'tree-reference':
2069
tt.set_tree_reference(entry.reference_revision, trans_id)
2071
elif kind == 'symlink':
2072
target = tree.get_symlink_target(entry.file_id)
2073
return tt.new_symlink(name, parent_id, target, entry.file_id)
2075
raise errors.BadFileKindError(name, kind)
2078
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2079
"""Create new file contents according to an inventory entry."""
2080
if entry.kind == "file":
2082
lines = tree.get_file(entry.file_id).readlines()
2083
tt.create_file(lines, trans_id, mode_id=mode_id)
2084
elif entry.kind == "symlink":
2085
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2086
elif entry.kind == "directory":
2087
tt.create_directory(trans_id)
2090
def create_entry_executability(tt, entry, trans_id):
2091
"""Set the executability of a trans_id according to an inventory entry"""
2092
if entry.kind == "file":
2093
tt.set_executability(entry.executable, trans_id)
2096
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2097
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2100
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2101
"""Produce a backup-style name that appears to be available"""
2105
yield "%s.~%d~" % (name, counter)
2107
for new_name in name_gen():
2108
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2112
def _entry_changes(file_id, entry, working_tree):
2113
"""Determine in which ways the inventory entry has changed.
2115
Returns booleans: has_contents, content_mod, meta_mod
2116
has_contents means there are currently contents, but they differ
2117
contents_mod means contents need to be modified
2118
meta_mod means the metadata needs to be modified
2120
cur_entry = working_tree.inventory[file_id]
2122
working_kind = working_tree.kind(file_id)
2125
has_contents = False
2128
if has_contents is True:
2129
if entry.kind != working_kind:
2130
contents_mod, meta_mod = True, False
2132
cur_entry._read_tree_state(working_tree.id2path(file_id),
2134
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2135
cur_entry._forget_tree_state()
2136
return has_contents, contents_mod, meta_mod
2139
def revert(working_tree, target_tree, filenames, backups=False,
2140
pb=DummyProgress(), change_reporter=None):
2141
"""Revert a working tree's contents to those of a target tree."""
2142
target_tree.lock_read()
2143
tt = TreeTransform(working_tree, pb)
2145
pp = ProgressPhase("Revert phase", 3, pb)
2146
conflicts, merge_modified = _prepare_revert_transform(
2147
working_tree, target_tree, tt, filenames, backups, pp)
2149
change_reporter = delta._ChangeReporter(
2150
unversioned_filter=working_tree.is_ignored)
2151
delta.report_changes(tt.iter_changes(), change_reporter)
2152
for conflict in conflicts:
2156
working_tree.set_merge_modified(merge_modified)
2158
target_tree.unlock()
2164
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2165
backups, pp, basis_tree=None,
2166
merge_modified=None):
2168
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2170
if merge_modified is None:
2171
merge_modified = working_tree.merge_modified()
2172
merge_modified = _alter_files(working_tree, target_tree, tt,
2173
child_pb, filenames, backups,
2174
merge_modified, basis_tree)
2178
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2180
raw_conflicts = resolve_conflicts(tt, child_pb,
2181
lambda t, c: conflict_pass(t, c, target_tree))
2184
conflicts = cook_conflicts(raw_conflicts, tt)
2185
return conflicts, merge_modified
2188
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2189
backups, merge_modified, basis_tree=None):
2190
change_list = target_tree.iter_changes(working_tree,
2191
specific_files=specific_files, pb=pb)
2192
if target_tree.get_root_id() is None:
2198
for id_num, (file_id, path, changed_content, versioned, parent, name,
2199
kind, executable) in enumerate(change_list):
2200
if skip_root and file_id[0] is not None and parent[0] is None:
2202
trans_id = tt.trans_id_file_id(file_id)
2205
keep_content = False
2206
if kind[0] == 'file' and (backups or kind[1] is None):
2207
wt_sha1 = working_tree.get_file_sha1(file_id)
2208
if merge_modified.get(file_id) != wt_sha1:
2209
# acquire the basis tree lazily to prevent the
2210
# expense of accessing it when it's not needed ?
2211
# (Guessing, RBC, 200702)
2212
if basis_tree is None:
2213
basis_tree = working_tree.basis_tree()
2214
basis_tree.lock_read()
2215
if file_id in basis_tree:
2216
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2218
elif kind[1] is None and not versioned[1]:
2220
if kind[0] is not None:
2221
if not keep_content:
2222
tt.delete_contents(trans_id)
2223
elif kind[1] is not None:
2224
parent_trans_id = tt.trans_id_file_id(parent[0])
2225
by_parent = tt.by_parent()
2226
backup_name = _get_backup_name(name[0], by_parent,
2227
parent_trans_id, tt)
2228
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2229
new_trans_id = tt.create_path(name[0], parent_trans_id)
2230
if versioned == (True, True):
2231
tt.unversion_file(trans_id)
2232
tt.version_file(file_id, new_trans_id)
2233
# New contents should have the same unix perms as old
2236
trans_id = new_trans_id
2237
if kind[1] in ('directory', 'tree-reference'):
2238
tt.create_directory(trans_id)
2239
if kind[1] == 'tree-reference':
2240
revision = target_tree.get_reference_revision(file_id,
2242
tt.set_tree_reference(revision, trans_id)
2243
elif kind[1] == 'symlink':
2244
tt.create_symlink(target_tree.get_symlink_target(file_id),
2246
elif kind[1] == 'file':
2247
deferred_files.append((file_id, (trans_id, mode_id)))
2248
if basis_tree is None:
2249
basis_tree = working_tree.basis_tree()
2250
basis_tree.lock_read()
2251
new_sha1 = target_tree.get_file_sha1(file_id)
2252
if (file_id in basis_tree and new_sha1 ==
2253
basis_tree.get_file_sha1(file_id)):
2254
if file_id in merge_modified:
2255
del merge_modified[file_id]
2257
merge_modified[file_id] = new_sha1
2259
# preserve the execute bit when backing up
2260
if keep_content and executable[0] == executable[1]:
2261
tt.set_executability(executable[1], trans_id)
2262
elif kind[1] is not None:
2263
raise AssertionError(kind[1])
2264
if versioned == (False, True):
2265
tt.version_file(file_id, trans_id)
2266
if versioned == (True, False):
2267
tt.unversion_file(trans_id)
2268
if (name[1] is not None and
2269
(name[0] != name[1] or parent[0] != parent[1])):
2270
if name[1] == '' and parent[1] is None:
2271
parent_trans = ROOT_PARENT
2273
parent_trans = tt.trans_id_file_id(parent[1])
2274
tt.adjust_path(name[1], parent_trans, trans_id)
2275
if executable[0] != executable[1] and kind[1] == "file":
2276
tt.set_executability(executable[1], trans_id)
2277
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2279
tt.create_file(bytes, trans_id, mode_id)
2281
if basis_tree is not None:
2283
return merge_modified
2286
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2287
"""Make many conflict-resolution attempts, but die if they fail"""
2288
if pass_func is None:
2289
pass_func = conflict_pass
2290
new_conflicts = set()
2293
pb.update('Resolution pass', n+1, 10)
2294
conflicts = tt.find_conflicts()
2295
if len(conflicts) == 0:
2296
return new_conflicts
2297
new_conflicts.update(pass_func(tt, conflicts))
2298
raise MalformedTransform(conflicts=conflicts)
2303
def conflict_pass(tt, conflicts, path_tree=None):
2304
"""Resolve some classes of conflicts.
2306
:param tt: The transform to resolve conflicts in
2307
:param conflicts: The conflicts to resolve
2308
:param path_tree: A Tree to get supplemental paths from
2310
new_conflicts = set()
2311
for c_type, conflict in ((c[0], c) for c in conflicts):
2312
if c_type == 'duplicate id':
2313
tt.unversion_file(conflict[1])
2314
new_conflicts.add((c_type, 'Unversioned existing file',
2315
conflict[1], conflict[2], ))
2316
elif c_type == 'duplicate':
2317
# files that were renamed take precedence
2318
final_parent = tt.final_parent(conflict[1])
2319
if tt.path_changed(conflict[1]):
2320
existing_file, new_file = conflict[2], conflict[1]
2322
existing_file, new_file = conflict[1], conflict[2]
2323
new_name = tt.final_name(existing_file)+'.moved'
2324
tt.adjust_path(new_name, final_parent, existing_file)
2325
new_conflicts.add((c_type, 'Moved existing file to',
2326
existing_file, new_file))
2327
elif c_type == 'parent loop':
2328
# break the loop by undoing one of the ops that caused the loop
2330
while not tt.path_changed(cur):
2331
cur = tt.final_parent(cur)
2332
new_conflicts.add((c_type, 'Cancelled move', cur,
2333
tt.final_parent(cur),))
2334
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2336
elif c_type == 'missing parent':
2337
trans_id = conflict[1]
2339
tt.cancel_deletion(trans_id)
2340
new_conflicts.add(('deleting parent', 'Not deleting',
2345
tt.final_name(trans_id)
2347
if path_tree is not None:
2348
file_id = tt.final_file_id(trans_id)
2350
file_id = tt.inactive_file_id(trans_id)
2351
entry = path_tree.inventory[file_id]
2352
# special-case the other tree root (move its
2353
# children to current root)
2354
if entry.parent_id is None:
2356
moved = _reparent_transform_children(
2357
tt, trans_id, tt.root)
2359
new_conflicts.add((c_type, 'Moved to root',
2362
parent_trans_id = tt.trans_id_file_id(
2364
tt.adjust_path(entry.name, parent_trans_id,
2367
tt.create_directory(trans_id)
2368
new_conflicts.add((c_type, 'Created directory', trans_id))
2369
elif c_type == 'unversioned parent':
2370
file_id = tt.inactive_file_id(conflict[1])
2371
# special-case the other tree root (move its children instead)
2372
if path_tree and file_id in path_tree:
2373
if path_tree.inventory[file_id].parent_id is None:
2375
tt.version_file(file_id, conflict[1])
2376
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2377
elif c_type == 'non-directory parent':
2378
parent_id = conflict[1]
2379
parent_parent = tt.final_parent(parent_id)
2380
parent_name = tt.final_name(parent_id)
2381
parent_file_id = tt.final_file_id(parent_id)
2382
new_parent_id = tt.new_directory(parent_name + '.new',
2383
parent_parent, parent_file_id)
2384
_reparent_transform_children(tt, parent_id, new_parent_id)
2385
if parent_file_id is not None:
2386
tt.unversion_file(parent_id)
2387
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2388
return new_conflicts
2391
def cook_conflicts(raw_conflicts, tt):
2392
"""Generate a list of cooked conflicts, sorted by file path"""
2393
from bzrlib.conflicts import Conflict
2394
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2395
return sorted(conflict_iter, key=Conflict.sort_key)
2398
def iter_cook_conflicts(raw_conflicts, tt):
2399
from bzrlib.conflicts import Conflict
2401
for conflict in raw_conflicts:
2402
c_type = conflict[0]
2403
action = conflict[1]
2404
modified_path = fp.get_path(conflict[2])
2405
modified_id = tt.final_file_id(conflict[2])
2406
if len(conflict) == 3:
2407
yield Conflict.factory(c_type, action=action, path=modified_path,
2408
file_id=modified_id)
2411
conflicting_path = fp.get_path(conflict[3])
2412
conflicting_id = tt.final_file_id(conflict[3])
2413
yield Conflict.factory(c_type, action=action, path=modified_path,
2414
file_id=modified_id,
2415
conflict_path=conflicting_path,
2416
conflict_file_id=conflicting_id)
2419
class _FileMover(object):
2420
"""Moves and deletes files for TreeTransform, tracking operations"""
2423
self.past_renames = []
2424
self.pending_deletions = []
2426
def rename(self, from_, to):
2427
"""Rename a file from one path to another. Functions like os.rename"""
2429
os.rename(from_, to)
2431
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2432
raise errors.FileExists(to, str(e))
2434
self.past_renames.append((from_, to))
2436
def pre_delete(self, from_, to):
2437
"""Rename a file out of the way and mark it for deletion.
2439
Unlike os.unlink, this works equally well for files and directories.
2440
:param from_: The current file path
2441
:param to: A temporary path for the file
2443
self.rename(from_, to)
2444
self.pending_deletions.append(to)
2447
"""Reverse all renames that have been performed"""
2448
for from_, to in reversed(self.past_renames):
2449
os.rename(to, from_)
2450
# after rollback, don't reuse _FileMover
2452
pending_deletions = None
2454
def apply_deletions(self):
2455
"""Apply all marked deletions"""
2456
for path in self.pending_deletions:
2458
# after apply_deletions, don't reuse _FileMover
2460
pending_deletions = None