1
# Copyright (C) 2006-2010 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
19
from stat import S_ISREG, S_IEXEC
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
32
revision as _mod_revision,
35
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
36
ReusingTransform, NotVersionedError, CantMoveRoot,
37
ExistingLimbo, ImmortalLimbo, NoFinalPath,
39
from bzrlib.filters import filtered_output_bytes, ContentFilterContext
40
from bzrlib.inventory import InventoryEntry
41
from bzrlib.osutils import (
51
from bzrlib.progress import DummyProgress, ProgressPhase
52
from bzrlib.symbol_versioning import (
56
from bzrlib.trace import mutter, warning
57
from bzrlib import tree
59
import bzrlib.urlutils as urlutils
62
ROOT_PARENT = "root-parent"
65
def unique_add(map, key, value):
67
raise DuplicateKey(key=key)
71
class _TransformResults(object):
72
def __init__(self, modified_paths, rename_count):
74
self.modified_paths = modified_paths
75
self.rename_count = rename_count
78
class TreeTransformBase(object):
79
"""The base class for TreeTransform and its kin."""
81
def __init__(self, tree, pb=DummyProgress(),
85
:param tree: The tree that will be transformed, but not necessarily
87
:param pb: A ProgressTask indicating how much progress is being made
88
:param case_sensitive: If True, the target of the transform is
89
case sensitive, not just case preserving.
94
# mapping of trans_id -> new basename
96
# mapping of trans_id -> new parent trans_id
98
# mapping of trans_id with new contents -> new file_kind
99
self._new_contents = {}
100
# Set of trans_ids whose contents will be removed
101
self._removed_contents = set()
102
# Mapping of trans_id -> new execute-bit value
103
self._new_executability = {}
104
# Mapping of trans_id -> new tree-reference value
105
self._new_reference_revision = {}
106
# Mapping of trans_id -> new file_id
108
# Mapping of old file-id -> trans_id
109
self._non_present_ids = {}
110
# Mapping of new file_id -> trans_id
112
# Set of trans_ids that will be removed
113
self._removed_id = set()
114
# Mapping of path in old tree -> trans_id
115
self._tree_path_ids = {}
116
# Mapping trans_id -> path in old tree
117
self._tree_id_paths = {}
118
# The trans_id that will be used as the tree root
119
root_id = tree.get_root_id()
120
if root_id is not None:
121
self._new_root = self.trans_id_tree_file_id(root_id)
123
self._new_root = None
124
# Indictor of whether the transform has been applied
128
# Whether the target is case sensitive
129
self._case_sensitive_target = case_sensitive
130
# A counter of how many files have been renamed
131
self.rename_count = 0
134
"""Release the working tree lock, if held.
136
This is required if apply has not been invoked, but can be invoked
139
if self._tree is None:
144
def __get_root(self):
145
return self._new_root
147
root = property(__get_root)
149
def _assign_id(self):
150
"""Produce a new tranform id"""
151
new_id = "new-%s" % self._id_number
155
def create_path(self, name, parent):
156
"""Assign a transaction id to a new path"""
157
trans_id = self._assign_id()
158
unique_add(self._new_name, trans_id, name)
159
unique_add(self._new_parent, trans_id, parent)
162
def adjust_path(self, name, parent, trans_id):
163
"""Change the path that is assigned to a transaction id."""
165
raise ValueError("Parent trans-id may not be None")
166
if trans_id == self._new_root:
168
self._new_name[trans_id] = name
169
self._new_parent[trans_id] = parent
171
def adjust_root_path(self, name, parent):
172
"""Emulate moving the root by moving all children, instead.
174
We do this by undoing the association of root's transaction id with the
175
current tree. This allows us to create a new directory with that
176
transaction id. We unversion the root directory and version the
177
physically new directory, and hope someone versions the tree root
180
old_root = self._new_root
181
old_root_file_id = self.final_file_id(old_root)
182
# force moving all children of root
183
for child_id in self.iter_tree_children(old_root):
184
if child_id != parent:
185
self.adjust_path(self.final_name(child_id),
186
self.final_parent(child_id), child_id)
187
file_id = self.final_file_id(child_id)
188
if file_id is not None:
189
self.unversion_file(child_id)
190
self.version_file(file_id, child_id)
192
# the physical root needs a new transaction id
193
self._tree_path_ids.pop("")
194
self._tree_id_paths.pop(old_root)
195
self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
196
if parent == old_root:
197
parent = self._new_root
198
self.adjust_path(name, parent, old_root)
199
self.create_directory(old_root)
200
self.version_file(old_root_file_id, old_root)
201
self.unversion_file(self._new_root)
203
def fixup_new_roots(self):
204
"""Reinterpret requests to change the root directory
206
Instead of creating a root directory, or moving an existing directory,
207
all the attributes and children of the new root are applied to the
208
existing root directory.
210
This means that the old root trans-id becomes obsolete, so it is
211
recommended only to invoke this after the root trans-id has become
214
new_roots = [k for k, v in self._new_parent.iteritems() if v is
216
if len(new_roots) < 1:
218
if len(new_roots) != 1:
219
raise ValueError('A tree cannot have two roots!')
220
if self._new_root is None:
221
self._new_root = new_roots[0]
223
old_new_root = new_roots[0]
224
# TODO: What to do if a old_new_root is present, but self._new_root is
225
# not listed as being removed? This code explicitly unversions
226
# the old root and versions it with the new file_id. Though that
227
# seems like an incomplete delta
229
# unversion the new root's directory.
230
file_id = self.final_file_id(old_new_root)
231
if old_new_root in self._new_id:
232
self.cancel_versioning(old_new_root)
234
self.unversion_file(old_new_root)
235
# if, at this stage, root still has an old file_id, zap it so we can
236
# stick a new one in.
237
if (self.tree_file_id(self._new_root) is not None and
238
self._new_root not in self._removed_id):
239
self.unversion_file(self._new_root)
240
self.version_file(file_id, self._new_root)
242
# Now move children of new root into old root directory.
243
# Ensure all children are registered with the transaction, but don't
244
# use directly-- some tree children have new parents
245
list(self.iter_tree_children(old_new_root))
246
# Move all children of new root into old root directory.
247
for child in self.by_parent().get(old_new_root, []):
248
self.adjust_path(self.final_name(child), self._new_root, child)
250
# Ensure old_new_root has no directory.
251
if old_new_root in self._new_contents:
252
self.cancel_creation(old_new_root)
254
self.delete_contents(old_new_root)
256
# prevent deletion of root directory.
257
if self._new_root in self._removed_contents:
258
self.cancel_deletion(self._new_root)
260
# destroy path info for old_new_root.
261
del self._new_parent[old_new_root]
262
del self._new_name[old_new_root]
264
def trans_id_tree_file_id(self, inventory_id):
265
"""Determine the transaction id of a working tree file.
267
This reflects only files that already exist, not ones that will be
268
added by transactions.
270
if inventory_id is None:
271
raise ValueError('None is not a valid file id')
272
path = self._tree.id2path(inventory_id)
273
return self.trans_id_tree_path(path)
275
def trans_id_file_id(self, file_id):
276
"""Determine or set the transaction id associated with a file ID.
277
A new id is only created for file_ids that were never present. If
278
a transaction has been unversioned, it is deliberately still returned.
279
(this will likely lead to an unversioned parent conflict.)
282
raise ValueError('None is not a valid file id')
283
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
284
return self._r_new_id[file_id]
287
self._tree.iter_entries_by_dir([file_id]).next()
288
except StopIteration:
289
if file_id in self._non_present_ids:
290
return self._non_present_ids[file_id]
292
trans_id = self._assign_id()
293
self._non_present_ids[file_id] = trans_id
296
return self.trans_id_tree_file_id(file_id)
298
def trans_id_tree_path(self, path):
299
"""Determine (and maybe set) the transaction ID for a tree path."""
300
path = self.canonical_path(path)
301
if path not in self._tree_path_ids:
302
self._tree_path_ids[path] = self._assign_id()
303
self._tree_id_paths[self._tree_path_ids[path]] = path
304
return self._tree_path_ids[path]
306
def get_tree_parent(self, trans_id):
307
"""Determine id of the parent in the tree."""
308
path = self._tree_id_paths[trans_id]
311
return self.trans_id_tree_path(os.path.dirname(path))
313
def delete_contents(self, trans_id):
314
"""Schedule the contents of a path entry for deletion"""
315
# Ensure that the object exists in the WorkingTree, this will raise an
316
# exception if there is a problem
317
self.tree_kind(trans_id)
318
self._removed_contents.add(trans_id)
320
def cancel_deletion(self, trans_id):
321
"""Cancel a scheduled deletion"""
322
self._removed_contents.remove(trans_id)
324
def unversion_file(self, trans_id):
325
"""Schedule a path entry to become unversioned"""
326
self._removed_id.add(trans_id)
328
def delete_versioned(self, trans_id):
329
"""Delete and unversion a versioned file"""
330
self.delete_contents(trans_id)
331
self.unversion_file(trans_id)
333
def set_executability(self, executability, trans_id):
334
"""Schedule setting of the 'execute' bit
335
To unschedule, set to None
337
if executability is None:
338
del self._new_executability[trans_id]
340
unique_add(self._new_executability, trans_id, executability)
342
def set_tree_reference(self, revision_id, trans_id):
343
"""Set the reference associated with a directory"""
344
unique_add(self._new_reference_revision, trans_id, revision_id)
346
def version_file(self, file_id, trans_id):
347
"""Schedule a file to become versioned."""
350
unique_add(self._new_id, trans_id, file_id)
351
unique_add(self._r_new_id, file_id, trans_id)
353
def cancel_versioning(self, trans_id):
354
"""Undo a previous versioning of a file"""
355
file_id = self._new_id[trans_id]
356
del self._new_id[trans_id]
357
del self._r_new_id[file_id]
359
def new_paths(self, filesystem_only=False):
360
"""Determine the paths of all new and changed files.
362
:param filesystem_only: if True, only calculate values for files
363
that require renames or execute bit changes.
367
stale_ids = self._needs_rename.difference(self._new_name)
368
stale_ids.difference_update(self._new_parent)
369
stale_ids.difference_update(self._new_contents)
370
stale_ids.difference_update(self._new_id)
371
needs_rename = self._needs_rename.difference(stale_ids)
372
id_sets = (needs_rename, self._new_executability)
374
id_sets = (self._new_name, self._new_parent, self._new_contents,
375
self._new_id, self._new_executability)
376
for id_set in id_sets:
377
new_ids.update(id_set)
378
return sorted(FinalPaths(self).get_paths(new_ids))
380
def _inventory_altered(self):
381
"""Get the trans_ids and paths of files needing new inv entries."""
383
for id_set in [self._new_name, self._new_parent, self._new_id,
384
self._new_executability]:
385
new_ids.update(id_set)
386
changed_kind = set(self._removed_contents)
387
changed_kind.intersection_update(self._new_contents)
388
changed_kind.difference_update(new_ids)
389
changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
391
new_ids.update(changed_kind)
392
return sorted(FinalPaths(self).get_paths(new_ids))
394
def final_kind(self, trans_id):
395
"""Determine the final file kind, after any changes applied.
397
Raises NoSuchFile if the file does not exist/has no contents.
398
(It is conceivable that a path would be created without the
399
corresponding contents insertion command)
401
if trans_id in self._new_contents:
402
return self._new_contents[trans_id]
403
elif trans_id in self._removed_contents:
404
raise NoSuchFile(None)
406
return self.tree_kind(trans_id)
408
def tree_file_id(self, trans_id):
409
"""Determine the file id associated with the trans_id in the tree"""
411
path = self._tree_id_paths[trans_id]
413
# the file is a new, unversioned file, or invalid trans_id
415
# the file is old; the old id is still valid
416
if self._new_root == trans_id:
417
return self._tree.get_root_id()
418
return self._tree.path2id(path)
420
def final_file_id(self, trans_id):
421
"""Determine the file id after any changes are applied, or None.
423
None indicates that the file will not be versioned after changes are
427
return self._new_id[trans_id]
429
if trans_id in self._removed_id:
431
return self.tree_file_id(trans_id)
433
def inactive_file_id(self, trans_id):
434
"""Return the inactive file_id associated with a transaction id.
435
That is, the one in the tree or in non_present_ids.
436
The file_id may actually be active, too.
438
file_id = self.tree_file_id(trans_id)
439
if file_id is not None:
441
for key, value in self._non_present_ids.iteritems():
442
if value == trans_id:
445
def final_parent(self, trans_id):
446
"""Determine the parent file_id, after any changes are applied.
448
ROOT_PARENT is returned for the tree root.
451
return self._new_parent[trans_id]
453
return self.get_tree_parent(trans_id)
455
def final_name(self, trans_id):
456
"""Determine the final filename, after all changes are applied."""
458
return self._new_name[trans_id]
461
return os.path.basename(self._tree_id_paths[trans_id])
463
raise NoFinalPath(trans_id, self)
466
"""Return a map of parent: children for known parents.
468
Only new paths and parents of tree files with assigned ids are used.
471
items = list(self._new_parent.iteritems())
472
items.extend((t, self.final_parent(t)) for t in
473
self._tree_id_paths.keys())
474
for trans_id, parent_id in items:
475
if parent_id not in by_parent:
476
by_parent[parent_id] = set()
477
by_parent[parent_id].add(trans_id)
480
def path_changed(self, trans_id):
481
"""Return True if a trans_id's path has changed."""
482
return (trans_id in self._new_name) or (trans_id in self._new_parent)
484
def new_contents(self, trans_id):
485
return (trans_id in self._new_contents)
487
def find_conflicts(self):
488
"""Find any violations of inventory or filesystem invariants"""
489
if self._done is True:
490
raise ReusingTransform()
492
# ensure all children of all existent parents are known
493
# all children of non-existent parents are known, by definition.
494
self._add_tree_children()
495
by_parent = self.by_parent()
496
conflicts.extend(self._unversioned_parents(by_parent))
497
conflicts.extend(self._parent_loops())
498
conflicts.extend(self._duplicate_entries(by_parent))
499
conflicts.extend(self._duplicate_ids())
500
conflicts.extend(self._parent_type_conflicts(by_parent))
501
conflicts.extend(self._improper_versioning())
502
conflicts.extend(self._executability_conflicts())
503
conflicts.extend(self._overwrite_conflicts())
506
def _check_malformed(self):
507
conflicts = self.find_conflicts()
508
if len(conflicts) != 0:
509
raise MalformedTransform(conflicts=conflicts)
511
def _add_tree_children(self):
512
"""Add all the children of all active parents to the known paths.
514
Active parents are those which gain children, and those which are
515
removed. This is a necessary first step in detecting conflicts.
517
parents = self.by_parent().keys()
518
parents.extend([t for t in self._removed_contents if
519
self.tree_kind(t) == 'directory'])
520
for trans_id in self._removed_id:
521
file_id = self.tree_file_id(trans_id)
522
if file_id is not None:
523
if self._tree.inventory[file_id].kind == 'directory':
524
parents.append(trans_id)
525
elif self.tree_kind(trans_id) == 'directory':
526
parents.append(trans_id)
528
for parent_id in parents:
529
# ensure that all children are registered with the transaction
530
list(self.iter_tree_children(parent_id))
532
def has_named_child(self, by_parent, parent_id, name):
534
children = by_parent[parent_id]
537
for child in children:
538
if self.final_name(child) == name:
541
path = self._tree_id_paths[parent_id]
544
childpath = joinpath(path, name)
545
child_id = self._tree_path_ids.get(childpath)
547
return lexists(self._tree.abspath(childpath))
549
if self.final_parent(child_id) != parent_id:
551
if child_id in self._removed_contents:
552
# XXX What about dangling file-ids?
557
def _parent_loops(self):
558
"""No entry should be its own ancestor"""
560
for trans_id in self._new_parent:
563
while parent_id is not ROOT_PARENT:
566
parent_id = self.final_parent(parent_id)
569
if parent_id == trans_id:
570
conflicts.append(('parent loop', trans_id))
571
if parent_id in seen:
575
def _unversioned_parents(self, by_parent):
576
"""If parent directories are versioned, children must be versioned."""
578
for parent_id, children in by_parent.iteritems():
579
if parent_id is ROOT_PARENT:
581
if self.final_file_id(parent_id) is not None:
583
for child_id in children:
584
if self.final_file_id(child_id) is not None:
585
conflicts.append(('unversioned parent', parent_id))
589
def _improper_versioning(self):
590
"""Cannot version a file with no contents, or a bad type.
592
However, existing entries with no contents are okay.
595
for trans_id in self._new_id.iterkeys():
597
kind = self.final_kind(trans_id)
599
conflicts.append(('versioning no contents', trans_id))
601
if not InventoryEntry.versionable_kind(kind):
602
conflicts.append(('versioning bad kind', trans_id, kind))
605
def _executability_conflicts(self):
606
"""Check for bad executability changes.
608
Only versioned files may have their executability set, because
609
1. only versioned entries can have executability under windows
610
2. only files can be executable. (The execute bit on a directory
611
does not indicate searchability)
614
for trans_id in self._new_executability:
615
if self.final_file_id(trans_id) is None:
616
conflicts.append(('unversioned executability', trans_id))
619
non_file = self.final_kind(trans_id) != "file"
623
conflicts.append(('non-file executability', trans_id))
626
def _overwrite_conflicts(self):
627
"""Check for overwrites (not permitted on Win32)"""
629
for trans_id in self._new_contents:
631
self.tree_kind(trans_id)
634
if trans_id not in self._removed_contents:
635
conflicts.append(('overwrite', trans_id,
636
self.final_name(trans_id)))
639
def _duplicate_entries(self, by_parent):
640
"""No directory may have two entries with the same name."""
642
if (self._new_name, self._new_parent) == ({}, {}):
644
for children in by_parent.itervalues():
645
name_ids = [(self.final_name(t), t) for t in children]
646
if not self._case_sensitive_target:
647
name_ids = [(n.lower(), t) for n, t in name_ids]
651
for name, trans_id in name_ids:
653
kind = self.final_kind(trans_id)
656
file_id = self.final_file_id(trans_id)
657
if kind is None and file_id is None:
659
if name == last_name:
660
conflicts.append(('duplicate', last_trans_id, trans_id,
663
last_trans_id = trans_id
666
def _duplicate_ids(self):
667
"""Each inventory id may only be used once"""
669
removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
671
all_ids = self._tree.all_file_ids()
672
active_tree_ids = all_ids.difference(removed_tree_ids)
673
for trans_id, file_id in self._new_id.iteritems():
674
if file_id in active_tree_ids:
675
old_trans_id = self.trans_id_tree_file_id(file_id)
676
conflicts.append(('duplicate id', old_trans_id, trans_id))
679
def _parent_type_conflicts(self, by_parent):
680
"""parents must have directory 'contents'."""
682
for parent_id, children in by_parent.iteritems():
683
if parent_id is ROOT_PARENT:
685
if not self._any_contents(children):
687
for child in children:
689
self.final_kind(child)
693
kind = self.final_kind(parent_id)
697
conflicts.append(('missing parent', parent_id))
698
elif kind != "directory":
699
conflicts.append(('non-directory parent', parent_id))
702
def _any_contents(self, trans_ids):
703
"""Return true if any of the trans_ids, will have contents."""
704
for trans_id in trans_ids:
706
kind = self.final_kind(trans_id)
712
def _set_executability(self, path, trans_id):
713
"""Set the executability of versioned files """
714
if supports_executable():
715
new_executability = self._new_executability[trans_id]
716
abspath = self._tree.abspath(path)
717
current_mode = os.stat(abspath).st_mode
718
if new_executability:
721
to_mode = current_mode | (0100 & ~umask)
722
# Enable x-bit for others only if they can read it.
723
if current_mode & 0004:
724
to_mode |= 0001 & ~umask
725
if current_mode & 0040:
726
to_mode |= 0010 & ~umask
728
to_mode = current_mode & ~0111
729
os.chmod(abspath, to_mode)
731
def _new_entry(self, name, parent_id, file_id):
732
"""Helper function to create a new filesystem entry."""
733
trans_id = self.create_path(name, parent_id)
734
if file_id is not None:
735
self.version_file(file_id, trans_id)
738
def new_file(self, name, parent_id, contents, file_id=None,
740
"""Convenience method to create files.
742
name is the name of the file to create.
743
parent_id is the transaction id of the parent directory of the file.
744
contents is an iterator of bytestrings, which will be used to produce
746
:param file_id: The inventory ID of the file, if it is to be versioned.
747
:param executable: Only valid when a file_id has been supplied.
749
trans_id = self._new_entry(name, parent_id, file_id)
750
# TODO: rather than scheduling a set_executable call,
751
# have create_file create the file with the right mode.
752
self.create_file(contents, trans_id)
753
if executable is not None:
754
self.set_executability(executable, trans_id)
757
def new_directory(self, name, parent_id, file_id=None):
758
"""Convenience method to create directories.
760
name is the name of the directory to create.
761
parent_id is the transaction id of the parent directory of the
763
file_id is the inventory ID of the directory, if it is to be versioned.
765
trans_id = self._new_entry(name, parent_id, file_id)
766
self.create_directory(trans_id)
769
def new_symlink(self, name, parent_id, target, file_id=None):
770
"""Convenience method to create symbolic link.
772
name is the name of the symlink to create.
773
parent_id is the transaction id of the parent directory of the symlink.
774
target is a bytestring of the target of the symlink.
775
file_id is the inventory ID of the file, if it is to be versioned.
777
trans_id = self._new_entry(name, parent_id, file_id)
778
self.create_symlink(target, trans_id)
781
def _affected_ids(self):
782
"""Return the set of transform ids affected by the transform"""
783
trans_ids = set(self._removed_id)
784
trans_ids.update(self._new_id.keys())
785
trans_ids.update(self._removed_contents)
786
trans_ids.update(self._new_contents.keys())
787
trans_ids.update(self._new_executability.keys())
788
trans_ids.update(self._new_name.keys())
789
trans_ids.update(self._new_parent.keys())
792
def _get_file_id_maps(self):
793
"""Return mapping of file_ids to trans_ids in the to and from states"""
794
trans_ids = self._affected_ids()
797
# Build up two dicts: trans_ids associated with file ids in the
798
# FROM state, vs the TO state.
799
for trans_id in trans_ids:
800
from_file_id = self.tree_file_id(trans_id)
801
if from_file_id is not None:
802
from_trans_ids[from_file_id] = trans_id
803
to_file_id = self.final_file_id(trans_id)
804
if to_file_id is not None:
805
to_trans_ids[to_file_id] = trans_id
806
return from_trans_ids, to_trans_ids
808
def _from_file_data(self, from_trans_id, from_versioned, file_id):
809
"""Get data about a file in the from (tree) state
811
Return a (name, parent, kind, executable) tuple
813
from_path = self._tree_id_paths.get(from_trans_id)
815
# get data from working tree if versioned
816
from_entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
817
from_name = from_entry.name
818
from_parent = from_entry.parent_id
821
if from_path is None:
822
# File does not exist in FROM state
826
# File exists, but is not versioned. Have to use path-
828
from_name = os.path.basename(from_path)
829
tree_parent = self.get_tree_parent(from_trans_id)
830
from_parent = self.tree_file_id(tree_parent)
831
if from_path is not None:
832
from_kind, from_executable, from_stats = \
833
self._tree._comparison_data(from_entry, from_path)
836
from_executable = False
837
return from_name, from_parent, from_kind, from_executable
839
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
840
"""Get data about a file in the to (target) state
842
Return a (name, parent, kind, executable) tuple
844
to_name = self.final_name(to_trans_id)
846
to_kind = self.final_kind(to_trans_id)
849
to_parent = self.final_file_id(self.final_parent(to_trans_id))
850
if to_trans_id in self._new_executability:
851
to_executable = self._new_executability[to_trans_id]
852
elif to_trans_id == from_trans_id:
853
to_executable = from_executable
855
to_executable = False
856
return to_name, to_parent, to_kind, to_executable
858
def iter_changes(self):
859
"""Produce output in the same format as Tree.iter_changes.
861
Will produce nonsensical results if invoked while inventory/filesystem
862
conflicts (as reported by TreeTransform.find_conflicts()) are present.
864
This reads the Transform, but only reproduces changes involving a
865
file_id. Files that are not versioned in either of the FROM or TO
866
states are not reflected.
868
final_paths = FinalPaths(self)
869
from_trans_ids, to_trans_ids = self._get_file_id_maps()
871
# Now iterate through all active file_ids
872
for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
874
from_trans_id = from_trans_ids.get(file_id)
875
# find file ids, and determine versioning state
876
if from_trans_id is None:
877
from_versioned = False
878
from_trans_id = to_trans_ids[file_id]
880
from_versioned = True
881
to_trans_id = to_trans_ids.get(file_id)
882
if to_trans_id is None:
884
to_trans_id = from_trans_id
888
from_name, from_parent, from_kind, from_executable = \
889
self._from_file_data(from_trans_id, from_versioned, file_id)
891
to_name, to_parent, to_kind, to_executable = \
892
self._to_file_data(to_trans_id, from_trans_id, from_executable)
894
if not from_versioned:
897
from_path = self._tree_id_paths.get(from_trans_id)
901
to_path = final_paths.get_path(to_trans_id)
902
if from_kind != to_kind:
904
elif to_kind in ('file', 'symlink') and (
905
to_trans_id != from_trans_id or
906
to_trans_id in self._new_contents):
908
if (not modified and from_versioned == to_versioned and
909
from_parent==to_parent and from_name == to_name and
910
from_executable == to_executable):
912
results.append((file_id, (from_path, to_path), modified,
913
(from_versioned, to_versioned),
914
(from_parent, to_parent),
915
(from_name, to_name),
916
(from_kind, to_kind),
917
(from_executable, to_executable)))
918
return iter(sorted(results, key=lambda x:x[1]))
920
def get_preview_tree(self):
921
"""Return a tree representing the result of the transform.
923
The tree is a snapshot, and altering the TreeTransform will invalidate
926
return _PreviewTree(self)
928
def commit(self, branch, message, merge_parents=None, strict=False):
929
"""Commit the result of this TreeTransform to a branch.
931
:param branch: The branch to commit to.
932
:param message: The message to attach to the commit.
933
:param merge_parents: Additional parents specified by pending merges.
934
:return: The revision_id of the revision committed.
936
self._check_malformed()
938
unversioned = set(self._new_contents).difference(set(self._new_id))
939
for trans_id in unversioned:
940
if self.final_file_id(trans_id) is None:
941
raise errors.StrictCommitFailed()
943
revno, last_rev_id = branch.last_revision_info()
944
if last_rev_id == _mod_revision.NULL_REVISION:
945
if merge_parents is not None:
946
raise ValueError('Cannot supply merge parents for first'
950
parent_ids = [last_rev_id]
951
if merge_parents is not None:
952
parent_ids.extend(merge_parents)
953
if self._tree.get_revision_id() != last_rev_id:
954
raise ValueError('TreeTransform not based on branch basis: %s' %
955
self._tree.get_revision_id())
956
builder = branch.get_commit_builder(parent_ids)
957
preview = self.get_preview_tree()
958
list(builder.record_iter_changes(preview, last_rev_id,
959
self.iter_changes()))
960
builder.finish_inventory()
961
revision_id = builder.commit(message)
962
branch.set_last_revision_info(revno + 1, revision_id)
965
def _text_parent(self, trans_id):
966
file_id = self.tree_file_id(trans_id)
968
if file_id is None or self._tree.kind(file_id) != 'file':
970
except errors.NoSuchFile:
974
def _get_parents_texts(self, trans_id):
975
"""Get texts for compression parents of this file."""
976
file_id = self._text_parent(trans_id)
979
return (self._tree.get_file_text(file_id),)
981
def _get_parents_lines(self, trans_id):
982
"""Get lines for compression parents of this file."""
983
file_id = self._text_parent(trans_id)
986
return (self._tree.get_file_lines(file_id),)
988
def serialize(self, serializer):
989
"""Serialize this TreeTransform.
991
:param serializer: A Serialiser like pack.ContainerSerializer.
993
new_name = dict((k, v.encode('utf-8')) for k, v in
994
self._new_name.items())
995
new_executability = dict((k, int(v)) for k, v in
996
self._new_executability.items())
997
tree_path_ids = dict((k.encode('utf-8'), v)
998
for k, v in self._tree_path_ids.items())
1000
'_id_number': self._id_number,
1001
'_new_name': new_name,
1002
'_new_parent': self._new_parent,
1003
'_new_executability': new_executability,
1004
'_new_id': self._new_id,
1005
'_tree_path_ids': tree_path_ids,
1006
'_removed_id': list(self._removed_id),
1007
'_removed_contents': list(self._removed_contents),
1008
'_non_present_ids': self._non_present_ids,
1010
yield serializer.bytes_record(bencode.bencode(attribs),
1012
for trans_id, kind in self._new_contents.items():
1014
lines = osutils.chunks_to_lines(
1015
self._read_file_chunks(trans_id))
1016
parents = self._get_parents_lines(trans_id)
1017
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1018
content = ''.join(mpdiff.to_patch())
1019
if kind == 'directory':
1021
if kind == 'symlink':
1022
content = self._read_symlink_target(trans_id)
1023
yield serializer.bytes_record(content, ((trans_id, kind),))
1025
def deserialize(self, records):
1026
"""Deserialize a stored TreeTransform.
1028
:param records: An iterable of (names, content) tuples, as per
1029
pack.ContainerPushParser.
1031
names, content = records.next()
1032
attribs = bencode.bdecode(content)
1033
self._id_number = attribs['_id_number']
1034
self._new_name = dict((k, v.decode('utf-8'))
1035
for k, v in attribs['_new_name'].items())
1036
self._new_parent = attribs['_new_parent']
1037
self._new_executability = dict((k, bool(v)) for k, v in
1038
attribs['_new_executability'].items())
1039
self._new_id = attribs['_new_id']
1040
self._r_new_id = dict((v, k) for k, v in self._new_id.items())
1041
self._tree_path_ids = {}
1042
self._tree_id_paths = {}
1043
for bytepath, trans_id in attribs['_tree_path_ids'].items():
1044
path = bytepath.decode('utf-8')
1045
self._tree_path_ids[path] = trans_id
1046
self._tree_id_paths[trans_id] = path
1047
self._removed_id = set(attribs['_removed_id'])
1048
self._removed_contents = set(attribs['_removed_contents'])
1049
self._non_present_ids = attribs['_non_present_ids']
1050
for ((trans_id, kind),), content in records:
1052
mpdiff = multiparent.MultiParent.from_patch(content)
1053
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1054
self.create_file(lines, trans_id)
1055
if kind == 'directory':
1056
self.create_directory(trans_id)
1057
if kind == 'symlink':
1058
self.create_symlink(content.decode('utf-8'), trans_id)
1061
class DiskTreeTransform(TreeTransformBase):
1062
"""Tree transform storing its contents on disk."""
1064
def __init__(self, tree, limbodir, pb=DummyProgress(),
1065
case_sensitive=True):
1067
:param tree: The tree that will be transformed, but not necessarily
1069
:param limbodir: A directory where new files can be stored until
1070
they are installed in their proper places
1071
:param pb: A ProgressBar indicating how much progress is being made
1072
:param case_sensitive: If True, the target of the transform is
1073
case sensitive, not just case preserving.
1075
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1076
self._limbodir = limbodir
1077
self._deletiondir = None
1078
# A mapping of transform ids to their limbo filename
1079
self._limbo_files = {}
1080
# A mapping of transform ids to a set of the transform ids of children
1081
# that their limbo directory has
1082
self._limbo_children = {}
1083
# Map transform ids to maps of child filename to child transform id
1084
self._limbo_children_names = {}
1085
# List of transform ids that need to be renamed from limbo into place
1086
self._needs_rename = set()
1089
"""Release the working tree lock, if held, clean up limbo dir.
1091
This is required if apply has not been invoked, but can be invoked
1094
if self._tree is None:
1097
entries = [(self._limbo_name(t), t, k) for t, k in
1098
self._new_contents.iteritems()]
1099
entries.sort(reverse=True)
1100
for path, trans_id, kind in entries:
1103
delete_any(self._limbodir)
1105
# We don't especially care *why* the dir is immortal.
1106
raise ImmortalLimbo(self._limbodir)
1108
if self._deletiondir is not None:
1109
delete_any(self._deletiondir)
1111
raise errors.ImmortalPendingDeletion(self._deletiondir)
1113
TreeTransformBase.finalize(self)
1115
def _limbo_name(self, trans_id):
1116
"""Generate the limbo name of a file"""
1117
limbo_name = self._limbo_files.get(trans_id)
1118
if limbo_name is None:
1119
limbo_name = self._generate_limbo_path(trans_id)
1120
self._limbo_files[trans_id] = limbo_name
1123
def _generate_limbo_path(self, trans_id):
1124
"""Generate a limbo path using the trans_id as the relative path.
1126
This is suitable as a fallback, and when the transform should not be
1127
sensitive to the path encoding of the limbo directory.
1129
self._needs_rename.add(trans_id)
1130
return pathjoin(self._limbodir, trans_id)
1132
def adjust_path(self, name, parent, trans_id):
1133
previous_parent = self._new_parent.get(trans_id)
1134
previous_name = self._new_name.get(trans_id)
1135
TreeTransformBase.adjust_path(self, name, parent, trans_id)
1136
if (trans_id in self._limbo_files and
1137
trans_id not in self._needs_rename):
1138
self._rename_in_limbo([trans_id])
1139
if previous_parent != parent:
1140
self._limbo_children[previous_parent].remove(trans_id)
1141
if previous_parent != parent or previous_name != name:
1142
del self._limbo_children_names[previous_parent][previous_name]
1144
def _rename_in_limbo(self, trans_ids):
1145
"""Fix limbo names so that the right final path is produced.
1147
This means we outsmarted ourselves-- we tried to avoid renaming
1148
these files later by creating them with their final names in their
1149
final parents. But now the previous name or parent is no longer
1150
suitable, so we have to rename them.
1152
Even for trans_ids that have no new contents, we must remove their
1153
entries from _limbo_files, because they are now stale.
1155
for trans_id in trans_ids:
1156
old_path = self._limbo_files.pop(trans_id)
1157
if trans_id not in self._new_contents:
1159
new_path = self._limbo_name(trans_id)
1160
os.rename(old_path, new_path)
1162
def create_file(self, contents, trans_id, mode_id=None):
1163
"""Schedule creation of a new file.
1167
Contents is an iterator of strings, all of which will be written
1168
to the target destination.
1170
New file takes the permissions of any existing file with that id,
1171
unless mode_id is specified.
1173
name = self._limbo_name(trans_id)
1174
f = open(name, 'wb')
1177
unique_add(self._new_contents, trans_id, 'file')
1179
# Clean up the file, it never got registered so
1180
# TreeTransform.finalize() won't clean it up.
1185
f.writelines(contents)
1188
self._set_mode(trans_id, mode_id, S_ISREG)
1190
def _read_file_chunks(self, trans_id):
1191
cur_file = open(self._limbo_name(trans_id), 'rb')
1193
return cur_file.readlines()
1197
def _read_symlink_target(self, trans_id):
1198
return os.readlink(self._limbo_name(trans_id))
1200
def create_hardlink(self, path, trans_id):
1201
"""Schedule creation of a hard link"""
1202
name = self._limbo_name(trans_id)
1206
if e.errno != errno.EPERM:
1208
raise errors.HardLinkNotSupported(path)
1210
unique_add(self._new_contents, trans_id, 'file')
1212
# Clean up the file, it never got registered so
1213
# TreeTransform.finalize() won't clean it up.
1217
def create_directory(self, trans_id):
1218
"""Schedule creation of a new directory.
1220
See also new_directory.
1222
os.mkdir(self._limbo_name(trans_id))
1223
unique_add(self._new_contents, trans_id, 'directory')
1225
def create_symlink(self, target, trans_id):
1226
"""Schedule creation of a new symbolic link.
1228
target is a bytestring.
1229
See also new_symlink.
1232
os.symlink(target, self._limbo_name(trans_id))
1233
unique_add(self._new_contents, trans_id, 'symlink')
1236
path = FinalPaths(self).get_path(trans_id)
1239
raise UnableCreateSymlink(path=path)
1241
def cancel_creation(self, trans_id):
1242
"""Cancel the creation of new file contents."""
1243
del self._new_contents[trans_id]
1244
children = self._limbo_children.get(trans_id)
1245
# if this is a limbo directory with children, move them before removing
1247
if children is not None:
1248
self._rename_in_limbo(children)
1249
del self._limbo_children[trans_id]
1250
del self._limbo_children_names[trans_id]
1251
delete_any(self._limbo_name(trans_id))
1254
class TreeTransform(DiskTreeTransform):
1255
"""Represent a tree transformation.
1257
This object is designed to support incremental generation of the transform,
1260
However, it gives optimum performance when parent directories are created
1261
before their contents. The transform is then able to put child files
1262
directly in their parent directory, avoiding later renames.
1264
It is easy to produce malformed transforms, but they are generally
1265
harmless. Attempting to apply a malformed transform will cause an
1266
exception to be raised before any modifications are made to the tree.
1268
Many kinds of malformed transforms can be corrected with the
1269
resolve_conflicts function. The remaining ones indicate programming error,
1270
such as trying to create a file with no path.
1272
Two sets of file creation methods are supplied. Convenience methods are:
1277
These are composed of the low-level methods:
1279
* create_file or create_directory or create_symlink
1283
Transform/Transaction ids
1284
-------------------------
1285
trans_ids are temporary ids assigned to all files involved in a transform.
1286
It's possible, even common, that not all files in the Tree have trans_ids.
1288
trans_ids are used because filenames and file_ids are not good enough
1289
identifiers; filenames change, and not all files have file_ids. File-ids
1290
are also associated with trans-ids, so that moving a file moves its
1293
trans_ids are only valid for the TreeTransform that generated them.
1297
Limbo is a temporary directory use to hold new versions of files.
1298
Files are added to limbo by create_file, create_directory, create_symlink,
1299
and their convenience variants (new_*). Files may be removed from limbo
1300
using cancel_creation. Files are renamed from limbo into their final
1301
location as part of TreeTransform.apply
1303
Limbo must be cleaned up, by either calling TreeTransform.apply or
1304
calling TreeTransform.finalize.
1306
Files are placed into limbo inside their parent directories, where
1307
possible. This reduces subsequent renames, and makes operations involving
1308
lots of files faster. This optimization is only possible if the parent
1309
directory is created *before* creating any of its children, so avoid
1310
creating children before parents, where possible.
1314
This temporary directory is used by _FileMover for storing files that are
1315
about to be deleted. In case of rollback, the files will be restored.
1316
FileMover does not delete files until it is sure that a rollback will not
1319
def __init__(self, tree, pb=DummyProgress()):
1320
"""Note: a tree_write lock is taken on the tree.
1322
Use TreeTransform.finalize() to release the lock (can be omitted if
1323
TreeTransform.apply() called).
1325
tree.lock_tree_write()
1328
limbodir = urlutils.local_path_from_url(
1329
tree._transport.abspath('limbo'))
1333
if e.errno == errno.EEXIST:
1334
raise ExistingLimbo(limbodir)
1335
deletiondir = urlutils.local_path_from_url(
1336
tree._transport.abspath('pending-deletion'))
1338
os.mkdir(deletiondir)
1340
if e.errno == errno.EEXIST:
1341
raise errors.ExistingPendingDeletion(deletiondir)
1346
# Cache of realpath results, to speed up canonical_path
1347
self._realpaths = {}
1348
# Cache of relpath results, to speed up canonical_path
1350
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1351
tree.case_sensitive)
1352
self._deletiondir = deletiondir
1354
def canonical_path(self, path):
1355
"""Get the canonical tree-relative path"""
1356
# don't follow final symlinks
1357
abs = self._tree.abspath(path)
1358
if abs in self._relpaths:
1359
return self._relpaths[abs]
1360
dirname, basename = os.path.split(abs)
1361
if dirname not in self._realpaths:
1362
self._realpaths[dirname] = os.path.realpath(dirname)
1363
dirname = self._realpaths[dirname]
1364
abs = pathjoin(dirname, basename)
1365
if dirname in self._relpaths:
1366
relpath = pathjoin(self._relpaths[dirname], basename)
1367
relpath = relpath.rstrip('/\\')
1369
relpath = self._tree.relpath(abs)
1370
self._relpaths[abs] = relpath
1373
def tree_kind(self, trans_id):
1374
"""Determine the file kind in the working tree.
1376
Raises NoSuchFile if the file does not exist
1378
path = self._tree_id_paths.get(trans_id)
1380
raise NoSuchFile(None)
1382
return file_kind(self._tree.abspath(path))
1384
if e.errno != errno.ENOENT:
1387
raise NoSuchFile(path)
1389
def _set_mode(self, trans_id, mode_id, typefunc):
1390
"""Set the mode of new file contents.
1391
The mode_id is the existing file to get the mode from (often the same
1392
as trans_id). The operation is only performed if there's a mode match
1393
according to typefunc.
1398
old_path = self._tree_id_paths[mode_id]
1402
mode = os.stat(self._tree.abspath(old_path)).st_mode
1404
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1405
# Either old_path doesn't exist, or the parent of the
1406
# target is not a directory (but will be one eventually)
1407
# Either way, we know it doesn't exist *right now*
1408
# See also bug #248448
1413
os.chmod(self._limbo_name(trans_id), mode)
1415
def iter_tree_children(self, parent_id):
1416
"""Iterate through the entry's tree children, if any"""
1418
path = self._tree_id_paths[parent_id]
1422
children = os.listdir(self._tree.abspath(path))
1424
if not (osutils._is_error_enotdir(e)
1425
or e.errno in (errno.ENOENT, errno.ESRCH)):
1429
for child in children:
1430
childpath = joinpath(path, child)
1431
if self._tree.is_control_filename(childpath):
1433
yield self.trans_id_tree_path(childpath)
1435
def _generate_limbo_path(self, trans_id):
1436
"""Generate a limbo path using the final path if possible.
1438
This optimizes the performance of applying the tree transform by
1439
avoiding renames. These renames can be avoided only when the parent
1440
directory is already scheduled for creation.
1442
If the final path cannot be used, falls back to using the trans_id as
1445
parent = self._new_parent.get(trans_id)
1446
# if the parent directory is already in limbo (e.g. when building a
1447
# tree), choose a limbo name inside the parent, to reduce further
1449
use_direct_path = False
1450
if self._new_contents.get(parent) == 'directory':
1451
filename = self._new_name.get(trans_id)
1452
if filename is not None:
1453
if parent not in self._limbo_children:
1454
self._limbo_children[parent] = set()
1455
self._limbo_children_names[parent] = {}
1456
use_direct_path = True
1457
# the direct path can only be used if no other file has
1458
# already taken this pathname, i.e. if the name is unused, or
1459
# if it is already associated with this trans_id.
1460
elif self._case_sensitive_target:
1461
if (self._limbo_children_names[parent].get(filename)
1462
in (trans_id, None)):
1463
use_direct_path = True
1465
for l_filename, l_trans_id in\
1466
self._limbo_children_names[parent].iteritems():
1467
if l_trans_id == trans_id:
1469
if l_filename.lower() == filename.lower():
1472
use_direct_path = True
1474
if not use_direct_path:
1475
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1477
limbo_name = pathjoin(self._limbo_files[parent], filename)
1478
self._limbo_children[parent].add(trans_id)
1479
self._limbo_children_names[parent][filename] = trans_id
1483
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1484
"""Apply all changes to the inventory and filesystem.
1486
If filesystem or inventory conflicts are present, MalformedTransform
1489
If apply succeeds, finalize is not necessary.
1491
:param no_conflicts: if True, the caller guarantees there are no
1492
conflicts, so no check is made.
1493
:param precomputed_delta: An inventory delta to use instead of
1495
:param _mover: Supply an alternate FileMover, for testing
1497
if not no_conflicts:
1498
self._check_malformed()
1499
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1501
if precomputed_delta is None:
1502
child_pb.update('Apply phase', 0, 2)
1503
inventory_delta = self._generate_inventory_delta()
1506
inventory_delta = precomputed_delta
1509
mover = _FileMover()
1513
child_pb.update('Apply phase', 0 + offset, 2 + offset)
1514
self._apply_removals(mover)
1515
child_pb.update('Apply phase', 1 + offset, 2 + offset)
1516
modified_paths = self._apply_insertions(mover)
1521
mover.apply_deletions()
1524
self._tree.apply_inventory_delta(inventory_delta)
1527
return _TransformResults(modified_paths, self.rename_count)
1529
def _generate_inventory_delta(self):
1530
"""Generate an inventory delta for the current transform."""
1531
inventory_delta = []
1532
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1533
new_paths = self._inventory_altered()
1534
total_entries = len(new_paths) + len(self._removed_id)
1536
for num, trans_id in enumerate(self._removed_id):
1538
child_pb.update('removing file', num, total_entries)
1539
if trans_id == self._new_root:
1540
file_id = self._tree.get_root_id()
1542
file_id = self.tree_file_id(trans_id)
1543
# File-id isn't really being deleted, just moved
1544
if file_id in self._r_new_id:
1546
path = self._tree_id_paths[trans_id]
1547
inventory_delta.append((path, None, file_id, None))
1548
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1550
entries = self._tree.iter_entries_by_dir(
1551
new_path_file_ids.values())
1552
old_paths = dict((e.file_id, p) for p, e in entries)
1554
for num, (path, trans_id) in enumerate(new_paths):
1556
child_pb.update('adding file',
1557
num + len(self._removed_id), total_entries)
1558
file_id = new_path_file_ids[trans_id]
1563
kind = self.final_kind(trans_id)
1565
kind = self._tree.stored_kind(file_id)
1566
parent_trans_id = self.final_parent(trans_id)
1567
parent_file_id = new_path_file_ids.get(parent_trans_id)
1568
if parent_file_id is None:
1569
parent_file_id = self.final_file_id(parent_trans_id)
1570
if trans_id in self._new_reference_revision:
1571
new_entry = inventory.TreeReference(
1573
self._new_name[trans_id],
1574
self.final_file_id(self._new_parent[trans_id]),
1575
None, self._new_reference_revision[trans_id])
1577
new_entry = inventory.make_entry(kind,
1578
self.final_name(trans_id),
1579
parent_file_id, file_id)
1580
old_path = old_paths.get(new_entry.file_id)
1581
new_executability = self._new_executability.get(trans_id)
1582
if new_executability is not None:
1583
new_entry.executable = new_executability
1584
inventory_delta.append(
1585
(old_path, path, new_entry.file_id, new_entry))
1588
return inventory_delta
1590
def _apply_removals(self, mover):
1591
"""Perform tree operations that remove directory/inventory names.
1593
That is, delete files that are to be deleted, and put any files that
1594
need renaming into limbo. This must be done in strict child-to-parent
1597
If inventory_delta is None, no inventory delta generation is performed.
1599
tree_paths = list(self._tree_path_ids.iteritems())
1600
tree_paths.sort(reverse=True)
1601
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1603
for num, data in enumerate(tree_paths):
1604
path, trans_id = data
1605
child_pb.update('removing file', num, len(tree_paths))
1606
full_path = self._tree.abspath(path)
1607
if trans_id in self._removed_contents:
1608
delete_path = os.path.join(self._deletiondir, trans_id)
1609
mover.pre_delete(full_path, delete_path)
1610
elif (trans_id in self._new_name
1611
or trans_id in self._new_parent):
1613
mover.rename(full_path, self._limbo_name(trans_id))
1615
if e.errno != errno.ENOENT:
1618
self.rename_count += 1
1622
def _apply_insertions(self, mover):
1623
"""Perform tree operations that insert directory/inventory names.
1625
That is, create any files that need to be created, and restore from
1626
limbo any files that needed renaming. This must be done in strict
1627
parent-to-child order.
1629
If inventory_delta is None, no inventory delta is calculated, and
1630
no list of modified paths is returned.
1632
new_paths = self.new_paths(filesystem_only=True)
1634
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1636
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
1638
for num, (path, trans_id) in enumerate(new_paths):
1640
child_pb.update('adding file', num, len(new_paths))
1641
full_path = self._tree.abspath(path)
1642
if trans_id in self._needs_rename:
1644
mover.rename(self._limbo_name(trans_id), full_path)
1646
# We may be renaming a dangling inventory id
1647
if e.errno != errno.ENOENT:
1650
self.rename_count += 1
1651
if (trans_id in self._new_contents or
1652
self.path_changed(trans_id)):
1653
if trans_id in self._new_contents:
1654
modified_paths.append(full_path)
1655
if trans_id in self._new_executability:
1656
self._set_executability(path, trans_id)
1659
self._new_contents.clear()
1660
return modified_paths
1663
class TransformPreview(DiskTreeTransform):
1664
"""A TreeTransform for generating preview trees.
1666
Unlike TreeTransform, this version works when the input tree is a
1667
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1668
unversioned files in the input tree.
1671
def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
1673
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1674
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1676
def canonical_path(self, path):
1679
def tree_kind(self, trans_id):
1680
path = self._tree_id_paths.get(trans_id)
1682
raise NoSuchFile(None)
1683
file_id = self._tree.path2id(path)
1684
return self._tree.kind(file_id)
1686
def _set_mode(self, trans_id, mode_id, typefunc):
1687
"""Set the mode of new file contents.
1688
The mode_id is the existing file to get the mode from (often the same
1689
as trans_id). The operation is only performed if there's a mode match
1690
according to typefunc.
1692
# is it ok to ignore this? probably
1695
def iter_tree_children(self, parent_id):
1696
"""Iterate through the entry's tree children, if any"""
1698
path = self._tree_id_paths[parent_id]
1701
file_id = self.tree_file_id(parent_id)
1704
entry = self._tree.iter_entries_by_dir([file_id]).next()[1]
1705
children = getattr(entry, 'children', {})
1706
for child in children:
1707
childpath = joinpath(path, child)
1708
yield self.trans_id_tree_path(childpath)
1711
class _PreviewTree(tree.Tree):
1712
"""Partial implementation of Tree to support show_diff_trees"""
1714
def __init__(self, transform):
1715
self._transform = transform
1716
self._final_paths = FinalPaths(transform)
1717
self.__by_parent = None
1718
self._parent_ids = []
1719
self._all_children_cache = {}
1720
self._path2trans_id_cache = {}
1721
self._final_name_cache = {}
1722
self._iter_changes_cache = dict((c[0], c) for c in
1723
self._transform.iter_changes())
1725
def _content_change(self, file_id):
1726
"""Return True if the content of this file changed"""
1727
changes = self._iter_changes_cache.get(file_id)
1728
# changes[2] is true if the file content changed. See
1729
# InterTree.iter_changes.
1730
return (changes is not None and changes[2])
1732
def _get_repository(self):
1733
repo = getattr(self._transform._tree, '_repository', None)
1735
repo = self._transform._tree.branch.repository
1738
def _iter_parent_trees(self):
1739
for revision_id in self.get_parent_ids():
1741
yield self.revision_tree(revision_id)
1742
except errors.NoSuchRevisionInTree:
1743
yield self._get_repository().revision_tree(revision_id)
1745
def _get_file_revision(self, file_id, vf, tree_revision):
1746
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
1747
self._iter_parent_trees()]
1748
vf.add_lines((file_id, tree_revision), parent_keys,
1749
self.get_file(file_id).readlines())
1750
repo = self._get_repository()
1751
base_vf = repo.texts
1752
if base_vf not in vf.fallback_versionedfiles:
1753
vf.fallback_versionedfiles.append(base_vf)
1754
return tree_revision
1756
def _stat_limbo_file(self, file_id):
1757
trans_id = self._transform.trans_id_file_id(file_id)
1758
name = self._transform._limbo_name(trans_id)
1759
return os.lstat(name)
1762
def _by_parent(self):
1763
if self.__by_parent is None:
1764
self.__by_parent = self._transform.by_parent()
1765
return self.__by_parent
1767
def _comparison_data(self, entry, path):
1768
kind, size, executable, link_or_sha1 = self.path_content_summary(path)
1769
if kind == 'missing':
1773
file_id = self._transform.final_file_id(self._path2trans_id(path))
1774
executable = self.is_executable(file_id, path)
1775
return kind, executable, None
1777
def lock_read(self):
1778
# Perhaps in theory, this should lock the TreeTransform?
1785
def inventory(self):
1786
"""This Tree does not use inventory as its backing data."""
1787
raise NotImplementedError(_PreviewTree.inventory)
1789
def get_root_id(self):
1790
return self._transform.final_file_id(self._transform.root)
1792
def all_file_ids(self):
1793
tree_ids = set(self._transform._tree.all_file_ids())
1794
tree_ids.difference_update(self._transform.tree_file_id(t)
1795
for t in self._transform._removed_id)
1796
tree_ids.update(self._transform._new_id.values())
1800
return iter(self.all_file_ids())
1802
def _has_id(self, file_id, fallback_check):
1803
if file_id in self._transform._r_new_id:
1805
elif file_id in set([self._transform.tree_file_id(trans_id) for
1806
trans_id in self._transform._removed_id]):
1809
return fallback_check(file_id)
1811
def has_id(self, file_id):
1812
return self._has_id(file_id, self._transform._tree.has_id)
1814
def has_or_had_id(self, file_id):
1815
return self._has_id(file_id, self._transform._tree.has_or_had_id)
1817
def _path2trans_id(self, path):
1818
# We must not use None here, because that is a valid value to store.
1819
trans_id = self._path2trans_id_cache.get(path, object)
1820
if trans_id is not object:
1822
segments = splitpath(path)
1823
cur_parent = self._transform.root
1824
for cur_segment in segments:
1825
for child in self._all_children(cur_parent):
1826
final_name = self._final_name_cache.get(child)
1827
if final_name is None:
1828
final_name = self._transform.final_name(child)
1829
self._final_name_cache[child] = final_name
1830
if final_name == cur_segment:
1834
self._path2trans_id_cache[path] = None
1836
self._path2trans_id_cache[path] = cur_parent
1839
def path2id(self, path):
1840
return self._transform.final_file_id(self._path2trans_id(path))
1842
def id2path(self, file_id):
1843
trans_id = self._transform.trans_id_file_id(file_id)
1845
return self._final_paths._determine_path(trans_id)
1847
raise errors.NoSuchId(self, file_id)
1849
def _all_children(self, trans_id):
1850
children = self._all_children_cache.get(trans_id)
1851
if children is not None:
1853
children = set(self._transform.iter_tree_children(trans_id))
1854
# children in the _new_parent set are provided by _by_parent.
1855
children.difference_update(self._transform._new_parent.keys())
1856
children.update(self._by_parent.get(trans_id, []))
1857
self._all_children_cache[trans_id] = children
1860
def iter_children(self, file_id):
1861
trans_id = self._transform.trans_id_file_id(file_id)
1862
for child_trans_id in self._all_children(trans_id):
1863
yield self._transform.final_file_id(child_trans_id)
1866
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1867
in self._transform._tree.extras())
1868
possible_extras.update(self._transform._new_contents)
1869
possible_extras.update(self._transform._removed_id)
1870
for trans_id in possible_extras:
1871
if self._transform.final_file_id(trans_id) is None:
1872
yield self._final_paths._determine_path(trans_id)
1874
def _make_inv_entries(self, ordered_entries, specific_file_ids=None,
1875
yield_parents=False):
1876
for trans_id, parent_file_id in ordered_entries:
1877
file_id = self._transform.final_file_id(trans_id)
1880
if (specific_file_ids is not None
1881
and file_id not in specific_file_ids):
1884
kind = self._transform.final_kind(trans_id)
1886
kind = self._transform._tree.stored_kind(file_id)
1887
new_entry = inventory.make_entry(
1889
self._transform.final_name(trans_id),
1890
parent_file_id, file_id)
1891
yield new_entry, trans_id
1893
def _list_files_by_dir(self):
1894
todo = [ROOT_PARENT]
1896
while len(todo) > 0:
1898
parent_file_id = self._transform.final_file_id(parent)
1899
children = list(self._all_children(parent))
1900
paths = dict(zip(children, self._final_paths.get_paths(children)))
1901
children.sort(key=paths.get)
1902
todo.extend(reversed(children))
1903
for trans_id in children:
1904
ordered_ids.append((trans_id, parent_file_id))
1907
def iter_entries_by_dir(self, specific_file_ids=None, yield_parents=False):
1908
# This may not be a maximally efficient implementation, but it is
1909
# reasonably straightforward. An implementation that grafts the
1910
# TreeTransform changes onto the tree's iter_entries_by_dir results
1911
# might be more efficient, but requires tricky inferences about stack
1913
ordered_ids = self._list_files_by_dir()
1914
for entry, trans_id in self._make_inv_entries(ordered_ids,
1915
specific_file_ids, yield_parents=yield_parents):
1916
yield unicode(self._final_paths.get_path(trans_id)), entry
1918
def _iter_entries_for_dir(self, dir_path):
1919
"""Return path, entry for items in a directory without recursing down."""
1920
dir_file_id = self.path2id(dir_path)
1922
for file_id in self.iter_children(dir_file_id):
1923
trans_id = self._transform.trans_id_file_id(file_id)
1924
ordered_ids.append((trans_id, file_id))
1925
for entry, trans_id in self._make_inv_entries(ordered_ids):
1926
yield unicode(self._final_paths.get_path(trans_id)), entry
1928
def list_files(self, include_root=False, from_dir=None, recursive=True):
1929
"""See WorkingTree.list_files."""
1930
# XXX This should behave like WorkingTree.list_files, but is really
1931
# more like RevisionTree.list_files.
1935
prefix = from_dir + '/'
1936
entries = self.iter_entries_by_dir()
1937
for path, entry in entries:
1938
if entry.name == '' and not include_root:
1941
if not path.startswith(prefix):
1943
path = path[len(prefix):]
1944
yield path, 'V', entry.kind, entry.file_id, entry
1946
if from_dir is None and include_root is True:
1947
root_entry = inventory.make_entry('directory', '',
1948
ROOT_PARENT, self.get_root_id())
1949
yield '', 'V', 'directory', root_entry.file_id, root_entry
1950
entries = self._iter_entries_for_dir(from_dir or '')
1951
for path, entry in entries:
1952
yield path, 'V', entry.kind, entry.file_id, entry
1954
def kind(self, file_id):
1955
trans_id = self._transform.trans_id_file_id(file_id)
1956
return self._transform.final_kind(trans_id)
1958
def stored_kind(self, file_id):
1959
trans_id = self._transform.trans_id_file_id(file_id)
1961
return self._transform._new_contents[trans_id]
1963
return self._transform._tree.stored_kind(file_id)
1965
def get_file_mtime(self, file_id, path=None):
1966
"""See Tree.get_file_mtime"""
1967
if not self._content_change(file_id):
1968
return self._transform._tree.get_file_mtime(file_id, path)
1969
return self._stat_limbo_file(file_id).st_mtime
1971
def _file_size(self, entry, stat_value):
1972
return self.get_file_size(entry.file_id)
1974
def get_file_size(self, file_id):
1975
"""See Tree.get_file_size"""
1976
if self.kind(file_id) == 'file':
1977
return self._transform._tree.get_file_size(file_id)
1981
def get_file_sha1(self, file_id, path=None, stat_value=None):
1982
trans_id = self._transform.trans_id_file_id(file_id)
1983
kind = self._transform._new_contents.get(trans_id)
1985
return self._transform._tree.get_file_sha1(file_id)
1987
fileobj = self.get_file(file_id)
1989
return sha_file(fileobj)
1993
def is_executable(self, file_id, path=None):
1996
trans_id = self._transform.trans_id_file_id(file_id)
1998
return self._transform._new_executability[trans_id]
2001
return self._transform._tree.is_executable(file_id, path)
2003
if e.errno == errno.ENOENT:
2006
except errors.NoSuchId:
2009
def path_content_summary(self, path):
2010
trans_id = self._path2trans_id(path)
2011
tt = self._transform
2012
tree_path = tt._tree_id_paths.get(trans_id)
2013
kind = tt._new_contents.get(trans_id)
2015
if tree_path is None or trans_id in tt._removed_contents:
2016
return 'missing', None, None, None
2017
summary = tt._tree.path_content_summary(tree_path)
2018
kind, size, executable, link_or_sha1 = summary
2021
limbo_name = tt._limbo_name(trans_id)
2022
if trans_id in tt._new_reference_revision:
2023
kind = 'tree-reference'
2025
statval = os.lstat(limbo_name)
2026
size = statval.st_size
2027
if not supports_executable():
2030
executable = statval.st_mode & S_IEXEC
2034
if kind == 'symlink':
2035
link_or_sha1 = os.readlink(limbo_name).decode(osutils._fs_enc)
2036
if supports_executable():
2037
executable = tt._new_executability.get(trans_id, executable)
2038
return kind, size, executable, link_or_sha1
2040
def iter_changes(self, from_tree, include_unchanged=False,
2041
specific_files=None, pb=None, extra_trees=None,
2042
require_versioned=True, want_unversioned=False):
2043
"""See InterTree.iter_changes.
2045
This has a fast path that is only used when the from_tree matches
2046
the transform tree, and no fancy options are supplied.
2048
if (from_tree is not self._transform._tree or include_unchanged or
2049
specific_files or want_unversioned):
2050
return tree.InterTree(from_tree, self).iter_changes(
2051
include_unchanged=include_unchanged,
2052
specific_files=specific_files,
2054
extra_trees=extra_trees,
2055
require_versioned=require_versioned,
2056
want_unversioned=want_unversioned)
2057
if want_unversioned:
2058
raise ValueError('want_unversioned is not supported')
2059
return self._transform.iter_changes()
2061
def get_file(self, file_id, path=None):
2062
"""See Tree.get_file"""
2063
if not self._content_change(file_id):
2064
return self._transform._tree.get_file(file_id, path)
2065
trans_id = self._transform.trans_id_file_id(file_id)
2066
name = self._transform._limbo_name(trans_id)
2067
return open(name, 'rb')
2069
def get_file_with_stat(self, file_id, path=None):
2070
return self.get_file(file_id, path), None
2072
def annotate_iter(self, file_id,
2073
default_revision=_mod_revision.CURRENT_REVISION):
2074
changes = self._iter_changes_cache.get(file_id)
2078
changed_content, versioned, kind = (changes[2], changes[3],
2082
get_old = (kind[0] == 'file' and versioned[0])
2084
old_annotation = self._transform._tree.annotate_iter(file_id,
2085
default_revision=default_revision)
2089
return old_annotation
2090
if not changed_content:
2091
return old_annotation
2092
# TODO: This is doing something similar to what WT.annotate_iter is
2093
# doing, however it fails slightly because it doesn't know what
2094
# the *other* revision_id is, so it doesn't know how to give the
2095
# other as the origin for some lines, they all get
2096
# 'default_revision'
2097
# It would be nice to be able to use the new Annotator based
2098
# approach, as well.
2099
return annotate.reannotate([old_annotation],
2100
self.get_file(file_id).readlines(),
2103
def get_symlink_target(self, file_id):
2104
"""See Tree.get_symlink_target"""
2105
if not self._content_change(file_id):
2106
return self._transform._tree.get_symlink_target(file_id)
2107
trans_id = self._transform.trans_id_file_id(file_id)
2108
name = self._transform._limbo_name(trans_id)
2109
return osutils.readlink(name)
2111
def walkdirs(self, prefix=''):
2112
pending = [self._transform.root]
2113
while len(pending) > 0:
2114
parent_id = pending.pop()
2117
prefix = prefix.rstrip('/')
2118
parent_path = self._final_paths.get_path(parent_id)
2119
parent_file_id = self._transform.final_file_id(parent_id)
2120
for child_id in self._all_children(parent_id):
2121
path_from_root = self._final_paths.get_path(child_id)
2122
basename = self._transform.final_name(child_id)
2123
file_id = self._transform.final_file_id(child_id)
2125
kind = self._transform.final_kind(child_id)
2126
versioned_kind = kind
2129
versioned_kind = self._transform._tree.stored_kind(file_id)
2130
if versioned_kind == 'directory':
2131
subdirs.append(child_id)
2132
children.append((path_from_root, basename, kind, None,
2133
file_id, versioned_kind))
2135
if parent_path.startswith(prefix):
2136
yield (parent_path, parent_file_id), children
2137
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2140
def get_parent_ids(self):
2141
return self._parent_ids
2143
def set_parent_ids(self, parent_ids):
2144
self._parent_ids = parent_ids
2146
def get_revision_tree(self, revision_id):
2147
return self._transform._tree.get_revision_tree(revision_id)
2150
def joinpath(parent, child):
2151
"""Join tree-relative paths, handling the tree root specially"""
2152
if parent is None or parent == "":
2155
return pathjoin(parent, child)
2158
class FinalPaths(object):
2159
"""Make path calculation cheap by memoizing paths.
2161
The underlying tree must not be manipulated between calls, or else
2162
the results will likely be incorrect.
2164
def __init__(self, transform):
2165
object.__init__(self)
2166
self._known_paths = {}
2167
self.transform = transform
2169
def _determine_path(self, trans_id):
2170
if (trans_id == self.transform.root or trans_id == ROOT_PARENT):
2172
name = self.transform.final_name(trans_id)
2173
parent_id = self.transform.final_parent(trans_id)
2174
if parent_id == self.transform.root:
2177
return pathjoin(self.get_path(parent_id), name)
2179
def get_path(self, trans_id):
2180
"""Find the final path associated with a trans_id"""
2181
if trans_id not in self._known_paths:
2182
self._known_paths[trans_id] = self._determine_path(trans_id)
2183
return self._known_paths[trans_id]
2185
def get_paths(self, trans_ids):
2186
return [(self.get_path(t), t) for t in trans_ids]
2190
def topology_sorted_ids(tree):
2191
"""Determine the topological order of the ids in a tree"""
2192
file_ids = list(tree)
2193
file_ids.sort(key=tree.id2path)
2197
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
2198
delta_from_tree=False):
2199
"""Create working tree for a branch, using a TreeTransform.
2201
This function should be used on empty trees, having a tree root at most.
2202
(see merge and revert functionality for working with existing trees)
2204
Existing files are handled like so:
2206
- Existing bzrdirs take precedence over creating new items. They are
2207
created as '%s.diverted' % name.
2208
- Otherwise, if the content on disk matches the content we are building,
2209
it is silently replaced.
2210
- Otherwise, conflict resolution will move the old file to 'oldname.moved'.
2212
:param tree: The tree to convert wt into a copy of
2213
:param wt: The working tree that files will be placed into
2214
:param accelerator_tree: A tree which can be used for retrieving file
2215
contents more quickly than tree itself, i.e. a workingtree. tree
2216
will be used for cases where accelerator_tree's content is different.
2217
:param hardlink: If true, hard-link files to accelerator_tree, where
2218
possible. accelerator_tree must implement abspath, i.e. be a
2220
:param delta_from_tree: If true, build_tree may use the input Tree to
2221
generate the inventory delta.
2223
wt.lock_tree_write()
2227
if accelerator_tree is not None:
2228
accelerator_tree.lock_read()
2230
return _build_tree(tree, wt, accelerator_tree, hardlink,
2233
if accelerator_tree is not None:
2234
accelerator_tree.unlock()
2241
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
2242
"""See build_tree."""
2243
for num, _unused in enumerate(wt.all_file_ids()):
2244
if num > 0: # more than just a root
2245
raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
2246
existing_files = set()
2247
for dir, files in wt.walkdirs():
2248
existing_files.update(f[0] for f in files)
2250
top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2251
pp = ProgressPhase("Build phase", 2, top_pb)
2252
if tree.inventory.root is not None:
2253
# This is kind of a hack: we should be altering the root
2254
# as part of the regular tree shape diff logic.
2255
# The conditional test here is to avoid doing an
2256
# expensive operation (flush) every time the root id
2257
# is set within the tree, nor setting the root and thus
2258
# marking the tree as dirty, because we use two different
2259
# idioms here: tree interfaces and inventory interfaces.
2260
if wt.get_root_id() != tree.get_root_id():
2261
wt.set_root_id(tree.get_root_id())
2263
tt = TreeTransform(wt)
2267
file_trans_id[wt.get_root_id()] = \
2268
tt.trans_id_tree_file_id(wt.get_root_id())
2269
pb = bzrlib.ui.ui_factory.nested_progress_bar()
2271
deferred_contents = []
2273
total = len(tree.inventory)
2275
precomputed_delta = []
2277
precomputed_delta = None
2278
for num, (tree_path, entry) in \
2279
enumerate(tree.inventory.iter_entries_by_dir()):
2280
pb.update("Building tree", num - len(deferred_contents), total)
2281
if entry.parent_id is None:
2284
file_id = entry.file_id
2286
precomputed_delta.append((None, tree_path, file_id, entry))
2287
if tree_path in existing_files:
2288
target_path = wt.abspath(tree_path)
2289
kind = file_kind(target_path)
2290
if kind == "directory":
2292
bzrdir.BzrDir.open(target_path)
2293
except errors.NotBranchError:
2297
if (file_id not in divert and
2298
_content_match(tree, entry, file_id, kind,
2300
tt.delete_contents(tt.trans_id_tree_path(tree_path))
2301
if kind == 'directory':
2303
parent_id = file_trans_id[entry.parent_id]
2304
if entry.kind == 'file':
2305
# We *almost* replicate new_by_entry, so that we can defer
2306
# getting the file text, and get them all at once.
2307
trans_id = tt.create_path(entry.name, parent_id)
2308
file_trans_id[file_id] = trans_id
2309
tt.version_file(file_id, trans_id)
2310
executable = tree.is_executable(file_id, tree_path)
2312
tt.set_executability(executable, trans_id)
2313
trans_data = (trans_id, tree_path)
2314
deferred_contents.append((file_id, trans_data))
2316
file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
2319
new_trans_id = file_trans_id[file_id]
2320
old_parent = tt.trans_id_tree_path(tree_path)
2321
_reparent_children(tt, old_parent, new_trans_id)
2322
offset = num + 1 - len(deferred_contents)
2323
_create_files(tt, tree, deferred_contents, pb, offset,
2324
accelerator_tree, hardlink)
2328
divert_trans = set(file_trans_id[f] for f in divert)
2329
resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
2330
raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2331
if len(raw_conflicts) > 0:
2332
precomputed_delta = None
2333
conflicts = cook_conflicts(raw_conflicts, tt)
2334
for conflict in conflicts:
2337
wt.add_conflicts(conflicts)
2338
except errors.UnsupportedOperation:
2340
result = tt.apply(no_conflicts=True,
2341
precomputed_delta=precomputed_delta)
2348
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
2350
total = len(desired_files) + offset
2352
if accelerator_tree is None:
2353
new_desired_files = desired_files
2355
iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
2356
unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
2357
in iter if not (c or e[0] != e[1]))
2358
new_desired_files = []
2360
for file_id, (trans_id, tree_path) in desired_files:
2361
accelerator_path = unchanged.get(file_id)
2362
if accelerator_path is None:
2363
new_desired_files.append((file_id, (trans_id, tree_path)))
2365
pb.update('Adding file contents', count + offset, total)
2367
tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
2370
contents = accelerator_tree.get_file(file_id, accelerator_path)
2371
if wt.supports_content_filtering():
2372
filters = wt._content_filter_stack(tree_path)
2373
contents = filtered_output_bytes(contents, filters,
2374
ContentFilterContext(tree_path, tree))
2376
tt.create_file(contents, trans_id)
2380
except AttributeError:
2381
# after filtering, contents may no longer be file-like
2385
for count, ((trans_id, tree_path), contents) in enumerate(
2386
tree.iter_files_bytes(new_desired_files)):
2387
if wt.supports_content_filtering():
2388
filters = wt._content_filter_stack(tree_path)
2389
contents = filtered_output_bytes(contents, filters,
2390
ContentFilterContext(tree_path, tree))
2391
tt.create_file(contents, trans_id)
2392
pb.update('Adding file contents', count + offset, total)
2395
def _reparent_children(tt, old_parent, new_parent):
2396
for child in tt.iter_tree_children(old_parent):
2397
tt.adjust_path(tt.final_name(child), new_parent, child)
2399
def _reparent_transform_children(tt, old_parent, new_parent):
2400
by_parent = tt.by_parent()
2401
for child in by_parent[old_parent]:
2402
tt.adjust_path(tt.final_name(child), new_parent, child)
2403
return by_parent[old_parent]
2405
def _content_match(tree, entry, file_id, kind, target_path):
2406
if entry.kind != kind:
2408
if entry.kind == "directory":
2410
if entry.kind == "file":
2411
if tree.get_file(file_id).read() == file(target_path, 'rb').read():
2413
elif entry.kind == "symlink":
2414
if tree.get_symlink_target(file_id) == os.readlink(target_path):
2419
def resolve_checkout(tt, conflicts, divert):
2420
new_conflicts = set()
2421
for c_type, conflict in ((c[0], c) for c in conflicts):
2422
# Anything but a 'duplicate' would indicate programmer error
2423
if c_type != 'duplicate':
2424
raise AssertionError(c_type)
2425
# Now figure out which is new and which is old
2426
if tt.new_contents(conflict[1]):
2427
new_file = conflict[1]
2428
old_file = conflict[2]
2430
new_file = conflict[2]
2431
old_file = conflict[1]
2433
# We should only get here if the conflict wasn't completely
2435
final_parent = tt.final_parent(old_file)
2436
if new_file in divert:
2437
new_name = tt.final_name(old_file)+'.diverted'
2438
tt.adjust_path(new_name, final_parent, new_file)
2439
new_conflicts.add((c_type, 'Diverted to',
2440
new_file, old_file))
2442
new_name = tt.final_name(old_file)+'.moved'
2443
tt.adjust_path(new_name, final_parent, old_file)
2444
new_conflicts.add((c_type, 'Moved existing file to',
2445
old_file, new_file))
2446
return new_conflicts
2449
def new_by_entry(tt, entry, parent_id, tree):
2450
"""Create a new file according to its inventory entry"""
2454
contents = tree.get_file(entry.file_id).readlines()
2455
executable = tree.is_executable(entry.file_id)
2456
return tt.new_file(name, parent_id, contents, entry.file_id,
2458
elif kind in ('directory', 'tree-reference'):
2459
trans_id = tt.new_directory(name, parent_id, entry.file_id)
2460
if kind == 'tree-reference':
2461
tt.set_tree_reference(entry.reference_revision, trans_id)
2463
elif kind == 'symlink':
2464
target = tree.get_symlink_target(entry.file_id)
2465
return tt.new_symlink(name, parent_id, target, entry.file_id)
2467
raise errors.BadFileKindError(name, kind)
2470
@deprecated_function(deprecated_in((1, 9, 0)))
2471
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
2472
"""Create new file contents according to an inventory entry.
2474
DEPRECATED. Use create_from_tree instead.
2476
if entry.kind == "file":
2478
lines = tree.get_file(entry.file_id).readlines()
2479
tt.create_file(lines, trans_id, mode_id=mode_id)
2480
elif entry.kind == "symlink":
2481
tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
2482
elif entry.kind == "directory":
2483
tt.create_directory(trans_id)
2486
def create_from_tree(tt, trans_id, tree, file_id, bytes=None,
2487
filter_tree_path=None):
2488
"""Create new file contents according to tree contents.
2490
:param filter_tree_path: the tree path to use to lookup
2491
content filters to apply to the bytes output in the working tree.
2492
This only applies if the working tree supports content filtering.
2494
kind = tree.kind(file_id)
2495
if kind == 'directory':
2496
tt.create_directory(trans_id)
2497
elif kind == "file":
2499
tree_file = tree.get_file(file_id)
2501
bytes = tree_file.readlines()
2505
if wt.supports_content_filtering() and filter_tree_path is not None:
2506
filters = wt._content_filter_stack(filter_tree_path)
2507
bytes = filtered_output_bytes(bytes, filters,
2508
ContentFilterContext(filter_tree_path, tree))
2509
tt.create_file(bytes, trans_id)
2510
elif kind == "symlink":
2511
tt.create_symlink(tree.get_symlink_target(file_id), trans_id)
2513
raise AssertionError('Unknown kind %r' % kind)
2516
def create_entry_executability(tt, entry, trans_id):
2517
"""Set the executability of a trans_id according to an inventory entry"""
2518
if entry.kind == "file":
2519
tt.set_executability(entry.executable, trans_id)
2522
def get_backup_name(entry, by_parent, parent_trans_id, tt):
2523
return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
2526
def _get_backup_name(name, by_parent, parent_trans_id, tt):
2527
"""Produce a backup-style name that appears to be available"""
2531
yield "%s.~%d~" % (name, counter)
2533
for new_name in name_gen():
2534
if not tt.has_named_child(by_parent, parent_trans_id, new_name):
2538
def _entry_changes(file_id, entry, working_tree):
2539
"""Determine in which ways the inventory entry has changed.
2541
Returns booleans: has_contents, content_mod, meta_mod
2542
has_contents means there are currently contents, but they differ
2543
contents_mod means contents need to be modified
2544
meta_mod means the metadata needs to be modified
2546
cur_entry = working_tree.inventory[file_id]
2548
working_kind = working_tree.kind(file_id)
2551
has_contents = False
2554
if has_contents is True:
2555
if entry.kind != working_kind:
2556
contents_mod, meta_mod = True, False
2558
cur_entry._read_tree_state(working_tree.id2path(file_id),
2560
contents_mod, meta_mod = entry.detect_changes(cur_entry)
2561
cur_entry._forget_tree_state()
2562
return has_contents, contents_mod, meta_mod
2565
def revert(working_tree, target_tree, filenames, backups=False,
2566
pb=DummyProgress(), change_reporter=None):
2567
"""Revert a working tree's contents to those of a target tree."""
2568
target_tree.lock_read()
2569
tt = TreeTransform(working_tree, pb)
2571
pp = ProgressPhase("Revert phase", 3, pb)
2572
conflicts, merge_modified = _prepare_revert_transform(
2573
working_tree, target_tree, tt, filenames, backups, pp)
2575
change_reporter = delta._ChangeReporter(
2576
unversioned_filter=working_tree.is_ignored)
2577
delta.report_changes(tt.iter_changes(), change_reporter)
2578
for conflict in conflicts:
2582
working_tree.set_merge_modified(merge_modified)
2584
target_tree.unlock()
2590
def _prepare_revert_transform(working_tree, target_tree, tt, filenames,
2591
backups, pp, basis_tree=None,
2592
merge_modified=None):
2594
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2596
if merge_modified is None:
2597
merge_modified = working_tree.merge_modified()
2598
merge_modified = _alter_files(working_tree, target_tree, tt,
2599
child_pb, filenames, backups,
2600
merge_modified, basis_tree)
2604
child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
2606
raw_conflicts = resolve_conflicts(tt, child_pb,
2607
lambda t, c: conflict_pass(t, c, target_tree))
2610
conflicts = cook_conflicts(raw_conflicts, tt)
2611
return conflicts, merge_modified
2614
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
2615
backups, merge_modified, basis_tree=None):
2616
if basis_tree is not None:
2617
basis_tree.lock_read()
2618
change_list = target_tree.iter_changes(working_tree,
2619
specific_files=specific_files, pb=pb)
2620
if target_tree.get_root_id() is None:
2626
for id_num, (file_id, path, changed_content, versioned, parent, name,
2627
kind, executable) in enumerate(change_list):
2628
if skip_root and file_id[0] is not None and parent[0] is None:
2630
trans_id = tt.trans_id_file_id(file_id)
2633
keep_content = False
2634
if kind[0] == 'file' and (backups or kind[1] is None):
2635
wt_sha1 = working_tree.get_file_sha1(file_id)
2636
if merge_modified.get(file_id) != wt_sha1:
2637
# acquire the basis tree lazily to prevent the
2638
# expense of accessing it when it's not needed ?
2639
# (Guessing, RBC, 200702)
2640
if basis_tree is None:
2641
basis_tree = working_tree.basis_tree()
2642
basis_tree.lock_read()
2643
if file_id in basis_tree:
2644
if wt_sha1 != basis_tree.get_file_sha1(file_id):
2646
elif kind[1] is None and not versioned[1]:
2648
if kind[0] is not None:
2649
if not keep_content:
2650
tt.delete_contents(trans_id)
2651
elif kind[1] is not None:
2652
parent_trans_id = tt.trans_id_file_id(parent[0])
2653
by_parent = tt.by_parent()
2654
backup_name = _get_backup_name(name[0], by_parent,
2655
parent_trans_id, tt)
2656
tt.adjust_path(backup_name, parent_trans_id, trans_id)
2657
new_trans_id = tt.create_path(name[0], parent_trans_id)
2658
if versioned == (True, True):
2659
tt.unversion_file(trans_id)
2660
tt.version_file(file_id, new_trans_id)
2661
# New contents should have the same unix perms as old
2664
trans_id = new_trans_id
2665
if kind[1] in ('directory', 'tree-reference'):
2666
tt.create_directory(trans_id)
2667
if kind[1] == 'tree-reference':
2668
revision = target_tree.get_reference_revision(file_id,
2670
tt.set_tree_reference(revision, trans_id)
2671
elif kind[1] == 'symlink':
2672
tt.create_symlink(target_tree.get_symlink_target(file_id),
2674
elif kind[1] == 'file':
2675
deferred_files.append((file_id, (trans_id, mode_id)))
2676
if basis_tree is None:
2677
basis_tree = working_tree.basis_tree()
2678
basis_tree.lock_read()
2679
new_sha1 = target_tree.get_file_sha1(file_id)
2680
if (file_id in basis_tree and new_sha1 ==
2681
basis_tree.get_file_sha1(file_id)):
2682
if file_id in merge_modified:
2683
del merge_modified[file_id]
2685
merge_modified[file_id] = new_sha1
2687
# preserve the execute bit when backing up
2688
if keep_content and executable[0] == executable[1]:
2689
tt.set_executability(executable[1], trans_id)
2690
elif kind[1] is not None:
2691
raise AssertionError(kind[1])
2692
if versioned == (False, True):
2693
tt.version_file(file_id, trans_id)
2694
if versioned == (True, False):
2695
tt.unversion_file(trans_id)
2696
if (name[1] is not None and
2697
(name[0] != name[1] or parent[0] != parent[1])):
2698
if name[1] == '' and parent[1] is None:
2699
parent_trans = ROOT_PARENT
2701
parent_trans = tt.trans_id_file_id(parent[1])
2702
if parent[0] is None and versioned[0]:
2703
tt.adjust_root_path(name[1], parent_trans)
2705
tt.adjust_path(name[1], parent_trans, trans_id)
2706
if executable[0] != executable[1] and kind[1] == "file":
2707
tt.set_executability(executable[1], trans_id)
2708
if working_tree.supports_content_filtering():
2709
for index, ((trans_id, mode_id), bytes) in enumerate(
2710
target_tree.iter_files_bytes(deferred_files)):
2711
file_id = deferred_files[index][0]
2712
# We're reverting a tree to the target tree so using the
2713
# target tree to find the file path seems the best choice
2714
# here IMO - Ian C 27/Oct/2009
2715
filter_tree_path = target_tree.id2path(file_id)
2716
filters = working_tree._content_filter_stack(filter_tree_path)
2717
bytes = filtered_output_bytes(bytes, filters,
2718
ContentFilterContext(filter_tree_path, working_tree))
2719
tt.create_file(bytes, trans_id, mode_id)
2721
for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
2723
tt.create_file(bytes, trans_id, mode_id)
2724
tt.fixup_new_roots()
2726
if basis_tree is not None:
2728
return merge_modified
2731
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
2732
"""Make many conflict-resolution attempts, but die if they fail"""
2733
if pass_func is None:
2734
pass_func = conflict_pass
2735
new_conflicts = set()
2738
pb.update('Resolution pass', n+1, 10)
2739
conflicts = tt.find_conflicts()
2740
if len(conflicts) == 0:
2741
return new_conflicts
2742
new_conflicts.update(pass_func(tt, conflicts))
2743
raise MalformedTransform(conflicts=conflicts)
2748
def conflict_pass(tt, conflicts, path_tree=None):
2749
"""Resolve some classes of conflicts.
2751
:param tt: The transform to resolve conflicts in
2752
:param conflicts: The conflicts to resolve
2753
:param path_tree: A Tree to get supplemental paths from
2755
new_conflicts = set()
2756
for c_type, conflict in ((c[0], c) for c in conflicts):
2757
if c_type == 'duplicate id':
2758
tt.unversion_file(conflict[1])
2759
new_conflicts.add((c_type, 'Unversioned existing file',
2760
conflict[1], conflict[2], ))
2761
elif c_type == 'duplicate':
2762
# files that were renamed take precedence
2763
final_parent = tt.final_parent(conflict[1])
2764
if tt.path_changed(conflict[1]):
2765
existing_file, new_file = conflict[2], conflict[1]
2767
existing_file, new_file = conflict[1], conflict[2]
2768
new_name = tt.final_name(existing_file)+'.moved'
2769
tt.adjust_path(new_name, final_parent, existing_file)
2770
new_conflicts.add((c_type, 'Moved existing file to',
2771
existing_file, new_file))
2772
elif c_type == 'parent loop':
2773
# break the loop by undoing one of the ops that caused the loop
2775
while not tt.path_changed(cur):
2776
cur = tt.final_parent(cur)
2777
new_conflicts.add((c_type, 'Cancelled move', cur,
2778
tt.final_parent(cur),))
2779
tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
2781
elif c_type == 'missing parent':
2782
trans_id = conflict[1]
2784
tt.cancel_deletion(trans_id)
2785
new_conflicts.add(('deleting parent', 'Not deleting',
2790
tt.final_name(trans_id)
2792
if path_tree is not None:
2793
file_id = tt.final_file_id(trans_id)
2795
file_id = tt.inactive_file_id(trans_id)
2796
entry = path_tree.inventory[file_id]
2797
# special-case the other tree root (move its
2798
# children to current root)
2799
if entry.parent_id is None:
2801
moved = _reparent_transform_children(
2802
tt, trans_id, tt.root)
2804
new_conflicts.add((c_type, 'Moved to root',
2807
parent_trans_id = tt.trans_id_file_id(
2809
tt.adjust_path(entry.name, parent_trans_id,
2812
tt.create_directory(trans_id)
2813
new_conflicts.add((c_type, 'Created directory', trans_id))
2814
elif c_type == 'unversioned parent':
2815
file_id = tt.inactive_file_id(conflict[1])
2816
# special-case the other tree root (move its children instead)
2817
if path_tree and file_id in path_tree:
2818
if path_tree.inventory[file_id].parent_id is None:
2820
tt.version_file(file_id, conflict[1])
2821
new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
2822
elif c_type == 'non-directory parent':
2823
parent_id = conflict[1]
2824
parent_parent = tt.final_parent(parent_id)
2825
parent_name = tt.final_name(parent_id)
2826
parent_file_id = tt.final_file_id(parent_id)
2827
new_parent_id = tt.new_directory(parent_name + '.new',
2828
parent_parent, parent_file_id)
2829
_reparent_transform_children(tt, parent_id, new_parent_id)
2830
if parent_file_id is not None:
2831
tt.unversion_file(parent_id)
2832
new_conflicts.add((c_type, 'Created directory', new_parent_id))
2833
elif c_type == 'versioning no contents':
2834
tt.cancel_versioning(conflict[1])
2835
return new_conflicts
2838
def cook_conflicts(raw_conflicts, tt):
2839
"""Generate a list of cooked conflicts, sorted by file path"""
2840
from bzrlib.conflicts import Conflict
2841
conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
2842
return sorted(conflict_iter, key=Conflict.sort_key)
2845
def iter_cook_conflicts(raw_conflicts, tt):
2846
from bzrlib.conflicts import Conflict
2848
for conflict in raw_conflicts:
2849
c_type = conflict[0]
2850
action = conflict[1]
2851
modified_path = fp.get_path(conflict[2])
2852
modified_id = tt.final_file_id(conflict[2])
2853
if len(conflict) == 3:
2854
yield Conflict.factory(c_type, action=action, path=modified_path,
2855
file_id=modified_id)
2858
conflicting_path = fp.get_path(conflict[3])
2859
conflicting_id = tt.final_file_id(conflict[3])
2860
yield Conflict.factory(c_type, action=action, path=modified_path,
2861
file_id=modified_id,
2862
conflict_path=conflicting_path,
2863
conflict_file_id=conflicting_id)
2866
class _FileMover(object):
2867
"""Moves and deletes files for TreeTransform, tracking operations"""
2870
self.past_renames = []
2871
self.pending_deletions = []
2873
def rename(self, from_, to):
2874
"""Rename a file from one path to another. Functions like os.rename"""
2876
os.rename(from_, to)
2878
if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
2879
raise errors.FileExists(to, str(e))
2881
self.past_renames.append((from_, to))
2883
def pre_delete(self, from_, to):
2884
"""Rename a file out of the way and mark it for deletion.
2886
Unlike os.unlink, this works equally well for files and directories.
2887
:param from_: The current file path
2888
:param to: A temporary path for the file
2890
self.rename(from_, to)
2891
self.pending_deletions.append(to)
2894
"""Reverse all renames that have been performed"""
2895
for from_, to in reversed(self.past_renames):
2896
os.rename(to, from_)
2897
# after rollback, don't reuse _FileMover
2899
pending_deletions = None
2901
def apply_deletions(self):
2902
"""Apply all marked deletions"""
2903
for path in self.pending_deletions:
2905
# after apply_deletions, don't reuse _FileMover
2907
pending_deletions = None