1
# Copyright (C) 2006, 2007 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from stat import S_ISREG
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
31
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
32
ReusingTransform, NotVersionedError, CantMoveRoot,
33
ExistingLimbo, ImmortalLimbo, NoFinalPath,
35
from bzrlib.inventory import InventoryEntry
36
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
37
delete_any, has_symlinks)
38
from bzrlib.progress import DummyProgress, ProgressPhase
39
from bzrlib.symbol_versioning import (
44
from bzrlib.trace import mutter, warning
45
from bzrlib import tree
47
import bzrlib.urlutils as urlutils
50
ROOT_PARENT = "root-parent"
53
def unique_add(map, key, value):
55
raise DuplicateKey(key=key)
59
class _TransformResults(object):
60
def __init__(self, modified_paths, rename_count):
62
self.modified_paths = modified_paths
63
self.rename_count = rename_count
66
class TreeTransformBase(object):
68
def __init__(self, tree, limbodir, pb=DummyProgress()):
71
self._limbodir = limbodir
73
# mapping of trans_id -> new basename
75
# mapping of trans_id -> new parent trans_id
77
# mapping of trans_id with new contents -> new file_kind
78
self._new_contents = {}
79
# A mapping of transform ids to their limbo filename
80
self._limbo_files = {}
81
# A mapping of transform ids to a set of the transform ids of children
82
# that their limbo directory has
83
self._limbo_children = {}
84
# Map transform ids to maps of child filename to child transform id
85
self._limbo_children_names = {}
86
# List of transform ids that need to be renamed from limbo into place
87
self._needs_rename = set()
88
# Set of trans_ids whose contents will be removed
89
self._removed_contents = set()
90
# Mapping of trans_id -> new execute-bit value
91
self._new_executability = {}
92
# Mapping of trans_id -> new tree-reference value
93
self._new_reference_revision = {}
94
# Mapping of trans_id -> new file_id
96
# Mapping of old file-id -> trans_id
97
self._non_present_ids = {}
98
# Mapping of new file_id -> trans_id
100
# Set of file_ids that will be removed
101
self._removed_id = set()
102
# Mapping of path in old tree -> trans_id
103
self._tree_path_ids = {}
104
# Mapping trans_id -> path in old tree
105
self._tree_id_paths = {}
106
# Cache of realpath results, to speed up canonical_path
108
# Cache of relpath results, to speed up canonical_path
110
# The trans_id that will be used as the tree root
111
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
112
# Indictor of whether the transform has been applied
116
# A counter of how many files have been renamed
117
self.rename_count = 0
119
def __get_root(self):
120
return self._new_root
122
root = property(__get_root)
125
"""Release the working tree lock, if held, clean up limbo dir.
127
This is required if apply has not been invoked, but can be invoked
130
if self._tree is None:
133
entries = [(self._limbo_name(t), t, k) for t, k in
134
self._new_contents.iteritems()]
135
entries.sort(reverse=True)
136
for path, trans_id, kind in entries:
137
if kind == "directory":
142
os.rmdir(self._limbodir)
144
# We don't especially care *why* the dir is immortal.
145
raise ImmortalLimbo(self._limbodir)
147
os.rmdir(self._deletiondir)
149
raise errors.ImmortalPendingDeletion(self._deletiondir)
154
def _assign_id(self):
155
"""Produce a new tranform id"""
156
new_id = "new-%s" % self._id_number
160
def create_path(self, name, parent):
161
"""Assign a transaction id to a new path"""
162
trans_id = self._assign_id()
163
unique_add(self._new_name, trans_id, name)
164
unique_add(self._new_parent, trans_id, parent)
167
def adjust_path(self, name, parent, trans_id):
168
"""Change the path that is assigned to a transaction id."""
169
if trans_id == self._new_root:
171
previous_parent = self._new_parent.get(trans_id)
172
previous_name = self._new_name.get(trans_id)
173
self._new_name[trans_id] = name
174
self._new_parent[trans_id] = parent
175
if (trans_id in self._limbo_files and
176
trans_id not in self._needs_rename):
177
self._rename_in_limbo([trans_id])
178
self._limbo_children[previous_parent].remove(trans_id)
179
del self._limbo_children_names[previous_parent][previous_name]
181
def _rename_in_limbo(self, trans_ids):
182
"""Fix limbo names so that the right final path is produced.
184
This means we outsmarted ourselves-- we tried to avoid renaming
185
these files later by creating them with their final names in their
186
final parents. But now the previous name or parent is no longer
187
suitable, so we have to rename them.
189
Even for trans_ids that have no new contents, we must remove their
190
entries from _limbo_files, because they are now stale.
192
for trans_id in trans_ids:
193
old_path = self._limbo_files.pop(trans_id)
194
if trans_id not in self._new_contents:
196
new_path = self._limbo_name(trans_id)
197
os.rename(old_path, new_path)
199
def adjust_root_path(self, name, parent):
200
"""Emulate moving the root by moving all children, instead.
202
We do this by undoing the association of root's transaction id with the
203
current tree. This allows us to create a new directory with that
204
transaction id. We unversion the root directory and version the
205
physically new directory, and hope someone versions the tree root
208
old_root = self._new_root
209
old_root_file_id = self.final_file_id(old_root)
210
# force moving all children of root
211
for child_id in self.iter_tree_children(old_root):
212
if child_id != parent:
213
self.adjust_path(self.final_name(child_id),
214
self.final_parent(child_id), child_id)
215
file_id = self.final_file_id(child_id)
216
if file_id is not None:
217
self.unversion_file(child_id)
218
self.version_file(file_id, child_id)
220
# the physical root needs a new transaction id
221
self._tree_path_ids.pop("")
222
self._tree_id_paths.pop(old_root)
223
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
224
if parent == old_root:
225
parent = self._new_root
226
self.adjust_path(name, parent, old_root)
227
self.create_directory(old_root)
228
self.version_file(old_root_file_id, old_root)
229
self.unversion_file(self._new_root)
231
def trans_id_tree_file_id(self, inventory_id):
232
"""Determine the transaction id of a working tree file.
234
This reflects only files that already exist, not ones that will be
235
added by transactions.
237
path = self._tree.inventory.id2path(inventory_id)
238
return self.trans_id_tree_path(path)
240
def trans_id_file_id(self, file_id):
241
"""Determine or set the transaction id associated with a file ID.
242
A new id is only created for file_ids that were never present. If
243
a transaction has been unversioned, it is deliberately still returned.
244
(this will likely lead to an unversioned parent conflict.)
246
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
247
return self._r_new_id[file_id]
248
elif file_id in self._tree.inventory:
249
return self.trans_id_tree_file_id(file_id)
250
elif file_id in self._non_present_ids:
251
return self._non_present_ids[file_id]
253
trans_id = self._assign_id()
254
self._non_present_ids[file_id] = trans_id
257
def canonical_path(self, path):
258
"""Get the canonical tree-relative path"""
259
# don't follow final symlinks
260
abs = self._tree.abspath(path)
261
if abs in self._relpaths:
262
return self._relpaths[abs]
263
dirname, basename = os.path.split(abs)
264
if dirname not in self._realpaths:
265
self._realpaths[dirname] = os.path.realpath(dirname)
266
dirname = self._realpaths[dirname]
267
abs = pathjoin(dirname, basename)
268
if dirname in self._relpaths:
269
relpath = pathjoin(self._relpaths[dirname], basename)
270
relpath = relpath.rstrip('/\\')
272
relpath = self._tree.relpath(abs)
273
self._relpaths[abs] = relpath
276
def trans_id_tree_path(self, path):
277
"""Determine (and maybe set) the transaction ID for a tree path."""
278
path = self.canonical_path(path)
279
if path not in self._tree_path_ids:
280
self._tree_path_ids[path] = self._assign_id()
281
self._tree_id_paths[self._tree_path_ids[path]] = path
282
return self._tree_path_ids[path]
284
def get_tree_parent(self, trans_id):
285
"""Determine id of the parent in the tree."""
286
path = self._tree_id_paths[trans_id]
289
return self.trans_id_tree_path(os.path.dirname(path))
291
def create_file(self, contents, trans_id, mode_id=None):
292
"""Schedule creation of a new file.
296
Contents is an iterator of strings, all of which will be written
297
to the target destination.
299
New file takes the permissions of any existing file with that id,
300
unless mode_id is specified.
302
name = self._limbo_name(trans_id)
306
unique_add(self._new_contents, trans_id, 'file')
308
# Clean up the file, it never got registered so
309
# TreeTransform.finalize() won't clean it up.
314
f.writelines(contents)
317
self._set_mode(trans_id, mode_id, S_ISREG)
319
def _set_mode(self, trans_id, mode_id, typefunc):
320
"""Set the mode of new file contents.
321
The mode_id is the existing file to get the mode from (often the same
322
as trans_id). The operation is only performed if there's a mode match
323
according to typefunc.
328
old_path = self._tree_id_paths[mode_id]
332
mode = os.stat(self._tree.abspath(old_path)).st_mode
334
if e.errno == errno.ENOENT:
339
os.chmod(self._limbo_name(trans_id), mode)
341
def create_directory(self, trans_id):
342
"""Schedule creation of a new directory.
344
See also new_directory.
346
os.mkdir(self._limbo_name(trans_id))
347
unique_add(self._new_contents, trans_id, 'directory')
349
def create_symlink(self, target, trans_id):
350
"""Schedule creation of a new symbolic link.
352
target is a bytestring.
353
See also new_symlink.
356
os.symlink(target, self._limbo_name(trans_id))
357
unique_add(self._new_contents, trans_id, 'symlink')
360
path = FinalPaths(self).get_path(trans_id)
363
raise UnableCreateSymlink(path=path)
365
def cancel_creation(self, trans_id):
366
"""Cancel the creation of new file contents."""
367
del self._new_contents[trans_id]
368
children = self._limbo_children.get(trans_id)
369
# if this is a limbo directory with children, move them before removing
371
if children is not None:
372
self._rename_in_limbo(children)
373
del self._limbo_children[trans_id]
374
del self._limbo_children_names[trans_id]
375
delete_any(self._limbo_name(trans_id))
377
def delete_contents(self, trans_id):
378
"""Schedule the contents of a path entry for deletion"""
379
self.tree_kind(trans_id)
380
self._removed_contents.add(trans_id)
382
def cancel_deletion(self, trans_id):
383
"""Cancel a scheduled deletion"""
384
self._removed_contents.remove(trans_id)
386
def unversion_file(self, trans_id):
387
"""Schedule a path entry to become unversioned"""
388
self._removed_id.add(trans_id)
390
def delete_versioned(self, trans_id):
391
"""Delete and unversion a versioned file"""
392
self.delete_contents(trans_id)
393
self.unversion_file(trans_id)
395
def set_executability(self, executability, trans_id):
396
"""Schedule setting of the 'execute' bit
397
To unschedule, set to None
399
if executability is None:
400
del self._new_executability[trans_id]
402
unique_add(self._new_executability, trans_id, executability)
404
def set_tree_reference(self, revision_id, trans_id):
405
"""Set the reference associated with a directory"""
406
unique_add(self._new_reference_revision, trans_id, revision_id)
408
def version_file(self, file_id, trans_id):
409
"""Schedule a file to become versioned."""
410
assert file_id is not None
411
unique_add(self._new_id, trans_id, file_id)
412
unique_add(self._r_new_id, file_id, trans_id)
414
def cancel_versioning(self, trans_id):
415
"""Undo a previous versioning of a file"""
416
file_id = self._new_id[trans_id]
417
del self._new_id[trans_id]
418
del self._r_new_id[file_id]
421
"""Determine the paths of all new and changed files"""
423
fp = FinalPaths(self)
424
for id_set in (self._new_name, self._new_parent, self._new_contents,
425
self._new_id, self._new_executability):
426
new_ids.update(id_set)
427
new_paths = [(fp.get_path(t), t) for t in new_ids]
431
def tree_kind(self, trans_id):
432
"""Determine the file kind in the working tree.
434
Raises NoSuchFile if the file does not exist
436
path = self._tree_id_paths.get(trans_id)
438
raise NoSuchFile(None)
440
return file_kind(self._tree.abspath(path))
442
if e.errno != errno.ENOENT:
445
raise NoSuchFile(path)
447
def final_kind(self, trans_id):
448
"""Determine the final file kind, after any changes applied.
450
Raises NoSuchFile if the file does not exist/has no contents.
451
(It is conceivable that a path would be created without the
452
corresponding contents insertion command)
454
if trans_id in self._new_contents:
455
return self._new_contents[trans_id]
456
elif trans_id in self._removed_contents:
457
raise NoSuchFile(None)
459
return self.tree_kind(trans_id)
461
def tree_file_id(self, trans_id):
462
"""Determine the file id associated with the trans_id in the tree"""
464
path = self._tree_id_paths[trans_id]
466
# the file is a new, unversioned file, or invalid trans_id
468
# the file is old; the old id is still valid
469
if self._new_root == trans_id:
470
return self._tree.get_root_id()
471
return self._tree.inventory.path2id(path)
473
def final_file_id(self, trans_id):
474
"""Determine the file id after any changes are applied, or None.
476
None indicates that the file will not be versioned after changes are
480
# there is a new id for this file
481
assert self._new_id[trans_id] is not None
482
return self._new_id[trans_id]
484
if trans_id in self._removed_id:
486
return self.tree_file_id(trans_id)
488
def inactive_file_id(self, trans_id):
489
"""Return the inactive file_id associated with a transaction id.
490
That is, the one in the tree or in non_present_ids.
491
The file_id may actually be active, too.
493
file_id = self.tree_file_id(trans_id)
494
if file_id is not None:
496
for key, value in self._non_present_ids.iteritems():
497
if value == trans_id:
500
def final_parent(self, trans_id):
501
"""Determine the parent file_id, after any changes are applied.
503
ROOT_PARENT is returned for the tree root.
506
return self._new_parent[trans_id]
508
return self.get_tree_parent(trans_id)
510
def final_name(self, trans_id):
511
"""Determine the final filename, after all changes are applied."""
513
return self._new_name[trans_id]
516
return os.path.basename(self._tree_id_paths[trans_id])
518
raise NoFinalPath(trans_id, self)
521
"""Return a map of parent: children for known parents.
523
Only new paths and parents of tree files with assigned ids are used.
526
items = list(self._new_parent.iteritems())
527
items.extend((t, self.final_parent(t)) for t in
528
self._tree_id_paths.keys())
529
for trans_id, parent_id in items:
530
if parent_id not in by_parent:
531
by_parent[parent_id] = set()
532
by_parent[parent_id].add(trans_id)
535
def path_changed(self, trans_id):
536
"""Return True if a trans_id's path has changed."""
537
return (trans_id in self._new_name) or (trans_id in self._new_parent)
539
def new_contents(self, trans_id):
540
return (trans_id in self._new_contents)
542
def find_conflicts(self):
543
"""Find any violations of inventory or filesystem invariants"""
544
if self.__done is True:
545
raise ReusingTransform()
547
# ensure all children of all existent parents are known
548
# all children of non-existent parents are known, by definition.
549
self._add_tree_children()
550
by_parent = self.by_parent()
551
conflicts.extend(self._unversioned_parents(by_parent))
552
conflicts.extend(self._parent_loops())
553
conflicts.extend(self._duplicate_entries(by_parent))
554
conflicts.extend(self._duplicate_ids())
555
conflicts.extend(self._parent_type_conflicts(by_parent))
556
conflicts.extend(self._improper_versioning())
557
conflicts.extend(self._executability_conflicts())
558
conflicts.extend(self._overwrite_conflicts())
561
def _add_tree_children(self):
562
"""Add all the children of all active parents to the known paths.
564
Active parents are those which gain children, and those which are
565
removed. This is a necessary first step in detecting conflicts.
567
parents = self.by_parent().keys()
568
parents.extend([t for t in self._removed_contents if
569
self.tree_kind(t) == 'directory'])
570
for trans_id in self._removed_id:
571
file_id = self.tree_file_id(trans_id)
572
if self._tree.inventory[file_id].kind == 'directory':
573
parents.append(trans_id)
575
for parent_id in parents:
576
# ensure that all children are registered with the transaction
577
list(self.iter_tree_children(parent_id))
579
def iter_tree_children(self, parent_id):
580
"""Iterate through the entry's tree children, if any"""
582
path = self._tree_id_paths[parent_id]
586
children = os.listdir(self._tree.abspath(path))
588
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
592
for child in children:
593
childpath = joinpath(path, child)
594
if self._tree.is_control_filename(childpath):
596
yield self.trans_id_tree_path(childpath)
598
def has_named_child(self, by_parent, parent_id, name):
600
children = by_parent[parent_id]
603
for child in children:
604
if self.final_name(child) == name:
607
path = self._tree_id_paths[parent_id]
610
childpath = joinpath(path, name)
611
child_id = self._tree_path_ids.get(childpath)
613
return lexists(self._tree.abspath(childpath))
615
if self.final_parent(child_id) != parent_id:
617
if child_id in self._removed_contents:
618
# XXX What about dangling file-ids?
623
def _parent_loops(self):
624
"""No entry should be its own ancestor"""
626
for trans_id in self._new_parent:
629
while parent_id is not ROOT_PARENT:
632
parent_id = self.final_parent(parent_id)
635
if parent_id == trans_id:
636
conflicts.append(('parent loop', trans_id))
637
if parent_id in seen:
641
def _unversioned_parents(self, by_parent):
642
"""If parent directories are versioned, children must be versioned."""
644
for parent_id, children in by_parent.iteritems():
645
if parent_id is ROOT_PARENT:
647
if self.final_file_id(parent_id) is not None:
649
for child_id in children:
650
if self.final_file_id(child_id) is not None:
651
conflicts.append(('unversioned parent', parent_id))
655
def _improper_versioning(self):
656
"""Cannot version a file with no contents, or a bad type.
658
However, existing entries with no contents are okay.
661
for trans_id in self._new_id.iterkeys():
663
kind = self.final_kind(trans_id)
665
conflicts.append(('versioning no contents', trans_id))
667
if not InventoryEntry.versionable_kind(kind):
668
conflicts.append(('versioning bad kind', trans_id, kind))
671
def _executability_conflicts(self):
672
"""Check for bad executability changes.
674
Only versioned files may have their executability set, because
675
1. only versioned entries can have executability under windows
676
2. only files can be executable. (The execute bit on a directory
677
does not indicate searchability)
680
for trans_id in self._new_executability:
681
if self.final_file_id(trans_id) is None:
682
conflicts.append(('unversioned executability', trans_id))
685
non_file = self.final_kind(trans_id) != "file"
689
conflicts.append(('non-file executability', trans_id))
692
def _overwrite_conflicts(self):
693
"""Check for overwrites (not permitted on Win32)"""
695
for trans_id in self._new_contents:
697
self.tree_kind(trans_id)
700
if trans_id not in self._removed_contents:
701
conflicts.append(('overwrite', trans_id,
702
self.final_name(trans_id)))
705
def _duplicate_entries(self, by_parent):
706
"""No directory may have two entries with the same name."""
708
if (self._new_name, self._new_parent) == ({}, {}):
710
for children in by_parent.itervalues():
711
name_ids = [(self.final_name(t), t) for t in children]
712
if not self._tree.case_sensitive:
713
name_ids = [(n.lower(), t) for n, t in name_ids]
717
for name, trans_id in name_ids:
719
kind = self.final_kind(trans_id)
722
file_id = self.final_file_id(trans_id)
723
if kind is None and file_id is None:
725
if name == last_name:
726
conflicts.append(('duplicate', last_trans_id, trans_id,
729
last_trans_id = trans_id
732
def _duplicate_ids(self):
733
"""Each inventory id may only be used once"""
735
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
737
active_tree_ids = set((f for f in self._tree.inventory if
738
f not in removed_tree_ids))
739
for trans_id, file_id in self._new_id.iteritems():
740
if file_id in active_tree_ids:
741
old_trans_id = self.trans_id_tree_file_id(file_id)
742
conflicts.append(('duplicate id', old_trans_id, trans_id))
745
def _parent_type_conflicts(self, by_parent):
746
"""parents must have directory 'contents'."""
748
for parent_id, children in by_parent.iteritems():
749
if parent_id is ROOT_PARENT:
751
if not self._any_contents(children):
753
for child in children:
755
self.final_kind(child)
759
kind = self.final_kind(parent_id)
763
conflicts.append(('missing parent', parent_id))
764
elif kind != "directory":
765
conflicts.append(('non-directory parent', parent_id))
768
def _any_contents(self, trans_ids):
769
"""Return true if any of the trans_ids, will have contents."""
770
for trans_id in trans_ids:
772
kind = self.final_kind(trans_id)
778
def _limbo_name(self, trans_id):
779
"""Generate the limbo name of a file"""
780
limbo_name = self._limbo_files.get(trans_id)
781
if limbo_name is not None:
783
parent = self._new_parent.get(trans_id)
784
# if the parent directory is already in limbo (e.g. when building a
785
# tree), choose a limbo name inside the parent, to reduce further
787
use_direct_path = False
788
if self._new_contents.get(parent) == 'directory':
789
filename = self._new_name.get(trans_id)
790
if filename is not None:
791
if parent not in self._limbo_children:
792
self._limbo_children[parent] = set()
793
self._limbo_children_names[parent] = {}
794
use_direct_path = True
795
# the direct path can only be used if no other file has
796
# already taken this pathname, i.e. if the name is unused, or
797
# if it is already associated with this trans_id.
798
elif self._tree.case_sensitive:
799
if (self._limbo_children_names[parent].get(filename)
800
in (trans_id, None)):
801
use_direct_path = True
803
for l_filename, l_trans_id in\
804
self._limbo_children_names[parent].iteritems():
805
if l_trans_id == trans_id:
807
if l_filename.lower() == filename.lower():
810
use_direct_path = True
813
limbo_name = pathjoin(self._limbo_files[parent], filename)
814
self._limbo_children[parent].add(trans_id)
815
self._limbo_children_names[parent][filename] = trans_id
817
limbo_name = pathjoin(self._limbodir, trans_id)
818
self._needs_rename.add(trans_id)
819
self._limbo_files[trans_id] = limbo_name
822
def _set_executability(self, path, entry, trans_id):
823
"""Set the executability of versioned files """
824
new_executability = self._new_executability[trans_id]
825
entry.executable = new_executability
826
if supports_executable():
827
abspath = self._tree.abspath(path)
828
current_mode = os.stat(abspath).st_mode
829
if new_executability:
832
to_mode = current_mode | (0100 & ~umask)
833
# Enable x-bit for others only if they can read it.
834
if current_mode & 0004:
835
to_mode |= 0001 & ~umask
836
if current_mode & 0040:
837
to_mode |= 0010 & ~umask
839
to_mode = current_mode & ~0111
840
os.chmod(abspath, to_mode)
842
def _new_entry(self, name, parent_id, file_id):
843
"""Helper function to create a new filesystem entry."""
844
trans_id = self.create_path(name, parent_id)
845
if file_id is not None:
846
self.version_file(file_id, trans_id)
849
def new_file(self, name, parent_id, contents, file_id=None,
851
"""Convenience method to create files.
853
name is the name of the file to create.
854
parent_id is the transaction id of the parent directory of the file.
855
contents is an iterator of bytestrings, which will be used to produce
857
:param file_id: The inventory ID of the file, if it is to be versioned.
858
:param executable: Only valid when a file_id has been supplied.
860
trans_id = self._new_entry(name, parent_id, file_id)
861
# TODO: rather than scheduling a set_executable call,
862
# have create_file create the file with the right mode.
863
self.create_file(contents, trans_id)
864
if executable is not None:
865
self.set_executability(executable, trans_id)
868
def new_directory(self, name, parent_id, file_id=None):
869
"""Convenience method to create directories.
871
name is the name of the directory to create.
872
parent_id is the transaction id of the parent directory of the
874
file_id is the inventory ID of the directory, if it is to be versioned.
876
trans_id = self._new_entry(name, parent_id, file_id)
877
self.create_directory(trans_id)
880
def new_symlink(self, name, parent_id, target, file_id=None):
881
"""Convenience method to create symbolic link.
883
name is the name of the symlink to create.
884
parent_id is the transaction id of the parent directory of the symlink.
885
target is a bytestring of the target of the symlink.
886
file_id is the inventory ID of the file, if it is to be versioned.
888
trans_id = self._new_entry(name, parent_id, file_id)
889
self.create_symlink(target, trans_id)
892
def _affected_ids(self):
893
"""Return the set of transform ids affected by the transform"""
894
trans_ids = set(self._removed_id)
895
trans_ids.update(self._new_id.keys())
896
trans_ids.update(self._removed_contents)
897
trans_ids.update(self._new_contents.keys())
898
trans_ids.update(self._new_executability.keys())
899
trans_ids.update(self._new_name.keys())
900
trans_ids.update(self._new_parent.keys())
903
def _get_file_id_maps(self):
904
"""Return mapping of file_ids to trans_ids in the to and from states"""
905
trans_ids = self._affected_ids()
908
# Build up two dicts: trans_ids associated with file ids in the
909
# FROM state, vs the TO state.
910
for trans_id in trans_ids:
911
from_file_id = self.tree_file_id(trans_id)
912
if from_file_id is not None:
913
from_trans_ids[from_file_id] = trans_id
914
to_file_id = self.final_file_id(trans_id)
915
if to_file_id is not None:
916
to_trans_ids[to_file_id] = trans_id
917
return from_trans_ids, to_trans_ids
919
def _from_file_data(self, from_trans_id, from_versioned, file_id):
920
"""Get data about a file in the from (tree) state
922
Return a (name, parent, kind, executable) tuple
924
from_path = self._tree_id_paths.get(from_trans_id)
926
# get data from working tree if versioned
927
from_entry = self._tree.inventory[file_id]
928
from_name = from_entry.name
929
from_parent = from_entry.parent_id
932
if from_path is None:
933
# File does not exist in FROM state
937
# File exists, but is not versioned. Have to use path-
939
from_name = os.path.basename(from_path)
940
tree_parent = self.get_tree_parent(from_trans_id)
941
from_parent = self.tree_file_id(tree_parent)
942
if from_path is not None:
943
from_kind, from_executable, from_stats = \
944
self._tree._comparison_data(from_entry, from_path)
947
from_executable = False
948
return from_name, from_parent, from_kind, from_executable
950
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
951
"""Get data about a file in the to (target) state
953
Return a (name, parent, kind, executable) tuple
955
to_name = self.final_name(to_trans_id)
957
to_kind = self.final_kind(to_trans_id)
960
to_parent = self.final_file_id(self.final_parent(to_trans_id))
961
if to_trans_id in self._new_executability:
962
to_executable = self._new_executability[to_trans_id]
963
elif to_trans_id == from_trans_id:
964
to_executable = from_executable
966
to_executable = False
967
return to_name, to_parent, to_kind, to_executable
969
def _iter_changes(self):
970
"""Produce output in the same format as Tree._iter_changes.
972
Will produce nonsensical results if invoked while inventory/filesystem
973
conflicts (as reported by TreeTransform.find_conflicts()) are present.
975
This reads the Transform, but only reproduces changes involving a
976
file_id. Files that are not versioned in either of the FROM or TO
977
states are not reflected.
979
final_paths = FinalPaths(self)
980
from_trans_ids, to_trans_ids = self._get_file_id_maps()
982
# Now iterate through all active file_ids
983
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
985
from_trans_id = from_trans_ids.get(file_id)
986
# find file ids, and determine versioning state
987
if from_trans_id is None:
988
from_versioned = False
989
from_trans_id = to_trans_ids[file_id]
991
from_versioned = True
992
to_trans_id = to_trans_ids.get(file_id)
993
if to_trans_id is None:
995
to_trans_id = from_trans_id
999
from_name, from_parent, from_kind, from_executable = \
1000
self._from_file_data(from_trans_id, from_versioned, file_id)
1002
to_name, to_parent, to_kind, to_executable = \
1003
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1005
if not from_versioned:
1008
from_path = self._tree_id_paths.get(from_trans_id)
1009
if not to_versioned:
1012
to_path = final_paths.get_path(to_trans_id)
1013
if from_kind != to_kind:
1015
elif to_kind in ('file', 'symlink') and (
1016
to_trans_id != from_trans_id or
1017
to_trans_id in self._new_contents):
1019
if (not modified and from_versioned == to_versioned and
1020
from_parent==to_parent and from_name == to_name and
1021
from_executable == to_executable):
1023
results.append((file_id, (from_path, to_path), modified,
1024
(from_versioned, to_versioned),
1025
(from_parent, to_parent),
1026
(from_name, to_name),
1027
(from_kind, to_kind),
1028
(from_executable, to_executable)))
1029
return iter(sorted(results, key=lambda x:x[1]))
1031
def get_preview_tree(self):
1032
return PreviewTree(self)
1034
class TreeTransform(TreeTransformBase):
1035
"""Represent a tree transformation.
1037
This object is designed to support incremental generation of the transform,
1040
However, it gives optimum performance when parent directories are created
1041
before their contents. The transform is then able to put child files
1042
directly in their parent directory, avoiding later renames.
1044
It is easy to produce malformed transforms, but they are generally
1045
harmless. Attempting to apply a malformed transform will cause an
1046
exception to be raised before any modifications are made to the tree.
1048
Many kinds of malformed transforms can be corrected with the
1049
resolve_conflicts function. The remaining ones indicate programming error,
1050
such as trying to create a file with no path.
1052
Two sets of file creation methods are supplied. Convenience methods are:
1057
These are composed of the low-level methods:
1059
* create_file or create_directory or create_symlink
1063
Transform/Transaction ids
1064
-------------------------
1065
trans_ids are temporary ids assigned to all files involved in a transform.
1066
It's possible, even common, that not all files in the Tree have trans_ids.
1068
trans_ids are used because filenames and file_ids are not good enough
1069
identifiers; filenames change, and not all files have file_ids. File-ids
1070
are also associated with trans-ids, so that moving a file moves its
1073
trans_ids are only valid for the TreeTransform that generated them.
1077
Limbo is a temporary directory use to hold new versions of files.
1078
Files are added to limbo by create_file, create_directory, create_symlink,
1079
and their convenience variants (new_*). Files may be removed from limbo
1080
using cancel_creation. Files are renamed from limbo into their final
1081
location as part of TreeTransform.apply
1083
Limbo must be cleaned up, by either calling TreeTransform.apply or
1084
calling TreeTransform.finalize.
1086
Files are placed into limbo inside their parent directories, where
1087
possible. This reduces subsequent renames, and makes operations involving
1088
lots of files faster. This optimization is only possible if the parent
1089
directory is created *before* creating any of its children, so avoid
1090
creating children before parents, where possible.
1094
This temporary directory is used by _FileMover for storing files that are
1095
about to be deleted. In case of rollback, the files will be restored.
1096
FileMover does not delete files until it is sure that a rollback will not
1099
def __init__(self, tree, pb=DummyProgress()):
1100
"""Note: a tree_write lock is taken on the tree.
1102
Use TreeTransform.finalize() to release the lock (can be omitted if
1103
TreeTransform.apply() called).
1105
tree.lock_tree_write()
1108
control_files = tree._control_files
1109
limbodir = urlutils.local_path_from_url(
1110
control_files.controlfilename('limbo'))
1114
if e.errno == errno.EEXIST:
1115
raise ExistingLimbo(limbodir)
1116
self._deletiondir = urlutils.local_path_from_url(
1117
control_files.controlfilename('pending-deletion'))
1119
os.mkdir(self._deletiondir)
1121
if e.errno == errno.EEXIST:
1122
raise errors.ExistingPendingDeletion(self._deletiondir)
1127
TreeTransformBase.__init__(self, tree, limbodir, pb)
1129
def apply(self, no_conflicts=False, _mover=None):
1130
"""Apply all changes to the inventory and filesystem.
1132
If filesystem or inventory conflicts are present, MalformedTransform
1135
If apply succeeds, finalize is not necessary.
1137
:param no_conflicts: if True, the caller guarantees there are no
1138
conflicts, so no check is made.
1139
:param _mover: Supply an alternate FileMover, for testing
1141
if not no_conflicts:
1142
conflicts = self.find_conflicts()
1143
if len(conflicts) != 0:
1144
raise MalformedTransform(conflicts=conflicts)
1145
inv = self._tree.inventory
1146
inventory_delta = []
1147
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1150
mover = _FileMover()
1154
child_pb.update('Apply phase', 0, 2)
1155
self._apply_removals(inv, inventory_delta, mover)
1156
child_pb.update('Apply phase', 1, 2)
1157
modified_paths = self._apply_insertions(inv, inventory_delta,
1163
mover.apply_deletions()
1166
self._tree.apply_inventory_delta(inventory_delta)
1168
self._TreeTransformBase__done = True
1170
return _TransformResults(modified_paths, self.rename_count)
1172
def _apply_removals(self, inv, inventory_delta, mover):
1173
"""Perform tree operations that remove directory/inventory names.
1175
That is, delete files that are to be deleted, and put any files that
1176
need renaming into limbo. This must be done in strict child-to-parent
1179
tree_paths = list(self._tree_path_ids.iteritems())
1180
tree_paths.sort(reverse=True)
1181
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1183
for num, data in enumerate(tree_paths):
1184
path, trans_id = data
1185
child_pb.update('removing file', num, len(tree_paths))
1186
full_path = self._tree.abspath(path)
1187
if trans_id in self._removed_contents:
1188
mover.pre_delete(full_path, os.path.join(self._deletiondir,
1190
elif trans_id in self._new_name or trans_id in \
1193
mover.rename(full_path, self._limbo_name(trans_id))
1195
if e.errno != errno.ENOENT:
1198
self.rename_count += 1
1199
if trans_id in self._removed_id:
1200
if trans_id == self._new_root:
1201
file_id = self._tree.get_root_id()
1203
file_id = self.tree_file_id(trans_id)
1204
assert file_id is not None
1205
inventory_delta.append((path, None, file_id, None))
1209
def _apply_insertions(self, inv, inventory_delta, mover):
1210
"""Perform tree operations that insert directory/inventory names.
1212
That is, create any files that need to be created, and restore from
1213
limbo any files that needed renaming. This must be done in strict
1214
parent-to-child order.
1216
new_paths = self.new_paths()
1218
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1220
for num, (path, trans_id) in enumerate(new_paths):
1222
child_pb.update('adding file', num, len(new_paths))
1224
kind = self._new_contents[trans_id]
1226
kind = contents = None
1227
if trans_id in self._new_contents or \
1228
self.path_changed(trans_id):
1229
full_path = self._tree.abspath(path)
1230
if trans_id in self._needs_rename:
1232
mover.rename(self._limbo_name(trans_id), full_path)
1234
# We may be renaming a dangling inventory id
1235
if e.errno != errno.ENOENT:
1238
self.rename_count += 1
1239
if trans_id in self._new_contents:
1240
modified_paths.append(full_path)
1241
del self._new_contents[trans_id]
1243
if trans_id in self._new_id:
1245
kind = file_kind(self._tree.abspath(path))
1246
if trans_id in self._new_reference_revision:
1247
new_entry = inventory.TreeReference(
1248
self._new_id[trans_id],
1249
self._new_name[trans_id],
1250
self.final_file_id(self._new_parent[trans_id]),
1251
None, self._new_reference_revision[trans_id])
1253
new_entry = inventory.make_entry(kind,
1254
self.final_name(trans_id),
1255
self.final_file_id(self.final_parent(trans_id)),
1256
self._new_id[trans_id])
1258
if trans_id in self._new_name or trans_id in\
1259
self._new_parent or\
1260
trans_id in self._new_executability:
1261
file_id = self.final_file_id(trans_id)
1262
if file_id is not None:
1263
entry = inv[file_id]
1264
new_entry = entry.copy()
1266
if trans_id in self._new_name or trans_id in\
1268
if new_entry is not None:
1269
new_entry.name = self.final_name(trans_id)
1270
parent = self.final_parent(trans_id)
1271
parent_id = self.final_file_id(parent)
1272
new_entry.parent_id = parent_id
1274
if trans_id in self._new_executability:
1275
self._set_executability(path, new_entry, trans_id)
1276
if new_entry is not None:
1277
if new_entry.file_id in inv:
1278
old_path = inv.id2path(new_entry.file_id)
1281
inventory_delta.append((old_path, path,
1286
return modified_paths
1289
class TransformPreview(TreeTransformBase):
1291
def __init__(self, tree, pb=DummyProgress()):
1292
limbodir = tempfile.mkdtemp()
1293
TreeTransformBase.__init__(self, tree, limbodir, pb)
1295
def canonical_path(self, path):
1298
def tree_kind(self, trans_id):
1299
path = self._tree_id_paths.get(trans_id)
1301
raise NoSuchFile(None)
1302
file_id = self._tree.path2id(path)
1303
return self._tree.kind(file_id)
1305
def _set_mode(self, trans_id, mode_id, typefunc):
1306
"""Set the mode of new file contents.
1307
The mode_id is the existing file to get the mode from (often the same
1308
as trans_id). The operation is only performed if there's a mode match
1309
according to typefunc.
1311
# is it ok to ignore this? probably
1314
def iter_tree_children(self, parent_id):
1315
"""Iterate through the entry's tree children, if any"""
1317
path = self._tree_id_paths[parent_id]
1320
file_id = self.tree_file_id(parent_id)
1321
for child in self._tree.inventory[file_id].children.iterkeys():
1322
childpath = joinpath(path, child)
1323
yield self.trans_id_tree_path(childpath)
1325
class PreviewTree(object):
1327
def __init__(self, transform):
1328
self._transform = transform
1330
def lock_read(self):
1336
def changes_from(self, other, want_unchanged=False, specific_files=None,
1337
extra_trees=None, require_versioned=False, include_root=False,
1338
want_unversioned=False):
1339
return tree.InterTree.get(other, self).compare(
1340
want_unchanged=want_unchanged,
1341
specific_files=specific_files,
1342
extra_trees=extra_trees,
1343
require_versioned=require_versioned,
1344
include_root=include_root,
1345
want_unversioned=want_unversioned,
1348
def _iter_changes(self, include_unchanged=False, specific_files=None,
1349
pb=None, extra_trees=[], require_versioned=True,
1350
want_unversioned=False):
1351
# FIXME: should error out when inputs aren't acceptable
1352
return self._transform._iter_changes()
1354
def kind(self, file_id):
1355
trans_id = self._transform.trans_id_file_id(file_id)
1356
return self._transform.final_kind(trans_id)
1358
def get_file_mtime(self, file_id, path=None):
1359
trans_id = self._transform.trans_id_file_id(file_id)
1360
name = self._transform._limbo_name(trans_id)
1361
return os.stat(name).st_mtime
1363
def get_file(self, file_id):
1364
trans_id = self._transform.trans_id_file_id(file_id)
1365
name = self._transform._limbo_name(trans_id)
1366
return open(name, 'rb')
1368
def paths2ids(self, specific_files, trees=None, require_versioned=False):
1372
def joinpath(parent, child):
1373
"""Join tree-relative paths, handling the tree root specially"""
1374
if parent is None or parent == "":
1377
return pathjoin(parent, child)
1380
class FinalPaths(object):
1381
"""Make path calculation cheap by memoizing paths.
1383
The underlying tree must not be manipulated between calls, or else
1384
the results will likely be incorrect.
1386
def __init__(self, transform):
1387
object.__init__(self)
1388
self._known_paths = {}
1389
self.transform = transform
1391
def _determine_path(self, trans_id):
1392
if trans_id == self.transform.root:
1394
name = self.transform.final_name(trans_id)
1395
parent_id = self.transform.final_parent(trans_id)
1396
if parent_id == self.transform.root:
1399
return pathjoin(self.get_path(parent_id), name)
1401
def get_path(self, trans_id):
1402
"""Find the final path associated with a trans_id"""
1403
if trans_id not in self._known_paths:
1404
self._known_paths[trans_id] = self._determine_path(trans_id)
1405
return self._known_paths[trans_id]
1408
def topology_sorted_ids(tree):
1409
"""Determine the topological order of the ids in a tree"""
1410
file_ids = list(tree)
1411
file_ids.sort(key=tree.id2path)
1415
def build_tree(tree, wt):
1416
"""Create working tree for a branch, using a TreeTransform.
1418
This function should be used on empty trees, having a tree root at most.
1419
(see merge and revert functionality for working with existing trees)
1421
Existing files are handled like so:
1423
- Existing bzrdirs take precedence over creating new items. They are
1424
created as '%s.diverted' % name.
1425
- Otherwise, if the content on disk matches the content we are building,
1426
it is silently replaced.
1427
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1429
wt.lock_tree_write()
1433
return _build_tree(tree, wt)
1440
def _build_tree(tree, wt):
1441
"""See build_tree."""
1442
if len(wt.inventory) > 1: # more than just a root
1443
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1445
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1446
pp = ProgressPhase("Build phase", 2, top_pb)
1447
if tree.inventory.root is not None:
1448
# This is kind of a hack: we should be altering the root
1449
# as part of the regular tree shape diff logic.
1450
# The conditional test here is to avoid doing an
1451
# expensive operation (flush) every time the root id
1452
# is set within the tree, nor setting the root and thus
1453
# marking the tree as dirty, because we use two different
1454
# idioms here: tree interfaces and inventory interfaces.
1455
if wt.get_root_id() != tree.get_root_id():
1456
wt.set_root_id(tree.get_root_id())
1458
tt = TreeTransform(wt)
1462
file_trans_id[wt.get_root_id()] = \
1463
tt.trans_id_tree_file_id(wt.get_root_id())
1464
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1466
deferred_contents = []
1467
for num, (tree_path, entry) in \
1468
enumerate(tree.inventory.iter_entries_by_dir()):
1469
pb.update("Building tree", num - len(deferred_contents),
1470
len(tree.inventory))
1471
if entry.parent_id is None:
1474
file_id = entry.file_id
1475
target_path = wt.abspath(tree_path)
1477
kind = file_kind(target_path)
1481
if kind == "directory":
1483
bzrdir.BzrDir.open(target_path)
1484
except errors.NotBranchError:
1488
if (file_id not in divert and
1489
_content_match(tree, entry, file_id, kind,
1491
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1492
if kind == 'directory':
1494
if entry.parent_id not in file_trans_id:
1495
raise AssertionError(
1496
'entry %s parent id %r is not in file_trans_id %r'
1497
% (entry, entry.parent_id, file_trans_id))
1498
parent_id = file_trans_id[entry.parent_id]
1499
if entry.kind == 'file':
1500
# We *almost* replicate new_by_entry, so that we can defer
1501
# getting the file text, and get them all at once.
1502
trans_id = tt.create_path(entry.name, parent_id)
1503
file_trans_id[file_id] = trans_id
1504
tt.version_file(entry.file_id, trans_id)
1505
executable = tree.is_executable(entry.file_id, tree_path)
1506
if executable is not None:
1507
tt.set_executability(executable, trans_id)
1508
deferred_contents.append((entry.file_id, trans_id))
1510
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1513
new_trans_id = file_trans_id[file_id]
1514
old_parent = tt.trans_id_tree_path(tree_path)
1515
_reparent_children(tt, old_parent, new_trans_id)
1516
for num, (trans_id, bytes) in enumerate(
1517
tree.iter_files_bytes(deferred_contents)):
1518
tt.create_file(bytes, trans_id)
1519
pb.update('Adding file contents',
1520
(num + len(tree.inventory) - len(deferred_contents)),
1521
len(tree.inventory))
1525
divert_trans = set(file_trans_id[f] for f in divert)
1526
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1527
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1528
conflicts = cook_conflicts(raw_conflicts, tt)
1529
for conflict in conflicts:
1532
wt.add_conflicts(conflicts)
1533
except errors.UnsupportedOperation:
1542
def _reparent_children(tt, old_parent, new_parent):
1543
for child in tt.iter_tree_children(old_parent):
1544
tt.adjust_path(tt.final_name(child), new_parent, child)
1547
def _content_match(tree, entry, file_id, kind, target_path):
1548
if entry.kind != kind:
1550
if entry.kind == "directory":
1552
if entry.kind == "file":
1553
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1555
elif entry.kind == "symlink":
1556
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1561
def resolve_checkout(tt, conflicts, divert):
1562
new_conflicts = set()
1563
for c_type, conflict in ((c[0], c) for c in conflicts):
1564
# Anything but a 'duplicate' would indicate programmer error
1565
assert c_type == 'duplicate', c_type
1566
# Now figure out which is new and which is old
1567
if tt.new_contents(conflict[1]):
1568
new_file = conflict[1]
1569
old_file = conflict[2]
1571
new_file = conflict[2]
1572
old_file = conflict[1]
1574
# We should only get here if the conflict wasn't completely
1576
final_parent = tt.final_parent(old_file)
1577
if new_file in divert:
1578
new_name = tt.final_name(old_file)+'.diverted'
1579
tt.adjust_path(new_name, final_parent, new_file)
1580
new_conflicts.add((c_type, 'Diverted to',
1581
new_file, old_file))
1583
new_name = tt.final_name(old_file)+'.moved'
1584
tt.adjust_path(new_name, final_parent, old_file)
1585
new_conflicts.add((c_type, 'Moved existing file to',
1586
old_file, new_file))
1587
return new_conflicts
1590
def new_by_entry(tt, entry, parent_id, tree):
1591
"""Create a new file according to its inventory entry"""
1595
contents = tree.get_file(entry.file_id).readlines()
1596
executable = tree.is_executable(entry.file_id)
1597
return tt.new_file(name, parent_id, contents, entry.file_id,
1599
elif kind in ('directory', 'tree-reference'):
1600
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1601
if kind == 'tree-reference':
1602
tt.set_tree_reference(entry.reference_revision, trans_id)
1604
elif kind == 'symlink':
1605
target = tree.get_symlink_target(entry.file_id)
1606
return tt.new_symlink(name, parent_id, target, entry.file_id)
1608
raise errors.BadFileKindError(name, kind)
1611
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1612
"""Create new file contents according to an inventory entry."""
1613
if entry.kind == "file":
1615
lines = tree.get_file(entry.file_id).readlines()
1616
tt.create_file(lines, trans_id, mode_id=mode_id)
1617
elif entry.kind == "symlink":
1618
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1619
elif entry.kind == "directory":
1620
tt.create_directory(trans_id)
1623
def create_entry_executability(tt, entry, trans_id):
1624
"""Set the executability of a trans_id according to an inventory entry"""
1625
if entry.kind == "file":
1626
tt.set_executability(entry.executable, trans_id)
1629
@deprecated_function(zero_fifteen)
1630
def find_interesting(working_tree, target_tree, filenames):
1631
"""Find the ids corresponding to specified filenames.
1633
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1635
working_tree.lock_read()
1637
target_tree.lock_read()
1639
return working_tree.paths2ids(filenames, [target_tree])
1641
target_tree.unlock()
1643
working_tree.unlock()
1646
@deprecated_function(zero_ninety)
1647
def change_entry(tt, file_id, working_tree, target_tree,
1648
trans_id_file_id, backups, trans_id, by_parent):
1649
"""Replace a file_id's contents with those from a target tree."""
1650
if file_id is None and target_tree is None:
1651
# skip the logic altogether in the deprecation test
1653
e_trans_id = trans_id_file_id(file_id)
1654
entry = target_tree.inventory[file_id]
1655
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1658
mode_id = e_trans_id
1661
tt.delete_contents(e_trans_id)
1663
parent_trans_id = trans_id_file_id(entry.parent_id)
1664
backup_name = get_backup_name(entry, by_parent,
1665
parent_trans_id, tt)
1666
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1667
tt.unversion_file(e_trans_id)
1668
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1669
tt.version_file(file_id, e_trans_id)
1670
trans_id[file_id] = e_trans_id
1671
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1672
create_entry_executability(tt, entry, e_trans_id)
1675
tt.set_executability(entry.executable, e_trans_id)
1676
if tt.final_name(e_trans_id) != entry.name:
1679
parent_id = tt.final_parent(e_trans_id)
1680
parent_file_id = tt.final_file_id(parent_id)
1681
if parent_file_id != entry.parent_id:
1686
parent_trans_id = trans_id_file_id(entry.parent_id)
1687
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1690
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1691
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1694
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1695
"""Produce a backup-style name that appears to be available"""
1699
yield "%s.~%d~" % (name, counter)
1701
for new_name in name_gen():
1702
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1706
def _entry_changes(file_id, entry, working_tree):
1707
"""Determine in which ways the inventory entry has changed.
1709
Returns booleans: has_contents, content_mod, meta_mod
1710
has_contents means there are currently contents, but they differ
1711
contents_mod means contents need to be modified
1712
meta_mod means the metadata needs to be modified
1714
cur_entry = working_tree.inventory[file_id]
1716
working_kind = working_tree.kind(file_id)
1719
has_contents = False
1722
if has_contents is True:
1723
if entry.kind != working_kind:
1724
contents_mod, meta_mod = True, False
1726
cur_entry._read_tree_state(working_tree.id2path(file_id),
1728
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1729
cur_entry._forget_tree_state()
1730
return has_contents, contents_mod, meta_mod
1733
def revert(working_tree, target_tree, filenames, backups=False,
1734
pb=DummyProgress(), change_reporter=None):
1735
"""Revert a working tree's contents to those of a target tree."""
1736
target_tree.lock_read()
1737
tt = TreeTransform(working_tree, pb)
1739
pp = ProgressPhase("Revert phase", 3, pb)
1741
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1743
merge_modified = _alter_files(working_tree, target_tree, tt,
1744
child_pb, filenames, backups)
1748
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1750
raw_conflicts = resolve_conflicts(tt, child_pb,
1751
lambda t, c: conflict_pass(t, c, target_tree))
1754
conflicts = cook_conflicts(raw_conflicts, tt)
1756
change_reporter = delta._ChangeReporter(
1757
unversioned_filter=working_tree.is_ignored)
1758
delta.report_changes(tt._iter_changes(), change_reporter)
1759
for conflict in conflicts:
1763
working_tree.set_merge_modified(merge_modified)
1765
target_tree.unlock()
1771
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1773
merge_modified = working_tree.merge_modified()
1774
change_list = target_tree._iter_changes(working_tree,
1775
specific_files=specific_files, pb=pb)
1776
if target_tree.inventory.root is None:
1783
for id_num, (file_id, path, changed_content, versioned, parent, name,
1784
kind, executable) in enumerate(change_list):
1785
if skip_root and file_id[0] is not None and parent[0] is None:
1787
trans_id = tt.trans_id_file_id(file_id)
1790
keep_content = False
1791
if kind[0] == 'file' and (backups or kind[1] is None):
1792
wt_sha1 = working_tree.get_file_sha1(file_id)
1793
if merge_modified.get(file_id) != wt_sha1:
1794
# acquire the basis tree lazily to prevent the
1795
# expense of accessing it when it's not needed ?
1796
# (Guessing, RBC, 200702)
1797
if basis_tree is None:
1798
basis_tree = working_tree.basis_tree()
1799
basis_tree.lock_read()
1800
if file_id in basis_tree:
1801
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1803
elif kind[1] is None and not versioned[1]:
1805
if kind[0] is not None:
1806
if not keep_content:
1807
tt.delete_contents(trans_id)
1808
elif kind[1] is not None:
1809
parent_trans_id = tt.trans_id_file_id(parent[0])
1810
by_parent = tt.by_parent()
1811
backup_name = _get_backup_name(name[0], by_parent,
1812
parent_trans_id, tt)
1813
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1814
new_trans_id = tt.create_path(name[0], parent_trans_id)
1815
if versioned == (True, True):
1816
tt.unversion_file(trans_id)
1817
tt.version_file(file_id, new_trans_id)
1818
# New contents should have the same unix perms as old
1821
trans_id = new_trans_id
1822
if kind[1] == 'directory':
1823
tt.create_directory(trans_id)
1824
elif kind[1] == 'symlink':
1825
tt.create_symlink(target_tree.get_symlink_target(file_id),
1827
elif kind[1] == 'file':
1828
deferred_files.append((file_id, (trans_id, mode_id)))
1829
if basis_tree is None:
1830
basis_tree = working_tree.basis_tree()
1831
basis_tree.lock_read()
1832
new_sha1 = target_tree.get_file_sha1(file_id)
1833
if (file_id in basis_tree and new_sha1 ==
1834
basis_tree.get_file_sha1(file_id)):
1835
if file_id in merge_modified:
1836
del merge_modified[file_id]
1838
merge_modified[file_id] = new_sha1
1840
# preserve the execute bit when backing up
1841
if keep_content and executable[0] == executable[1]:
1842
tt.set_executability(executable[1], trans_id)
1844
assert kind[1] is None
1845
if versioned == (False, True):
1846
tt.version_file(file_id, trans_id)
1847
if versioned == (True, False):
1848
tt.unversion_file(trans_id)
1849
if (name[1] is not None and
1850
(name[0] != name[1] or parent[0] != parent[1])):
1852
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1853
if executable[0] != executable[1] and kind[1] == "file":
1854
tt.set_executability(executable[1], trans_id)
1855
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
1857
tt.create_file(bytes, trans_id, mode_id)
1859
if basis_tree is not None:
1861
return merge_modified
1864
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1865
"""Make many conflict-resolution attempts, but die if they fail"""
1866
if pass_func is None:
1867
pass_func = conflict_pass
1868
new_conflicts = set()
1871
pb.update('Resolution pass', n+1, 10)
1872
conflicts = tt.find_conflicts()
1873
if len(conflicts) == 0:
1874
return new_conflicts
1875
new_conflicts.update(pass_func(tt, conflicts))
1876
raise MalformedTransform(conflicts=conflicts)
1881
def conflict_pass(tt, conflicts, path_tree=None):
1882
"""Resolve some classes of conflicts.
1884
:param tt: The transform to resolve conflicts in
1885
:param conflicts: The conflicts to resolve
1886
:param path_tree: A Tree to get supplemental paths from
1888
new_conflicts = set()
1889
for c_type, conflict in ((c[0], c) for c in conflicts):
1890
if c_type == 'duplicate id':
1891
tt.unversion_file(conflict[1])
1892
new_conflicts.add((c_type, 'Unversioned existing file',
1893
conflict[1], conflict[2], ))
1894
elif c_type == 'duplicate':
1895
# files that were renamed take precedence
1896
final_parent = tt.final_parent(conflict[1])
1897
if tt.path_changed(conflict[1]):
1898
existing_file, new_file = conflict[2], conflict[1]
1900
existing_file, new_file = conflict[1], conflict[2]
1901
new_name = tt.final_name(existing_file)+'.moved'
1902
tt.adjust_path(new_name, final_parent, existing_file)
1903
new_conflicts.add((c_type, 'Moved existing file to',
1904
existing_file, new_file))
1905
elif c_type == 'parent loop':
1906
# break the loop by undoing one of the ops that caused the loop
1908
while not tt.path_changed(cur):
1909
cur = tt.final_parent(cur)
1910
new_conflicts.add((c_type, 'Cancelled move', cur,
1911
tt.final_parent(cur),))
1912
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1914
elif c_type == 'missing parent':
1915
trans_id = conflict[1]
1917
tt.cancel_deletion(trans_id)
1918
new_conflicts.add(('deleting parent', 'Not deleting',
1921
tt.create_directory(trans_id)
1922
new_conflicts.add((c_type, 'Created directory', trans_id))
1924
tt.final_name(trans_id)
1926
if path_tree is not None:
1927
file_id = tt.final_file_id(trans_id)
1928
entry = path_tree.inventory[file_id]
1929
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1930
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1931
elif c_type == 'unversioned parent':
1932
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1933
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1934
return new_conflicts
1937
def cook_conflicts(raw_conflicts, tt):
1938
"""Generate a list of cooked conflicts, sorted by file path"""
1939
from bzrlib.conflicts import Conflict
1940
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1941
return sorted(conflict_iter, key=Conflict.sort_key)
1944
def iter_cook_conflicts(raw_conflicts, tt):
1945
from bzrlib.conflicts import Conflict
1947
for conflict in raw_conflicts:
1948
c_type = conflict[0]
1949
action = conflict[1]
1950
modified_path = fp.get_path(conflict[2])
1951
modified_id = tt.final_file_id(conflict[2])
1952
if len(conflict) == 3:
1953
yield Conflict.factory(c_type, action=action, path=modified_path,
1954
file_id=modified_id)
1957
conflicting_path = fp.get_path(conflict[3])
1958
conflicting_id = tt.final_file_id(conflict[3])
1959
yield Conflict.factory(c_type, action=action, path=modified_path,
1960
file_id=modified_id,
1961
conflict_path=conflicting_path,
1962
conflict_file_id=conflicting_id)
1965
class _FileMover(object):
1966
"""Moves and deletes files for TreeTransform, tracking operations"""
1969
self.past_renames = []
1970
self.pending_deletions = []
1972
def rename(self, from_, to):
1973
"""Rename a file from one path to another. Functions like os.rename"""
1974
os.rename(from_, to)
1975
self.past_renames.append((from_, to))
1977
def pre_delete(self, from_, to):
1978
"""Rename a file out of the way and mark it for deletion.
1980
Unlike os.unlink, this works equally well for files and directories.
1981
:param from_: The current file path
1982
:param to: A temporary path for the file
1984
self.rename(from_, to)
1985
self.pending_deletions.append(to)
1988
"""Reverse all renames that have been performed"""
1989
for from_, to in reversed(self.past_renames):
1990
os.rename(to, from_)
1991
# after rollback, don't reuse _FileMover
1993
pending_deletions = None
1995
def apply_deletions(self):
1996
"""Apply all marked deletions"""
1997
for path in self.pending_deletions:
1999
# after apply_deletions, don't reuse _FileMover
2001
pending_deletions = None