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 _comparison_data(self, entry, path):
1475
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1476
if kind == 'missing':
1480
file_id = self._transform.final_file_id(self._path2trans_id(path))
1481
executable = self.is_executable(file_id, path)
1482
return kind, executable, None
1484
def lock_read(self):
1485
# Perhaps in theory, this should lock the TreeTransform?
1492
def inventory(self):
1493
"""This Tree does not use inventory as its backing data."""
1494
raise NotImplementedError(_PreviewTree.inventory)
1496
def get_root_id(self):
1497
return self._transform.final_file_id(self._transform.root)
1499
def all_file_ids(self):
1500
tree_ids = set(self._transform._tree.all_file_ids())
1501
tree_ids.difference_update(self._transform.tree_file_id(t)
1502
for t in self._transform._removed_id)
1503
tree_ids.update(self._transform._new_id.values())
1507
return iter(self.all_file_ids())
1509
def has_id(self, file_id):
1510
if file_id in self._transform._r_new_id:
1512
elif file_id in self._transform._removed_id:
1515
return self._transform._tree.has_id(file_id)
1517
def _path2trans_id(self, path):
1518
segments = splitpath(path)
1519
cur_parent = self._transform.root
1520
for cur_segment in segments:
1521
for child in self._all_children(cur_parent):
1522
if self._transform.final_name(child) == cur_segment:
1529
def path2id(self, path):
1530
return self._transform.final_file_id(self._path2trans_id(path))
1532
def id2path(self, file_id):
1533
trans_id = self._transform.trans_id_file_id(file_id)
1535
return self._final_paths._determine_path(trans_id)
1537
raise errors.NoSuchId(self, file_id)
1539
def _all_children(self, trans_id):
1540
children = set(self._transform.iter_tree_children(trans_id))
1541
# children in the _new_parent set are provided by _by_parent.
1542
children.difference_update(self._transform._new_parent.keys())
1543
children.update(self._by_parent.get(trans_id, []))
1546
def iter_children(self, file_id):
1547
trans_id = self._transform.trans_id_file_id(file_id)
1548
for child_trans_id in self._all_children(trans_id):
1549
yield self._transform.final_file_id(child_trans_id)
1552
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1553
in self._transform._tree.extras())
1554
possible_extras.update(self._transform._new_contents)
1555
possible_extras.update(self._transform._removed_id)
1556
for trans_id in possible_extras:
1557
if self._transform.final_file_id(trans_id) is None:
1558
yield self._final_paths._determine_path(trans_id)
1560
def _make_inv_entries(self, ordered_entries, specific_file_ids):
1561
for trans_id, parent_file_id in ordered_entries:
1562
file_id = self._transform.final_file_id(trans_id)
1565
if (specific_file_ids is not None
1566
and file_id not in specific_file_ids):
1569
kind = self._transform.final_kind(trans_id)
1571
kind = self._transform._tree.stored_kind(file_id)
1572
new_entry = inventory.make_entry(
1574
self._transform.final_name(trans_id),
1575
parent_file_id, file_id)
1576
yield new_entry, trans_id
1578
def _list_files_by_dir(self):
1579
todo = [ROOT_PARENT]
1581
while len(todo) > 0:
1583
parent_file_id = self._transform.final_file_id(parent)
1584
children = list(self._all_children(parent))
1585
paths = dict(zip(children, self._final_paths.get_paths(children)))
1586
children.sort(key=paths.get)
1587
todo.extend(reversed(children))
1588
for trans_id in children:
1589
ordered_ids.append((trans_id, parent_file_id))
1592
def iter_entries_by_dir(self, specific_file_ids=None):
1593
# This may not be a maximally efficient implementation, but it is
1594
# reasonably straightforward. An implementation that grafts the
1595
# TreeTransform changes onto the tree's iter_entries_by_dir results
1596
# might be more efficient, but requires tricky inferences about stack
1598
ordered_ids = self._list_files_by_dir()
1599
for entry, trans_id in self._make_inv_entries(ordered_ids,
1601
yield unicode(self._final_paths.get_path(trans_id)), entry
1603
def list_files(self, include_root=False):
1604
"""See Tree.list_files."""
1605
# XXX This should behave like WorkingTree.list_files, but is really
1606
# more like RevisionTree.list_files.
1607
for path, entry in self.iter_entries_by_dir():
1608
if entry.name == '' and not include_root:
1610
yield path, 'V', entry.kind, entry.file_id, entry
1612
def kind(self, file_id):
1613
trans_id = self._transform.trans_id_file_id(file_id)
1614
return self._transform.final_kind(trans_id)
1616
def stored_kind(self, file_id):
1617
trans_id = self._transform.trans_id_file_id(file_id)
1619
return self._transform._new_contents[trans_id]
1621
return self._transform._tree.stored_kind(file_id)
1623
def get_file_mtime(self, file_id, path=None):
1624
"""See Tree.get_file_mtime"""
1625
if not self._content_change(file_id):
1626
return self._transform._tree.get_file_mtime(file_id, path)
1627
return self._stat_limbo_file(file_id).st_mtime
1629
def _file_size(self, entry, stat_value):
1630
return self.get_file_size(entry.file_id)
1632
def get_file_size(self, file_id):
1633
"""See Tree.get_file_size"""
1634
if self.kind(file_id) == 'file':
1635
return self._transform._tree.get_file_size(file_id)
1639
def get_file_sha1(self, file_id, path=None, stat_value=None):
1640
return self._transform._tree.get_file_sha1(file_id)
1642
def is_executable(self, file_id, path=None):
1645
trans_id = self._transform.trans_id_file_id(file_id)
1647
return self._transform._new_executability[trans_id]
1649
return self._transform._tree.is_executable(file_id, path)
1651
def path_content_summary(self, path):
1652
trans_id = self._path2trans_id(path)
1653
tt = self._transform
1654
tree_path = tt._tree_id_paths.get(trans_id)
1655
kind = tt._new_contents.get(trans_id)
1657
if tree_path is None or trans_id in tt._removed_contents:
1658
return 'missing', None, None, None
1659
summary = tt._tree.path_content_summary(tree_path)
1660
kind, size, executable, link_or_sha1 = summary
1663
limbo_name = tt._limbo_name(trans_id)
1664
if trans_id in tt._new_reference_revision:
1665
kind = 'tree-reference'
1667
statval = os.lstat(limbo_name)
1668
size = statval.st_size
1669
if not supports_executable():
1672
executable = statval.st_mode & S_IEXEC
1676
if kind == 'symlink':
1677
link_or_sha1 = os.readlink(limbo_name)
1678
if supports_executable():
1679
executable = tt._new_executability.get(trans_id, executable)
1680
return kind, size, executable, link_or_sha1
1682
def iter_changes(self, from_tree, include_unchanged=False,
1683
specific_files=None, pb=None, extra_trees=None,
1684
require_versioned=True, want_unversioned=False):
1685
"""See InterTree.iter_changes.
1687
This implementation does not support include_unchanged, specific_files,
1688
or want_unversioned. extra_trees, require_versioned, and pb are
1691
if from_tree is not self._transform._tree:
1692
return tree.InterTree(from_tree, self).iter_changes(
1693
include_unchanged=include_unchanged,
1694
specific_files=specific_files,
1696
extra_trees=extra_trees,
1697
require_versioned=require_versioned,
1698
want_unversioned=want_unversioned)
1699
if include_unchanged:
1700
raise ValueError('include_unchanged is not supported')
1701
if specific_files is not None:
1702
raise ValueError('specific_files is not supported')
1703
if want_unversioned:
1704
raise ValueError('want_unversioned is not supported')
1705
return self._transform.iter_changes()
1707
def get_file(self, file_id, path=None):
1708
"""See Tree.get_file"""
1709
if not self._content_change(file_id):
1710
return self._transform._tree.get_file(file_id, path)
1711
trans_id = self._transform.trans_id_file_id(file_id)
1712
name = self._transform._limbo_name(trans_id)
1713
return open(name, 'rb')
1715
def get_file_text(self, file_id):
1716
text_file = self.get_file(file_id)
1718
return text_file.read()
1722
def annotate_iter(self, file_id,
1723
default_revision=_mod_revision.CURRENT_REVISION):
1724
changes = self._changes(file_id)
1728
changed_content, versioned, kind = (changes[2], changes[3],
1732
get_old = (kind[0] == 'file' and versioned[0])
1734
old_annotation = self._transform._tree.annotate_iter(file_id,
1735
default_revision=default_revision)
1739
return old_annotation
1740
if not changed_content:
1741
return old_annotation
1742
return annotate.reannotate([old_annotation],
1743
self.get_file(file_id).readlines(),
1746
def get_symlink_target(self, file_id):
1747
"""See Tree.get_symlink_target"""
1748
if not self._content_change(file_id):
1749
return self._transform._tree.get_symlink_target(file_id)
1750
trans_id = self._transform.trans_id_file_id(file_id)
1751
name = self._transform._limbo_name(trans_id)
1752
return os.readlink(name)
1754
def walkdirs(self, prefix=''):
1755
pending = [self._transform.root]
1756
while len(pending) > 0:
1757
parent_id = pending.pop()
1760
prefix = prefix.rstrip('/')
1761
parent_path = self._final_paths.get_path(parent_id)
1762
parent_file_id = self._transform.final_file_id(parent_id)
1763
for child_id in self._all_children(parent_id):
1764
path_from_root = self._final_paths.get_path(child_id)
1765
basename = self._transform.final_name(child_id)
1766
file_id = self._transform.final_file_id(child_id)
1768
kind = self._transform.final_kind(child_id)
1769
versioned_kind = kind
1772
versioned_kind = self._transform._tree.stored_kind(file_id)
1773
if versioned_kind == 'directory':
1774
subdirs.append(child_id)
1775
children.append((path_from_root, basename, kind, None,
1776
file_id, versioned_kind))
1778
if parent_path.startswith(prefix):
1779
yield (parent_path, parent_file_id), children
1780
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1783
def get_parent_ids(self):
1784
return self._parent_ids
1786
def set_parent_ids(self, parent_ids):
1787
self._parent_ids = parent_ids
1789
def get_revision_tree(self, revision_id):
1790
return self._transform._tree.get_revision_tree(revision_id)
1793
def joinpath(parent, child):
1794
"""Join tree-relative paths, handling the tree root specially"""
1795
if parent is None or parent == "":
1798
return pathjoin(parent, child)
1801
class FinalPaths(object):
1802
"""Make path calculation cheap by memoizing paths.
1804
The underlying tree must not be manipulated between calls, or else
1805
the results will likely be incorrect.
1807
def __init__(self, transform):
1808
object.__init__(self)
1809
self._known_paths = {}
1810
self.transform = transform
1812
def _determine_path(self, trans_id):
1813
if trans_id == self.transform.root:
1815
name = self.transform.final_name(trans_id)
1816
parent_id = self.transform.final_parent(trans_id)
1817
if parent_id == self.transform.root:
1820
return pathjoin(self.get_path(parent_id), name)
1822
def get_path(self, trans_id):
1823
"""Find the final path associated with a trans_id"""
1824
if trans_id not in self._known_paths:
1825
self._known_paths[trans_id] = self._determine_path(trans_id)
1826
return self._known_paths[trans_id]
1828
def get_paths(self, trans_ids):
1829
return [(self.get_path(t), t) for t in trans_ids]
1833
def topology_sorted_ids(tree):
1834
"""Determine the topological order of the ids in a tree"""
1835
file_ids = list(tree)
1836
file_ids.sort(key=tree.id2path)
1840
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
1841
delta_from_tree=False):
1842
"""Create working tree for a branch, using a TreeTransform.
1844
This function should be used on empty trees, having a tree root at most.
1845
(see merge and revert functionality for working with existing trees)
1847
Existing files are handled like so:
1849
- Existing bzrdirs take precedence over creating new items. They are
1850
created as '%s.diverted' % name.
1851
- Otherwise, if the content on disk matches the content we are building,
1852
it is silently replaced.
1853
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1855
:param tree: The tree to convert wt into a copy of
1856
:param wt: The working tree that files will be placed into
1857
:param accelerator_tree: A tree which can be used for retrieving file
1858
contents more quickly than tree itself, i.e. a workingtree. tree
1859
will be used for cases where accelerator_tree's content is different.
1860
:param hardlink: If true, hard-link files to accelerator_tree, where
1861
possible. accelerator_tree must implement abspath, i.e. be a
1863
:param delta_from_tree: If true, build_tree may use the input Tree to
1864
generate the inventory delta.
1866
wt.lock_tree_write()
1870
if accelerator_tree is not None:
1871
accelerator_tree.lock_read()
1873
return _build_tree(tree, wt, accelerator_tree, hardlink,
1876
if accelerator_tree is not None:
1877
accelerator_tree.unlock()
1884
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
1885
"""See build_tree."""
1886
for num, _unused in enumerate(wt.all_file_ids()):
1887
if num > 0: # more than just a root
1888
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1889
existing_files = set()
1890
for dir, files in wt.walkdirs():
1891
existing_files.update(f[0] for f in files)
1893
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1894
pp = ProgressPhase("Build phase", 2, top_pb)
1895
if tree.inventory.root is not None:
1896
# This is kind of a hack: we should be altering the root
1897
# as part of the regular tree shape diff logic.
1898
# The conditional test here is to avoid doing an
1899
# expensive operation (flush) every time the root id
1900
# is set within the tree, nor setting the root and thus
1901
# marking the tree as dirty, because we use two different
1902
# idioms here: tree interfaces and inventory interfaces.
1903
if wt.get_root_id() != tree.get_root_id():
1904
wt.set_root_id(tree.get_root_id())
1906
tt = TreeTransform(wt)
1910
file_trans_id[wt.get_root_id()] = \
1911
tt.trans_id_tree_file_id(wt.get_root_id())
1912
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1914
deferred_contents = []
1916
total = len(tree.inventory)
1918
precomputed_delta = []
1920
precomputed_delta = None
1921
for num, (tree_path, entry) in \
1922
enumerate(tree.inventory.iter_entries_by_dir()):
1923
pb.update("Building tree", num - len(deferred_contents), total)
1924
if entry.parent_id is None:
1927
file_id = entry.file_id
1929
precomputed_delta.append((None, tree_path, file_id, entry))
1930
if tree_path in existing_files:
1931
target_path = wt.abspath(tree_path)
1932
kind = file_kind(target_path)
1933
if kind == "directory":
1935
bzrdir.BzrDir.open(target_path)
1936
except errors.NotBranchError:
1940
if (file_id not in divert and
1941
_content_match(tree, entry, file_id, kind,
1943
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1944
if kind == 'directory':
1946
parent_id = file_trans_id[entry.parent_id]
1947
if entry.kind == 'file':
1948
# We *almost* replicate new_by_entry, so that we can defer
1949
# getting the file text, and get them all at once.
1950
trans_id = tt.create_path(entry.name, parent_id)
1951
file_trans_id[file_id] = trans_id
1952
tt.version_file(file_id, trans_id)
1953
executable = tree.is_executable(file_id, tree_path)
1955
tt.set_executability(executable, trans_id)
1956
deferred_contents.append((file_id, trans_id))
1958
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1961
new_trans_id = file_trans_id[file_id]
1962
old_parent = tt.trans_id_tree_path(tree_path)
1963
_reparent_children(tt, old_parent, new_trans_id)
1964
offset = num + 1 - len(deferred_contents)
1965
_create_files(tt, tree, deferred_contents, pb, offset,
1966
accelerator_tree, hardlink)
1970
divert_trans = set(file_trans_id[f] for f in divert)
1971
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1972
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1973
if len(raw_conflicts) > 0:
1974
precomputed_delta = None
1975
conflicts = cook_conflicts(raw_conflicts, tt)
1976
for conflict in conflicts:
1979
wt.add_conflicts(conflicts)
1980
except errors.UnsupportedOperation:
1982
result = tt.apply(no_conflicts=True,
1983
precomputed_delta=precomputed_delta)
1990
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
1992
total = len(desired_files) + offset
1993
if accelerator_tree is None:
1994
new_desired_files = desired_files
1996
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
1997
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
1998
in iter if not (c or e[0] != e[1]))
1999
new_desired_files = []
2001
for file_id, trans_id in desired_files:
2002
accelerator_path = unchanged.get(file_id)
2003
if accelerator_path is None:
2004
new_desired_files.append((file_id, trans_id))
2006
pb.update('Adding file contents', count + offset, total)
2008
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2011
contents = accelerator_tree.get_file(file_id, accelerator_path)
2013
tt.create_file(contents, trans_id)
2018
for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
2019
new_desired_files)):
2020
tt.create_file(contents, trans_id)
2021
pb.update('Adding file contents', count + offset, total)
2024
def _reparent_children(tt, old_parent, new_parent):
2025
for child in tt.iter_tree_children(old_parent):
2026
tt.adjust_path(tt.final_name(child), new_parent, child)
2028
def _reparent_transform_children(tt, old_parent, new_parent):
2029
by_parent = tt.by_parent()
2030
for child in by_parent[old_parent]:
2031
tt.adjust_path(tt.final_name(child), new_parent, child)
2032
return by_parent[old_parent]
2034
def _content_match(tree, entry, file_id, kind, target_path):
2035
if entry.kind != kind:
2037
if entry.kind == "directory":
2039
if entry.kind == "file":
2040
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2042
elif entry.kind == "symlink":
2043
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2048
def resolve_checkout(tt, conflicts, divert):
2049
new_conflicts = set()
2050
for c_type, conflict in ((c[0], c) for c in conflicts):
2051
# Anything but a 'duplicate' would indicate programmer error
2052
if c_type != 'duplicate':
2053
raise AssertionError(c_type)
2054
# Now figure out which is new and which is old
2055
if tt.new_contents(conflict[1]):
2056
new_file = conflict[1]
2057
old_file = conflict[2]
2059
new_file = conflict[2]
2060
old_file = conflict[1]
2062
# We should only get here if the conflict wasn't completely
2064
final_parent = tt.final_parent(old_file)
2065
if new_file in divert:
2066
new_name = tt.final_name(old_file)+'.diverted'
2067
tt.adjust_path(new_name, final_parent, new_file)
2068
new_conflicts.add((c_type, 'Diverted to',
2069
new_file, old_file))
2071
new_name = tt.final_name(old_file)+'.moved'
2072
tt.adjust_path(new_name, final_parent, old_file)
2073
new_conflicts.add((c_type, 'Moved existing file to',
2074
old_file, new_file))
2075
return new_conflicts
2078
def new_by_entry(tt, entry, parent_id, tree):
2079
"""Create a new file according to its inventory entry"""
2083
contents = tree.get_file(entry.file_id).readlines()
2084
executable = tree.is_executable(entry.file_id)
2085
return tt.new_file(name, parent_id, contents, entry.file_id,
2087
elif kind in ('directory', 'tree-reference'):
2088
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2089
if kind == 'tree-reference':
2090
tt.set_tree_reference(entry.reference_revision, trans_id)
2092
elif kind == 'symlink':
2093
target = tree.get_symlink_target(entry.file_id)
2094
return tt.new_symlink(name, parent_id, target, entry.file_id)
2096
raise errors.BadFileKindError(name, kind)
2099
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2100
"""Create new file contents according to an inventory entry."""
2101
if entry.kind == "file":
2103
lines = tree.get_file(entry.file_id).readlines()
2104
tt.create_file(lines, trans_id, mode_id=mode_id)
2105
elif entry.kind == "symlink":
2106
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2107
elif entry.kind == "directory":
2108
tt.create_directory(trans_id)
2111
def create_entry_executability(tt, entry, trans_id):
2112
"""Set the executability of a trans_id according to an inventory entry"""
2113
if entry.kind == "file":
2114
tt.set_executability(entry.executable, trans_id)
2117
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2118
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2121
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2122
"""Produce a backup-style name that appears to be available"""
2126
yield "%s.~%d~" % (name, counter)
2128
for new_name in name_gen():
2129
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2133
def _entry_changes(file_id, entry, working_tree):
2134
"""Determine in which ways the inventory entry has changed.
2136
Returns booleans: has_contents, content_mod, meta_mod
2137
has_contents means there are currently contents, but they differ
2138
contents_mod means contents need to be modified
2139
meta_mod means the metadata needs to be modified
2141
cur_entry = working_tree.inventory[file_id]
2143
working_kind = working_tree.kind(file_id)
2146
has_contents = False
2149
if has_contents is True:
2150
if entry.kind != working_kind:
2151
contents_mod, meta_mod = True, False
2153
cur_entry._read_tree_state(working_tree.id2path(file_id),
2155
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2156
cur_entry._forget_tree_state()
2157
return has_contents, contents_mod, meta_mod
2160
def revert(working_tree, target_tree, filenames, backups=False,
2161
pb=DummyProgress(), change_reporter=None):
2162
"""Revert a working tree's contents to those of a target tree."""
2163
target_tree.lock_read()
2164
tt = TreeTransform(working_tree, pb)
2166
pp = ProgressPhase("Revert phase", 3, pb)
2167
conflicts, merge_modified = _prepare_revert_transform(
2168
working_tree, target_tree, tt, filenames, backups, pp)
2170
change_reporter = delta._ChangeReporter(
2171
unversioned_filter=working_tree.is_ignored)
2172
delta.report_changes(tt.iter_changes(), change_reporter)
2173
for conflict in conflicts:
2177
working_tree.set_merge_modified(merge_modified)
2179
target_tree.unlock()
2185
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2186
backups, pp, basis_tree=None,
2187
merge_modified=None):
2189
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2191
if merge_modified is None:
2192
merge_modified = working_tree.merge_modified()
2193
merge_modified = _alter_files(working_tree, target_tree, tt,
2194
child_pb, filenames, backups,
2195
merge_modified, basis_tree)
2199
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2201
raw_conflicts = resolve_conflicts(tt, child_pb,
2202
lambda t, c: conflict_pass(t, c, target_tree))
2205
conflicts = cook_conflicts(raw_conflicts, tt)
2206
return conflicts, merge_modified
2209
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2210
backups, merge_modified, basis_tree=None):
2211
change_list = target_tree.iter_changes(working_tree,
2212
specific_files=specific_files, pb=pb)
2213
if target_tree.get_root_id() is None:
2219
for id_num, (file_id, path, changed_content, versioned, parent, name,
2220
kind, executable) in enumerate(change_list):
2221
if skip_root and file_id[0] is not None and parent[0] is None:
2223
trans_id = tt.trans_id_file_id(file_id)
2226
keep_content = False
2227
if kind[0] == 'file' and (backups or kind[1] is None):
2228
wt_sha1 = working_tree.get_file_sha1(file_id)
2229
if merge_modified.get(file_id) != wt_sha1:
2230
# acquire the basis tree lazily to prevent the
2231
# expense of accessing it when it's not needed ?
2232
# (Guessing, RBC, 200702)
2233
if basis_tree is None:
2234
basis_tree = working_tree.basis_tree()
2235
basis_tree.lock_read()
2236
if file_id in basis_tree:
2237
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2239
elif kind[1] is None and not versioned[1]:
2241
if kind[0] is not None:
2242
if not keep_content:
2243
tt.delete_contents(trans_id)
2244
elif kind[1] is not None:
2245
parent_trans_id = tt.trans_id_file_id(parent[0])
2246
by_parent = tt.by_parent()
2247
backup_name = _get_backup_name(name[0], by_parent,
2248
parent_trans_id, tt)
2249
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2250
new_trans_id = tt.create_path(name[0], parent_trans_id)
2251
if versioned == (True, True):
2252
tt.unversion_file(trans_id)
2253
tt.version_file(file_id, new_trans_id)
2254
# New contents should have the same unix perms as old
2257
trans_id = new_trans_id
2258
if kind[1] in ('directory', 'tree-reference'):
2259
tt.create_directory(trans_id)
2260
if kind[1] == 'tree-reference':
2261
revision = target_tree.get_reference_revision(file_id,
2263
tt.set_tree_reference(revision, trans_id)
2264
elif kind[1] == 'symlink':
2265
tt.create_symlink(target_tree.get_symlink_target(file_id),
2267
elif kind[1] == 'file':
2268
deferred_files.append((file_id, (trans_id, mode_id)))
2269
if basis_tree is None:
2270
basis_tree = working_tree.basis_tree()
2271
basis_tree.lock_read()
2272
new_sha1 = target_tree.get_file_sha1(file_id)
2273
if (file_id in basis_tree and new_sha1 ==
2274
basis_tree.get_file_sha1(file_id)):
2275
if file_id in merge_modified:
2276
del merge_modified[file_id]
2278
merge_modified[file_id] = new_sha1
2280
# preserve the execute bit when backing up
2281
if keep_content and executable[0] == executable[1]:
2282
tt.set_executability(executable[1], trans_id)
2283
elif kind[1] is not None:
2284
raise AssertionError(kind[1])
2285
if versioned == (False, True):
2286
tt.version_file(file_id, trans_id)
2287
if versioned == (True, False):
2288
tt.unversion_file(trans_id)
2289
if (name[1] is not None and
2290
(name[0] != name[1] or parent[0] != parent[1])):
2291
if name[1] == '' and parent[1] is None:
2292
parent_trans = ROOT_PARENT
2294
parent_trans = tt.trans_id_file_id(parent[1])
2295
tt.adjust_path(name[1], parent_trans, trans_id)
2296
if executable[0] != executable[1] and kind[1] == "file":
2297
tt.set_executability(executable[1], trans_id)
2298
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2300
tt.create_file(bytes, trans_id, mode_id)
2302
if basis_tree is not None:
2304
return merge_modified
2307
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2308
"""Make many conflict-resolution attempts, but die if they fail"""
2309
if pass_func is None:
2310
pass_func = conflict_pass
2311
new_conflicts = set()
2314
pb.update('Resolution pass', n+1, 10)
2315
conflicts = tt.find_conflicts()
2316
if len(conflicts) == 0:
2317
return new_conflicts
2318
new_conflicts.update(pass_func(tt, conflicts))
2319
raise MalformedTransform(conflicts=conflicts)
2324
def conflict_pass(tt, conflicts, path_tree=None):
2325
"""Resolve some classes of conflicts.
2327
:param tt: The transform to resolve conflicts in
2328
:param conflicts: The conflicts to resolve
2329
:param path_tree: A Tree to get supplemental paths from
2331
new_conflicts = set()
2332
for c_type, conflict in ((c[0], c) for c in conflicts):
2333
if c_type == 'duplicate id':
2334
tt.unversion_file(conflict[1])
2335
new_conflicts.add((c_type, 'Unversioned existing file',
2336
conflict[1], conflict[2], ))
2337
elif c_type == 'duplicate':
2338
# files that were renamed take precedence
2339
final_parent = tt.final_parent(conflict[1])
2340
if tt.path_changed(conflict[1]):
2341
existing_file, new_file = conflict[2], conflict[1]
2343
existing_file, new_file = conflict[1], conflict[2]
2344
new_name = tt.final_name(existing_file)+'.moved'
2345
tt.adjust_path(new_name, final_parent, existing_file)
2346
new_conflicts.add((c_type, 'Moved existing file to',
2347
existing_file, new_file))
2348
elif c_type == 'parent loop':
2349
# break the loop by undoing one of the ops that caused the loop
2351
while not tt.path_changed(cur):
2352
cur = tt.final_parent(cur)
2353
new_conflicts.add((c_type, 'Cancelled move', cur,
2354
tt.final_parent(cur),))
2355
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2357
elif c_type == 'missing parent':
2358
trans_id = conflict[1]
2360
tt.cancel_deletion(trans_id)
2361
new_conflicts.add(('deleting parent', 'Not deleting',
2366
tt.final_name(trans_id)
2368
if path_tree is not None:
2369
file_id = tt.final_file_id(trans_id)
2371
file_id = tt.inactive_file_id(trans_id)
2372
entry = path_tree.inventory[file_id]
2373
# special-case the other tree root (move its
2374
# children to current root)
2375
if entry.parent_id is None:
2377
moved = _reparent_transform_children(
2378
tt, trans_id, tt.root)
2380
new_conflicts.add((c_type, 'Moved to root',
2383
parent_trans_id = tt.trans_id_file_id(
2385
tt.adjust_path(entry.name, parent_trans_id,
2388
tt.create_directory(trans_id)
2389
new_conflicts.add((c_type, 'Created directory', trans_id))
2390
elif c_type == 'unversioned parent':
2391
file_id = tt.inactive_file_id(conflict[1])
2392
# special-case the other tree root (move its children instead)
2393
if path_tree and file_id in path_tree:
2394
if path_tree.inventory[file_id].parent_id is None:
2396
tt.version_file(file_id, conflict[1])
2397
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2398
elif c_type == 'non-directory parent':
2399
parent_id = conflict[1]
2400
parent_parent = tt.final_parent(parent_id)
2401
parent_name = tt.final_name(parent_id)
2402
parent_file_id = tt.final_file_id(parent_id)
2403
new_parent_id = tt.new_directory(parent_name + '.new',
2404
parent_parent, parent_file_id)
2405
_reparent_transform_children(tt, parent_id, new_parent_id)
2406
if parent_file_id is not None:
2407
tt.unversion_file(parent_id)
2408
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2409
return new_conflicts
2412
def cook_conflicts(raw_conflicts, tt):
2413
"""Generate a list of cooked conflicts, sorted by file path"""
2414
from bzrlib.conflicts import Conflict
2415
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2416
return sorted(conflict_iter, key=Conflict.sort_key)
2419
def iter_cook_conflicts(raw_conflicts, tt):
2420
from bzrlib.conflicts import Conflict
2422
for conflict in raw_conflicts:
2423
c_type = conflict[0]
2424
action = conflict[1]
2425
modified_path = fp.get_path(conflict[2])
2426
modified_id = tt.final_file_id(conflict[2])
2427
if len(conflict) == 3:
2428
yield Conflict.factory(c_type, action=action, path=modified_path,
2429
file_id=modified_id)
2432
conflicting_path = fp.get_path(conflict[3])
2433
conflicting_id = tt.final_file_id(conflict[3])
2434
yield Conflict.factory(c_type, action=action, path=modified_path,
2435
file_id=modified_id,
2436
conflict_path=conflicting_path,
2437
conflict_file_id=conflicting_id)
2440
class _FileMover(object):
2441
"""Moves and deletes files for TreeTransform, tracking operations"""
2444
self.past_renames = []
2445
self.pending_deletions = []
2447
def rename(self, from_, to):
2448
"""Rename a file from one path to another. Functions like os.rename"""
2450
os.rename(from_, to)
2452
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2453
raise errors.FileExists(to, str(e))
2455
self.past_renames.append((from_, to))
2457
def pre_delete(self, from_, to):
2458
"""Rename a file out of the way and mark it for deletion.
2460
Unlike os.unlink, this works equally well for files and directories.
2461
:param from_: The current file path
2462
:param to: A temporary path for the file
2464
self.rename(from_, to)
2465
self.pending_deletions.append(to)
2468
"""Reverse all renames that have been performed"""
2469
for from_, to in reversed(self.past_renames):
2470
os.rename(to, from_)
2471
# after rollback, don't reuse _FileMover
2473
pending_deletions = None
2475
def apply_deletions(self):
2476
"""Apply all marked deletions"""
2477
for path in self.pending_deletions:
2479
# after apply_deletions, don't reuse _FileMover
2481
pending_deletions = None