1
# Copyright (C) 2006-2011 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
21
from stat import S_ISREG, S_IEXEC
31
lazy_import.lazy_import(globals(), """
42
revision as _mod_revision,
46
from bzrlib.i18n import gettext
48
from bzrlib.errors import (DuplicateKey, MalformedTransform,
49
ReusingTransform, CantMoveRoot,
50
ExistingLimbo, ImmortalLimbo, NoFinalPath,
52
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
53
from bzrlib.osutils import (
62
from bzrlib.progress import ProgressPhase
63
from bzrlib.symbol_versioning import (
70
ROOT_PARENT = "root-parent"
72
def unique_add(map, key, value):
74
raise DuplicateKey(key=key)
79
class _TransformResults(object):
80
def __init__(self, modified_paths, rename_count):
82
self.modified_paths = modified_paths
83
self.rename_count = rename_count
86
class TreeTransformBase(object):
87
"""The base class for TreeTransform and its kin."""
89
def __init__(self, tree, pb=None,
93
:param tree: The tree that will be transformed, but not necessarily
96
:param case_sensitive: If True, the target of the transform is
97
case sensitive, not just case preserving.
102
# mapping of trans_id -> new basename
104
# mapping of trans_id -> new parent trans_id
105
self._new_parent = {}
106
# mapping of trans_id with new contents -> new file_kind
107
self._new_contents = {}
108
# mapping of trans_id => (sha1 of content, stat_value)
109
self._observed_sha1s = {}
110
# Set of trans_ids whose contents will be removed
111
self._removed_contents = set()
112
# Mapping of trans_id -> new execute-bit value
113
self._new_executability = {}
114
# Mapping of trans_id -> new tree-reference value
115
self._new_reference_revision = {}
116
# Mapping of trans_id -> new file_id
118
# Mapping of old file-id -> trans_id
119
self._non_present_ids = {}
120
# Mapping of new file_id -> trans_id
122
# Set of trans_ids that will be removed
123
self._removed_id = set()
124
# Mapping of path in old tree -> trans_id
125
self._tree_path_ids = {}
126
# Mapping trans_id -> path in old tree
127
self._tree_id_paths = {}
128
# The trans_id that will be used as the tree root
129
root_id = tree.get_root_id()
130
if root_id is not None:
131
self._new_root = self.trans_id_tree_file_id(root_id)
133
self._new_root = None
134
# Indicator of whether the transform has been applied
138
# Whether the target is case sensitive
139
self._case_sensitive_target = case_sensitive
140
# A counter of how many files have been renamed
141
self.rename_count = 0
144
"""Support Context Manager API."""
147
def __exit__(self, exc_type, exc_val, exc_tb):
148
"""Support Context Manager API."""
152
"""Release the working tree lock, if held.
154
This is required if apply has not been invoked, but can be invoked
157
if self._tree is None:
162
def __get_root(self):
163
return self._new_root
165
root = property(__get_root)
167
def _assign_id(self):
168
"""Produce a new tranform id"""
169
new_id = "new-%s" % self._id_number
173
def create_path(self, name, parent):
174
"""Assign a transaction id to a new path"""
175
trans_id = self._assign_id()
176
unique_add(self._new_name, trans_id, name)
177
unique_add(self._new_parent, trans_id, parent)
180
def adjust_path(self, name, parent, trans_id):
181
"""Change the path that is assigned to a transaction id."""
183
raise ValueError("Parent trans-id may not be None")
184
if trans_id == self._new_root:
186
self._new_name[trans_id] = name
187
self._new_parent[trans_id] = parent
189
def adjust_root_path(self, name, parent):
190
"""Emulate moving the root by moving all children, instead.
192
We do this by undoing the association of root's transaction id with the
193
current tree. This allows us to create a new directory with that
194
transaction id. We unversion the root directory and version the
195
physically new directory, and hope someone versions the tree root
198
old_root = self._new_root
199
old_root_file_id = self.final_file_id(old_root)
200
# force moving all children of root
201
for child_id in self.iter_tree_children(old_root):
202
if child_id != parent:
203
self.adjust_path(self.final_name(child_id),
204
self.final_parent(child_id), child_id)
205
file_id = self.final_file_id(child_id)
206
if file_id is not None:
207
self.unversion_file(child_id)
208
self.version_file(file_id, child_id)
210
# the physical root needs a new transaction id
211
self._tree_path_ids.pop("")
212
self._tree_id_paths.pop(old_root)
213
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
214
if parent == old_root:
215
parent = self._new_root
216
self.adjust_path(name, parent, old_root)
217
self.create_directory(old_root)
218
self.version_file(old_root_file_id, old_root)
219
self.unversion_file(self._new_root)
221
def fixup_new_roots(self):
222
"""Reinterpret requests to change the root directory
224
Instead of creating a root directory, or moving an existing directory,
225
all the attributes and children of the new root are applied to the
226
existing root directory.
228
This means that the old root trans-id becomes obsolete, so it is
229
recommended only to invoke this after the root trans-id has become
233
new_roots = [k for k, v in self._new_parent.iteritems() if v is
235
if len(new_roots) < 1:
237
if len(new_roots) != 1:
238
raise ValueError('A tree cannot have two roots!')
239
if self._new_root is None:
240
self._new_root = new_roots[0]
242
old_new_root = new_roots[0]
243
# unversion the new root's directory.
244
if self.final_kind(self._new_root) is None:
245
file_id = self.final_file_id(old_new_root)
247
file_id = self.final_file_id(self._new_root)
248
if old_new_root in self._new_id:
249
self.cancel_versioning(old_new_root)
251
self.unversion_file(old_new_root)
252
# if, at this stage, root still has an old file_id, zap it so we can
253
# stick a new one in.
254
if (self.tree_file_id(self._new_root) is not None and
255
self._new_root not in self._removed_id):
256
self.unversion_file(self._new_root)
257
if file_id is not None:
258
self.version_file(file_id, self._new_root)
260
# Now move children of new root into old root directory.
261
# Ensure all children are registered with the transaction, but don't
262
# use directly-- some tree children have new parents
263
list(self.iter_tree_children(old_new_root))
264
# Move all children of new root into old root directory.
265
for child in self.by_parent().get(old_new_root, []):
266
self.adjust_path(self.final_name(child), self._new_root, child)
268
# Ensure old_new_root has no directory.
269
if old_new_root in self._new_contents:
270
self.cancel_creation(old_new_root)
272
self.delete_contents(old_new_root)
274
# prevent deletion of root directory.
275
if self._new_root in self._removed_contents:
276
self.cancel_deletion(self._new_root)
278
# destroy path info for old_new_root.
279
del self._new_parent[old_new_root]
280
del self._new_name[old_new_root]
282
def trans_id_tree_file_id(self, inventory_id):
283
"""Determine the transaction id of a working tree file.
285
This reflects only files that already exist, not ones that will be
286
added by transactions.
288
if inventory_id is None:
289
raise ValueError('None is not a valid file id')
290
path = self._tree.id2path(inventory_id)
291
return self.trans_id_tree_path(path)
293
def trans_id_file_id(self, file_id):
294
"""Determine or set the transaction id associated with a file ID.
295
A new id is only created for file_ids that were never present. If
296
a transaction has been unversioned, it is deliberately still returned.
297
(this will likely lead to an unversioned parent conflict.)
300
raise ValueError('None is not a valid file id')
301
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
302
return self._r_new_id[file_id]
305
self._tree.iter_entries_by_dir([file_id]).next()
306
except StopIteration:
307
if file_id in self._non_present_ids:
308
return self._non_present_ids[file_id]
310
trans_id = self._assign_id()
311
self._non_present_ids[file_id] = trans_id
314
return self.trans_id_tree_file_id(file_id)
316
def trans_id_tree_path(self, path):
317
"""Determine (and maybe set) the transaction ID for a tree path."""
318
path = self.canonical_path(path)
319
if path not in self._tree_path_ids:
320
self._tree_path_ids[path] = self._assign_id()
321
self._tree_id_paths[self._tree_path_ids[path]] = path
322
return self._tree_path_ids[path]
324
def get_tree_parent(self, trans_id):
325
"""Determine id of the parent in the tree."""
326
path = self._tree_id_paths[trans_id]
329
return self.trans_id_tree_path(os.path.dirname(path))
331
def delete_contents(self, trans_id):
332
"""Schedule the contents of a path entry for deletion"""
333
kind = self.tree_kind(trans_id)
335
self._removed_contents.add(trans_id)
337
def cancel_deletion(self, trans_id):
338
"""Cancel a scheduled deletion"""
339
self._removed_contents.remove(trans_id)
341
def unversion_file(self, trans_id):
342
"""Schedule a path entry to become unversioned"""
343
self._removed_id.add(trans_id)
345
def delete_versioned(self, trans_id):
346
"""Delete and unversion a versioned file"""
347
self.delete_contents(trans_id)
348
self.unversion_file(trans_id)
350
def set_executability(self, executability, trans_id):
351
"""Schedule setting of the 'execute' bit
352
To unschedule, set to None
354
if executability is None:
355
del self._new_executability[trans_id]
357
unique_add(self._new_executability, trans_id, executability)
359
def set_tree_reference(self, revision_id, trans_id):
360
"""Set the reference associated with a directory"""
361
unique_add(self._new_reference_revision, trans_id, revision_id)
363
def version_file(self, file_id, trans_id):
364
"""Schedule a file to become versioned."""
367
unique_add(self._new_id, trans_id, file_id)
368
unique_add(self._r_new_id, file_id, trans_id)
370
def cancel_versioning(self, trans_id):
371
"""Undo a previous versioning of a file"""
372
file_id = self._new_id[trans_id]
373
del self._new_id[trans_id]
374
del self._r_new_id[file_id]
376
def new_paths(self, filesystem_only=False):
377
"""Determine the paths of all new and changed files.
379
:param filesystem_only: if True, only calculate values for files
380
that require renames or execute bit changes.
384
stale_ids = self._needs_rename.difference(self._new_name)
385
stale_ids.difference_update(self._new_parent)
386
stale_ids.difference_update(self._new_contents)
387
stale_ids.difference_update(self._new_id)
388
needs_rename = self._needs_rename.difference(stale_ids)
389
id_sets = (needs_rename, self._new_executability)
391
id_sets = (self._new_name, self._new_parent, self._new_contents,
392
self._new_id, self._new_executability)
393
for id_set in id_sets:
394
new_ids.update(id_set)
395
return sorted(FinalPaths(self).get_paths(new_ids))
397
def _inventory_altered(self):
398
"""Determine which trans_ids need new Inventory entries.
400
An new entry is needed when anything that would be reflected by an
401
inventory entry changes, including file name, file_id, parent file_id,
402
file kind, and the execute bit.
404
Some care is taken to return entries with real changes, not cases
405
where the value is deleted and then restored to its original value,
406
but some actually unchanged values may be returned.
408
:returns: A list of (path, trans_id) for all items requiring an
409
inventory change. Ordered by path.
412
# Find entries whose file_ids are new (or changed).
413
new_file_id = set(t for t in self._new_id
414
if self._new_id[t] != self.tree_file_id(t))
415
for id_set in [self._new_name, self._new_parent, new_file_id,
416
self._new_executability]:
417
changed_ids.update(id_set)
418
# removing implies a kind change
419
changed_kind = set(self._removed_contents)
421
changed_kind.intersection_update(self._new_contents)
422
# Ignore entries that are already known to have changed.
423
changed_kind.difference_update(changed_ids)
424
# to keep only the truly changed ones
425
changed_kind = (t for t in changed_kind
426
if self.tree_kind(t) != self.final_kind(t))
427
# all kind changes will alter the inventory
428
changed_ids.update(changed_kind)
429
# To find entries with changed parent_ids, find parents which existed,
430
# but changed file_id.
431
changed_file_id = set(t for t in new_file_id if t in self._removed_id)
432
# Now add all their children to the set.
433
for parent_trans_id in new_file_id:
434
changed_ids.update(self.iter_tree_children(parent_trans_id))
435
return sorted(FinalPaths(self).get_paths(changed_ids))
437
def final_kind(self, trans_id):
438
"""Determine the final file kind, after any changes applied.
440
:return: None if the file does not exist/has no contents. (It is
441
conceivable that a path would be created without the corresponding
442
contents insertion command)
444
if trans_id in self._new_contents:
445
return self._new_contents[trans_id]
446
elif trans_id in self._removed_contents:
449
return self.tree_kind(trans_id)
451
def tree_file_id(self, trans_id):
452
"""Determine the file id associated with the trans_id in the tree"""
454
path = self._tree_id_paths[trans_id]
456
# the file is a new, unversioned file, or invalid trans_id
458
# the file is old; the old id is still valid
459
if self._new_root == trans_id:
460
return self._tree.get_root_id()
461
return self._tree.path2id(path)
463
def final_file_id(self, trans_id):
464
"""Determine the file id after any changes are applied, or None.
466
None indicates that the file will not be versioned after changes are
470
return self._new_id[trans_id]
472
if trans_id in self._removed_id:
474
return self.tree_file_id(trans_id)
476
def inactive_file_id(self, trans_id):
477
"""Return the inactive file_id associated with a transaction id.
478
That is, the one in the tree or in non_present_ids.
479
The file_id may actually be active, too.
481
file_id = self.tree_file_id(trans_id)
482
if file_id is not None:
484
for key, value in self._non_present_ids.iteritems():
485
if value == trans_id:
488
def final_parent(self, trans_id):
489
"""Determine the parent file_id, after any changes are applied.
491
ROOT_PARENT is returned for the tree root.
494
return self._new_parent[trans_id]
496
return self.get_tree_parent(trans_id)
498
def final_name(self, trans_id):
499
"""Determine the final filename, after all changes are applied."""
501
return self._new_name[trans_id]
504
return os.path.basename(self._tree_id_paths[trans_id])
506
raise NoFinalPath(trans_id, self)
509
"""Return a map of parent: children for known parents.
511
Only new paths and parents of tree files with assigned ids are used.
514
items = list(self._new_parent.iteritems())
515
items.extend((t, self.final_parent(t)) for t in
516
self._tree_id_paths.keys())
517
for trans_id, parent_id in items:
518
if parent_id not in by_parent:
519
by_parent[parent_id] = set()
520
by_parent[parent_id].add(trans_id)
523
def path_changed(self, trans_id):
524
"""Return True if a trans_id's path has changed."""
525
return (trans_id in self._new_name) or (trans_id in self._new_parent)
527
def new_contents(self, trans_id):
528
return (trans_id in self._new_contents)
530
def find_conflicts(self):
531
"""Find any violations of inventory or filesystem invariants"""
532
if self._done is True:
533
raise ReusingTransform()
535
# ensure all children of all existent parents are known
536
# all children of non-existent parents are known, by definition.
537
self._add_tree_children()
538
by_parent = self.by_parent()
539
conflicts.extend(self._unversioned_parents(by_parent))
540
conflicts.extend(self._parent_loops())
541
conflicts.extend(self._duplicate_entries(by_parent))
542
conflicts.extend(self._duplicate_ids())
543
conflicts.extend(self._parent_type_conflicts(by_parent))
544
conflicts.extend(self._improper_versioning())
545
conflicts.extend(self._executability_conflicts())
546
conflicts.extend(self._overwrite_conflicts())
549
def _check_malformed(self):
550
conflicts = self.find_conflicts()
551
if len(conflicts) != 0:
552
raise MalformedTransform(conflicts=conflicts)
554
def _add_tree_children(self):
555
"""Add all the children of all active parents to the known paths.
557
Active parents are those which gain children, and those which are
558
removed. This is a necessary first step in detecting conflicts.
560
parents = self.by_parent().keys()
561
parents.extend([t for t in self._removed_contents if
562
self.tree_kind(t) == 'directory'])
563
for trans_id in self._removed_id:
564
file_id = self.tree_file_id(trans_id)
565
if file_id is not None:
566
if self._tree.stored_kind(file_id) == 'directory':
567
parents.append(trans_id)
568
elif self.tree_kind(trans_id) == 'directory':
569
parents.append(trans_id)
571
for parent_id in parents:
572
# ensure that all children are registered with the transaction
573
list(self.iter_tree_children(parent_id))
575
@deprecated_method(deprecated_in((2, 3, 0)))
576
def has_named_child(self, by_parent, parent_id, name):
577
return self._has_named_child(
578
name, parent_id, known_children=by_parent.get(parent_id, []))
580
def _has_named_child(self, name, parent_id, known_children):
581
"""Does a parent already have a name child.
583
:param name: The searched for name.
585
:param parent_id: The parent for which the check is made.
587
:param known_children: The already known children. This should have
588
been recently obtained from `self.by_parent.get(parent_id)`
589
(or will be if None is passed).
591
if known_children is None:
592
known_children = self.by_parent().get(parent_id, [])
593
for child in known_children:
594
if self.final_name(child) == name:
596
parent_path = self._tree_id_paths.get(parent_id, None)
597
if parent_path is None:
598
# No parent... no children
600
child_path = joinpath(parent_path, name)
601
child_id = self._tree_path_ids.get(child_path, None)
603
# Not known by the tree transform yet, check the filesystem
604
return osutils.lexists(self._tree.abspath(child_path))
606
raise AssertionError('child_id is missing: %s, %s, %s'
607
% (name, parent_id, child_id))
609
def _available_backup_name(self, name, target_id):
610
"""Find an available backup name.
612
:param name: The basename of the file.
614
:param target_id: The directory trans_id where the backup should
617
known_children = self.by_parent().get(target_id, [])
618
return osutils.available_backup_name(
620
lambda base: self._has_named_child(
621
base, target_id, known_children))
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():
662
kind = self.final_kind(trans_id)
664
conflicts.append(('versioning no contents', trans_id))
666
if not inventory.InventoryEntry.versionable_kind(kind):
667
conflicts.append(('versioning bad kind', trans_id, kind))
670
def _executability_conflicts(self):
671
"""Check for bad executability changes.
673
Only versioned files may have their executability set, because
674
1. only versioned entries can have executability under windows
675
2. only files can be executable. (The execute bit on a directory
676
does not indicate searchability)
679
for trans_id in self._new_executability:
680
if self.final_file_id(trans_id) is None:
681
conflicts.append(('unversioned executability', trans_id))
683
if self.final_kind(trans_id) != "file":
684
conflicts.append(('non-file executability', trans_id))
687
def _overwrite_conflicts(self):
688
"""Check for overwrites (not permitted on Win32)"""
690
for trans_id in self._new_contents:
691
if self.tree_kind(trans_id) is None:
693
if trans_id not in self._removed_contents:
694
conflicts.append(('overwrite', trans_id,
695
self.final_name(trans_id)))
698
def _duplicate_entries(self, by_parent):
699
"""No directory may have two entries with the same name."""
701
if (self._new_name, self._new_parent) == ({}, {}):
703
for children in by_parent.itervalues():
705
for child_tid in children:
706
name = self.final_name(child_tid)
708
# Keep children only if they still exist in the end
709
if not self._case_sensitive_target:
711
name_ids.append((name, child_tid))
715
for name, trans_id in name_ids:
716
kind = self.final_kind(trans_id)
717
file_id = self.final_file_id(trans_id)
718
if kind is None and file_id is None:
720
if name == last_name:
721
conflicts.append(('duplicate', last_trans_id, trans_id,
724
last_trans_id = trans_id
727
def _duplicate_ids(self):
728
"""Each inventory id may only be used once"""
730
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
732
all_ids = self._tree.all_file_ids()
733
active_tree_ids = all_ids.difference(removed_tree_ids)
734
for trans_id, file_id in self._new_id.iteritems():
735
if file_id in active_tree_ids:
736
old_trans_id = self.trans_id_tree_file_id(file_id)
737
conflicts.append(('duplicate id', old_trans_id, trans_id))
740
def _parent_type_conflicts(self, by_parent):
741
"""Children must have a directory parent"""
743
for parent_id, children in by_parent.iteritems():
744
if parent_id is ROOT_PARENT:
747
for child_id in children:
748
if self.final_kind(child_id) is not None:
753
# There is at least a child, so we need an existing directory to
755
kind = self.final_kind(parent_id)
757
# The directory will be deleted
758
conflicts.append(('missing parent', parent_id))
759
elif kind != "directory":
760
# Meh, we need a *directory* to put something in it
761
conflicts.append(('non-directory parent', parent_id))
764
def _set_executability(self, path, trans_id):
765
"""Set the executability of versioned files """
766
if supports_executable():
767
new_executability = self._new_executability[trans_id]
768
abspath = self._tree.abspath(path)
769
current_mode = os.stat(abspath).st_mode
770
if new_executability:
773
to_mode = current_mode | (0100 & ~umask)
774
# Enable x-bit for others only if they can read it.
775
if current_mode & 0004:
776
to_mode |= 0001 & ~umask
777
if current_mode & 0040:
778
to_mode |= 0010 & ~umask
780
to_mode = current_mode & ~0111
781
osutils.chmod_if_possible(abspath, to_mode)
783
def _new_entry(self, name, parent_id, file_id):
784
"""Helper function to create a new filesystem entry."""
785
trans_id = self.create_path(name, parent_id)
786
if file_id is not None:
787
self.version_file(file_id, trans_id)
790
def new_file(self, name, parent_id, contents, file_id=None,
791
executable=None, sha1=None):
792
"""Convenience method to create files.
794
name is the name of the file to create.
795
parent_id is the transaction id of the parent directory of the file.
796
contents is an iterator of bytestrings, which will be used to produce
798
:param file_id: The inventory ID of the file, if it is to be versioned.
799
:param executable: Only valid when a file_id has been supplied.
801
trans_id = self._new_entry(name, parent_id, file_id)
802
# TODO: rather than scheduling a set_executable call,
803
# have create_file create the file with the right mode.
804
self.create_file(contents, trans_id, sha1=sha1)
805
if executable is not None:
806
self.set_executability(executable, trans_id)
809
def new_directory(self, name, parent_id, file_id=None):
810
"""Convenience method to create directories.
812
name is the name of the directory to create.
813
parent_id is the transaction id of the parent directory of the
815
file_id is the inventory ID of the directory, if it is to be versioned.
817
trans_id = self._new_entry(name, parent_id, file_id)
818
self.create_directory(trans_id)
821
def new_symlink(self, name, parent_id, target, file_id=None):
822
"""Convenience method to create symbolic link.
824
name is the name of the symlink to create.
825
parent_id is the transaction id of the parent directory of the symlink.
826
target is a bytestring of the target of the symlink.
827
file_id is the inventory ID of the file, if it is to be versioned.
829
trans_id = self._new_entry(name, parent_id, file_id)
830
self.create_symlink(target, trans_id)
833
def new_orphan(self, trans_id, parent_id):
834
"""Schedule an item to be orphaned.
836
When a directory is about to be removed, its children, if they are not
837
versioned are moved out of the way: they don't have a parent anymore.
839
:param trans_id: The trans_id of the existing item.
840
:param parent_id: The parent trans_id of the item.
842
raise NotImplementedError(self.new_orphan)
844
def _get_potential_orphans(self, dir_id):
845
"""Find the potential orphans in a directory.
847
A directory can't be safely deleted if there are versioned files in it.
848
If all the contained files are unversioned then they can be orphaned.
850
The 'None' return value means that the directory contains at least one
851
versioned file and should not be deleted.
853
:param dir_id: The directory trans id.
855
:return: A list of the orphan trans ids or None if at least one
856
versioned file is present.
859
# Find the potential orphans, stop if one item should be kept
860
for child_tid in self.by_parent()[dir_id]:
861
if child_tid in self._removed_contents:
862
# The child is removed as part of the transform. Since it was
863
# versioned before, it's not an orphan
865
elif self.final_file_id(child_tid) is None:
866
# The child is not versioned
867
orphans.append(child_tid)
869
# We have a versioned file here, searching for orphans is
875
def _affected_ids(self):
876
"""Return the set of transform ids affected by the transform"""
877
trans_ids = set(self._removed_id)
878
trans_ids.update(self._new_id.keys())
879
trans_ids.update(self._removed_contents)
880
trans_ids.update(self._new_contents.keys())
881
trans_ids.update(self._new_executability.keys())
882
trans_ids.update(self._new_name.keys())
883
trans_ids.update(self._new_parent.keys())
886
def _get_file_id_maps(self):
887
"""Return mapping of file_ids to trans_ids in the to and from states"""
888
trans_ids = self._affected_ids()
891
# Build up two dicts: trans_ids associated with file ids in the
892
# FROM state, vs the TO state.
893
for trans_id in trans_ids:
894
from_file_id = self.tree_file_id(trans_id)
895
if from_file_id is not None:
896
from_trans_ids[from_file_id] = trans_id
897
to_file_id = self.final_file_id(trans_id)
898
if to_file_id is not None:
899
to_trans_ids[to_file_id] = trans_id
900
return from_trans_ids, to_trans_ids
902
def _from_file_data(self, from_trans_id, from_versioned, file_id):
903
"""Get data about a file in the from (tree) state
905
Return a (name, parent, kind, executable) tuple
907
from_path = self._tree_id_paths.get(from_trans_id)
909
# get data from working tree if versioned
910
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
911
from_name = from_entry.name
912
from_parent = from_entry.parent_id
915
if from_path is None:
916
# File does not exist in FROM state
920
# File exists, but is not versioned. Have to use path-
922
from_name = os.path.basename(from_path)
923
tree_parent = self.get_tree_parent(from_trans_id)
924
from_parent = self.tree_file_id(tree_parent)
925
if from_path is not None:
926
from_kind, from_executable, from_stats = \
927
self._tree._comparison_data(from_entry, from_path)
930
from_executable = False
931
return from_name, from_parent, from_kind, from_executable
933
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
934
"""Get data about a file in the to (target) state
936
Return a (name, parent, kind, executable) tuple
938
to_name = self.final_name(to_trans_id)
939
to_kind = self.final_kind(to_trans_id)
940
to_parent = self.final_file_id(self.final_parent(to_trans_id))
941
if to_trans_id in self._new_executability:
942
to_executable = self._new_executability[to_trans_id]
943
elif to_trans_id == from_trans_id:
944
to_executable = from_executable
946
to_executable = False
947
return to_name, to_parent, to_kind, to_executable
949
def iter_changes(self):
950
"""Produce output in the same format as Tree.iter_changes.
952
Will produce nonsensical results if invoked while inventory/filesystem
953
conflicts (as reported by TreeTransform.find_conflicts()) are present.
955
This reads the Transform, but only reproduces changes involving a
956
file_id. Files that are not versioned in either of the FROM or TO
957
states are not reflected.
959
final_paths = FinalPaths(self)
960
from_trans_ids, to_trans_ids = self._get_file_id_maps()
962
# Now iterate through all active file_ids
963
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
965
from_trans_id = from_trans_ids.get(file_id)
966
# find file ids, and determine versioning state
967
if from_trans_id is None:
968
from_versioned = False
969
from_trans_id = to_trans_ids[file_id]
971
from_versioned = True
972
to_trans_id = to_trans_ids.get(file_id)
973
if to_trans_id is None:
975
to_trans_id = from_trans_id
979
from_name, from_parent, from_kind, from_executable = \
980
self._from_file_data(from_trans_id, from_versioned, file_id)
982
to_name, to_parent, to_kind, to_executable = \
983
self._to_file_data(to_trans_id, from_trans_id, from_executable)
985
if not from_versioned:
988
from_path = self._tree_id_paths.get(from_trans_id)
992
to_path = final_paths.get_path(to_trans_id)
993
if from_kind != to_kind:
995
elif to_kind in ('file', 'symlink') and (
996
to_trans_id != from_trans_id or
997
to_trans_id in self._new_contents):
999
if (not modified and from_versioned == to_versioned and
1000
from_parent==to_parent and from_name == to_name and
1001
from_executable == to_executable):
1003
results.append((file_id, (from_path, to_path), modified,
1004
(from_versioned, to_versioned),
1005
(from_parent, to_parent),
1006
(from_name, to_name),
1007
(from_kind, to_kind),
1008
(from_executable, to_executable)))
1009
return iter(sorted(results, key=lambda x:x[1]))
1011
def get_preview_tree(self):
1012
"""Return a tree representing the result of the transform.
1014
The tree is a snapshot, and altering the TreeTransform will invalidate
1017
return _PreviewTree(self)
1019
def commit(self, branch, message, merge_parents=None, strict=False,
1020
timestamp=None, timezone=None, committer=None, authors=None,
1021
revprops=None, revision_id=None):
1022
"""Commit the result of this TreeTransform to a branch.
1024
:param branch: The branch to commit to.
1025
:param message: The message to attach to the commit.
1026
:param merge_parents: Additional parent revision-ids specified by
1028
:param strict: If True, abort the commit if there are unversioned
1030
:param timestamp: if not None, seconds-since-epoch for the time and
1031
date. (May be a float.)
1032
:param timezone: Optional timezone for timestamp, as an offset in
1034
:param committer: Optional committer in email-id format.
1035
(e.g. "J Random Hacker <jrandom@example.com>")
1036
:param authors: Optional list of authors in email-id format.
1037
:param revprops: Optional dictionary of revision properties.
1038
:param revision_id: Optional revision id. (Specifying a revision-id
1039
may reduce performance for some non-native formats.)
1040
:return: The revision_id of the revision committed.
1042
self._check_malformed()
1044
unversioned = set(self._new_contents).difference(set(self._new_id))
1045
for trans_id in unversioned:
1046
if self.final_file_id(trans_id) is None:
1047
raise errors.StrictCommitFailed()
1049
revno, last_rev_id = branch.last_revision_info()
1050
if last_rev_id == _mod_revision.NULL_REVISION:
1051
if merge_parents is not None:
1052
raise ValueError('Cannot supply merge parents for first'
1056
parent_ids = [last_rev_id]
1057
if merge_parents is not None:
1058
parent_ids.extend(merge_parents)
1059
if self._tree.get_revision_id() != last_rev_id:
1060
raise ValueError('TreeTransform not based on branch basis: %s' %
1061
self._tree.get_revision_id())
1062
revprops = commit.Commit.update_revprops(revprops, branch, authors)
1063
builder = branch.get_commit_builder(parent_ids,
1064
timestamp=timestamp,
1066
committer=committer,
1068
revision_id=revision_id)
1069
preview = self.get_preview_tree()
1070
list(builder.record_iter_changes(preview, last_rev_id,
1071
self.iter_changes()))
1072
builder.finish_inventory()
1073
revision_id = builder.commit(message)
1074
branch.set_last_revision_info(revno + 1, revision_id)
1077
def _text_parent(self, trans_id):
1078
file_id = self.tree_file_id(trans_id)
1080
if file_id is None or self._tree.kind(file_id) != 'file':
1082
except errors.NoSuchFile:
1086
def _get_parents_texts(self, trans_id):
1087
"""Get texts for compression parents of this file."""
1088
file_id = self._text_parent(trans_id)
1091
return (self._tree.get_file_text(file_id),)
1093
def _get_parents_lines(self, trans_id):
1094
"""Get lines for compression parents of this file."""
1095
file_id = self._text_parent(trans_id)
1098
return (self._tree.get_file_lines(file_id),)
1100
def serialize(self, serializer):
1101
"""Serialize this TreeTransform.
1103
:param serializer: A Serialiser like pack.ContainerSerializer.
1105
new_name = dict((k, v.encode('utf-8')) for k, v in
1106
self._new_name.items())
1107
new_executability = dict((k, int(v)) for k, v in
1108
self._new_executability.items())
1109
tree_path_ids = dict((k.encode('utf-8'), v)
1110
for k, v in self._tree_path_ids.items())
1112
'_id_number': self._id_number,
1113
'_new_name': new_name,
1114
'_new_parent': self._new_parent,
1115
'_new_executability': new_executability,
1116
'_new_id': self._new_id,
1117
'_tree_path_ids': tree_path_ids,
1118
'_removed_id': list(self._removed_id),
1119
'_removed_contents': list(self._removed_contents),
1120
'_non_present_ids': self._non_present_ids,
1122
yield serializer.bytes_record(bencode.bencode(attribs),
1124
for trans_id, kind in self._new_contents.items():
1126
lines = osutils.chunks_to_lines(
1127
self._read_file_chunks(trans_id))
1128
parents = self._get_parents_lines(trans_id)
1129
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1130
content = ''.join(mpdiff.to_patch())
1131
if kind == 'directory':
1133
if kind == 'symlink':
1134
content = self._read_symlink_target(trans_id)
1135
yield serializer.bytes_record(content, ((trans_id, kind),))
1137
def deserialize(self, records):
1138
"""Deserialize a stored TreeTransform.
1140
:param records: An iterable of (names, content) tuples, as per
1141
pack.ContainerPushParser.
1143
names, content = records.next()
1144
attribs = bencode.bdecode(content)
1145
self._id_number = attribs['_id_number']
1146
self._new_name = dict((k, v.decode('utf-8'))
1147
for k, v in attribs['_new_name'].items())
1148
self._new_parent = attribs['_new_parent']
1149
self._new_executability = dict((k, bool(v)) for k, v in
1150
attribs['_new_executability'].items())
1151
self._new_id = attribs['_new_id']
1152
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1153
self._tree_path_ids = {}
1154
self._tree_id_paths = {}
1155
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1156
path = bytepath.decode('utf-8')
1157
self._tree_path_ids[path] = trans_id
1158
self._tree_id_paths[trans_id] = path
1159
self._removed_id = set(attribs['_removed_id'])
1160
self._removed_contents = set(attribs['_removed_contents'])
1161
self._non_present_ids = attribs['_non_present_ids']
1162
for ((trans_id, kind),), content in records:
1164
mpdiff = multiparent.MultiParent.from_patch(content)
1165
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1166
self.create_file(lines, trans_id)
1167
if kind == 'directory':
1168
self.create_directory(trans_id)
1169
if kind == 'symlink':
1170
self.create_symlink(content.decode('utf-8'), trans_id)
1173
class DiskTreeTransform(TreeTransformBase):
1174
"""Tree transform storing its contents on disk."""
1176
def __init__(self, tree, limbodir, pb=None,
1177
case_sensitive=True):
1179
:param tree: The tree that will be transformed, but not necessarily
1181
:param limbodir: A directory where new files can be stored until
1182
they are installed in their proper places
1184
:param case_sensitive: If True, the target of the transform is
1185
case sensitive, not just case preserving.
1187
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1188
self._limbodir = limbodir
1189
self._deletiondir = None
1190
# A mapping of transform ids to their limbo filename
1191
self._limbo_files = {}
1192
self._possibly_stale_limbo_files = set()
1193
# A mapping of transform ids to a set of the transform ids of children
1194
# that their limbo directory has
1195
self._limbo_children = {}
1196
# Map transform ids to maps of child filename to child transform id
1197
self._limbo_children_names = {}
1198
# List of transform ids that need to be renamed from limbo into place
1199
self._needs_rename = set()
1200
self._creation_mtime = None
1203
"""Release the working tree lock, if held, clean up limbo dir.
1205
This is required if apply has not been invoked, but can be invoked
1208
if self._tree is None:
1211
limbo_paths = self._limbo_files.values() + list(
1212
self._possibly_stale_limbo_files)
1213
limbo_paths = sorted(limbo_paths, reverse=True)
1214
for path in limbo_paths:
1218
if e.errno != errno.ENOENT:
1220
# XXX: warn? perhaps we just got interrupted at an
1221
# inconvenient moment, but perhaps files are disappearing
1224
delete_any(self._limbodir)
1226
# We don't especially care *why* the dir is immortal.
1227
raise ImmortalLimbo(self._limbodir)
1229
if self._deletiondir is not None:
1230
delete_any(self._deletiondir)
1232
raise errors.ImmortalPendingDeletion(self._deletiondir)
1234
TreeTransformBase.finalize(self)
1236
def _limbo_name(self, trans_id):
1237
"""Generate the limbo name of a file"""
1238
limbo_name = self._limbo_files.get(trans_id)
1239
if limbo_name is None:
1240
limbo_name = self._generate_limbo_path(trans_id)
1241
self._limbo_files[trans_id] = limbo_name
1244
def _generate_limbo_path(self, trans_id):
1245
"""Generate a limbo path using the trans_id as the relative path.
1247
This is suitable as a fallback, and when the transform should not be
1248
sensitive to the path encoding of the limbo directory.
1250
self._needs_rename.add(trans_id)
1251
return pathjoin(self._limbodir, trans_id)
1253
def adjust_path(self, name, parent, trans_id):
1254
previous_parent = self._new_parent.get(trans_id)
1255
previous_name = self._new_name.get(trans_id)
1256
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1257
if (trans_id in self._limbo_files and
1258
trans_id not in self._needs_rename):
1259
self._rename_in_limbo([trans_id])
1260
if previous_parent != parent:
1261
self._limbo_children[previous_parent].remove(trans_id)
1262
if previous_parent != parent or previous_name != name:
1263
del self._limbo_children_names[previous_parent][previous_name]
1265
def _rename_in_limbo(self, trans_ids):
1266
"""Fix limbo names so that the right final path is produced.
1268
This means we outsmarted ourselves-- we tried to avoid renaming
1269
these files later by creating them with their final names in their
1270
final parents. But now the previous name or parent is no longer
1271
suitable, so we have to rename them.
1273
Even for trans_ids that have no new contents, we must remove their
1274
entries from _limbo_files, because they are now stale.
1276
for trans_id in trans_ids:
1277
old_path = self._limbo_files[trans_id]
1278
self._possibly_stale_limbo_files.add(old_path)
1279
del self._limbo_files[trans_id]
1280
if trans_id not in self._new_contents:
1282
new_path = self._limbo_name(trans_id)
1283
os.rename(old_path, new_path)
1284
self._possibly_stale_limbo_files.remove(old_path)
1285
for descendant in self._limbo_descendants(trans_id):
1286
desc_path = self._limbo_files[descendant]
1287
desc_path = new_path + desc_path[len(old_path):]
1288
self._limbo_files[descendant] = desc_path
1290
def _limbo_descendants(self, trans_id):
1291
"""Return the set of trans_ids whose limbo paths descend from this."""
1292
descendants = set(self._limbo_children.get(trans_id, []))
1293
for descendant in list(descendants):
1294
descendants.update(self._limbo_descendants(descendant))
1297
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1298
"""Schedule creation of a new file.
1302
:param contents: an iterator of strings, all of which will be written
1303
to the target destination.
1304
:param trans_id: TreeTransform handle
1305
:param mode_id: If not None, force the mode of the target file to match
1306
the mode of the object referenced by mode_id.
1307
Otherwise, we will try to preserve mode bits of an existing file.
1308
:param sha1: If the sha1 of this content is already known, pass it in.
1309
We can use it to prevent future sha1 computations.
1311
name = self._limbo_name(trans_id)
1312
f = open(name, 'wb')
1314
unique_add(self._new_contents, trans_id, 'file')
1315
f.writelines(contents)
1318
self._set_mtime(name)
1319
self._set_mode(trans_id, mode_id, S_ISREG)
1320
# It is unfortunate we have to use lstat instead of fstat, but we just
1321
# used utime and chmod on the file, so we need the accurate final
1323
if sha1 is not None:
1324
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1326
def _read_file_chunks(self, trans_id):
1327
cur_file = open(self._limbo_name(trans_id), 'rb')
1329
return cur_file.readlines()
1333
def _read_symlink_target(self, trans_id):
1334
return os.readlink(self._limbo_name(trans_id))
1336
def _set_mtime(self, path):
1337
"""All files that are created get the same mtime.
1339
This time is set by the first object to be created.
1341
if self._creation_mtime is None:
1342
self._creation_mtime = time.time()
1343
os.utime(path, (self._creation_mtime, self._creation_mtime))
1345
def create_hardlink(self, path, trans_id):
1346
"""Schedule creation of a hard link"""
1347
name = self._limbo_name(trans_id)
1351
if e.errno != errno.EPERM:
1353
raise errors.HardLinkNotSupported(path)
1355
unique_add(self._new_contents, trans_id, 'file')
1357
# Clean up the file, it never got registered so
1358
# TreeTransform.finalize() won't clean it up.
1362
def create_directory(self, trans_id):
1363
"""Schedule creation of a new directory.
1365
See also new_directory.
1367
os.mkdir(self._limbo_name(trans_id))
1368
unique_add(self._new_contents, trans_id, 'directory')
1370
def create_symlink(self, target, trans_id):
1371
"""Schedule creation of a new symbolic link.
1373
target is a bytestring.
1374
See also new_symlink.
1377
os.symlink(target, self._limbo_name(trans_id))
1378
unique_add(self._new_contents, trans_id, 'symlink')
1381
path = FinalPaths(self).get_path(trans_id)
1384
raise UnableCreateSymlink(path=path)
1386
def cancel_creation(self, trans_id):
1387
"""Cancel the creation of new file contents."""
1388
del self._new_contents[trans_id]
1389
if trans_id in self._observed_sha1s:
1390
del self._observed_sha1s[trans_id]
1391
children = self._limbo_children.get(trans_id)
1392
# if this is a limbo directory with children, move them before removing
1394
if children is not None:
1395
self._rename_in_limbo(children)
1396
del self._limbo_children[trans_id]
1397
del self._limbo_children_names[trans_id]
1398
delete_any(self._limbo_name(trans_id))
1400
def new_orphan(self, trans_id, parent_id):
1401
# FIXME: There is no tree config, so we use the branch one (it's weird
1402
# to define it this way as orphaning can only occur in a working tree,
1403
# but that's all we have (for now). It will find the option in
1404
# locations.conf or bazaar.conf though) -- vila 20100916
1405
conf = self._tree.branch.get_config()
1406
conf_var_name = 'bzr.transform.orphan_policy'
1407
orphan_policy = conf.get_user_option(conf_var_name)
1408
default_policy = orphaning_registry.default_key
1409
if orphan_policy is None:
1410
orphan_policy = default_policy
1411
if orphan_policy not in orphaning_registry:
1412
trace.warning('%s (from %s) is not a known policy, defaulting '
1413
'to %s' % (orphan_policy, conf_var_name, default_policy))
1414
orphan_policy = default_policy
1415
handle_orphan = orphaning_registry.get(orphan_policy)
1416
handle_orphan(self, trans_id, parent_id)
1419
class OrphaningError(errors.BzrError):
1421
# Only bugs could lead to such exception being seen by the user
1422
internal_error = True
1423
_fmt = "Error while orphaning %s in %s directory"
1425
def __init__(self, orphan, parent):
1426
errors.BzrError.__init__(self)
1427
self.orphan = orphan
1428
self.parent = parent
1431
class OrphaningForbidden(OrphaningError):
1433
_fmt = "Policy: %s doesn't allow creating orphans."
1435
def __init__(self, policy):
1436
errors.BzrError.__init__(self)
1437
self.policy = policy
1440
def move_orphan(tt, orphan_id, parent_id):
1441
"""See TreeTransformBase.new_orphan.
1443
This creates a new orphan in the `bzr-orphans` dir at the root of the
1446
:param tt: The TreeTransform orphaning `trans_id`.
1448
:param orphan_id: The trans id that should be orphaned.
1450
:param parent_id: The orphan parent trans id.
1452
# Add the orphan dir if it doesn't exist
1453
orphan_dir_basename = 'bzr-orphans'
1454
od_id = tt.trans_id_tree_path(orphan_dir_basename)
1455
if tt.final_kind(od_id) is None:
1456
tt.create_directory(od_id)
1457
parent_path = tt._tree_id_paths[parent_id]
1458
# Find a name that doesn't exist yet in the orphan dir
1459
actual_name = tt.final_name(orphan_id)
1460
new_name = tt._available_backup_name(actual_name, od_id)
1461
tt.adjust_path(new_name, od_id, orphan_id)
1462
trace.warning('%s has been orphaned in %s'
1463
% (joinpath(parent_path, actual_name), orphan_dir_basename))
1466
def refuse_orphan(tt, orphan_id, parent_id):
1467
"""See TreeTransformBase.new_orphan.
1469
This refuses to create orphan, letting the caller handle the conflict.
1471
raise OrphaningForbidden('never')
1474
orphaning_registry = registry.Registry()
1475
orphaning_registry.register(
1476
'conflict', refuse_orphan,
1477
'Leave orphans in place and create a conflict on the directory.')
1478
orphaning_registry.register(
1479
'move', move_orphan,
1480
'Move orphans into the bzr-orphans directory.')
1481
orphaning_registry._set_default_key('conflict')
1484
class TreeTransform(DiskTreeTransform):
1485
"""Represent a tree transformation.
1487
This object is designed to support incremental generation of the transform,
1490
However, it gives optimum performance when parent directories are created
1491
before their contents. The transform is then able to put child files
1492
directly in their parent directory, avoiding later renames.
1494
It is easy to produce malformed transforms, but they are generally
1495
harmless. Attempting to apply a malformed transform will cause an
1496
exception to be raised before any modifications are made to the tree.
1498
Many kinds of malformed transforms can be corrected with the
1499
resolve_conflicts function. The remaining ones indicate programming error,
1500
such as trying to create a file with no path.
1502
Two sets of file creation methods are supplied. Convenience methods are:
1507
These are composed of the low-level methods:
1509
* create_file or create_directory or create_symlink
1513
Transform/Transaction ids
1514
-------------------------
1515
trans_ids are temporary ids assigned to all files involved in a transform.
1516
It's possible, even common, that not all files in the Tree have trans_ids.
1518
trans_ids are used because filenames and file_ids are not good enough
1519
identifiers; filenames change, and not all files have file_ids. File-ids
1520
are also associated with trans-ids, so that moving a file moves its
1523
trans_ids are only valid for the TreeTransform that generated them.
1527
Limbo is a temporary directory use to hold new versions of files.
1528
Files are added to limbo by create_file, create_directory, create_symlink,
1529
and their convenience variants (new_*). Files may be removed from limbo
1530
using cancel_creation. Files are renamed from limbo into their final
1531
location as part of TreeTransform.apply
1533
Limbo must be cleaned up, by either calling TreeTransform.apply or
1534
calling TreeTransform.finalize.
1536
Files are placed into limbo inside their parent directories, where
1537
possible. This reduces subsequent renames, and makes operations involving
1538
lots of files faster. This optimization is only possible if the parent
1539
directory is created *before* creating any of its children, so avoid
1540
creating children before parents, where possible.
1544
This temporary directory is used by _FileMover for storing files that are
1545
about to be deleted. In case of rollback, the files will be restored.
1546
FileMover does not delete files until it is sure that a rollback will not
1549
def __init__(self, tree, pb=None):
1550
"""Note: a tree_write lock is taken on the tree.
1552
Use TreeTransform.finalize() to release the lock (can be omitted if
1553
TreeTransform.apply() called).
1555
tree.lock_tree_write()
1558
limbodir = urlutils.local_path_from_url(
1559
tree._transport.abspath('limbo'))
1560
osutils.ensure_empty_directory_exists(
1562
errors.ExistingLimbo)
1563
deletiondir = urlutils.local_path_from_url(
1564
tree._transport.abspath('pending-deletion'))
1565
osutils.ensure_empty_directory_exists(
1567
errors.ExistingPendingDeletion)
1572
# Cache of realpath results, to speed up canonical_path
1573
self._realpaths = {}
1574
# Cache of relpath results, to speed up canonical_path
1576
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1577
tree.case_sensitive)
1578
self._deletiondir = deletiondir
1580
def canonical_path(self, path):
1581
"""Get the canonical tree-relative path"""
1582
# don't follow final symlinks
1583
abs = self._tree.abspath(path)
1584
if abs in self._relpaths:
1585
return self._relpaths[abs]
1586
dirname, basename = os.path.split(abs)
1587
if dirname not in self._realpaths:
1588
self._realpaths[dirname] = os.path.realpath(dirname)
1589
dirname = self._realpaths[dirname]
1590
abs = pathjoin(dirname, basename)
1591
if dirname in self._relpaths:
1592
relpath = pathjoin(self._relpaths[dirname], basename)
1593
relpath = relpath.rstrip('/\\')
1595
relpath = self._tree.relpath(abs)
1596
self._relpaths[abs] = relpath
1599
def tree_kind(self, trans_id):
1600
"""Determine the file kind in the working tree.
1602
:returns: The file kind or None if the file does not exist
1604
path = self._tree_id_paths.get(trans_id)
1608
return file_kind(self._tree.abspath(path))
1609
except errors.NoSuchFile:
1612
def _set_mode(self, trans_id, mode_id, typefunc):
1613
"""Set the mode of new file contents.
1614
The mode_id is the existing file to get the mode from (often the same
1615
as trans_id). The operation is only performed if there's a mode match
1616
according to typefunc.
1621
old_path = self._tree_id_paths[mode_id]
1625
mode = os.stat(self._tree.abspath(old_path)).st_mode
1627
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1628
# Either old_path doesn't exist, or the parent of the
1629
# target is not a directory (but will be one eventually)
1630
# Either way, we know it doesn't exist *right now*
1631
# See also bug #248448
1636
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1638
def iter_tree_children(self, parent_id):
1639
"""Iterate through the entry's tree children, if any"""
1641
path = self._tree_id_paths[parent_id]
1645
children = os.listdir(self._tree.abspath(path))
1647
if not (osutils._is_error_enotdir(e)
1648
or e.errno in (errno.ENOENT, errno.ESRCH)):
1652
for child in children:
1653
childpath = joinpath(path, child)
1654
if self._tree.is_control_filename(childpath):
1656
yield self.trans_id_tree_path(childpath)
1658
def _generate_limbo_path(self, trans_id):
1659
"""Generate a limbo path using the final path if possible.
1661
This optimizes the performance of applying the tree transform by
1662
avoiding renames. These renames can be avoided only when the parent
1663
directory is already scheduled for creation.
1665
If the final path cannot be used, falls back to using the trans_id as
1668
parent = self._new_parent.get(trans_id)
1669
# if the parent directory is already in limbo (e.g. when building a
1670
# tree), choose a limbo name inside the parent, to reduce further
1672
use_direct_path = False
1673
if self._new_contents.get(parent) == 'directory':
1674
filename = self._new_name.get(trans_id)
1675
if filename is not None:
1676
if parent not in self._limbo_children:
1677
self._limbo_children[parent] = set()
1678
self._limbo_children_names[parent] = {}
1679
use_direct_path = True
1680
# the direct path can only be used if no other file has
1681
# already taken this pathname, i.e. if the name is unused, or
1682
# if it is already associated with this trans_id.
1683
elif self._case_sensitive_target:
1684
if (self._limbo_children_names[parent].get(filename)
1685
in (trans_id, None)):
1686
use_direct_path = True
1688
for l_filename, l_trans_id in\
1689
self._limbo_children_names[parent].iteritems():
1690
if l_trans_id == trans_id:
1692
if l_filename.lower() == filename.lower():
1695
use_direct_path = True
1697
if not use_direct_path:
1698
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1700
limbo_name = pathjoin(self._limbo_files[parent], filename)
1701
self._limbo_children[parent].add(trans_id)
1702
self._limbo_children_names[parent][filename] = trans_id
1706
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1707
"""Apply all changes to the inventory and filesystem.
1709
If filesystem or inventory conflicts are present, MalformedTransform
1712
If apply succeeds, finalize is not necessary.
1714
:param no_conflicts: if True, the caller guarantees there are no
1715
conflicts, so no check is made.
1716
:param precomputed_delta: An inventory delta to use instead of
1718
:param _mover: Supply an alternate FileMover, for testing
1720
if not no_conflicts:
1721
self._check_malformed()
1722
child_pb = ui.ui_factory.nested_progress_bar()
1724
if precomputed_delta is None:
1725
child_pb.update(gettext('Apply phase'), 0, 2)
1726
inventory_delta = self._generate_inventory_delta()
1729
inventory_delta = precomputed_delta
1732
mover = _FileMover()
1736
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1737
self._apply_removals(mover)
1738
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1739
modified_paths = self._apply_insertions(mover)
1744
mover.apply_deletions()
1747
if self.final_file_id(self.root) is None:
1748
inventory_delta = [e for e in inventory_delta if e[0] != '']
1749
self._tree.apply_inventory_delta(inventory_delta)
1750
self._apply_observed_sha1s()
1753
return _TransformResults(modified_paths, self.rename_count)
1755
def _generate_inventory_delta(self):
1756
"""Generate an inventory delta for the current transform."""
1757
inventory_delta = []
1758
child_pb = ui.ui_factory.nested_progress_bar()
1759
new_paths = self._inventory_altered()
1760
total_entries = len(new_paths) + len(self._removed_id)
1762
for num, trans_id in enumerate(self._removed_id):
1764
child_pb.update(gettext('removing file'), num, total_entries)
1765
if trans_id == self._new_root:
1766
file_id = self._tree.get_root_id()
1768
file_id = self.tree_file_id(trans_id)
1769
# File-id isn't really being deleted, just moved
1770
if file_id in self._r_new_id:
1772
path = self._tree_id_paths[trans_id]
1773
inventory_delta.append((path, None, file_id, None))
1774
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1776
entries = self._tree.iter_entries_by_dir(
1777
new_path_file_ids.values())
1778
old_paths = dict((e.file_id, p) for p, e in entries)
1780
for num, (path, trans_id) in enumerate(new_paths):
1782
child_pb.update(gettext('adding file'),
1783
num + len(self._removed_id), total_entries)
1784
file_id = new_path_file_ids[trans_id]
1788
kind = self.final_kind(trans_id)
1790
kind = self._tree.stored_kind(file_id)
1791
parent_trans_id = self.final_parent(trans_id)
1792
parent_file_id = new_path_file_ids.get(parent_trans_id)
1793
if parent_file_id is None:
1794
parent_file_id = self.final_file_id(parent_trans_id)
1795
if trans_id in self._new_reference_revision:
1796
new_entry = inventory.TreeReference(
1798
self._new_name[trans_id],
1799
self.final_file_id(self._new_parent[trans_id]),
1800
None, self._new_reference_revision[trans_id])
1802
new_entry = inventory.make_entry(kind,
1803
self.final_name(trans_id),
1804
parent_file_id, file_id)
1805
old_path = old_paths.get(new_entry.file_id)
1806
new_executability = self._new_executability.get(trans_id)
1807
if new_executability is not None:
1808
new_entry.executable = new_executability
1809
inventory_delta.append(
1810
(old_path, path, new_entry.file_id, new_entry))
1813
return inventory_delta
1815
def _apply_removals(self, mover):
1816
"""Perform tree operations that remove directory/inventory names.
1818
That is, delete files that are to be deleted, and put any files that
1819
need renaming into limbo. This must be done in strict child-to-parent
1822
If inventory_delta is None, no inventory delta generation is performed.
1824
tree_paths = list(self._tree_path_ids.iteritems())
1825
tree_paths.sort(reverse=True)
1826
child_pb = ui.ui_factory.nested_progress_bar()
1828
for num, (path, trans_id) in enumerate(tree_paths):
1829
# do not attempt to move root into a subdirectory of itself.
1832
child_pb.update(gettext('removing file'), num, len(tree_paths))
1833
full_path = self._tree.abspath(path)
1834
if trans_id in self._removed_contents:
1835
delete_path = os.path.join(self._deletiondir, trans_id)
1836
mover.pre_delete(full_path, delete_path)
1837
elif (trans_id in self._new_name
1838
or trans_id in self._new_parent):
1840
mover.rename(full_path, self._limbo_name(trans_id))
1841
except errors.TransformRenameFailed, e:
1842
if e.errno != errno.ENOENT:
1845
self.rename_count += 1
1849
def _apply_insertions(self, mover):
1850
"""Perform tree operations that insert directory/inventory names.
1852
That is, create any files that need to be created, and restore from
1853
limbo any files that needed renaming. This must be done in strict
1854
parent-to-child order.
1856
If inventory_delta is None, no inventory delta is calculated, and
1857
no list of modified paths is returned.
1859
new_paths = self.new_paths(filesystem_only=True)
1861
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1863
child_pb = ui.ui_factory.nested_progress_bar()
1865
for num, (path, trans_id) in enumerate(new_paths):
1867
child_pb.update(gettext('adding file'), num, len(new_paths))
1868
full_path = self._tree.abspath(path)
1869
if trans_id in self._needs_rename:
1871
mover.rename(self._limbo_name(trans_id), full_path)
1872
except errors.TransformRenameFailed, e:
1873
# We may be renaming a dangling inventory id
1874
if e.errno != errno.ENOENT:
1877
self.rename_count += 1
1878
# TODO: if trans_id in self._observed_sha1s, we should
1879
# re-stat the final target, since ctime will be
1880
# updated by the change.
1881
if (trans_id in self._new_contents or
1882
self.path_changed(trans_id)):
1883
if trans_id in self._new_contents:
1884
modified_paths.append(full_path)
1885
if trans_id in self._new_executability:
1886
self._set_executability(path, trans_id)
1887
if trans_id in self._observed_sha1s:
1888
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1889
st = osutils.lstat(full_path)
1890
self._observed_sha1s[trans_id] = (o_sha1, st)
1893
for path, trans_id in new_paths:
1894
# new_paths includes stuff like workingtree conflicts. Only the
1895
# stuff in new_contents actually comes from limbo.
1896
if trans_id in self._limbo_files:
1897
del self._limbo_files[trans_id]
1898
self._new_contents.clear()
1899
return modified_paths
1901
def _apply_observed_sha1s(self):
1902
"""After we have finished renaming everything, update observed sha1s
1904
This has to be done after self._tree.apply_inventory_delta, otherwise
1905
it doesn't know anything about the files we are updating. Also, we want
1906
to do this as late as possible, so that most entries end up cached.
1908
# TODO: this doesn't update the stat information for directories. So
1909
# the first 'bzr status' will still need to rewrite
1910
# .bzr/checkout/dirstate. However, we at least don't need to
1911
# re-read all of the files.
1912
# TODO: If the operation took a while, we could do a time.sleep(3) here
1913
# to allow the clock to tick over and ensure we won't have any
1914
# problems. (we could observe start time, and finish time, and if
1915
# it is less than eg 10% overhead, add a sleep call.)
1916
paths = FinalPaths(self)
1917
for trans_id, observed in self._observed_sha1s.iteritems():
1918
path = paths.get_path(trans_id)
1919
# We could get the file_id, but dirstate prefers to use the path
1920
# anyway, and it is 'cheaper' to determine.
1921
# file_id = self._new_id[trans_id]
1922
self._tree._observed_sha1(None, path, observed)
1925
class TransformPreview(DiskTreeTransform):
1926
"""A TreeTransform for generating preview trees.
1928
Unlike TreeTransform, this version works when the input tree is a
1929
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1930
unversioned files in the input tree.
1933
def __init__(self, tree, pb=None, case_sensitive=True):
1935
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1936
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1938
def canonical_path(self, path):
1941
def tree_kind(self, trans_id):
1942
path = self._tree_id_paths.get(trans_id)
1945
kind = self._tree.path_content_summary(path)[0]
1946
if kind == 'missing':
1950
def _set_mode(self, trans_id, mode_id, typefunc):
1951
"""Set the mode of new file contents.
1952
The mode_id is the existing file to get the mode from (often the same
1953
as trans_id). The operation is only performed if there's a mode match
1954
according to typefunc.
1956
# is it ok to ignore this? probably
1959
def iter_tree_children(self, parent_id):
1960
"""Iterate through the entry's tree children, if any"""
1962
path = self._tree_id_paths[parent_id]
1965
file_id = self.tree_file_id(parent_id)
1968
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1969
children = getattr(entry, 'children', {})
1970
for child in children:
1971
childpath = joinpath(path, child)
1972
yield self.trans_id_tree_path(childpath)
1974
def new_orphan(self, trans_id, parent_id):
1975
raise NotImplementedError(self.new_orphan)
1978
class _PreviewTree(tree.InventoryTree):
1979
"""Partial implementation of Tree to support show_diff_trees"""
1981
def __init__(self, transform):
1982
self._transform = transform
1983
self._final_paths = FinalPaths(transform)
1984
self.__by_parent = None
1985
self._parent_ids = []
1986
self._all_children_cache = {}
1987
self._path2trans_id_cache = {}
1988
self._final_name_cache = {}
1989
self._iter_changes_cache = dict((c[0], c) for c in
1990
self._transform.iter_changes())
1992
def _content_change(self, file_id):
1993
"""Return True if the content of this file changed"""
1994
changes = self._iter_changes_cache.get(file_id)
1995
# changes[2] is true if the file content changed. See
1996
# InterTree.iter_changes.
1997
return (changes is not None and changes[2])
1999
def _get_repository(self):
2000
repo = getattr(self._transform._tree, '_repository', None)
2002
repo = self._transform._tree.branch.repository
2005
def _iter_parent_trees(self):
2006
for revision_id in self.get_parent_ids():
2008
yield self.revision_tree(revision_id)
2009
except errors.NoSuchRevisionInTree:
2010
yield self._get_repository().revision_tree(revision_id)
2012
def _get_file_revision(self, file_id, vf, tree_revision):
2013
parent_keys = [(file_id, t.get_file_revision(file_id)) for t in
2014
self._iter_parent_trees()]
2015
vf.add_lines((file_id, tree_revision), parent_keys,
2016
self.get_file_lines(file_id))
2017
repo = self._get_repository()
2018
base_vf = repo.texts
2019
if base_vf not in vf.fallback_versionedfiles:
2020
vf.fallback_versionedfiles.append(base_vf)
2021
return tree_revision
2023
def _stat_limbo_file(self, file_id=None, trans_id=None):
2024
if trans_id is None:
2025
trans_id = self._transform.trans_id_file_id(file_id)
2026
name = self._transform._limbo_name(trans_id)
2027
return os.lstat(name)
2030
def _by_parent(self):
2031
if self.__by_parent is None:
2032
self.__by_parent = self._transform.by_parent()
2033
return self.__by_parent
2035
def _comparison_data(self, entry, path):
2036
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2037
if kind == 'missing':
2041
file_id = self._transform.final_file_id(self._path2trans_id(path))
2042
executable = self.is_executable(file_id, path)
2043
return kind, executable, None
2045
def is_locked(self):
2048
def lock_read(self):
2049
# Perhaps in theory, this should lock the TreeTransform?
2056
def inventory(self):
2057
"""This Tree does not use inventory as its backing data."""
2058
raise NotImplementedError(_PreviewTree.inventory)
2060
def get_root_id(self):
2061
return self._transform.final_file_id(self._transform.root)
2063
def all_file_ids(self):
2064
tree_ids = set(self._transform._tree.all_file_ids())
2065
tree_ids.difference_update(self._transform.tree_file_id(t)
2066
for t in self._transform._removed_id)
2067
tree_ids.update(self._transform._new_id.values())
2071
return iter(self.all_file_ids())
2073
def _has_id(self, file_id, fallback_check):
2074
if file_id in self._transform._r_new_id:
2076
elif file_id in set([self._transform.tree_file_id(trans_id) for
2077
trans_id in self._transform._removed_id]):
2080
return fallback_check(file_id)
2082
def has_id(self, file_id):
2083
return self._has_id(file_id, self._transform._tree.has_id)
2085
def has_or_had_id(self, file_id):
2086
return self._has_id(file_id, self._transform._tree.has_or_had_id)
2088
def _path2trans_id(self, path):
2089
# We must not use None here, because that is a valid value to store.
2090
trans_id = self._path2trans_id_cache.get(path, object)
2091
if trans_id is not object:
2093
segments = splitpath(path)
2094
cur_parent = self._transform.root
2095
for cur_segment in segments:
2096
for child in self._all_children(cur_parent):
2097
final_name = self._final_name_cache.get(child)
2098
if final_name is None:
2099
final_name = self._transform.final_name(child)
2100
self._final_name_cache[child] = final_name
2101
if final_name == cur_segment:
2105
self._path2trans_id_cache[path] = None
2107
self._path2trans_id_cache[path] = cur_parent
2110
def path2id(self, path):
2111
return self._transform.final_file_id(self._path2trans_id(path))
2113
def id2path(self, file_id):
2114
trans_id = self._transform.trans_id_file_id(file_id)
2116
return self._final_paths._determine_path(trans_id)
2118
raise errors.NoSuchId(self, file_id)
2120
def _all_children(self, trans_id):
2121
children = self._all_children_cache.get(trans_id)
2122
if children is not None:
2124
children = set(self._transform.iter_tree_children(trans_id))
2125
# children in the _new_parent set are provided by _by_parent.
2126
children.difference_update(self._transform._new_parent.keys())
2127
children.update(self._by_parent.get(trans_id, []))
2128
self._all_children_cache[trans_id] = children
2131
def iter_children(self, file_id):
2132
trans_id = self._transform.trans_id_file_id(file_id)
2133
for child_trans_id in self._all_children(trans_id):
2134
yield self._transform.final_file_id(child_trans_id)
2137
possible_extras = set(self._transform.trans_id_tree_path(p) for p
2138
in self._transform._tree.extras())
2139
possible_extras.update(self._transform._new_contents)
2140
possible_extras.update(self._transform._removed_id)
2141
for trans_id in possible_extras:
2142
if self._transform.final_file_id(trans_id) is None:
2143
yield self._final_paths._determine_path(trans_id)
2145
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
2146
yield_parents=False):
2147
for trans_id, parent_file_id in ordered_entries:
2148
file_id = self._transform.final_file_id(trans_id)
2151
if (specific_file_ids is not None
2152
and file_id not in specific_file_ids):
2154
kind = self._transform.final_kind(trans_id)
2156
kind = self._transform._tree.stored_kind(file_id)
2157
new_entry = inventory.make_entry(
2159
self._transform.final_name(trans_id),
2160
parent_file_id, file_id)
2161
yield new_entry, trans_id
2163
def _list_files_by_dir(self):
2164
todo = [ROOT_PARENT]
2166
while len(todo) > 0:
2168
parent_file_id = self._transform.final_file_id(parent)
2169
children = list(self._all_children(parent))
2170
paths = dict(zip(children, self._final_paths.get_paths(children)))
2171
children.sort(key=paths.get)
2172
todo.extend(reversed(children))
2173
for trans_id in children:
2174
ordered_ids.append((trans_id, parent_file_id))
2177
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
2178
# This may not be a maximally efficient implementation, but it is
2179
# reasonably straightforward. An implementation that grafts the
2180
# TreeTransform changes onto the tree's iter_entries_by_dir results
2181
# might be more efficient, but requires tricky inferences about stack
2183
ordered_ids = self._list_files_by_dir()
2184
for entry, trans_id in self._make_inv_entries(ordered_ids,
2185
specific_file_ids, yield_parents=yield_parents):
2186
yield unicode(self._final_paths.get_path(trans_id)), entry
2188
def _iter_entries_for_dir(self, dir_path):
2189
"""Return path, entry for items in a directory without recursing down."""
2190
dir_file_id = self.path2id(dir_path)
2192
for file_id in self.iter_children(dir_file_id):
2193
trans_id = self._transform.trans_id_file_id(file_id)
2194
ordered_ids.append((trans_id, file_id))
2195
for entry, trans_id in self._make_inv_entries(ordered_ids):
2196
yield unicode(self._final_paths.get_path(trans_id)), entry
2198
def list_files(self, include_root=False, from_dir=None, recursive=True):
2199
"""See WorkingTree.list_files."""
2200
# XXX This should behave like WorkingTree.list_files, but is really
2201
# more like RevisionTree.list_files.
2205
prefix = from_dir + '/'
2206
entries = self.iter_entries_by_dir()
2207
for path, entry in entries:
2208
if entry.name == '' and not include_root:
2211
if not path.startswith(prefix):
2213
path = path[len(prefix):]
2214
yield path, 'V', entry.kind, entry.file_id, entry
2216
if from_dir is None and include_root is True:
2217
root_entry = inventory.make_entry('directory', '',
2218
ROOT_PARENT, self.get_root_id())
2219
yield '', 'V', 'directory', root_entry.file_id, root_entry
2220
entries = self._iter_entries_for_dir(from_dir or '')
2221
for path, entry in entries:
2222
yield path, 'V', entry.kind, entry.file_id, entry
2224
def kind(self, file_id):
2225
trans_id = self._transform.trans_id_file_id(file_id)
2226
return self._transform.final_kind(trans_id)
2228
def stored_kind(self, file_id):
2229
trans_id = self._transform.trans_id_file_id(file_id)
2231
return self._transform._new_contents[trans_id]
2233
return self._transform._tree.stored_kind(file_id)
2235
def get_file_mtime(self, file_id, path=None):
2236
"""See Tree.get_file_mtime"""
2237
if not self._content_change(file_id):
2238
return self._transform._tree.get_file_mtime(file_id)
2239
return self._stat_limbo_file(file_id).st_mtime
2241
def _file_size(self, entry, stat_value):
2242
return self.get_file_size(entry.file_id)
2244
def get_file_size(self, file_id):
2245
"""See Tree.get_file_size"""
2246
trans_id = self._transform.trans_id_file_id(file_id)
2247
kind = self._transform.final_kind(trans_id)
2250
if trans_id in self._transform._new_contents:
2251
return self._stat_limbo_file(trans_id=trans_id).st_size
2252
if self.kind(file_id) == 'file':
2253
return self._transform._tree.get_file_size(file_id)
2257
def get_file_verifier(self, file_id, path=None, stat_value=None):
2258
trans_id = self._transform.trans_id_file_id(file_id)
2259
kind = self._transform._new_contents.get(trans_id)
2261
return self._transform._tree.get_file_verifier(file_id)
2263
fileobj = self.get_file(file_id)
2265
return ("SHA1", sha_file(fileobj))
2269
def get_file_sha1(self, file_id, path=None, stat_value=None):
2270
trans_id = self._transform.trans_id_file_id(file_id)
2271
kind = self._transform._new_contents.get(trans_id)
2273
return self._transform._tree.get_file_sha1(file_id)
2275
fileobj = self.get_file(file_id)
2277
return sha_file(fileobj)
2281
def is_executable(self, file_id, path=None):
2284
trans_id = self._transform.trans_id_file_id(file_id)
2286
return self._transform._new_executability[trans_id]
2289
return self._transform._tree.is_executable(file_id, path)
2291
if e.errno == errno.ENOENT:
2294
except errors.NoSuchId:
2297
def has_filename(self, path):
2298
trans_id = self._path2trans_id(path)
2299
if trans_id in self._transform._new_contents:
2301
elif trans_id in self._transform._removed_contents:
2304
return self._transform._tree.has_filename(path)
2306
def path_content_summary(self, path):
2307
trans_id = self._path2trans_id(path)
2308
tt = self._transform
2309
tree_path = tt._tree_id_paths.get(trans_id)
2310
kind = tt._new_contents.get(trans_id)
2312
if tree_path is None or trans_id in tt._removed_contents:
2313
return 'missing', None, None, None
2314
summary = tt._tree.path_content_summary(tree_path)
2315
kind, size, executable, link_or_sha1 = summary
2318
limbo_name = tt._limbo_name(trans_id)
2319
if trans_id in tt._new_reference_revision:
2320
kind = 'tree-reference'
2322
statval = os.lstat(limbo_name)
2323
size = statval.st_size
2324
if not supports_executable():
2327
executable = statval.st_mode & S_IEXEC
2331
if kind == 'symlink':
2332
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2333
executable = tt._new_executability.get(trans_id, executable)
2334
return kind, size, executable, link_or_sha1
2336
def iter_changes(self, from_tree, include_unchanged=False,
2337
specific_files=None, pb=None, extra_trees=None,
2338
require_versioned=True, want_unversioned=False):
2339
"""See InterTree.iter_changes.
2341
This has a fast path that is only used when the from_tree matches
2342
the transform tree, and no fancy options are supplied.
2344
if (from_tree is not self._transform._tree or include_unchanged or
2345
specific_files or want_unversioned):
2346
return tree.InterTree(from_tree, self).iter_changes(
2347
include_unchanged=include_unchanged,
2348
specific_files=specific_files,
2350
extra_trees=extra_trees,
2351
require_versioned=require_versioned,
2352
want_unversioned=want_unversioned)
2353
if want_unversioned:
2354
raise ValueError('want_unversioned is not supported')
2355
return self._transform.iter_changes()
2357
def get_file(self, file_id, path=None):
2358
"""See Tree.get_file"""
2359
if not self._content_change(file_id):
2360
return self._transform._tree.get_file(file_id, path)
2361
trans_id = self._transform.trans_id_file_id(file_id)
2362
name = self._transform._limbo_name(trans_id)
2363
return open(name, 'rb')
2365
def get_file_with_stat(self, file_id, path=None):
2366
return self.get_file(file_id, path), None
2368
def annotate_iter(self, file_id,
2369
default_revision=_mod_revision.CURRENT_REVISION):
2370
changes = self._iter_changes_cache.get(file_id)
2374
changed_content, versioned, kind = (changes[2], changes[3],
2378
get_old = (kind[0] == 'file' and versioned[0])
2380
old_annotation = self._transform._tree.annotate_iter(file_id,
2381
default_revision=default_revision)
2385
return old_annotation
2386
if not changed_content:
2387
return old_annotation
2388
# TODO: This is doing something similar to what WT.annotate_iter is
2389
# doing, however it fails slightly because it doesn't know what
2390
# the *other* revision_id is, so it doesn't know how to give the
2391
# other as the origin for some lines, they all get
2392
# 'default_revision'
2393
# It would be nice to be able to use the new Annotator based
2394
# approach, as well.
2395
return annotate.reannotate([old_annotation],
2396
self.get_file(file_id).readlines(),
2399
def get_symlink_target(self, file_id, path=None):
2400
"""See Tree.get_symlink_target"""
2401
if not self._content_change(file_id):
2402
return self._transform._tree.get_symlink_target(file_id)
2403
trans_id = self._transform.trans_id_file_id(file_id)
2404
name = self._transform._limbo_name(trans_id)
2405
return osutils.readlink(name)
2407
def walkdirs(self, prefix=''):
2408
pending = [self._transform.root]
2409
while len(pending) > 0:
2410
parent_id = pending.pop()
2413
prefix = prefix.rstrip('/')
2414
parent_path = self._final_paths.get_path(parent_id)
2415
parent_file_id = self._transform.final_file_id(parent_id)
2416
for child_id in self._all_children(parent_id):
2417
path_from_root = self._final_paths.get_path(child_id)
2418
basename = self._transform.final_name(child_id)
2419
file_id = self._transform.final_file_id(child_id)
2420
kind = self._transform.final_kind(child_id)
2421
if kind is not None:
2422
versioned_kind = kind
2425
versioned_kind = self._transform._tree.stored_kind(file_id)
2426
if versioned_kind == 'directory':
2427
subdirs.append(child_id)
2428
children.append((path_from_root, basename, kind, None,
2429
file_id, versioned_kind))
2431
if parent_path.startswith(prefix):
2432
yield (parent_path, parent_file_id), children
2433
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2436
def get_parent_ids(self):
2437
return self._parent_ids
2439
def set_parent_ids(self, parent_ids):
2440
self._parent_ids = parent_ids
2442
def get_revision_tree(self, revision_id):
2443
return self._transform._tree.get_revision_tree(revision_id)
2446
def joinpath(parent, child):
2447
"""Join tree-relative paths, handling the tree root specially"""
2448
if parent is None or parent == "":
2451
return pathjoin(parent, child)
2454
class FinalPaths(object):
2455
"""Make path calculation cheap by memoizing paths.
2457
The underlying tree must not be manipulated between calls, or else
2458
the results will likely be incorrect.
2460
def __init__(self, transform):
2461
object.__init__(self)
2462
self._known_paths = {}
2463
self.transform = transform
2465
def _determine_path(self, trans_id):
2466
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2468
name = self.transform.final_name(trans_id)
2469
parent_id = self.transform.final_parent(trans_id)
2470
if parent_id == self.transform.root:
2473
return pathjoin(self.get_path(parent_id), name)
2475
def get_path(self, trans_id):
2476
"""Find the final path associated with a trans_id"""
2477
if trans_id not in self._known_paths:
2478
self._known_paths[trans_id] = self._determine_path(trans_id)
2479
return self._known_paths[trans_id]
2481
def get_paths(self, trans_ids):
2482
return [(self.get_path(t), t) for t in trans_ids]
2486
def topology_sorted_ids(tree):
2487
"""Determine the topological order of the ids in a tree"""
2488
file_ids = list(tree)
2489
file_ids.sort(key=tree.id2path)
2493
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2494
delta_from_tree=False):
2495
"""Create working tree for a branch, using a TreeTransform.
2497
This function should be used on empty trees, having a tree root at most.
2498
(see merge and revert functionality for working with existing trees)
2500
Existing files are handled like so:
2502
- Existing bzrdirs take precedence over creating new items. They are
2503
created as '%s.diverted' % name.
2504
- Otherwise, if the content on disk matches the content we are building,
2505
it is silently replaced.
2506
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2508
:param tree: The tree to convert wt into a copy of
2509
:param wt: The working tree that files will be placed into
2510
:param accelerator_tree: A tree which can be used for retrieving file
2511
contents more quickly than tree itself, i.e. a workingtree. tree
2512
will be used for cases where accelerator_tree's content is different.
2513
:param hardlink: If true, hard-link files to accelerator_tree, where
2514
possible. accelerator_tree must implement abspath, i.e. be a
2516
:param delta_from_tree: If true, build_tree may use the input Tree to
2517
generate the inventory delta.
2519
wt.lock_tree_write()
2523
if accelerator_tree is not None:
2524
accelerator_tree.lock_read()
2526
return _build_tree(tree, wt, accelerator_tree, hardlink,
2529
if accelerator_tree is not None:
2530
accelerator_tree.unlock()
2537
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2538
"""See build_tree."""
2539
for num, _unused in enumerate(wt.all_file_ids()):
2540
if num > 0: # more than just a root
2541
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2543
top_pb = ui.ui_factory.nested_progress_bar()
2544
pp = ProgressPhase("Build phase", 2, top_pb)
2545
if tree.get_root_id() is not None:
2546
# This is kind of a hack: we should be altering the root
2547
# as part of the regular tree shape diff logic.
2548
# The conditional test here is to avoid doing an
2549
# expensive operation (flush) every time the root id
2550
# is set within the tree, nor setting the root and thus
2551
# marking the tree as dirty, because we use two different
2552
# idioms here: tree interfaces and inventory interfaces.
2553
if wt.get_root_id() != tree.get_root_id():
2554
wt.set_root_id(tree.get_root_id())
2556
tt = TreeTransform(wt)
2560
file_trans_id[wt.get_root_id()] = \
2561
tt.trans_id_tree_file_id(wt.get_root_id())
2562
pb = ui.ui_factory.nested_progress_bar()
2564
deferred_contents = []
2566
total = len(tree.all_file_ids())
2568
precomputed_delta = []
2570
precomputed_delta = None
2571
# Check if tree inventory has content. If so, we populate
2572
# existing_files with the directory content. If there are no
2573
# entries we skip populating existing_files as its not used.
2574
# This improves performance and unncessary work on large
2575
# directory trees. (#501307)
2577
existing_files = set()
2578
for dir, files in wt.walkdirs():
2579
existing_files.update(f[0] for f in files)
2580
for num, (tree_path, entry) in \
2581
enumerate(tree.iter_entries_by_dir()):
2582
pb.update(gettext("Building tree"), num - len(deferred_contents), total)
2583
if entry.parent_id is None:
2586
file_id = entry.file_id
2588
precomputed_delta.append((None, tree_path, file_id, entry))
2589
if tree_path in existing_files:
2590
target_path = wt.abspath(tree_path)
2591
kind = file_kind(target_path)
2592
if kind == "directory":
2594
controldir.ControlDir.open(target_path)
2595
except errors.NotBranchError:
2599
if (file_id not in divert and
2600
_content_match(tree, entry, file_id, kind,
2602
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2603
if kind == 'directory':
2605
parent_id = file_trans_id[entry.parent_id]
2606
if entry.kind == 'file':
2607
# We *almost* replicate new_by_entry, so that we can defer
2608
# getting the file text, and get them all at once.
2609
trans_id = tt.create_path(entry.name, parent_id)
2610
file_trans_id[file_id] = trans_id
2611
tt.version_file(file_id, trans_id)
2612
executable = tree.is_executable(file_id, tree_path)
2614
tt.set_executability(executable, trans_id)
2615
trans_data = (trans_id, tree_path, entry.text_sha1)
2616
deferred_contents.append((file_id, trans_data))
2618
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2621
new_trans_id = file_trans_id[file_id]
2622
old_parent = tt.trans_id_tree_path(tree_path)
2623
_reparent_children(tt, old_parent, new_trans_id)
2624
offset = num + 1 - len(deferred_contents)
2625
_create_files(tt, tree, deferred_contents, pb, offset,
2626
accelerator_tree, hardlink)
2630
divert_trans = set(file_trans_id[f] for f in divert)
2631
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2632
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2633
if len(raw_conflicts) > 0:
2634
precomputed_delta = None
2635
conflicts = cook_conflicts(raw_conflicts, tt)
2636
for conflict in conflicts:
2637
trace.warning(unicode(conflict))
2639
wt.add_conflicts(conflicts)
2640
except errors.UnsupportedOperation:
2642
result = tt.apply(no_conflicts=True,
2643
precomputed_delta=precomputed_delta)
2650
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2652
total = len(desired_files) + offset
2654
if accelerator_tree is None:
2655
new_desired_files = desired_files
2657
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2658
unchanged = [(f, p[1]) for (f, p, c, v, d, n, k, e)
2659
in iter if not (c or e[0] != e[1])]
2660
if accelerator_tree.supports_content_filtering():
2661
unchanged = [(f, p) for (f, p) in unchanged
2662
if not accelerator_tree.iter_search_rules([p]).next()]
2663
unchanged = dict(unchanged)
2664
new_desired_files = []
2666
for file_id, (trans_id, tree_path, text_sha1) in desired_files:
2667
accelerator_path = unchanged.get(file_id)
2668
if accelerator_path is None:
2669
new_desired_files.append((file_id,
2670
(trans_id, tree_path, text_sha1)))
2672
pb.update(gettext('Adding file contents'), count + offset, total)
2674
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2677
contents = accelerator_tree.get_file(file_id, accelerator_path)
2678
if wt.supports_content_filtering():
2679
filters = wt._content_filter_stack(tree_path)
2680
contents = filtered_output_bytes(contents, filters,
2681
ContentFilterContext(tree_path, tree))
2683
tt.create_file(contents, trans_id, sha1=text_sha1)
2687
except AttributeError:
2688
# after filtering, contents may no longer be file-like
2692
for count, ((trans_id, tree_path, text_sha1), contents) in enumerate(
2693
tree.iter_files_bytes(new_desired_files)):
2694
if wt.supports_content_filtering():
2695
filters = wt._content_filter_stack(tree_path)
2696
contents = filtered_output_bytes(contents, filters,
2697
ContentFilterContext(tree_path, tree))
2698
tt.create_file(contents, trans_id, sha1=text_sha1)
2699
pb.update(gettext('Adding file contents'), count + offset, total)
2702
def _reparent_children(tt, old_parent, new_parent):
2703
for child in tt.iter_tree_children(old_parent):
2704
tt.adjust_path(tt.final_name(child), new_parent, child)
2707
def _reparent_transform_children(tt, old_parent, new_parent):
2708
by_parent = tt.by_parent()
2709
for child in by_parent[old_parent]:
2710
tt.adjust_path(tt.final_name(child), new_parent, child)
2711
return by_parent[old_parent]
2714
def _content_match(tree, entry, file_id, kind, target_path):
2715
if entry.kind != kind:
2717
if entry.kind == "directory":
2719
if entry.kind == "file":
2720
f = file(target_path, 'rb')
2722
if tree.get_file_text(file_id) == f.read():
2726
elif entry.kind == "symlink":
2727
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2732
def resolve_checkout(tt, conflicts, divert):
2733
new_conflicts = set()
2734
for c_type, conflict in ((c[0], c) for c in conflicts):
2735
# Anything but a 'duplicate' would indicate programmer error
2736
if c_type != 'duplicate':
2737
raise AssertionError(c_type)
2738
# Now figure out which is new and which is old
2739
if tt.new_contents(conflict[1]):
2740
new_file = conflict[1]
2741
old_file = conflict[2]
2743
new_file = conflict[2]
2744
old_file = conflict[1]
2746
# We should only get here if the conflict wasn't completely
2748
final_parent = tt.final_parent(old_file)
2749
if new_file in divert:
2750
new_name = tt.final_name(old_file)+'.diverted'
2751
tt.adjust_path(new_name, final_parent, new_file)
2752
new_conflicts.add((c_type, 'Diverted to',
2753
new_file, old_file))
2755
new_name = tt.final_name(old_file)+'.moved'
2756
tt.adjust_path(new_name, final_parent, old_file)
2757
new_conflicts.add((c_type, 'Moved existing file to',
2758
old_file, new_file))
2759
return new_conflicts
2762
def new_by_entry(tt, entry, parent_id, tree):
2763
"""Create a new file according to its inventory entry"""
2767
contents = tree.get_file(entry.file_id).readlines()
2768
executable = tree.is_executable(entry.file_id)
2769
return tt.new_file(name, parent_id, contents, entry.file_id,
2771
elif kind in ('directory', 'tree-reference'):
2772
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2773
if kind == 'tree-reference':
2774
tt.set_tree_reference(entry.reference_revision, trans_id)
2776
elif kind == 'symlink':
2777
target = tree.get_symlink_target(entry.file_id)
2778
return tt.new_symlink(name, parent_id, target, entry.file_id)
2780
raise errors.BadFileKindError(name, kind)
2783
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2784
filter_tree_path=None):
2785
"""Create new file contents according to tree contents.
2787
:param filter_tree_path: the tree path to use to lookup
2788
content filters to apply to the bytes output in the working tree.
2789
This only applies if the working tree supports content filtering.
2791
kind = tree.kind(file_id)
2792
if kind == 'directory':
2793
tt.create_directory(trans_id)
2794
elif kind == "file":
2796
tree_file = tree.get_file(file_id)
2798
bytes = tree_file.readlines()
2802
if wt.supports_content_filtering() and filter_tree_path is not None:
2803
filters = wt._content_filter_stack(filter_tree_path)
2804
bytes = filtered_output_bytes(bytes, filters,
2805
ContentFilterContext(filter_tree_path, tree))
2806
tt.create_file(bytes, trans_id)
2807
elif kind == "symlink":
2808
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2810
raise AssertionError('Unknown kind %r' % kind)
2813
def create_entry_executability(tt, entry, trans_id):
2814
"""Set the executability of a trans_id according to an inventory entry"""
2815
if entry.kind == "file":
2816
tt.set_executability(entry.executable, trans_id)
2819
@deprecated_function(deprecated_in((2, 3, 0)))
2820
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2821
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2824
@deprecated_function(deprecated_in((2, 3, 0)))
2825
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2826
"""Produce a backup-style name that appears to be available"""
2830
yield "%s.~%d~" % (name, counter)
2832
for new_name in name_gen():
2833
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2837
def revert(working_tree, target_tree, filenames, backups=False,
2838
pb=None, change_reporter=None):
2839
"""Revert a working tree's contents to those of a target tree."""
2840
target_tree.lock_read()
2841
pb = ui.ui_factory.nested_progress_bar()
2842
tt = TreeTransform(working_tree, pb)
2844
pp = ProgressPhase("Revert phase", 3, pb)
2845
conflicts, merge_modified = _prepare_revert_transform(
2846
working_tree, target_tree, tt, filenames, backups, pp)
2848
change_reporter = delta._ChangeReporter(
2849
unversioned_filter=working_tree.is_ignored)
2850
delta.report_changes(tt.iter_changes(), change_reporter)
2851
for conflict in conflicts:
2852
trace.warning(unicode(conflict))
2855
working_tree.set_merge_modified(merge_modified)
2857
target_tree.unlock()
2863
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2864
backups, pp, basis_tree=None,
2865
merge_modified=None):
2866
child_pb = ui.ui_factory.nested_progress_bar()
2868
if merge_modified is None:
2869
merge_modified = working_tree.merge_modified()
2870
merge_modified = _alter_files(working_tree, target_tree, tt,
2871
child_pb, filenames, backups,
2872
merge_modified, basis_tree)
2875
child_pb = ui.ui_factory.nested_progress_bar()
2877
raw_conflicts = resolve_conflicts(tt, child_pb,
2878
lambda t, c: conflict_pass(t, c, target_tree))
2881
conflicts = cook_conflicts(raw_conflicts, tt)
2882
return conflicts, merge_modified
2885
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2886
backups, merge_modified, basis_tree=None):
2887
if basis_tree is not None:
2888
basis_tree.lock_read()
2889
# We ask the working_tree for its changes relative to the target, rather
2890
# than the target changes relative to the working tree. Because WT4 has an
2891
# optimizer to compare itself to a target, but no optimizer for the
2893
change_list = working_tree.iter_changes(target_tree,
2894
specific_files=specific_files, pb=pb)
2895
if target_tree.get_root_id() is None:
2901
for id_num, (file_id, path, changed_content, versioned, parent, name,
2902
kind, executable) in enumerate(change_list):
2903
target_path, wt_path = path
2904
target_versioned, wt_versioned = versioned
2905
target_parent, wt_parent = parent
2906
target_name, wt_name = name
2907
target_kind, wt_kind = kind
2908
target_executable, wt_executable = executable
2909
if skip_root and wt_parent is None:
2911
trans_id = tt.trans_id_file_id(file_id)
2914
keep_content = False
2915
if wt_kind == 'file' and (backups or target_kind is None):
2916
wt_sha1 = working_tree.get_file_sha1(file_id)
2917
if merge_modified.get(file_id) != wt_sha1:
2918
# acquire the basis tree lazily to prevent the
2919
# expense of accessing it when it's not needed ?
2920
# (Guessing, RBC, 200702)
2921
if basis_tree is None:
2922
basis_tree = working_tree.basis_tree()
2923
basis_tree.lock_read()
2924
if basis_tree.has_id(file_id):
2925
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2927
elif target_kind is None and not target_versioned:
2929
if wt_kind is not None:
2930
if not keep_content:
2931
tt.delete_contents(trans_id)
2932
elif target_kind is not None:
2933
parent_trans_id = tt.trans_id_file_id(wt_parent)
2934
backup_name = tt._available_backup_name(
2935
wt_name, parent_trans_id)
2936
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2937
new_trans_id = tt.create_path(wt_name, parent_trans_id)
2938
if wt_versioned and target_versioned:
2939
tt.unversion_file(trans_id)
2940
tt.version_file(file_id, new_trans_id)
2941
# New contents should have the same unix perms as old
2944
trans_id = new_trans_id
2945
if target_kind in ('directory', 'tree-reference'):
2946
tt.create_directory(trans_id)
2947
if target_kind == 'tree-reference':
2948
revision = target_tree.get_reference_revision(file_id,
2950
tt.set_tree_reference(revision, trans_id)
2951
elif target_kind == 'symlink':
2952
tt.create_symlink(target_tree.get_symlink_target(file_id),
2954
elif target_kind == 'file':
2955
deferred_files.append((file_id, (trans_id, mode_id)))
2956
if basis_tree is None:
2957
basis_tree = working_tree.basis_tree()
2958
basis_tree.lock_read()
2959
new_sha1 = target_tree.get_file_sha1(file_id)
2960
if (basis_tree.has_id(file_id) and
2961
new_sha1 == basis_tree.get_file_sha1(file_id)):
2962
if file_id in merge_modified:
2963
del merge_modified[file_id]
2965
merge_modified[file_id] = new_sha1
2967
# preserve the execute bit when backing up
2968
if keep_content and wt_executable == target_executable:
2969
tt.set_executability(target_executable, trans_id)
2970
elif target_kind is not None:
2971
raise AssertionError(target_kind)
2972
if not wt_versioned and target_versioned:
2973
tt.version_file(file_id, trans_id)
2974
if wt_versioned and not target_versioned:
2975
tt.unversion_file(trans_id)
2976
if (target_name is not None and
2977
(wt_name != target_name or wt_parent != target_parent)):
2978
if target_name == '' and target_parent is None:
2979
parent_trans = ROOT_PARENT
2981
parent_trans = tt.trans_id_file_id(target_parent)
2982
if wt_parent is None and wt_versioned:
2983
tt.adjust_root_path(target_name, parent_trans)
2985
tt.adjust_path(target_name, parent_trans, trans_id)
2986
if wt_executable != target_executable and target_kind == "file":
2987
tt.set_executability(target_executable, trans_id)
2988
if working_tree.supports_content_filtering():
2989
for index, ((trans_id, mode_id), bytes) in enumerate(
2990
target_tree.iter_files_bytes(deferred_files)):
2991
file_id = deferred_files[index][0]
2992
# We're reverting a tree to the target tree so using the
2993
# target tree to find the file path seems the best choice
2994
# here IMO - Ian C 27/Oct/2009
2995
filter_tree_path = target_tree.id2path(file_id)
2996
filters = working_tree._content_filter_stack(filter_tree_path)
2997
bytes = filtered_output_bytes(bytes, filters,
2998
ContentFilterContext(filter_tree_path, working_tree))
2999
tt.create_file(bytes, trans_id, mode_id)
3001
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
3003
tt.create_file(bytes, trans_id, mode_id)
3004
tt.fixup_new_roots()
3006
if basis_tree is not None:
3008
return merge_modified
3011
def resolve_conflicts(tt, pb=None, pass_func=None):
3012
"""Make many conflict-resolution attempts, but die if they fail"""
3013
if pass_func is None:
3014
pass_func = conflict_pass
3015
new_conflicts = set()
3016
pb = ui.ui_factory.nested_progress_bar()
3019
pb.update(gettext('Resolution pass'), n+1, 10)
3020
conflicts = tt.find_conflicts()
3021
if len(conflicts) == 0:
3022
return new_conflicts
3023
new_conflicts.update(pass_func(tt, conflicts))
3024
raise MalformedTransform(conflicts=conflicts)
3029
def conflict_pass(tt, conflicts, path_tree=None):
3030
"""Resolve some classes of conflicts.
3032
:param tt: The transform to resolve conflicts in
3033
:param conflicts: The conflicts to resolve
3034
:param path_tree: A Tree to get supplemental paths from
3036
new_conflicts = set()
3037
for c_type, conflict in ((c[0], c) for c in conflicts):
3038
if c_type == 'duplicate id':
3039
tt.unversion_file(conflict[1])
3040
new_conflicts.add((c_type, 'Unversioned existing file',
3041
conflict[1], conflict[2], ))
3042
elif c_type == 'duplicate':
3043
# files that were renamed take precedence
3044
final_parent = tt.final_parent(conflict[1])
3045
if tt.path_changed(conflict[1]):
3046
existing_file, new_file = conflict[2], conflict[1]
3048
existing_file, new_file = conflict[1], conflict[2]
3049
new_name = tt.final_name(existing_file) + '.moved'
3050
tt.adjust_path(new_name, final_parent, existing_file)
3051
new_conflicts.add((c_type, 'Moved existing file to',
3052
existing_file, new_file))
3053
elif c_type == 'parent loop':
3054
# break the loop by undoing one of the ops that caused the loop
3056
while not tt.path_changed(cur):
3057
cur = tt.final_parent(cur)
3058
new_conflicts.add((c_type, 'Cancelled move', cur,
3059
tt.final_parent(cur),))
3060
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3062
elif c_type == 'missing parent':
3063
trans_id = conflict[1]
3064
if trans_id in tt._removed_contents:
3065
cancel_deletion = True
3066
orphans = tt._get_potential_orphans(trans_id)
3068
cancel_deletion = False
3069
# All children are orphans
3072
tt.new_orphan(o, trans_id)
3073
except OrphaningError:
3074
# Something bad happened so we cancel the directory
3075
# deletion which will leave it in place with a
3076
# conflict. The user can deal with it from there.
3077
# Note that this also catch the case where we don't
3078
# want to create orphans and leave the directory in
3080
cancel_deletion = True
3083
# Cancel the directory deletion
3084
tt.cancel_deletion(trans_id)
3085
new_conflicts.add(('deleting parent', 'Not deleting',
3090
tt.final_name(trans_id)
3092
if path_tree is not None:
3093
file_id = tt.final_file_id(trans_id)
3095
file_id = tt.inactive_file_id(trans_id)
3096
_, entry = path_tree.iter_entries_by_dir(
3098
# special-case the other tree root (move its
3099
# children to current root)
3100
if entry.parent_id is None:
3102
moved = _reparent_transform_children(
3103
tt, trans_id, tt.root)
3105
new_conflicts.add((c_type, 'Moved to root',
3108
parent_trans_id = tt.trans_id_file_id(
3110
tt.adjust_path(entry.name, parent_trans_id,
3113
tt.create_directory(trans_id)
3114
new_conflicts.add((c_type, 'Created directory', trans_id))
3115
elif c_type == 'unversioned parent':
3116
file_id = tt.inactive_file_id(conflict[1])
3117
# special-case the other tree root (move its children instead)
3118
if path_tree and path_tree.has_id(file_id):
3119
if path_tree.path2id('') == file_id:
3120
# This is the root entry, skip it
3122
tt.version_file(file_id, conflict[1])
3123
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3124
elif c_type == 'non-directory parent':
3125
parent_id = conflict[1]
3126
parent_parent = tt.final_parent(parent_id)
3127
parent_name = tt.final_name(parent_id)
3128
parent_file_id = tt.final_file_id(parent_id)
3129
new_parent_id = tt.new_directory(parent_name + '.new',
3130
parent_parent, parent_file_id)
3131
_reparent_transform_children(tt, parent_id, new_parent_id)
3132
if parent_file_id is not None:
3133
tt.unversion_file(parent_id)
3134
new_conflicts.add((c_type, 'Created directory', new_parent_id))
3135
elif c_type == 'versioning no contents':
3136
tt.cancel_versioning(conflict[1])
3137
return new_conflicts
3140
def cook_conflicts(raw_conflicts, tt):
3141
"""Generate a list of cooked conflicts, sorted by file path"""
3142
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3143
return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3146
def iter_cook_conflicts(raw_conflicts, tt):
3148
for conflict in raw_conflicts:
3149
c_type = conflict[0]
3150
action = conflict[1]
3151
modified_path = fp.get_path(conflict[2])
3152
modified_id = tt.final_file_id(conflict[2])
3153
if len(conflict) == 3:
3154
yield conflicts.Conflict.factory(
3155
c_type, action=action, path=modified_path, file_id=modified_id)
3158
conflicting_path = fp.get_path(conflict[3])
3159
conflicting_id = tt.final_file_id(conflict[3])
3160
yield conflicts.Conflict.factory(
3161
c_type, action=action, path=modified_path,
3162
file_id=modified_id,
3163
conflict_path=conflicting_path,
3164
conflict_file_id=conflicting_id)
3167
class _FileMover(object):
3168
"""Moves and deletes files for TreeTransform, tracking operations"""
3171
self.past_renames = []
3172
self.pending_deletions = []
3174
def rename(self, from_, to):
3175
"""Rename a file from one path to another."""
3177
os.rename(from_, to)
3179
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
3180
raise errors.FileExists(to, str(e))
3181
# normal OSError doesn't include filenames so it's hard to see where
3182
# the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3183
raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
3184
self.past_renames.append((from_, to))
3186
def pre_delete(self, from_, to):
3187
"""Rename a file out of the way and mark it for deletion.
3189
Unlike os.unlink, this works equally well for files and directories.
3190
:param from_: The current file path
3191
:param to: A temporary path for the file
3193
self.rename(from_, to)
3194
self.pending_deletions.append(to)
3197
"""Reverse all renames that have been performed"""
3198
for from_, to in reversed(self.past_renames):
3200
os.rename(to, from_)
3202
raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
3203
# after rollback, don't reuse _FileMover
3205
pending_deletions = None
3207
def apply_deletions(self):
3208
"""Apply all marked deletions"""
3209
for path in self.pending_deletions:
3211
# after apply_deletions, don't reuse _FileMover
3213
pending_deletions = None