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
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
30
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
31
ReusingTransform, NotVersionedError, CantMoveRoot,
32
ExistingLimbo, ImmortalLimbo, NoFinalPath)
33
from bzrlib.inventory import InventoryEntry
34
from bzrlib.osutils import (file_kind, supports_executable, pathjoin, lexists,
36
from bzrlib.progress import DummyProgress, ProgressPhase
37
from bzrlib.symbol_versioning import (
42
from bzrlib.trace import mutter, warning
43
from bzrlib import tree
45
import bzrlib.urlutils as urlutils
48
ROOT_PARENT = "root-parent"
51
def unique_add(map, key, value):
53
raise DuplicateKey(key=key)
57
class _TransformResults(object):
58
def __init__(self, modified_paths, rename_count):
60
self.modified_paths = modified_paths
61
self.rename_count = rename_count
64
class TreeTransform(object):
65
"""Represent a tree transformation.
67
This object is designed to support incremental generation of the transform,
70
However, it gives optimum performance when parent directories are created
71
before their contents. The transform is then able to put child files
72
directly in their parent directory, avoiding later renames.
74
It is easy to produce malformed transforms, but they are generally
75
harmless. Attempting to apply a malformed transform will cause an
76
exception to be raised before any modifications are made to the tree.
78
Many kinds of malformed transforms can be corrected with the
79
resolve_conflicts function. The remaining ones indicate programming error,
80
such as trying to create a file with no path.
82
Two sets of file creation methods are supplied. Convenience methods are:
87
These are composed of the low-level methods:
89
* create_file or create_directory or create_symlink
93
Transform/Transaction ids
94
-------------------------
95
trans_ids are temporary ids assigned to all files involved in a transform.
96
It's possible, even common, that not all files in the Tree have trans_ids.
98
trans_ids are used because filenames and file_ids are not good enough
99
identifiers; filenames change, and not all files have file_ids. File-ids
100
are also associated with trans-ids, so that moving a file moves its
103
trans_ids are only valid for the TreeTransform that generated them.
107
Limbo is a temporary directory use to hold new versions of files.
108
Files are added to limbo by new_file, new_directory, new_symlink, and their
109
convenience variants (create_*). Files may be removed from limbo using
110
cancel_creation. Files are renamed from limbo into their final location as
111
part of TreeTransform.apply
113
Limbo must be cleaned up, by either calling TreeTransform.apply or
114
calling TreeTransform.finalize.
116
Files are placed into limbo inside their parent directories, where
117
possible. This reduces subsequent renames, and makes operations involving
118
lots of files faster. This is only possible if the parent directory
119
is created *before* creating any of its children.
123
This temporary directory is used by _FileMover for storing files that are
124
about to be deleted. FileMover does not delete files until it is
125
sure that a rollback will not happen. In case of rollback, the files
128
def __init__(self, tree, pb=DummyProgress()):
129
"""Note: a tree_write lock is taken on the tree.
131
Use TreeTransform.finalize() to release the lock (can be omitted if
132
TreeTransform.apply() called).
134
object.__init__(self)
136
self._tree.lock_tree_write()
138
control_files = self._tree._control_files
139
self._limbodir = urlutils.local_path_from_url(
140
control_files.controlfilename('limbo'))
142
os.mkdir(self._limbodir)
144
if e.errno == errno.EEXIST:
145
raise ExistingLimbo(self._limbodir)
146
self._deletiondir = urlutils.local_path_from_url(
147
control_files.controlfilename('pending-deletion'))
149
os.mkdir(self._deletiondir)
151
if e.errno == errno.EEXIST:
152
raise errors.ExistingPendingDeletion(self._deletiondir)
158
# counter used to generate trans-ids (which are locally unique)
160
# mapping of trans_id -> new basename
162
# mapping of trans_id -> new parent trans_id
163
self._new_parent = {}
164
# mapping of trans_id with new contents -> new file_kind
165
self._new_contents = {}
166
# A mapping of transform ids to their limbo filename
167
self._limbo_files = {}
168
# A mapping of transform ids to a set of the transform ids of children
169
# that their limbo directory has
170
self._limbo_children = {}
171
# Map transform ids to maps of child filename to child transform id
172
self._limbo_children_names = {}
173
# List of transform ids that need to be renamed from limbo into place
174
self._needs_rename = set()
175
# Set of trans_ids whose contents will be removed
176
self._removed_contents = set()
177
# Mapping of trans_id -> new execute-bit value
178
self._new_executability = {}
179
# Mapping of trans_id -> new tree-reference value
180
self._new_reference_revision = {}
181
# Mapping of trans_id -> new file_id
183
# Mapping of old file-id -> trans_id
184
self._non_present_ids = {}
185
# Mapping of new file_id -> trans_id
187
# Set of file_ids that will be removed
188
self._removed_id = set()
189
# Mapping of path in old tree -> trans_id
190
self._tree_path_ids = {}
191
# Mapping trans_id -> path in old tree
192
self._tree_id_paths = {}
193
# Cache of realpath results, to speed up canonical_path
195
# Cache of relpath results, to speed up canonical_path
197
# The trans_id that will be used as the tree root
198
self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
199
# Indictor of whether the transform has been applied
203
# A counter of how many files have been renamed
204
self.rename_count = 0
206
def __get_root(self):
207
return self._new_root
209
root = property(__get_root)
212
"""Release the working tree lock, if held, clean up limbo dir.
214
This is required if apply has not been invoked, but can be invoked
217
if self._tree is None:
220
entries = [(self._limbo_name(t), t, k) for t, k in
221
self._new_contents.iteritems()]
222
entries.sort(reverse=True)
223
for path, trans_id, kind in entries:
224
if kind == "directory":
229
os.rmdir(self._limbodir)
231
# We don't especially care *why* the dir is immortal.
232
raise ImmortalLimbo(self._limbodir)
234
os.rmdir(self._deletiondir)
236
raise errors.ImmortalPendingDeletion(self._deletiondir)
241
def _assign_id(self):
242
"""Produce a new tranform id"""
243
new_id = "new-%s" % self._id_number
247
def create_path(self, name, parent):
248
"""Assign a transaction id to a new path"""
249
trans_id = self._assign_id()
250
unique_add(self._new_name, trans_id, name)
251
unique_add(self._new_parent, trans_id, parent)
254
def adjust_path(self, name, parent, trans_id):
255
"""Change the path that is assigned to a transaction id."""
256
if trans_id == self._new_root:
258
previous_parent = self._new_parent.get(trans_id)
259
previous_name = self._new_name.get(trans_id)
260
self._new_name[trans_id] = name
261
self._new_parent[trans_id] = parent
262
if (trans_id in self._limbo_files and
263
trans_id not in self._needs_rename):
264
self._rename_in_limbo([trans_id])
265
self._limbo_children[previous_parent].remove(trans_id)
266
del self._limbo_children_names[previous_parent][previous_name]
268
def _rename_in_limbo(self, trans_ids):
269
"""Fix limbo names so that the right final path is produced.
271
This means we outsmarted ourselves-- we tried to avoid renaming
272
these files later by creating them with their final names in their
273
final parents. But now the previous name or parent is no longer
274
suitable, so we have to rename them.
276
Even for trans_ids that have no new contents, we must remove their
277
entries from _limbo_files, because they are now stale.
279
for trans_id in trans_ids:
280
old_path = self._limbo_files.pop(trans_id)
281
if trans_id not in self._new_contents:
283
new_path = self._limbo_name(trans_id)
284
os.rename(old_path, new_path)
286
def adjust_root_path(self, name, parent):
287
"""Emulate moving the root by moving all children, instead.
289
We do this by undoing the association of root's transaction id with the
290
current tree. This allows us to create a new directory with that
291
transaction id. We unversion the root directory and version the
292
physically new directory, and hope someone versions the tree root
295
old_root = self._new_root
296
old_root_file_id = self.final_file_id(old_root)
297
# force moving all children of root
298
for child_id in self.iter_tree_children(old_root):
299
if child_id != parent:
300
self.adjust_path(self.final_name(child_id),
301
self.final_parent(child_id), child_id)
302
file_id = self.final_file_id(child_id)
303
if file_id is not None:
304
self.unversion_file(child_id)
305
self.version_file(file_id, child_id)
307
# the physical root needs a new transaction id
308
self._tree_path_ids.pop("")
309
self._tree_id_paths.pop(old_root)
310
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
311
if parent == old_root:
312
parent = self._new_root
313
self.adjust_path(name, parent, old_root)
314
self.create_directory(old_root)
315
self.version_file(old_root_file_id, old_root)
316
self.unversion_file(self._new_root)
318
def trans_id_tree_file_id(self, inventory_id):
319
"""Determine the transaction id of a working tree file.
321
This reflects only files that already exist, not ones that will be
322
added by transactions.
324
path = self._tree.inventory.id2path(inventory_id)
325
return self.trans_id_tree_path(path)
327
def trans_id_file_id(self, file_id):
328
"""Determine or set the transaction id associated with a file ID.
329
A new id is only created for file_ids that were never present. If
330
a transaction has been unversioned, it is deliberately still returned.
331
(this will likely lead to an unversioned parent conflict.)
333
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
334
return self._r_new_id[file_id]
335
elif file_id in self._tree.inventory:
336
return self.trans_id_tree_file_id(file_id)
337
elif file_id in self._non_present_ids:
338
return self._non_present_ids[file_id]
340
trans_id = self._assign_id()
341
self._non_present_ids[file_id] = trans_id
344
def canonical_path(self, path):
345
"""Get the canonical tree-relative path"""
346
# don't follow final symlinks
347
abs = self._tree.abspath(path)
348
if abs in self._relpaths:
349
return self._relpaths[abs]
350
dirname, basename = os.path.split(abs)
351
if dirname not in self._realpaths:
352
self._realpaths[dirname] = os.path.realpath(dirname)
353
dirname = self._realpaths[dirname]
354
abs = pathjoin(dirname, basename)
355
if dirname in self._relpaths:
356
relpath = pathjoin(self._relpaths[dirname], basename)
357
relpath = relpath.rstrip('/\\')
359
relpath = self._tree.relpath(abs)
360
self._relpaths[abs] = relpath
363
def trans_id_tree_path(self, path):
364
"""Determine (and maybe set) the transaction ID for a tree path."""
365
path = self.canonical_path(path)
366
if path not in self._tree_path_ids:
367
self._tree_path_ids[path] = self._assign_id()
368
self._tree_id_paths[self._tree_path_ids[path]] = path
369
return self._tree_path_ids[path]
371
def get_tree_parent(self, trans_id):
372
"""Determine id of the parent in the tree."""
373
path = self._tree_id_paths[trans_id]
376
return self.trans_id_tree_path(os.path.dirname(path))
378
def create_file(self, contents, trans_id, mode_id=None):
379
"""Schedule creation of a new file.
383
Contents is an iterator of strings, all of which will be written
384
to the target destination.
386
New file takes the permissions of any existing file with that id,
387
unless mode_id is specified.
389
name = self._limbo_name(trans_id)
393
unique_add(self._new_contents, trans_id, 'file')
395
# Clean up the file, it never got registered so
396
# TreeTransform.finalize() won't clean it up.
401
f.writelines(contents)
404
self._set_mode(trans_id, mode_id, S_ISREG)
406
def _set_mode(self, trans_id, mode_id, typefunc):
407
"""Set the mode of new file contents.
408
The mode_id is the existing file to get the mode from (often the same
409
as trans_id). The operation is only performed if there's a mode match
410
according to typefunc.
415
old_path = self._tree_id_paths[mode_id]
419
mode = os.stat(self._tree.abspath(old_path)).st_mode
421
if e.errno == errno.ENOENT:
426
os.chmod(self._limbo_name(trans_id), mode)
428
def create_directory(self, trans_id):
429
"""Schedule creation of a new directory.
431
See also new_directory.
433
os.mkdir(self._limbo_name(trans_id))
434
unique_add(self._new_contents, trans_id, 'directory')
436
def create_symlink(self, target, trans_id):
437
"""Schedule creation of a new symbolic link.
439
target is a bytestring.
440
See also new_symlink.
442
os.symlink(target, self._limbo_name(trans_id))
443
unique_add(self._new_contents, trans_id, 'symlink')
445
def cancel_creation(self, trans_id):
446
"""Cancel the creation of new file contents."""
447
del self._new_contents[trans_id]
448
children = self._limbo_children.get(trans_id)
449
# if this is a limbo directory with children, move them before removing
451
if children is not None:
452
self._rename_in_limbo(children)
453
del self._limbo_children[trans_id]
454
del self._limbo_children_names[trans_id]
455
delete_any(self._limbo_name(trans_id))
457
def delete_contents(self, trans_id):
458
"""Schedule the contents of a path entry for deletion"""
459
self.tree_kind(trans_id)
460
self._removed_contents.add(trans_id)
462
def cancel_deletion(self, trans_id):
463
"""Cancel a scheduled deletion"""
464
self._removed_contents.remove(trans_id)
466
def unversion_file(self, trans_id):
467
"""Schedule a path entry to become unversioned"""
468
self._removed_id.add(trans_id)
470
def delete_versioned(self, trans_id):
471
"""Delete and unversion a versioned file"""
472
self.delete_contents(trans_id)
473
self.unversion_file(trans_id)
475
def set_executability(self, executability, trans_id):
476
"""Schedule setting of the 'execute' bit
477
To unschedule, set to None
479
if executability is None:
480
del self._new_executability[trans_id]
482
unique_add(self._new_executability, trans_id, executability)
484
def set_tree_reference(self, revision_id, trans_id):
485
"""Set the reference associated with a directory"""
486
unique_add(self._new_reference_revision, trans_id, revision_id)
488
def version_file(self, file_id, trans_id):
489
"""Schedule a file to become versioned."""
490
assert file_id is not None
491
unique_add(self._new_id, trans_id, file_id)
492
unique_add(self._r_new_id, file_id, trans_id)
494
def cancel_versioning(self, trans_id):
495
"""Undo a previous versioning of a file"""
496
file_id = self._new_id[trans_id]
497
del self._new_id[trans_id]
498
del self._r_new_id[file_id]
501
"""Determine the paths of all new and changed files"""
503
fp = FinalPaths(self)
504
for id_set in (self._new_name, self._new_parent, self._new_contents,
505
self._new_id, self._new_executability):
506
new_ids.update(id_set)
507
new_paths = [(fp.get_path(t), t) for t in new_ids]
511
def tree_kind(self, trans_id):
512
"""Determine the file kind in the working tree.
514
Raises NoSuchFile if the file does not exist
516
path = self._tree_id_paths.get(trans_id)
518
raise NoSuchFile(None)
520
return file_kind(self._tree.abspath(path))
522
if e.errno != errno.ENOENT:
525
raise NoSuchFile(path)
527
def final_kind(self, trans_id):
528
"""Determine the final file kind, after any changes applied.
530
Raises NoSuchFile if the file does not exist/has no contents.
531
(It is conceivable that a path would be created without the
532
corresponding contents insertion command)
534
if trans_id in self._new_contents:
535
return self._new_contents[trans_id]
536
elif trans_id in self._removed_contents:
537
raise NoSuchFile(None)
539
return self.tree_kind(trans_id)
541
def tree_file_id(self, trans_id):
542
"""Determine the file id associated with the trans_id in the tree"""
544
path = self._tree_id_paths[trans_id]
546
# the file is a new, unversioned file, or invalid trans_id
548
# the file is old; the old id is still valid
549
if self._new_root == trans_id:
550
return self._tree.get_root_id()
551
return self._tree.inventory.path2id(path)
553
def final_file_id(self, trans_id):
554
"""Determine the file id after any changes are applied, or None.
556
None indicates that the file will not be versioned after changes are
560
# there is a new id for this file
561
assert self._new_id[trans_id] is not None
562
return self._new_id[trans_id]
564
if trans_id in self._removed_id:
566
return self.tree_file_id(trans_id)
568
def inactive_file_id(self, trans_id):
569
"""Return the inactive file_id associated with a transaction id.
570
That is, the one in the tree or in non_present_ids.
571
The file_id may actually be active, too.
573
file_id = self.tree_file_id(trans_id)
574
if file_id is not None:
576
for key, value in self._non_present_ids.iteritems():
577
if value == trans_id:
580
def final_parent(self, trans_id):
581
"""Determine the parent file_id, after any changes are applied.
583
ROOT_PARENT is returned for the tree root.
586
return self._new_parent[trans_id]
588
return self.get_tree_parent(trans_id)
590
def final_name(self, trans_id):
591
"""Determine the final filename, after all changes are applied."""
593
return self._new_name[trans_id]
596
return os.path.basename(self._tree_id_paths[trans_id])
598
raise NoFinalPath(trans_id, self)
601
"""Return a map of parent: children for known parents.
603
Only new paths and parents of tree files with assigned ids are used.
606
items = list(self._new_parent.iteritems())
607
items.extend((t, self.final_parent(t)) for t in
608
self._tree_id_paths.keys())
609
for trans_id, parent_id in items:
610
if parent_id not in by_parent:
611
by_parent[parent_id] = set()
612
by_parent[parent_id].add(trans_id)
615
def path_changed(self, trans_id):
616
"""Return True if a trans_id's path has changed."""
617
return (trans_id in self._new_name) or (trans_id in self._new_parent)
619
def new_contents(self, trans_id):
620
return (trans_id in self._new_contents)
622
def find_conflicts(self):
623
"""Find any violations of inventory or filesystem invariants"""
624
if self.__done is True:
625
raise ReusingTransform()
627
# ensure all children of all existent parents are known
628
# all children of non-existent parents are known, by definition.
629
self._add_tree_children()
630
by_parent = self.by_parent()
631
conflicts.extend(self._unversioned_parents(by_parent))
632
conflicts.extend(self._parent_loops())
633
conflicts.extend(self._duplicate_entries(by_parent))
634
conflicts.extend(self._duplicate_ids())
635
conflicts.extend(self._parent_type_conflicts(by_parent))
636
conflicts.extend(self._improper_versioning())
637
conflicts.extend(self._executability_conflicts())
638
conflicts.extend(self._overwrite_conflicts())
641
def _add_tree_children(self):
642
"""Add all the children of all active parents to the known paths.
644
Active parents are those which gain children, and those which are
645
removed. This is a necessary first step in detecting conflicts.
647
parents = self.by_parent().keys()
648
parents.extend([t for t in self._removed_contents if
649
self.tree_kind(t) == 'directory'])
650
for trans_id in self._removed_id:
651
file_id = self.tree_file_id(trans_id)
652
if self._tree.inventory[file_id].kind == 'directory':
653
parents.append(trans_id)
655
for parent_id in parents:
656
# ensure that all children are registered with the transaction
657
list(self.iter_tree_children(parent_id))
659
def iter_tree_children(self, parent_id):
660
"""Iterate through the entry's tree children, if any"""
662
path = self._tree_id_paths[parent_id]
666
children = os.listdir(self._tree.abspath(path))
668
if e.errno != errno.ENOENT and e.errno != errno.ESRCH:
672
for child in children:
673
childpath = joinpath(path, child)
674
if self._tree.is_control_filename(childpath):
676
yield self.trans_id_tree_path(childpath)
678
def has_named_child(self, by_parent, parent_id, name):
680
children = by_parent[parent_id]
683
for child in children:
684
if self.final_name(child) == name:
687
path = self._tree_id_paths[parent_id]
690
childpath = joinpath(path, name)
691
child_id = self._tree_path_ids.get(childpath)
693
return lexists(self._tree.abspath(childpath))
695
if self.final_parent(child_id) != parent_id:
697
if child_id in self._removed_contents:
698
# XXX What about dangling file-ids?
703
def _parent_loops(self):
704
"""No entry should be its own ancestor"""
706
for trans_id in self._new_parent:
709
while parent_id is not ROOT_PARENT:
712
parent_id = self.final_parent(parent_id)
715
if parent_id == trans_id:
716
conflicts.append(('parent loop', trans_id))
717
if parent_id in seen:
721
def _unversioned_parents(self, by_parent):
722
"""If parent directories are versioned, children must be versioned."""
724
for parent_id, children in by_parent.iteritems():
725
if parent_id is ROOT_PARENT:
727
if self.final_file_id(parent_id) is not None:
729
for child_id in children:
730
if self.final_file_id(child_id) is not None:
731
conflicts.append(('unversioned parent', parent_id))
735
def _improper_versioning(self):
736
"""Cannot version a file with no contents, or a bad type.
738
However, existing entries with no contents are okay.
741
for trans_id in self._new_id.iterkeys():
743
kind = self.final_kind(trans_id)
745
conflicts.append(('versioning no contents', trans_id))
747
if not InventoryEntry.versionable_kind(kind):
748
conflicts.append(('versioning bad kind', trans_id, kind))
751
def _executability_conflicts(self):
752
"""Check for bad executability changes.
754
Only versioned files may have their executability set, because
755
1. only versioned entries can have executability under windows
756
2. only files can be executable. (The execute bit on a directory
757
does not indicate searchability)
760
for trans_id in self._new_executability:
761
if self.final_file_id(trans_id) is None:
762
conflicts.append(('unversioned executability', trans_id))
765
non_file = self.final_kind(trans_id) != "file"
769
conflicts.append(('non-file executability', trans_id))
772
def _overwrite_conflicts(self):
773
"""Check for overwrites (not permitted on Win32)"""
775
for trans_id in self._new_contents:
777
self.tree_kind(trans_id)
780
if trans_id not in self._removed_contents:
781
conflicts.append(('overwrite', trans_id,
782
self.final_name(trans_id)))
785
def _duplicate_entries(self, by_parent):
786
"""No directory may have two entries with the same name."""
788
if (self._new_name, self._new_parent) == ({}, {}):
790
for children in by_parent.itervalues():
791
name_ids = [(self.final_name(t), t) for t in children]
795
for name, trans_id in name_ids:
797
kind = self.final_kind(trans_id)
800
file_id = self.final_file_id(trans_id)
801
if kind is None and file_id is None:
803
if name == last_name:
804
conflicts.append(('duplicate', last_trans_id, trans_id,
807
last_trans_id = trans_id
810
def _duplicate_ids(self):
811
"""Each inventory id may only be used once"""
813
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
815
active_tree_ids = set((f for f in self._tree.inventory if
816
f not in removed_tree_ids))
817
for trans_id, file_id in self._new_id.iteritems():
818
if file_id in active_tree_ids:
819
old_trans_id = self.trans_id_tree_file_id(file_id)
820
conflicts.append(('duplicate id', old_trans_id, trans_id))
823
def _parent_type_conflicts(self, by_parent):
824
"""parents must have directory 'contents'."""
826
for parent_id, children in by_parent.iteritems():
827
if parent_id is ROOT_PARENT:
829
if not self._any_contents(children):
831
for child in children:
833
self.final_kind(child)
837
kind = self.final_kind(parent_id)
841
conflicts.append(('missing parent', parent_id))
842
elif kind != "directory":
843
conflicts.append(('non-directory parent', parent_id))
846
def _any_contents(self, trans_ids):
847
"""Return true if any of the trans_ids, will have contents."""
848
for trans_id in trans_ids:
850
kind = self.final_kind(trans_id)
856
def apply(self, no_conflicts=False, _mover=None):
857
"""Apply all changes to the inventory and filesystem.
859
If filesystem or inventory conflicts are present, MalformedTransform
862
If apply succeeds, finalize is not necessary.
864
:param no_conflicts: if True, the caller guarantees there are no
865
conflicts, so no check is made.
866
:param _mover: Supply an alternate FileMover, for testing
869
conflicts = self.find_conflicts()
870
if len(conflicts) != 0:
871
raise MalformedTransform(conflicts=conflicts)
872
inv = self._tree.inventory
874
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
881
child_pb.update('Apply phase', 0, 2)
882
self._apply_removals(inv, inventory_delta, mover)
883
child_pb.update('Apply phase', 1, 2)
884
modified_paths = self._apply_insertions(inv, inventory_delta,
890
mover.apply_deletions()
893
self._tree.apply_inventory_delta(inventory_delta)
896
return _TransformResults(modified_paths, self.rename_count)
898
def _limbo_name(self, trans_id):
899
"""Generate the limbo name of a file"""
900
limbo_name = self._limbo_files.get(trans_id)
901
if limbo_name is not None:
903
parent = self._new_parent.get(trans_id)
904
# if the parent directory is already in limbo (e.g. when building a
905
# tree), choose a limbo name inside the parent, to reduce further
907
use_direct_path = False
908
if self._new_contents.get(parent) == 'directory':
909
filename = self._new_name.get(trans_id)
910
if filename is not None:
911
if parent not in self._limbo_children:
912
self._limbo_children[parent] = set()
913
self._limbo_children_names[parent] = {}
914
use_direct_path = True
915
# the direct path can only be used if no other file has
916
# already taken this pathname, i.e. if the name is unused, or
917
# if it is already associated with this trans_id.
918
elif (self._limbo_children_names[parent].get(filename)
919
in (trans_id, None)):
920
use_direct_path = True
922
limbo_name = pathjoin(self._limbo_files[parent], filename)
923
self._limbo_children[parent].add(trans_id)
924
self._limbo_children_names[parent][filename] = trans_id
926
limbo_name = pathjoin(self._limbodir, trans_id)
927
self._needs_rename.add(trans_id)
928
self._limbo_files[trans_id] = limbo_name
931
def _apply_removals(self, inv, inventory_delta, mover):
932
"""Perform tree operations that remove directory/inventory names.
934
That is, delete files that are to be deleted, and put any files that
935
need renaming into limbo. This must be done in strict child-to-parent
938
tree_paths = list(self._tree_path_ids.iteritems())
939
tree_paths.sort(reverse=True)
940
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
942
for num, data in enumerate(tree_paths):
943
path, trans_id = data
944
child_pb.update('removing file', num, len(tree_paths))
945
full_path = self._tree.abspath(path)
946
if trans_id in self._removed_contents:
947
mover.pre_delete(full_path, os.path.join(self._deletiondir,
949
elif trans_id in self._new_name or trans_id in \
952
mover.rename(full_path, self._limbo_name(trans_id))
954
if e.errno != errno.ENOENT:
957
self.rename_count += 1
958
if trans_id in self._removed_id:
959
if trans_id == self._new_root:
960
file_id = self._tree.get_root_id()
962
file_id = self.tree_file_id(trans_id)
963
assert file_id is not None
964
inventory_delta.append((path, None, file_id, None))
968
def _apply_insertions(self, inv, inventory_delta, mover):
969
"""Perform tree operations that insert directory/inventory names.
971
That is, create any files that need to be created, and restore from
972
limbo any files that needed renaming. This must be done in strict
973
parent-to-child order.
975
new_paths = self.new_paths()
977
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
979
for num, (path, trans_id) in enumerate(new_paths):
981
child_pb.update('adding file', num, len(new_paths))
983
kind = self._new_contents[trans_id]
985
kind = contents = None
986
if trans_id in self._new_contents or \
987
self.path_changed(trans_id):
988
full_path = self._tree.abspath(path)
989
if trans_id in self._needs_rename:
991
mover.rename(self._limbo_name(trans_id), full_path)
993
# We may be renaming a dangling inventory id
994
if e.errno != errno.ENOENT:
997
self.rename_count += 1
998
if trans_id in self._new_contents:
999
modified_paths.append(full_path)
1000
del self._new_contents[trans_id]
1002
if trans_id in self._new_id:
1004
kind = file_kind(self._tree.abspath(path))
1005
if trans_id in self._new_reference_revision:
1006
new_entry = inventory.TreeReference(
1007
self._new_id[trans_id],
1008
self._new_name[trans_id],
1009
self.final_file_id(self._new_parent[trans_id]),
1010
None, self._new_reference_revision[trans_id])
1012
new_entry = inventory.make_entry(kind,
1013
self.final_name(trans_id),
1014
self.final_file_id(self.final_parent(trans_id)),
1015
self._new_id[trans_id])
1017
if trans_id in self._new_name or trans_id in\
1018
self._new_parent or\
1019
trans_id in self._new_executability:
1020
file_id = self.final_file_id(trans_id)
1021
if file_id is not None:
1022
entry = inv[file_id]
1023
new_entry = entry.copy()
1025
if trans_id in self._new_name or trans_id in\
1027
if new_entry is not None:
1028
new_entry.name = self.final_name(trans_id)
1029
parent = self.final_parent(trans_id)
1030
parent_id = self.final_file_id(parent)
1031
new_entry.parent_id = parent_id
1033
if trans_id in self._new_executability:
1034
self._set_executability(path, new_entry, trans_id)
1035
if new_entry is not None:
1036
if new_entry.file_id in inv:
1037
old_path = inv.id2path(new_entry.file_id)
1040
inventory_delta.append((old_path, path,
1045
return modified_paths
1047
def _set_executability(self, path, entry, trans_id):
1048
"""Set the executability of versioned files """
1049
new_executability = self._new_executability[trans_id]
1050
entry.executable = new_executability
1051
if supports_executable():
1052
abspath = self._tree.abspath(path)
1053
current_mode = os.stat(abspath).st_mode
1054
if new_executability:
1057
to_mode = current_mode | (0100 & ~umask)
1058
# Enable x-bit for others only if they can read it.
1059
if current_mode & 0004:
1060
to_mode |= 0001 & ~umask
1061
if current_mode & 0040:
1062
to_mode |= 0010 & ~umask
1064
to_mode = current_mode & ~0111
1065
os.chmod(abspath, to_mode)
1067
def _new_entry(self, name, parent_id, file_id):
1068
"""Helper function to create a new filesystem entry."""
1069
trans_id = self.create_path(name, parent_id)
1070
if file_id is not None:
1071
self.version_file(file_id, trans_id)
1074
def new_file(self, name, parent_id, contents, file_id=None,
1076
"""Convenience method to create files.
1078
name is the name of the file to create.
1079
parent_id is the transaction id of the parent directory of the file.
1080
contents is an iterator of bytestrings, which will be used to produce
1082
:param file_id: The inventory ID of the file, if it is to be versioned.
1083
:param executable: Only valid when a file_id has been supplied.
1085
trans_id = self._new_entry(name, parent_id, file_id)
1086
# TODO: rather than scheduling a set_executable call,
1087
# have create_file create the file with the right mode.
1088
self.create_file(contents, trans_id)
1089
if executable is not None:
1090
self.set_executability(executable, trans_id)
1093
def new_directory(self, name, parent_id, file_id=None):
1094
"""Convenience method to create directories.
1096
name is the name of the directory to create.
1097
parent_id is the transaction id of the parent directory of the
1099
file_id is the inventory ID of the directory, if it is to be versioned.
1101
trans_id = self._new_entry(name, parent_id, file_id)
1102
self.create_directory(trans_id)
1105
def new_symlink(self, name, parent_id, target, file_id=None):
1106
"""Convenience method to create symbolic link.
1108
name is the name of the symlink to create.
1109
parent_id is the transaction id of the parent directory of the symlink.
1110
target is a bytestring of the target of the symlink.
1111
file_id is the inventory ID of the file, if it is to be versioned.
1113
trans_id = self._new_entry(name, parent_id, file_id)
1114
self.create_symlink(target, trans_id)
1117
def _affected_ids(self):
1118
"""Return the set of transform ids affected by the transform"""
1119
trans_ids = set(self._removed_id)
1120
trans_ids.update(self._new_id.keys())
1121
trans_ids.update(self._removed_contents)
1122
trans_ids.update(self._new_contents.keys())
1123
trans_ids.update(self._new_executability.keys())
1124
trans_ids.update(self._new_name.keys())
1125
trans_ids.update(self._new_parent.keys())
1128
def _get_file_id_maps(self):
1129
"""Return mapping of file_ids to trans_ids in the to and from states"""
1130
trans_ids = self._affected_ids()
1133
# Build up two dicts: trans_ids associated with file ids in the
1134
# FROM state, vs the TO state.
1135
for trans_id in trans_ids:
1136
from_file_id = self.tree_file_id(trans_id)
1137
if from_file_id is not None:
1138
from_trans_ids[from_file_id] = trans_id
1139
to_file_id = self.final_file_id(trans_id)
1140
if to_file_id is not None:
1141
to_trans_ids[to_file_id] = trans_id
1142
return from_trans_ids, to_trans_ids
1144
def _from_file_data(self, from_trans_id, from_versioned, file_id):
1145
"""Get data about a file in the from (tree) state
1147
Return a (name, parent, kind, executable) tuple
1149
from_path = self._tree_id_paths.get(from_trans_id)
1151
# get data from working tree if versioned
1152
from_entry = self._tree.inventory[file_id]
1153
from_name = from_entry.name
1154
from_parent = from_entry.parent_id
1157
if from_path is None:
1158
# File does not exist in FROM state
1162
# File exists, but is not versioned. Have to use path-
1164
from_name = os.path.basename(from_path)
1165
tree_parent = self.get_tree_parent(from_trans_id)
1166
from_parent = self.tree_file_id(tree_parent)
1167
if from_path is not None:
1168
from_kind, from_executable, from_stats = \
1169
self._tree._comparison_data(from_entry, from_path)
1172
from_executable = False
1173
return from_name, from_parent, from_kind, from_executable
1175
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
1176
"""Get data about a file in the to (target) state
1178
Return a (name, parent, kind, executable) tuple
1180
to_name = self.final_name(to_trans_id)
1182
to_kind = self.final_kind(to_trans_id)
1185
to_parent = self.final_file_id(self.final_parent(to_trans_id))
1186
if to_trans_id in self._new_executability:
1187
to_executable = self._new_executability[to_trans_id]
1188
elif to_trans_id == from_trans_id:
1189
to_executable = from_executable
1191
to_executable = False
1192
return to_name, to_parent, to_kind, to_executable
1194
def _iter_changes(self):
1195
"""Produce output in the same format as Tree._iter_changes.
1197
Will produce nonsensical results if invoked while inventory/filesystem
1198
conflicts (as reported by TreeTransform.find_conflicts()) are present.
1200
This reads the Transform, but only reproduces changes involving a
1201
file_id. Files that are not versioned in either of the FROM or TO
1202
states are not reflected.
1204
final_paths = FinalPaths(self)
1205
from_trans_ids, to_trans_ids = self._get_file_id_maps()
1207
# Now iterate through all active file_ids
1208
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
1210
from_trans_id = from_trans_ids.get(file_id)
1211
# find file ids, and determine versioning state
1212
if from_trans_id is None:
1213
from_versioned = False
1214
from_trans_id = to_trans_ids[file_id]
1216
from_versioned = True
1217
to_trans_id = to_trans_ids.get(file_id)
1218
if to_trans_id is None:
1219
to_versioned = False
1220
to_trans_id = from_trans_id
1224
from_name, from_parent, from_kind, from_executable = \
1225
self._from_file_data(from_trans_id, from_versioned, file_id)
1227
to_name, to_parent, to_kind, to_executable = \
1228
self._to_file_data(to_trans_id, from_trans_id, from_executable)
1230
if not from_versioned:
1233
from_path = self._tree_id_paths.get(from_trans_id)
1234
if not to_versioned:
1237
to_path = final_paths.get_path(to_trans_id)
1238
if from_kind != to_kind:
1240
elif to_kind in ('file', 'symlink') and (
1241
to_trans_id != from_trans_id or
1242
to_trans_id in self._new_contents):
1244
if (not modified and from_versioned == to_versioned and
1245
from_parent==to_parent and from_name == to_name and
1246
from_executable == to_executable):
1248
results.append((file_id, (from_path, to_path), modified,
1249
(from_versioned, to_versioned),
1250
(from_parent, to_parent),
1251
(from_name, to_name),
1252
(from_kind, to_kind),
1253
(from_executable, to_executable)))
1254
return iter(sorted(results, key=lambda x:x[1]))
1257
def joinpath(parent, child):
1258
"""Join tree-relative paths, handling the tree root specially"""
1259
if parent is None or parent == "":
1262
return pathjoin(parent, child)
1265
class FinalPaths(object):
1266
"""Make path calculation cheap by memoizing paths.
1268
The underlying tree must not be manipulated between calls, or else
1269
the results will likely be incorrect.
1271
def __init__(self, transform):
1272
object.__init__(self)
1273
self._known_paths = {}
1274
self.transform = transform
1276
def _determine_path(self, trans_id):
1277
if trans_id == self.transform.root:
1279
name = self.transform.final_name(trans_id)
1280
parent_id = self.transform.final_parent(trans_id)
1281
if parent_id == self.transform.root:
1284
return pathjoin(self.get_path(parent_id), name)
1286
def get_path(self, trans_id):
1287
"""Find the final path associated with a trans_id"""
1288
if trans_id not in self._known_paths:
1289
self._known_paths[trans_id] = self._determine_path(trans_id)
1290
return self._known_paths[trans_id]
1292
def topology_sorted_ids(tree):
1293
"""Determine the topological order of the ids in a tree"""
1294
file_ids = list(tree)
1295
file_ids.sort(key=tree.id2path)
1299
def build_tree(tree, wt):
1300
"""Create working tree for a branch, using a TreeTransform.
1302
This function should be used on empty trees, having a tree root at most.
1303
(see merge and revert functionality for working with existing trees)
1305
Existing files are handled like so:
1307
- Existing bzrdirs take precedence over creating new items. They are
1308
created as '%s.diverted' % name.
1309
- Otherwise, if the content on disk matches the content we are building,
1310
it is silently replaced.
1311
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
1313
wt.lock_tree_write()
1317
return _build_tree(tree, wt)
1323
def _build_tree(tree, wt):
1324
"""See build_tree."""
1325
if len(wt.inventory) > 1: # more than just a root
1326
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
1328
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1329
pp = ProgressPhase("Build phase", 2, top_pb)
1330
if tree.inventory.root is not None:
1331
# This is kind of a hack: we should be altering the root
1332
# as part of the regular tree shape diff logic.
1333
# The conditional test here is to avoid doing an
1334
# expensive operation (flush) every time the root id
1335
# is set within the tree, nor setting the root and thus
1336
# marking the tree as dirty, because we use two different
1337
# idioms here: tree interfaces and inventory interfaces.
1338
if wt.get_root_id() != tree.get_root_id():
1339
wt.set_root_id(tree.get_root_id())
1341
tt = TreeTransform(wt)
1345
file_trans_id[wt.get_root_id()] = \
1346
tt.trans_id_tree_file_id(wt.get_root_id())
1347
pb = bzrlib.ui.ui_factory.nested_progress_bar()
1349
deferred_contents = []
1350
for num, (tree_path, entry) in \
1351
enumerate(tree.inventory.iter_entries_by_dir()):
1352
pb.update("Building tree", num - len(deferred_contents),
1353
len(tree.inventory))
1354
if entry.parent_id is None:
1357
file_id = entry.file_id
1358
target_path = wt.abspath(tree_path)
1360
kind = file_kind(target_path)
1364
if kind == "directory":
1366
bzrdir.BzrDir.open(target_path)
1367
except errors.NotBranchError:
1371
if (file_id not in divert and
1372
_content_match(tree, entry, file_id, kind,
1374
tt.delete_contents(tt.trans_id_tree_path(tree_path))
1375
if kind == 'directory':
1377
if entry.parent_id not in file_trans_id:
1378
raise AssertionError(
1379
'entry %s parent id %r is not in file_trans_id %r'
1380
% (entry, entry.parent_id, file_trans_id))
1381
parent_id = file_trans_id[entry.parent_id]
1382
if entry.kind == 'file':
1383
# We *almost* replicate new_by_entry, so that we can defer
1384
# getting the file text, and get them all at once.
1385
trans_id = tt.create_path(entry.name, parent_id)
1386
file_trans_id[file_id] = trans_id
1387
tt.version_file(entry.file_id, trans_id)
1388
executable = tree.is_executable(entry.file_id, tree_path)
1389
if executable is not None:
1390
tt.set_executability(executable, trans_id)
1391
deferred_contents.append((entry.file_id, trans_id))
1393
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
1396
new_trans_id = file_trans_id[file_id]
1397
old_parent = tt.trans_id_tree_path(tree_path)
1398
_reparent_children(tt, old_parent, new_trans_id)
1399
for num, (trans_id, bytes) in enumerate(
1400
tree.iter_files_bytes(deferred_contents)):
1401
tt.create_file(bytes, trans_id)
1402
pb.update('Adding file contents',
1403
(num + len(tree.inventory) - len(deferred_contents)),
1404
len(tree.inventory))
1408
divert_trans = set(file_trans_id[f] for f in divert)
1409
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
1410
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
1411
conflicts = cook_conflicts(raw_conflicts, tt)
1412
for conflict in conflicts:
1415
wt.add_conflicts(conflicts)
1416
except errors.UnsupportedOperation:
1425
def _reparent_children(tt, old_parent, new_parent):
1426
for child in tt.iter_tree_children(old_parent):
1427
tt.adjust_path(tt.final_name(child), new_parent, child)
1430
def _content_match(tree, entry, file_id, kind, target_path):
1431
if entry.kind != kind:
1433
if entry.kind == "directory":
1435
if entry.kind == "file":
1436
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
1438
elif entry.kind == "symlink":
1439
if tree.get_symlink_target(file_id) == os.readlink(target_path):
1444
def resolve_checkout(tt, conflicts, divert):
1445
new_conflicts = set()
1446
for c_type, conflict in ((c[0], c) for c in conflicts):
1447
# Anything but a 'duplicate' would indicate programmer error
1448
assert c_type == 'duplicate', c_type
1449
# Now figure out which is new and which is old
1450
if tt.new_contents(conflict[1]):
1451
new_file = conflict[1]
1452
old_file = conflict[2]
1454
new_file = conflict[2]
1455
old_file = conflict[1]
1457
# We should only get here if the conflict wasn't completely
1459
final_parent = tt.final_parent(old_file)
1460
if new_file in divert:
1461
new_name = tt.final_name(old_file)+'.diverted'
1462
tt.adjust_path(new_name, final_parent, new_file)
1463
new_conflicts.add((c_type, 'Diverted to',
1464
new_file, old_file))
1466
new_name = tt.final_name(old_file)+'.moved'
1467
tt.adjust_path(new_name, final_parent, old_file)
1468
new_conflicts.add((c_type, 'Moved existing file to',
1469
old_file, new_file))
1470
return new_conflicts
1473
def new_by_entry(tt, entry, parent_id, tree):
1474
"""Create a new file according to its inventory entry"""
1478
contents = tree.get_file(entry.file_id).readlines()
1479
executable = tree.is_executable(entry.file_id)
1480
return tt.new_file(name, parent_id, contents, entry.file_id,
1482
elif kind in ('directory', 'tree-reference'):
1483
trans_id = tt.new_directory(name, parent_id, entry.file_id)
1484
if kind == 'tree-reference':
1485
tt.set_tree_reference(entry.reference_revision, trans_id)
1487
elif kind == 'symlink':
1488
target = tree.get_symlink_target(entry.file_id)
1489
return tt.new_symlink(name, parent_id, target, entry.file_id)
1491
raise errors.BadFileKindError(name, kind)
1493
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
1494
"""Create new file contents according to an inventory entry."""
1495
if entry.kind == "file":
1497
lines = tree.get_file(entry.file_id).readlines()
1498
tt.create_file(lines, trans_id, mode_id=mode_id)
1499
elif entry.kind == "symlink":
1500
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
1501
elif entry.kind == "directory":
1502
tt.create_directory(trans_id)
1504
def create_entry_executability(tt, entry, trans_id):
1505
"""Set the executability of a trans_id according to an inventory entry"""
1506
if entry.kind == "file":
1507
tt.set_executability(entry.executable, trans_id)
1510
@deprecated_function(zero_fifteen)
1511
def find_interesting(working_tree, target_tree, filenames):
1512
"""Find the ids corresponding to specified filenames.
1514
Deprecated: Please use tree1.paths2ids(filenames, [tree2]).
1516
working_tree.lock_read()
1518
target_tree.lock_read()
1520
return working_tree.paths2ids(filenames, [target_tree])
1522
target_tree.unlock()
1524
working_tree.unlock()
1527
@deprecated_function(zero_ninety)
1528
def change_entry(tt, file_id, working_tree, target_tree,
1529
trans_id_file_id, backups, trans_id, by_parent):
1530
"""Replace a file_id's contents with those from a target tree."""
1531
if file_id is None and target_tree is None:
1532
# skip the logic altogether in the deprecation test
1534
e_trans_id = trans_id_file_id(file_id)
1535
entry = target_tree.inventory[file_id]
1536
has_contents, contents_mod, meta_mod, = _entry_changes(file_id, entry,
1539
mode_id = e_trans_id
1542
tt.delete_contents(e_trans_id)
1544
parent_trans_id = trans_id_file_id(entry.parent_id)
1545
backup_name = get_backup_name(entry, by_parent,
1546
parent_trans_id, tt)
1547
tt.adjust_path(backup_name, parent_trans_id, e_trans_id)
1548
tt.unversion_file(e_trans_id)
1549
e_trans_id = tt.create_path(entry.name, parent_trans_id)
1550
tt.version_file(file_id, e_trans_id)
1551
trans_id[file_id] = e_trans_id
1552
create_by_entry(tt, entry, target_tree, e_trans_id, mode_id=mode_id)
1553
create_entry_executability(tt, entry, e_trans_id)
1556
tt.set_executability(entry.executable, e_trans_id)
1557
if tt.final_name(e_trans_id) != entry.name:
1560
parent_id = tt.final_parent(e_trans_id)
1561
parent_file_id = tt.final_file_id(parent_id)
1562
if parent_file_id != entry.parent_id:
1567
parent_trans_id = trans_id_file_id(entry.parent_id)
1568
tt.adjust_path(entry.name, parent_trans_id, e_trans_id)
1571
def get_backup_name(entry, by_parent, parent_trans_id, tt):
1572
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
1575
def _get_backup_name(name, by_parent, parent_trans_id, tt):
1576
"""Produce a backup-style name that appears to be available"""
1580
yield "%s.~%d~" % (name, counter)
1582
for new_name in name_gen():
1583
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
1587
def _entry_changes(file_id, entry, working_tree):
1588
"""Determine in which ways the inventory entry has changed.
1590
Returns booleans: has_contents, content_mod, meta_mod
1591
has_contents means there are currently contents, but they differ
1592
contents_mod means contents need to be modified
1593
meta_mod means the metadata needs to be modified
1595
cur_entry = working_tree.inventory[file_id]
1597
working_kind = working_tree.kind(file_id)
1600
has_contents = False
1603
if has_contents is True:
1604
if entry.kind != working_kind:
1605
contents_mod, meta_mod = True, False
1607
cur_entry._read_tree_state(working_tree.id2path(file_id),
1609
contents_mod, meta_mod = entry.detect_changes(cur_entry)
1610
cur_entry._forget_tree_state()
1611
return has_contents, contents_mod, meta_mod
1614
def revert(working_tree, target_tree, filenames, backups=False,
1615
pb=DummyProgress(), change_reporter=None):
1616
"""Revert a working tree's contents to those of a target tree."""
1617
target_tree.lock_read()
1618
tt = TreeTransform(working_tree, pb)
1620
pp = ProgressPhase("Revert phase", 3, pb)
1622
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1624
merge_modified = _alter_files(working_tree, target_tree, tt,
1625
child_pb, filenames, backups)
1629
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1631
raw_conflicts = resolve_conflicts(tt, child_pb)
1634
conflicts = cook_conflicts(raw_conflicts, tt)
1636
change_reporter = delta._ChangeReporter(
1637
unversioned_filter=working_tree.is_ignored)
1638
delta.report_changes(tt._iter_changes(), change_reporter)
1639
for conflict in conflicts:
1643
working_tree.set_merge_modified(merge_modified)
1645
target_tree.unlock()
1651
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
1653
merge_modified = working_tree.merge_modified()
1654
change_list = target_tree._iter_changes(working_tree,
1655
specific_files=specific_files, pb=pb)
1656
if target_tree.inventory.root is None:
1663
for id_num, (file_id, path, changed_content, versioned, parent, name,
1664
kind, executable) in enumerate(change_list):
1665
if skip_root and file_id[0] is not None and parent[0] is None:
1667
trans_id = tt.trans_id_file_id(file_id)
1670
keep_content = False
1671
if kind[0] == 'file' and (backups or kind[1] is None):
1672
wt_sha1 = working_tree.get_file_sha1(file_id)
1673
if merge_modified.get(file_id) != wt_sha1:
1674
# acquire the basis tree lazily to prevent the
1675
# expense of accessing it when it's not needed ?
1676
# (Guessing, RBC, 200702)
1677
if basis_tree is None:
1678
basis_tree = working_tree.basis_tree()
1679
basis_tree.lock_read()
1680
if file_id in basis_tree:
1681
if wt_sha1 != basis_tree.get_file_sha1(file_id):
1683
elif kind[1] is None and not versioned[1]:
1685
if kind[0] is not None:
1686
if not keep_content:
1687
tt.delete_contents(trans_id)
1688
elif kind[1] is not None:
1689
parent_trans_id = tt.trans_id_file_id(parent[0])
1690
by_parent = tt.by_parent()
1691
backup_name = _get_backup_name(name[0], by_parent,
1692
parent_trans_id, tt)
1693
tt.adjust_path(backup_name, parent_trans_id, trans_id)
1694
new_trans_id = tt.create_path(name[0], parent_trans_id)
1695
if versioned == (True, True):
1696
tt.unversion_file(trans_id)
1697
tt.version_file(file_id, new_trans_id)
1698
# New contents should have the same unix perms as old
1701
trans_id = new_trans_id
1702
if kind[1] == 'directory':
1703
tt.create_directory(trans_id)
1704
elif kind[1] == 'symlink':
1705
tt.create_symlink(target_tree.get_symlink_target(file_id),
1707
elif kind[1] == 'file':
1708
deferred_files.append((file_id, (trans_id, mode_id)))
1709
if basis_tree is None:
1710
basis_tree = working_tree.basis_tree()
1711
basis_tree.lock_read()
1712
new_sha1 = target_tree.get_file_sha1(file_id)
1713
if (file_id in basis_tree and new_sha1 ==
1714
basis_tree.get_file_sha1(file_id)):
1715
if file_id in merge_modified:
1716
del merge_modified[file_id]
1718
merge_modified[file_id] = new_sha1
1720
# preserve the execute bit when backing up
1721
if keep_content and executable[0] == executable[1]:
1722
tt.set_executability(executable[1], trans_id)
1724
assert kind[1] is None
1725
if versioned == (False, True):
1726
tt.version_file(file_id, trans_id)
1727
if versioned == (True, False):
1728
tt.unversion_file(trans_id)
1729
if (name[1] is not None and
1730
(name[0] != name[1] or parent[0] != parent[1])):
1732
name[1], tt.trans_id_file_id(parent[1]), trans_id)
1733
if executable[0] != executable[1] and kind[1] == "file":
1734
tt.set_executability(executable[1], trans_id)
1735
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
1737
tt.create_file(bytes, trans_id, mode_id)
1739
if basis_tree is not None:
1741
return merge_modified
1744
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
1745
"""Make many conflict-resolution attempts, but die if they fail"""
1746
if pass_func is None:
1747
pass_func = conflict_pass
1748
new_conflicts = set()
1751
pb.update('Resolution pass', n+1, 10)
1752
conflicts = tt.find_conflicts()
1753
if len(conflicts) == 0:
1754
return new_conflicts
1755
new_conflicts.update(pass_func(tt, conflicts))
1756
raise MalformedTransform(conflicts=conflicts)
1761
def conflict_pass(tt, conflicts, path_tree=None):
1762
"""Resolve some classes of conflicts.
1764
:param tt: The transform to resolve conflicts in
1765
:param conflicts: The conflicts to resolve
1766
:param path_tree: A Tree to get supplemental paths from
1768
new_conflicts = set()
1769
for c_type, conflict in ((c[0], c) for c in conflicts):
1770
if c_type == 'duplicate id':
1771
tt.unversion_file(conflict[1])
1772
new_conflicts.add((c_type, 'Unversioned existing file',
1773
conflict[1], conflict[2], ))
1774
elif c_type == 'duplicate':
1775
# files that were renamed take precedence
1776
new_name = tt.final_name(conflict[1])+'.moved'
1777
final_parent = tt.final_parent(conflict[1])
1778
if tt.path_changed(conflict[1]):
1779
tt.adjust_path(new_name, final_parent, conflict[2])
1780
new_conflicts.add((c_type, 'Moved existing file to',
1781
conflict[2], conflict[1]))
1783
tt.adjust_path(new_name, final_parent, conflict[1])
1784
new_conflicts.add((c_type, 'Moved existing file to',
1785
conflict[1], conflict[2]))
1786
elif c_type == 'parent loop':
1787
# break the loop by undoing one of the ops that caused the loop
1789
while not tt.path_changed(cur):
1790
cur = tt.final_parent(cur)
1791
new_conflicts.add((c_type, 'Cancelled move', cur,
1792
tt.final_parent(cur),))
1793
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
1795
elif c_type == 'missing parent':
1796
trans_id = conflict[1]
1798
tt.cancel_deletion(trans_id)
1799
new_conflicts.add(('deleting parent', 'Not deleting',
1802
tt.create_directory(trans_id)
1803
new_conflicts.add((c_type, 'Created directory', trans_id))
1805
tt.final_name(trans_id)
1807
file_id = tt.final_file_id(trans_id)
1808
entry = path_tree.inventory[file_id]
1809
parent_trans_id = tt.trans_id_file_id(entry.parent_id)
1810
tt.adjust_path(entry.name, parent_trans_id, trans_id)
1811
elif c_type == 'unversioned parent':
1812
tt.version_file(tt.inactive_file_id(conflict[1]), conflict[1])
1813
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
1814
return new_conflicts
1817
def cook_conflicts(raw_conflicts, tt):
1818
"""Generate a list of cooked conflicts, sorted by file path"""
1819
from bzrlib.conflicts import Conflict
1820
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
1821
return sorted(conflict_iter, key=Conflict.sort_key)
1824
def iter_cook_conflicts(raw_conflicts, tt):
1825
from bzrlib.conflicts import Conflict
1827
for conflict in raw_conflicts:
1828
c_type = conflict[0]
1829
action = conflict[1]
1830
modified_path = fp.get_path(conflict[2])
1831
modified_id = tt.final_file_id(conflict[2])
1832
if len(conflict) == 3:
1833
yield Conflict.factory(c_type, action=action, path=modified_path,
1834
file_id=modified_id)
1837
conflicting_path = fp.get_path(conflict[3])
1838
conflicting_id = tt.final_file_id(conflict[3])
1839
yield Conflict.factory(c_type, action=action, path=modified_path,
1840
file_id=modified_id,
1841
conflict_path=conflicting_path,
1842
conflict_file_id=conflicting_id)
1845
class _FileMover(object):
1846
"""Moves and deletes files for TreeTransform, tracking operations"""
1849
self.past_renames = []
1850
self.pending_deletions = []
1852
def rename(self, from_, to):
1853
"""Rename a file from one path to another. Functions like os.rename"""
1854
os.rename(from_, to)
1855
self.past_renames.append((from_, to))
1857
def pre_delete(self, from_, to):
1858
"""Rename a file out of the way and mark it for deletion.
1860
Unlike os.unlink, this works equally well for files and directories.
1861
:param from_: The current file path
1862
:param to: A temporary path for the file
1864
self.rename(from_, to)
1865
self.pending_deletions.append(to)
1868
"""Reverse all renames that have been performed"""
1869
for from_, to in reversed(self.past_renames):
1870
os.rename(to, from_)
1871
# after rollback, don't reuse _FileMover
1873
pending_deletions = None
1875
def apply_deletions(self):
1876
"""Apply all marked deletions"""
1877
for path in self.pending_deletions:
1879
# after apply_deletions, don't reuse _FileMover
1881
pending_deletions = None