1
# Copyright (C) 2006-2011 Canonical Ltd
2
# Copyright (C) 2020 Breezy Developers
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
from __future__ import absolute_import
22
from stat import S_ISREG
25
from .. import errors, multiparent, osutils, trace, ui, urlutils
26
from ..i18n import gettext
27
from ..mutabletree import MutableTree
28
from ..sixish import viewitems, viewvalues
29
from ..transform import (
36
TransformRenameFailed,
43
from ..bzr import inventory
44
from ..bzr.transform import TransformPreview as GitTransformPreview
47
class TreeTransformBase(TreeTransform):
48
"""The base class for TreeTransform and its kin."""
50
def __init__(self, tree, pb=None, case_sensitive=True):
53
:param tree: The tree that will be transformed, but not necessarily
56
:param case_sensitive: If True, the target of the transform is
57
case sensitive, not just case preserving.
59
super(TreeTransformBase, self).__init__(tree, pb=pb)
60
# mapping of trans_id => (sha1 of content, stat_value)
61
self._observed_sha1s = {}
62
# Mapping of trans_id -> new file_id
64
# Mapping of old file-id -> trans_id
65
self._non_present_ids = {}
66
# Mapping of new file_id -> trans_id
68
# The trans_id that will be used as the tree root
69
if tree.is_versioned(''):
70
self._new_root = self.trans_id_tree_path('')
73
# Whether the target is case sensitive
74
self._case_sensitive_target = case_sensitive
77
"""Release the working tree lock, if held.
79
This is required if apply has not been invoked, but can be invoked
82
if self._tree is None:
84
for hook in MutableTree.hooks['post_transform']:
85
hook(self._tree, self)
92
root = property(__get_root)
94
def create_path(self, name, parent):
95
"""Assign a transaction id to a new path"""
96
trans_id = self._assign_id()
97
unique_add(self._new_name, trans_id, name)
98
unique_add(self._new_parent, trans_id, parent)
101
def adjust_root_path(self, name, parent):
102
"""Emulate moving the root by moving all children, instead.
104
We do this by undoing the association of root's transaction id with the
105
current tree. This allows us to create a new directory with that
106
transaction id. We unversion the root directory and version the
107
physically new directory, and hope someone versions the tree root
110
old_root = self._new_root
111
old_root_file_id = self.final_file_id(old_root)
112
# force moving all children of root
113
for child_id in self.iter_tree_children(old_root):
114
if child_id != parent:
115
self.adjust_path(self.final_name(child_id),
116
self.final_parent(child_id), child_id)
117
file_id = self.final_file_id(child_id)
118
if file_id is not None:
119
self.unversion_file(child_id)
120
self.version_file(child_id, file_id=file_id)
122
# the physical root needs a new transaction id
123
self._tree_path_ids.pop("")
124
self._tree_id_paths.pop(old_root)
125
self._new_root = self.trans_id_tree_path('')
126
if parent == old_root:
127
parent = self._new_root
128
self.adjust_path(name, parent, old_root)
129
self.create_directory(old_root)
130
self.version_file(old_root, file_id=old_root_file_id)
131
self.unversion_file(self._new_root)
133
def fixup_new_roots(self):
134
"""Reinterpret requests to change the root directory
136
Instead of creating a root directory, or moving an existing directory,
137
all the attributes and children of the new root are applied to the
138
existing root directory.
140
This means that the old root trans-id becomes obsolete, so it is
141
recommended only to invoke this after the root trans-id has become
145
new_roots = [k for k, v in viewitems(self._new_parent)
147
if len(new_roots) < 1:
149
if len(new_roots) != 1:
150
raise ValueError('A tree cannot have two roots!')
151
if self._new_root is None:
152
self._new_root = new_roots[0]
154
old_new_root = new_roots[0]
155
# unversion the new root's directory.
156
if self.final_kind(self._new_root) is None:
157
file_id = self.final_file_id(old_new_root)
159
file_id = self.final_file_id(self._new_root)
160
if old_new_root in self._new_id:
161
self.cancel_versioning(old_new_root)
163
self.unversion_file(old_new_root)
164
# if, at this stage, root still has an old file_id, zap it so we can
165
# stick a new one in.
166
if (self.tree_file_id(self._new_root) is not None
167
and self._new_root not in self._removed_id):
168
self.unversion_file(self._new_root)
169
if file_id is not None:
170
self.version_file(self._new_root, file_id=file_id)
172
# Now move children of new root into old root directory.
173
# Ensure all children are registered with the transaction, but don't
174
# use directly-- some tree children have new parents
175
list(self.iter_tree_children(old_new_root))
176
# Move all children of new root into old root directory.
177
for child in self.by_parent().get(old_new_root, []):
178
self.adjust_path(self.final_name(child), self._new_root, child)
180
# Ensure old_new_root has no directory.
181
if old_new_root in self._new_contents:
182
self.cancel_creation(old_new_root)
184
self.delete_contents(old_new_root)
186
# prevent deletion of root directory.
187
if self._new_root in self._removed_contents:
188
self.cancel_deletion(self._new_root)
190
# destroy path info for old_new_root.
191
del self._new_parent[old_new_root]
192
del self._new_name[old_new_root]
194
def trans_id_file_id(self, file_id):
195
"""Determine or set the transaction id associated with a file ID.
196
A new id is only created for file_ids that were never present. If
197
a transaction has been unversioned, it is deliberately still returned.
198
(this will likely lead to an unversioned parent conflict.)
201
raise ValueError('None is not a valid file id')
202
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
203
return self._r_new_id[file_id]
206
path = self._tree.id2path(file_id)
207
except errors.NoSuchId:
208
if file_id in self._non_present_ids:
209
return self._non_present_ids[file_id]
211
trans_id = self._assign_id()
212
self._non_present_ids[file_id] = trans_id
215
return self.trans_id_tree_path(path)
217
def version_file(self, trans_id, file_id=None):
218
"""Schedule a file to become versioned."""
219
raise NotImplementedError(self.version_file)
221
def cancel_versioning(self, trans_id):
222
"""Undo a previous versioning of a file"""
223
raise NotImplementedError(self.cancel_versioning)
225
def new_paths(self, filesystem_only=False):
226
"""Determine the paths of all new and changed files.
228
:param filesystem_only: if True, only calculate values for files
229
that require renames or execute bit changes.
233
stale_ids = self._needs_rename.difference(self._new_name)
234
stale_ids.difference_update(self._new_parent)
235
stale_ids.difference_update(self._new_contents)
236
stale_ids.difference_update(self._new_id)
237
needs_rename = self._needs_rename.difference(stale_ids)
238
id_sets = (needs_rename, self._new_executability)
240
id_sets = (self._new_name, self._new_parent, self._new_contents,
241
self._new_id, self._new_executability)
242
for id_set in id_sets:
243
new_ids.update(id_set)
244
return sorted(FinalPaths(self).get_paths(new_ids))
246
def tree_file_id(self, trans_id):
247
"""Determine the file id associated with the trans_id in the tree"""
248
path = self.tree_path(trans_id)
251
# the file is old; the old id is still valid
252
if self._new_root == trans_id:
253
return self._tree.path2id('')
254
return self._tree.path2id(path)
256
def final_is_versioned(self, trans_id):
257
return self.final_file_id(trans_id) is not None
259
def final_file_id(self, trans_id):
260
"""Determine the file id after any changes are applied, or None.
262
None indicates that the file will not be versioned after changes are
266
return self._new_id[trans_id]
268
if trans_id in self._removed_id:
270
return self.tree_file_id(trans_id)
272
def inactive_file_id(self, trans_id):
273
"""Return the inactive file_id associated with a transaction id.
274
That is, the one in the tree or in non_present_ids.
275
The file_id may actually be active, too.
277
file_id = self.tree_file_id(trans_id)
278
if file_id is not None:
280
for key, value in viewitems(self._non_present_ids):
281
if value == trans_id:
284
def find_conflicts(self):
285
"""Find any violations of inventory or filesystem invariants"""
286
if self._done is True:
287
raise ReusingTransform()
289
# ensure all children of all existent parents are known
290
# all children of non-existent parents are known, by definition.
291
self._add_tree_children()
292
by_parent = self.by_parent()
293
conflicts.extend(self._unversioned_parents(by_parent))
294
conflicts.extend(self._parent_loops())
295
conflicts.extend(self._duplicate_entries(by_parent))
296
conflicts.extend(self._parent_type_conflicts(by_parent))
297
conflicts.extend(self._improper_versioning())
298
conflicts.extend(self._executability_conflicts())
299
conflicts.extend(self._overwrite_conflicts())
302
def _check_malformed(self):
303
conflicts = self.find_conflicts()
304
if len(conflicts) != 0:
305
raise MalformedTransform(conflicts=conflicts)
307
def _add_tree_children(self):
308
"""Add all the children of all active parents to the known paths.
310
Active parents are those which gain children, and those which are
311
removed. This is a necessary first step in detecting conflicts.
313
parents = list(self.by_parent())
314
parents.extend([t for t in self._removed_contents if
315
self.tree_kind(t) == 'directory'])
316
for trans_id in self._removed_id:
317
path = self.tree_path(trans_id)
319
if self._tree.stored_kind(path) == 'directory':
320
parents.append(trans_id)
321
elif self.tree_kind(trans_id) == 'directory':
322
parents.append(trans_id)
324
for parent_id in parents:
325
# ensure that all children are registered with the transaction
326
list(self.iter_tree_children(parent_id))
328
def _has_named_child(self, name, parent_id, known_children):
329
"""Does a parent already have a name child.
331
:param name: The searched for name.
333
:param parent_id: The parent for which the check is made.
335
:param known_children: The already known children. This should have
336
been recently obtained from `self.by_parent.get(parent_id)`
337
(or will be if None is passed).
339
if known_children is None:
340
known_children = self.by_parent().get(parent_id, [])
341
for child in known_children:
342
if self.final_name(child) == name:
344
parent_path = self._tree_id_paths.get(parent_id, None)
345
if parent_path is None:
346
# No parent... no children
348
child_path = joinpath(parent_path, name)
349
child_id = self._tree_path_ids.get(child_path, None)
351
# Not known by the tree transform yet, check the filesystem
352
return osutils.lexists(self._tree.abspath(child_path))
354
raise AssertionError('child_id is missing: %s, %s, %s'
355
% (name, parent_id, child_id))
357
def _available_backup_name(self, name, target_id):
358
"""Find an available backup name.
360
:param name: The basename of the file.
362
:param target_id: The directory trans_id where the backup should
365
known_children = self.by_parent().get(target_id, [])
366
return osutils.available_backup_name(
368
lambda base: self._has_named_child(
369
base, target_id, known_children))
371
def _parent_loops(self):
372
"""No entry should be its own ancestor"""
374
for trans_id in self._new_parent:
377
while parent_id != ROOT_PARENT:
380
parent_id = self.final_parent(parent_id)
383
if parent_id == trans_id:
384
conflicts.append(('parent loop', trans_id))
385
if parent_id in seen:
389
def _unversioned_parents(self, by_parent):
390
"""If parent directories are versioned, children must be versioned."""
392
for parent_id, children in viewitems(by_parent):
393
if parent_id == ROOT_PARENT:
395
if self.final_is_versioned(parent_id):
397
for child_id in children:
398
if self.final_is_versioned(child_id):
399
conflicts.append(('unversioned parent', parent_id))
403
def _improper_versioning(self):
404
"""Cannot version a file with no contents, or a bad type.
406
However, existing entries with no contents are okay.
409
for trans_id in self._new_id:
410
kind = self.final_kind(trans_id)
411
if kind == 'symlink' and not self._tree.supports_symlinks():
412
# Ignore symlinks as they are not supported on this platform
415
conflicts.append(('versioning no contents', trans_id))
417
if not self._tree.versionable_kind(kind):
418
conflicts.append(('versioning bad kind', trans_id, kind))
421
def _executability_conflicts(self):
422
"""Check for bad executability changes.
424
Only versioned files may have their executability set, because
425
1. only versioned entries can have executability under windows
426
2. only files can be executable. (The execute bit on a directory
427
does not indicate searchability)
430
for trans_id in self._new_executability:
431
if not self.final_is_versioned(trans_id):
432
conflicts.append(('unversioned executability', trans_id))
434
if self.final_kind(trans_id) != "file":
435
conflicts.append(('non-file executability', trans_id))
438
def _overwrite_conflicts(self):
439
"""Check for overwrites (not permitted on Win32)"""
441
for trans_id in self._new_contents:
442
if self.tree_kind(trans_id) is None:
444
if trans_id not in self._removed_contents:
445
conflicts.append(('overwrite', trans_id,
446
self.final_name(trans_id)))
449
def _duplicate_entries(self, by_parent):
450
"""No directory may have two entries with the same name."""
452
if (self._new_name, self._new_parent) == ({}, {}):
454
for children in viewvalues(by_parent):
456
for child_tid in children:
457
name = self.final_name(child_tid)
459
# Keep children only if they still exist in the end
460
if not self._case_sensitive_target:
462
name_ids.append((name, child_tid))
466
for name, trans_id in name_ids:
467
kind = self.final_kind(trans_id)
468
if kind is None and not self.final_is_versioned(trans_id):
470
if name == last_name:
471
conflicts.append(('duplicate', last_trans_id, trans_id,
474
last_trans_id = trans_id
477
def _parent_type_conflicts(self, by_parent):
478
"""Children must have a directory parent"""
480
for parent_id, children in viewitems(by_parent):
481
if parent_id == ROOT_PARENT:
484
for child_id in children:
485
if self.final_kind(child_id) is not None:
490
# There is at least a child, so we need an existing directory to
492
kind = self.final_kind(parent_id)
494
# The directory will be deleted
495
conflicts.append(('missing parent', parent_id))
496
elif kind != "directory":
497
# Meh, we need a *directory* to put something in it
498
conflicts.append(('non-directory parent', parent_id))
501
def _set_executability(self, path, trans_id):
502
"""Set the executability of versioned files """
503
if self._tree._supports_executable():
504
new_executability = self._new_executability[trans_id]
505
abspath = self._tree.abspath(path)
506
current_mode = os.stat(abspath).st_mode
507
if new_executability:
510
to_mode = current_mode | (0o100 & ~umask)
511
# Enable x-bit for others only if they can read it.
512
if current_mode & 0o004:
513
to_mode |= 0o001 & ~umask
514
if current_mode & 0o040:
515
to_mode |= 0o010 & ~umask
517
to_mode = current_mode & ~0o111
518
osutils.chmod_if_possible(abspath, to_mode)
520
def _new_entry(self, name, parent_id, file_id):
521
"""Helper function to create a new filesystem entry."""
522
trans_id = self.create_path(name, parent_id)
523
if file_id is not None:
524
self.version_file(trans_id, file_id=file_id)
527
def new_file(self, name, parent_id, contents, file_id=None,
528
executable=None, sha1=None):
529
"""Convenience method to create files.
531
name is the name of the file to create.
532
parent_id is the transaction id of the parent directory of the file.
533
contents is an iterator of bytestrings, which will be used to produce
535
:param file_id: The inventory ID of the file, if it is to be versioned.
536
:param executable: Only valid when a file_id has been supplied.
538
trans_id = self._new_entry(name, parent_id, file_id)
539
# TODO: rather than scheduling a set_executable call,
540
# have create_file create the file with the right mode.
541
self.create_file(contents, trans_id, sha1=sha1)
542
if executable is not None:
543
self.set_executability(executable, trans_id)
546
def new_directory(self, name, parent_id, file_id=None):
547
"""Convenience method to create directories.
549
name is the name of the directory to create.
550
parent_id is the transaction id of the parent directory of the
552
file_id is the inventory ID of the directory, if it is to be versioned.
554
trans_id = self._new_entry(name, parent_id, file_id)
555
self.create_directory(trans_id)
558
def new_symlink(self, name, parent_id, target, file_id=None):
559
"""Convenience method to create symbolic link.
561
name is the name of the symlink to create.
562
parent_id is the transaction id of the parent directory of the symlink.
563
target is a bytestring of the target of the symlink.
564
file_id is the inventory ID of the file, if it is to be versioned.
566
trans_id = self._new_entry(name, parent_id, file_id)
567
self.create_symlink(target, trans_id)
570
def new_orphan(self, trans_id, parent_id):
571
"""Schedule an item to be orphaned.
573
When a directory is about to be removed, its children, if they are not
574
versioned are moved out of the way: they don't have a parent anymore.
576
:param trans_id: The trans_id of the existing item.
577
:param parent_id: The parent trans_id of the item.
579
raise NotImplementedError(self.new_orphan)
581
def _get_potential_orphans(self, dir_id):
582
"""Find the potential orphans in a directory.
584
A directory can't be safely deleted if there are versioned files in it.
585
If all the contained files are unversioned then they can be orphaned.
587
The 'None' return value means that the directory contains at least one
588
versioned file and should not be deleted.
590
:param dir_id: The directory trans id.
592
:return: A list of the orphan trans ids or None if at least one
593
versioned file is present.
596
# Find the potential orphans, stop if one item should be kept
597
for child_tid in self.by_parent()[dir_id]:
598
if child_tid in self._removed_contents:
599
# The child is removed as part of the transform. Since it was
600
# versioned before, it's not an orphan
602
if not self.final_is_versioned(child_tid):
603
# The child is not versioned
604
orphans.append(child_tid)
606
# We have a versioned file here, searching for orphans is
612
def _affected_ids(self):
613
"""Return the set of transform ids affected by the transform"""
614
trans_ids = set(self._removed_id)
615
trans_ids.update(self._new_id)
616
trans_ids.update(self._removed_contents)
617
trans_ids.update(self._new_contents)
618
trans_ids.update(self._new_executability)
619
trans_ids.update(self._new_name)
620
trans_ids.update(self._new_parent)
623
def _get_file_id_maps(self):
624
"""Return mapping of file_ids to trans_ids in the to and from states"""
625
trans_ids = self._affected_ids()
628
# Build up two dicts: trans_ids associated with file ids in the
629
# FROM state, vs the TO state.
630
for trans_id in trans_ids:
631
from_file_id = self.tree_file_id(trans_id)
632
if from_file_id is not None:
633
from_trans_ids[from_file_id] = trans_id
634
to_file_id = self.final_file_id(trans_id)
635
if to_file_id is not None:
636
to_trans_ids[to_file_id] = trans_id
637
return from_trans_ids, to_trans_ids
639
def _from_file_data(self, from_trans_id, from_versioned, from_path):
640
"""Get data about a file in the from (tree) state
642
Return a (name, parent, kind, executable) tuple
644
from_path = self._tree_id_paths.get(from_trans_id)
646
# get data from working tree if versioned
647
from_entry = next(self._tree.iter_entries_by_dir(
648
specific_files=[from_path]))[1]
649
from_name = from_entry.name
650
from_parent = from_entry.parent_id
653
if from_path is None:
654
# File does not exist in FROM state
658
# File exists, but is not versioned. Have to use path-
660
from_name = os.path.basename(from_path)
661
tree_parent = self.get_tree_parent(from_trans_id)
662
from_parent = self.tree_file_id(tree_parent)
663
if from_path is not None:
664
from_kind, from_executable, from_stats = \
665
self._tree._comparison_data(from_entry, from_path)
668
from_executable = False
669
return from_name, from_parent, from_kind, from_executable
671
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
672
"""Get data about a file in the to (target) state
674
Return a (name, parent, kind, executable) tuple
676
to_name = self.final_name(to_trans_id)
677
to_kind = self.final_kind(to_trans_id)
678
to_parent = self.final_file_id(self.final_parent(to_trans_id))
679
if to_trans_id in self._new_executability:
680
to_executable = self._new_executability[to_trans_id]
681
elif to_trans_id == from_trans_id:
682
to_executable = from_executable
684
to_executable = False
685
return to_name, to_parent, to_kind, to_executable
687
def iter_changes(self):
688
"""Produce output in the same format as Tree.iter_changes.
690
Will produce nonsensical results if invoked while inventory/filesystem
691
conflicts (as reported by TreeTransform.find_conflicts()) are present.
693
This reads the Transform, but only reproduces changes involving a
694
file_id. Files that are not versioned in either of the FROM or TO
695
states are not reflected.
697
final_paths = FinalPaths(self)
698
from_trans_ids, to_trans_ids = self._get_file_id_maps()
700
# Now iterate through all active file_ids
701
for file_id in set(from_trans_ids).union(to_trans_ids):
703
from_trans_id = from_trans_ids.get(file_id)
704
# find file ids, and determine versioning state
705
if from_trans_id is None:
706
from_versioned = False
707
from_trans_id = to_trans_ids[file_id]
709
from_versioned = True
710
to_trans_id = to_trans_ids.get(file_id)
711
if to_trans_id is None:
713
to_trans_id = from_trans_id
717
if not from_versioned:
720
from_path = self._tree_id_paths.get(from_trans_id)
724
to_path = final_paths.get_path(to_trans_id)
726
from_name, from_parent, from_kind, from_executable = \
727
self._from_file_data(from_trans_id, from_versioned, from_path)
729
to_name, to_parent, to_kind, to_executable = \
730
self._to_file_data(to_trans_id, from_trans_id, from_executable)
732
if from_kind != to_kind:
734
elif to_kind in ('file', 'symlink') and (
735
to_trans_id != from_trans_id
736
or to_trans_id in self._new_contents):
738
if (not modified and from_versioned == to_versioned
739
and from_parent == to_parent and from_name == to_name
740
and from_executable == to_executable):
744
file_id, (from_path, to_path), modified,
745
(from_versioned, to_versioned),
746
(from_parent, to_parent),
747
(from_name, to_name),
748
(from_kind, to_kind),
749
(from_executable, to_executable)))
752
return (c.path[0] or '', c.path[1] or '')
753
return iter(sorted(results, key=path_key))
755
def get_preview_tree(self):
756
"""Return a tree representing the result of the transform.
758
The tree is a snapshot, and altering the TreeTransform will invalidate
761
raise NotImplementedError(self.get_preview_tree)
763
def commit(self, branch, message, merge_parents=None, strict=False,
764
timestamp=None, timezone=None, committer=None, authors=None,
765
revprops=None, revision_id=None):
766
"""Commit the result of this TreeTransform to a branch.
768
:param branch: The branch to commit to.
769
:param message: The message to attach to the commit.
770
:param merge_parents: Additional parent revision-ids specified by
772
:param strict: If True, abort the commit if there are unversioned
774
:param timestamp: if not None, seconds-since-epoch for the time and
775
date. (May be a float.)
776
:param timezone: Optional timezone for timestamp, as an offset in
778
:param committer: Optional committer in email-id format.
779
(e.g. "J Random Hacker <jrandom@example.com>")
780
:param authors: Optional list of authors in email-id format.
781
:param revprops: Optional dictionary of revision properties.
782
:param revision_id: Optional revision id. (Specifying a revision-id
783
may reduce performance for some non-native formats.)
784
:return: The revision_id of the revision committed.
786
self._check_malformed()
788
unversioned = set(self._new_contents).difference(set(self._new_id))
789
for trans_id in unversioned:
790
if not self.final_is_versioned(trans_id):
791
raise errors.StrictCommitFailed()
793
revno, last_rev_id = branch.last_revision_info()
794
if last_rev_id == _mod_revision.NULL_REVISION:
795
if merge_parents is not None:
796
raise ValueError('Cannot supply merge parents for first'
800
parent_ids = [last_rev_id]
801
if merge_parents is not None:
802
parent_ids.extend(merge_parents)
803
if self._tree.get_revision_id() != last_rev_id:
804
raise ValueError('TreeTransform not based on branch basis: %s' %
805
self._tree.get_revision_id().decode('utf-8'))
806
from .. import commit
807
revprops = commit.Commit.update_revprops(revprops, branch, authors)
808
builder = branch.get_commit_builder(parent_ids,
813
revision_id=revision_id)
814
preview = self.get_preview_tree()
815
list(builder.record_iter_changes(preview, last_rev_id,
816
self.iter_changes()))
817
builder.finish_inventory()
818
revision_id = builder.commit(message)
819
branch.set_last_revision_info(revno + 1, revision_id)
822
def _text_parent(self, trans_id):
823
path = self.tree_path(trans_id)
825
if path is None or self._tree.kind(path) != 'file':
827
except errors.NoSuchFile:
831
def _get_parents_texts(self, trans_id):
832
"""Get texts for compression parents of this file."""
833
path = self._text_parent(trans_id)
836
return (self._tree.get_file_text(path),)
838
def _get_parents_lines(self, trans_id):
839
"""Get lines for compression parents of this file."""
840
path = self._text_parent(trans_id)
843
return (self._tree.get_file_lines(path),)
845
def serialize(self, serializer):
846
"""Serialize this TreeTransform.
848
:param serializer: A Serialiser like pack.ContainerSerializer.
850
from .. import bencode
851
new_name = {k.encode('utf-8'): v.encode('utf-8')
852
for k, v in viewitems(self._new_name)}
853
new_parent = {k.encode('utf-8'): v.encode('utf-8')
854
for k, v in viewitems(self._new_parent)}
855
new_id = {k.encode('utf-8'): v
856
for k, v in viewitems(self._new_id)}
857
new_executability = {k.encode('utf-8'): int(v)
858
for k, v in viewitems(self._new_executability)}
859
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
860
for k, v in viewitems(self._tree_path_ids)}
861
non_present_ids = {k: v.encode('utf-8')
862
for k, v in viewitems(self._non_present_ids)}
863
removed_contents = [trans_id.encode('utf-8')
864
for trans_id in self._removed_contents]
865
removed_id = [trans_id.encode('utf-8')
866
for trans_id in self._removed_id]
868
b'_id_number': self._id_number,
869
b'_new_name': new_name,
870
b'_new_parent': new_parent,
871
b'_new_executability': new_executability,
873
b'_tree_path_ids': tree_path_ids,
874
b'_removed_id': removed_id,
875
b'_removed_contents': removed_contents,
876
b'_non_present_ids': non_present_ids,
878
yield serializer.bytes_record(bencode.bencode(attribs),
880
for trans_id, kind in sorted(viewitems(self._new_contents)):
882
with open(self._limbo_name(trans_id), 'rb') as cur_file:
883
lines = cur_file.readlines()
884
parents = self._get_parents_lines(trans_id)
885
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
886
content = b''.join(mpdiff.to_patch())
887
if kind == 'directory':
889
if kind == 'symlink':
890
content = self._read_symlink_target(trans_id)
891
if not isinstance(content, bytes):
892
content = content.encode('utf-8')
893
yield serializer.bytes_record(
894
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
896
def deserialize(self, records):
897
"""Deserialize a stored TreeTransform.
899
:param records: An iterable of (names, content) tuples, as per
900
pack.ContainerPushParser.
902
from .. import bencode
903
names, content = next(records)
904
attribs = bencode.bdecode(content)
905
self._id_number = attribs[b'_id_number']
906
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
907
for k, v in viewitems(attribs[b'_new_name'])}
908
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
909
for k, v in viewitems(attribs[b'_new_parent'])}
910
self._new_executability = {
911
k.decode('utf-8'): bool(v)
912
for k, v in viewitems(attribs[b'_new_executability'])}
913
self._new_id = {k.decode('utf-8'): v
914
for k, v in viewitems(attribs[b'_new_id'])}
915
self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
916
self._tree_path_ids = {}
917
self._tree_id_paths = {}
918
for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
919
path = bytepath.decode('utf-8')
920
trans_id = trans_id.decode('utf-8')
921
self._tree_path_ids[path] = trans_id
922
self._tree_id_paths[trans_id] = path
923
self._removed_id = {trans_id.decode('utf-8')
924
for trans_id in attribs[b'_removed_id']}
925
self._removed_contents = set(
926
trans_id.decode('utf-8')
927
for trans_id in attribs[b'_removed_contents'])
928
self._non_present_ids = {
930
for k, v in viewitems(attribs[b'_non_present_ids'])}
931
for ((trans_id, kind),), content in records:
932
trans_id = trans_id.decode('utf-8')
933
kind = kind.decode('ascii')
935
mpdiff = multiparent.MultiParent.from_patch(content)
936
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
937
self.create_file(lines, trans_id)
938
if kind == 'directory':
939
self.create_directory(trans_id)
940
if kind == 'symlink':
941
self.create_symlink(content.decode('utf-8'), trans_id)
943
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
944
"""Schedule creation of a new file.
948
:param contents: an iterator of strings, all of which will be written
949
to the target destination.
950
:param trans_id: TreeTransform handle
951
:param mode_id: If not None, force the mode of the target file to match
952
the mode of the object referenced by mode_id.
953
Otherwise, we will try to preserve mode bits of an existing file.
954
:param sha1: If the sha1 of this content is already known, pass it in.
955
We can use it to prevent future sha1 computations.
957
raise NotImplementedError(self.create_file)
959
def create_directory(self, trans_id):
960
"""Schedule creation of a new directory.
962
See also new_directory.
964
raise NotImplementedError(self.create_directory)
966
def create_symlink(self, target, trans_id):
967
"""Schedule creation of a new symbolic link.
969
target is a bytestring.
970
See also new_symlink.
972
raise NotImplementedError(self.create_symlink)
974
def create_hardlink(self, path, trans_id):
975
"""Schedule creation of a hard link"""
976
raise NotImplementedError(self.create_hardlink)
978
def cancel_creation(self, trans_id):
979
"""Cancel the creation of new file contents."""
980
raise NotImplementedError(self.cancel_creation)
982
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
983
"""Apply all changes to the inventory and filesystem.
985
If filesystem or inventory conflicts are present, MalformedTransform
988
If apply succeeds, finalize is not necessary.
990
:param no_conflicts: if True, the caller guarantees there are no
991
conflicts, so no check is made.
992
:param precomputed_delta: An inventory delta to use instead of
994
:param _mover: Supply an alternate FileMover, for testing
996
raise NotImplementedError(self.apply)
999
class DiskTreeTransform(TreeTransformBase):
1000
"""Tree transform storing its contents on disk."""
1002
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1004
:param tree: The tree that will be transformed, but not necessarily
1006
:param limbodir: A directory where new files can be stored until
1007
they are installed in their proper places
1009
:param case_sensitive: If True, the target of the transform is
1010
case sensitive, not just case preserving.
1012
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1013
self._limbodir = limbodir
1014
self._deletiondir = None
1015
# A mapping of transform ids to their limbo filename
1016
self._limbo_files = {}
1017
self._possibly_stale_limbo_files = set()
1018
# A mapping of transform ids to a set of the transform ids of children
1019
# that their limbo directory has
1020
self._limbo_children = {}
1021
# Map transform ids to maps of child filename to child transform id
1022
self._limbo_children_names = {}
1023
# List of transform ids that need to be renamed from limbo into place
1024
self._needs_rename = set()
1025
self._creation_mtime = None
1026
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1029
"""Release the working tree lock, if held, clean up limbo dir.
1031
This is required if apply has not been invoked, but can be invoked
1034
if self._tree is None:
1037
limbo_paths = list(viewvalues(self._limbo_files))
1038
limbo_paths.extend(self._possibly_stale_limbo_files)
1039
limbo_paths.sort(reverse=True)
1040
for path in limbo_paths:
1042
osutils.delete_any(path)
1043
except OSError as e:
1044
if e.errno != errno.ENOENT:
1046
# XXX: warn? perhaps we just got interrupted at an
1047
# inconvenient moment, but perhaps files are disappearing
1050
osutils.delete_any(self._limbodir)
1052
# We don't especially care *why* the dir is immortal.
1053
raise ImmortalLimbo(self._limbodir)
1055
if self._deletiondir is not None:
1056
osutils.delete_any(self._deletiondir)
1058
raise errors.ImmortalPendingDeletion(self._deletiondir)
1060
TreeTransformBase.finalize(self)
1062
def _limbo_supports_executable(self):
1063
"""Check if the limbo path supports the executable bit."""
1064
return osutils.supports_executable(self._limbodir)
1066
def _limbo_name(self, trans_id):
1067
"""Generate the limbo name of a file"""
1068
limbo_name = self._limbo_files.get(trans_id)
1069
if limbo_name is None:
1070
limbo_name = self._generate_limbo_path(trans_id)
1071
self._limbo_files[trans_id] = limbo_name
1074
def _generate_limbo_path(self, trans_id):
1075
"""Generate a limbo path using the trans_id as the relative path.
1077
This is suitable as a fallback, and when the transform should not be
1078
sensitive to the path encoding of the limbo directory.
1080
self._needs_rename.add(trans_id)
1081
return osutils.pathjoin(self._limbodir, trans_id)
1083
def adjust_path(self, name, parent, trans_id):
1084
previous_parent = self._new_parent.get(trans_id)
1085
previous_name = self._new_name.get(trans_id)
1086
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1087
if (trans_id in self._limbo_files
1088
and trans_id not in self._needs_rename):
1089
self._rename_in_limbo([trans_id])
1090
if previous_parent != parent:
1091
self._limbo_children[previous_parent].remove(trans_id)
1092
if previous_parent != parent or previous_name != name:
1093
del self._limbo_children_names[previous_parent][previous_name]
1095
def _rename_in_limbo(self, trans_ids):
1096
"""Fix limbo names so that the right final path is produced.
1098
This means we outsmarted ourselves-- we tried to avoid renaming
1099
these files later by creating them with their final names in their
1100
final parents. But now the previous name or parent is no longer
1101
suitable, so we have to rename them.
1103
Even for trans_ids that have no new contents, we must remove their
1104
entries from _limbo_files, because they are now stale.
1106
for trans_id in trans_ids:
1107
old_path = self._limbo_files[trans_id]
1108
self._possibly_stale_limbo_files.add(old_path)
1109
del self._limbo_files[trans_id]
1110
if trans_id not in self._new_contents:
1112
new_path = self._limbo_name(trans_id)
1113
os.rename(old_path, new_path)
1114
self._possibly_stale_limbo_files.remove(old_path)
1115
for descendant in self._limbo_descendants(trans_id):
1116
desc_path = self._limbo_files[descendant]
1117
desc_path = new_path + desc_path[len(old_path):]
1118
self._limbo_files[descendant] = desc_path
1120
def _limbo_descendants(self, trans_id):
1121
"""Return the set of trans_ids whose limbo paths descend from this."""
1122
descendants = set(self._limbo_children.get(trans_id, []))
1123
for descendant in list(descendants):
1124
descendants.update(self._limbo_descendants(descendant))
1127
def _set_mode(self, trans_id, mode_id, typefunc):
1128
raise NotImplementedError(self._set_mode)
1130
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1131
"""Schedule creation of a new file.
1135
:param contents: an iterator of strings, all of which will be written
1136
to the target destination.
1137
:param trans_id: TreeTransform handle
1138
:param mode_id: If not None, force the mode of the target file to match
1139
the mode of the object referenced by mode_id.
1140
Otherwise, we will try to preserve mode bits of an existing file.
1141
:param sha1: If the sha1 of this content is already known, pass it in.
1142
We can use it to prevent future sha1 computations.
1144
name = self._limbo_name(trans_id)
1145
with open(name, 'wb') as f:
1146
unique_add(self._new_contents, trans_id, 'file')
1147
f.writelines(contents)
1148
self._set_mtime(name)
1149
self._set_mode(trans_id, mode_id, S_ISREG)
1150
# It is unfortunate we have to use lstat instead of fstat, but we just
1151
# used utime and chmod on the file, so we need the accurate final
1153
if sha1 is not None:
1154
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1156
def _read_symlink_target(self, trans_id):
1157
return os.readlink(self._limbo_name(trans_id))
1159
def _set_mtime(self, path):
1160
"""All files that are created get the same mtime.
1162
This time is set by the first object to be created.
1164
if self._creation_mtime is None:
1165
self._creation_mtime = time.time()
1166
os.utime(path, (self._creation_mtime, self._creation_mtime))
1168
def create_hardlink(self, path, trans_id):
1169
"""Schedule creation of a hard link"""
1170
name = self._limbo_name(trans_id)
1173
except OSError as e:
1174
if e.errno != errno.EPERM:
1176
raise errors.HardLinkNotSupported(path)
1178
unique_add(self._new_contents, trans_id, 'file')
1179
except BaseException:
1180
# Clean up the file, it never got registered so
1181
# TreeTransform.finalize() won't clean it up.
1185
def create_directory(self, trans_id):
1186
"""Schedule creation of a new directory.
1188
See also new_directory.
1190
os.mkdir(self._limbo_name(trans_id))
1191
unique_add(self._new_contents, trans_id, 'directory')
1193
def create_symlink(self, target, trans_id):
1194
"""Schedule creation of a new symbolic link.
1196
target is a bytestring.
1197
See also new_symlink.
1199
if self._create_symlinks:
1200
os.symlink(target, self._limbo_name(trans_id))
1203
path = FinalPaths(self).get_path(trans_id)
1207
'Unable to create symlink "%s" on this filesystem.' % (path,))
1208
# We add symlink to _new_contents even if they are unsupported
1209
# and not created. These entries are subsequently used to avoid
1210
# conflicts on platforms that don't support symlink
1211
unique_add(self._new_contents, trans_id, 'symlink')
1213
def cancel_creation(self, trans_id):
1214
"""Cancel the creation of new file contents."""
1215
del self._new_contents[trans_id]
1216
if trans_id in self._observed_sha1s:
1217
del self._observed_sha1s[trans_id]
1218
children = self._limbo_children.get(trans_id)
1219
# if this is a limbo directory with children, move them before removing
1221
if children is not None:
1222
self._rename_in_limbo(children)
1223
del self._limbo_children[trans_id]
1224
del self._limbo_children_names[trans_id]
1225
osutils.delete_any(self._limbo_name(trans_id))
1227
def new_orphan(self, trans_id, parent_id):
1228
conf = self._tree.get_config_stack()
1229
handle_orphan = conf.get('transform.orphan_policy')
1230
handle_orphan(self, trans_id, parent_id)
1233
class GitTreeTransform(DiskTreeTransform):
1234
"""Represent a tree transformation.
1236
This object is designed to support incremental generation of the transform,
1239
However, it gives optimum performance when parent directories are created
1240
before their contents. The transform is then able to put child files
1241
directly in their parent directory, avoiding later renames.
1243
It is easy to produce malformed transforms, but they are generally
1244
harmless. Attempting to apply a malformed transform will cause an
1245
exception to be raised before any modifications are made to the tree.
1247
Many kinds of malformed transforms can be corrected with the
1248
resolve_conflicts function. The remaining ones indicate programming error,
1249
such as trying to create a file with no path.
1251
Two sets of file creation methods are supplied. Convenience methods are:
1256
These are composed of the low-level methods:
1258
* create_file or create_directory or create_symlink
1262
Transform/Transaction ids
1263
-------------------------
1264
trans_ids are temporary ids assigned to all files involved in a transform.
1265
It's possible, even common, that not all files in the Tree have trans_ids.
1267
trans_ids are used because filenames and file_ids are not good enough
1268
identifiers; filenames change.
1270
trans_ids are only valid for the TreeTransform that generated them.
1274
Limbo is a temporary directory use to hold new versions of files.
1275
Files are added to limbo by create_file, create_directory, create_symlink,
1276
and their convenience variants (new_*). Files may be removed from limbo
1277
using cancel_creation. Files are renamed from limbo into their final
1278
location as part of TreeTransform.apply
1280
Limbo must be cleaned up, by either calling TreeTransform.apply or
1281
calling TreeTransform.finalize.
1283
Files are placed into limbo inside their parent directories, where
1284
possible. This reduces subsequent renames, and makes operations involving
1285
lots of files faster. This optimization is only possible if the parent
1286
directory is created *before* creating any of its children, so avoid
1287
creating children before parents, where possible.
1291
This temporary directory is used by _FileMover for storing files that are
1292
about to be deleted. In case of rollback, the files will be restored.
1293
FileMover does not delete files until it is sure that a rollback will not
1297
def __init__(self, tree, pb=None):
1298
"""Note: a tree_write lock is taken on the tree.
1300
Use TreeTransform.finalize() to release the lock (can be omitted if
1301
TreeTransform.apply() called).
1303
tree.lock_tree_write()
1305
limbodir = urlutils.local_path_from_url(
1306
tree._transport.abspath('limbo'))
1307
osutils.ensure_empty_directory_exists(
1309
errors.ExistingLimbo)
1310
deletiondir = urlutils.local_path_from_url(
1311
tree._transport.abspath('pending-deletion'))
1312
osutils.ensure_empty_directory_exists(
1314
errors.ExistingPendingDeletion)
1315
except BaseException:
1319
# Cache of realpath results, to speed up canonical_path
1320
self._realpaths = {}
1321
# Cache of relpath results, to speed up canonical_path
1323
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1324
tree.case_sensitive)
1325
self._deletiondir = deletiondir
1327
def canonical_path(self, path):
1328
"""Get the canonical tree-relative path"""
1329
# don't follow final symlinks
1330
abs = self._tree.abspath(path)
1331
if abs in self._relpaths:
1332
return self._relpaths[abs]
1333
dirname, basename = os.path.split(abs)
1334
if dirname not in self._realpaths:
1335
self._realpaths[dirname] = os.path.realpath(dirname)
1336
dirname = self._realpaths[dirname]
1337
abs = osutils.pathjoin(dirname, basename)
1338
if dirname in self._relpaths:
1339
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1340
relpath = relpath.rstrip('/\\')
1342
relpath = self._tree.relpath(abs)
1343
self._relpaths[abs] = relpath
1346
def tree_kind(self, trans_id):
1347
"""Determine the file kind in the working tree.
1349
:returns: The file kind or None if the file does not exist
1351
path = self._tree_id_paths.get(trans_id)
1355
return osutils.file_kind(self._tree.abspath(path))
1356
except errors.NoSuchFile:
1359
def _set_mode(self, trans_id, mode_id, typefunc):
1360
"""Set the mode of new file contents.
1361
The mode_id is the existing file to get the mode from (often the same
1362
as trans_id). The operation is only performed if there's a mode match
1363
according to typefunc.
1368
old_path = self._tree_id_paths[mode_id]
1372
mode = os.stat(self._tree.abspath(old_path)).st_mode
1373
except OSError as e:
1374
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1375
# Either old_path doesn't exist, or the parent of the
1376
# target is not a directory (but will be one eventually)
1377
# Either way, we know it doesn't exist *right now*
1378
# See also bug #248448
1383
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1385
def iter_tree_children(self, parent_id):
1386
"""Iterate through the entry's tree children, if any"""
1388
path = self._tree_id_paths[parent_id]
1392
children = os.listdir(self._tree.abspath(path))
1393
except OSError as e:
1394
if not (osutils._is_error_enotdir(e) or
1395
e.errno in (errno.ENOENT, errno.ESRCH)):
1399
for child in children:
1400
childpath = joinpath(path, child)
1401
if self._tree.is_control_filename(childpath):
1403
yield self.trans_id_tree_path(childpath)
1405
def _generate_limbo_path(self, trans_id):
1406
"""Generate a limbo path using the final path if possible.
1408
This optimizes the performance of applying the tree transform by
1409
avoiding renames. These renames can be avoided only when the parent
1410
directory is already scheduled for creation.
1412
If the final path cannot be used, falls back to using the trans_id as
1415
parent = self._new_parent.get(trans_id)
1416
# if the parent directory is already in limbo (e.g. when building a
1417
# tree), choose a limbo name inside the parent, to reduce further
1419
use_direct_path = False
1420
if self._new_contents.get(parent) == 'directory':
1421
filename = self._new_name.get(trans_id)
1422
if filename is not None:
1423
if parent not in self._limbo_children:
1424
self._limbo_children[parent] = set()
1425
self._limbo_children_names[parent] = {}
1426
use_direct_path = True
1427
# the direct path can only be used if no other file has
1428
# already taken this pathname, i.e. if the name is unused, or
1429
# if it is already associated with this trans_id.
1430
elif self._case_sensitive_target:
1431
if (self._limbo_children_names[parent].get(filename)
1432
in (trans_id, None)):
1433
use_direct_path = True
1435
for l_filename, l_trans_id in viewitems(
1436
self._limbo_children_names[parent]):
1437
if l_trans_id == trans_id:
1439
if l_filename.lower() == filename.lower():
1442
use_direct_path = True
1444
if not use_direct_path:
1445
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1447
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1448
self._limbo_children[parent].add(trans_id)
1449
self._limbo_children_names[parent][filename] = trans_id
1452
def version_file(self, trans_id, file_id=None):
1453
"""Schedule a file to become versioned."""
1456
unique_add(self._new_id, trans_id, file_id)
1457
unique_add(self._r_new_id, file_id, trans_id)
1459
def cancel_versioning(self, trans_id):
1460
"""Undo a previous versioning of a file"""
1461
file_id = self._new_id[trans_id]
1462
del self._new_id[trans_id]
1463
del self._r_new_id[file_id]
1465
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1466
"""Apply all changes to the inventory and filesystem.
1468
If filesystem or inventory conflicts are present, MalformedTransform
1471
If apply succeeds, finalize is not necessary.
1473
:param no_conflicts: if True, the caller guarantees there are no
1474
conflicts, so no check is made.
1475
:param precomputed_delta: An inventory delta to use instead of
1477
:param _mover: Supply an alternate FileMover, for testing
1479
for hook in MutableTree.hooks['pre_transform']:
1480
hook(self._tree, self)
1481
if not no_conflicts:
1482
self._check_malformed()
1483
self.rename_count = 0
1484
with ui.ui_factory.nested_progress_bar() as child_pb:
1485
if precomputed_delta is None:
1486
child_pb.update(gettext('Apply phase'), 0, 2)
1487
changes = self._generate_transform_changes()
1491
(op, np, ie) for (op, np, fid, ie) in precomputed_delta]
1494
mover = _FileMover()
1498
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1499
self._apply_removals(mover)
1500
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1501
modified_paths = self._apply_insertions(mover)
1502
except BaseException:
1506
mover.apply_deletions()
1507
if self.final_file_id(self.root) is None:
1508
changes = [e for e in changes if e[0] != '']
1509
self._tree._apply_transform_delta(changes)
1512
return _TransformResults(modified_paths, self.rename_count)
1514
def _apply_removals(self, mover):
1515
"""Perform tree operations that remove directory/inventory names.
1517
That is, delete files that are to be deleted, and put any files that
1518
need renaming into limbo. This must be done in strict child-to-parent
1521
If inventory_delta is None, no inventory delta generation is performed.
1523
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1524
with ui.ui_factory.nested_progress_bar() as child_pb:
1525
for num, (path, trans_id) in enumerate(tree_paths):
1526
# do not attempt to move root into a subdirectory of itself.
1529
child_pb.update(gettext('removing file'), num, len(tree_paths))
1530
full_path = self._tree.abspath(path)
1531
if trans_id in self._removed_contents:
1532
delete_path = os.path.join(self._deletiondir, trans_id)
1533
mover.pre_delete(full_path, delete_path)
1534
elif (trans_id in self._new_name or
1535
trans_id in self._new_parent):
1537
mover.rename(full_path, self._limbo_name(trans_id))
1538
except TransformRenameFailed as e:
1539
if e.errno != errno.ENOENT:
1542
self.rename_count += 1
1544
def _apply_insertions(self, mover):
1545
"""Perform tree operations that insert directory/inventory names.
1547
That is, create any files that need to be created, and restore from
1548
limbo any files that needed renaming. This must be done in strict
1549
parent-to-child order.
1551
If inventory_delta is None, no inventory delta is calculated, and
1552
no list of modified paths is returned.
1554
new_paths = self.new_paths(filesystem_only=True)
1556
with ui.ui_factory.nested_progress_bar() as child_pb:
1557
for num, (path, trans_id) in enumerate(new_paths):
1559
child_pb.update(gettext('adding file'),
1560
num, len(new_paths))
1561
full_path = self._tree.abspath(path)
1562
if trans_id in self._needs_rename:
1564
mover.rename(self._limbo_name(trans_id), full_path)
1565
except TransformRenameFailed as e:
1566
# We may be renaming a dangling inventory id
1567
if e.errno != errno.ENOENT:
1570
self.rename_count += 1
1571
# TODO: if trans_id in self._observed_sha1s, we should
1572
# re-stat the final target, since ctime will be
1573
# updated by the change.
1574
if (trans_id in self._new_contents
1575
or self.path_changed(trans_id)):
1576
if trans_id in self._new_contents:
1577
modified_paths.append(full_path)
1578
if trans_id in self._new_executability:
1579
self._set_executability(path, trans_id)
1580
if trans_id in self._observed_sha1s:
1581
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1582
st = osutils.lstat(full_path)
1583
self._observed_sha1s[trans_id] = (o_sha1, st)
1584
for path, trans_id in new_paths:
1585
# new_paths includes stuff like workingtree conflicts. Only the
1586
# stuff in new_contents actually comes from limbo.
1587
if trans_id in self._limbo_files:
1588
del self._limbo_files[trans_id]
1589
self._new_contents.clear()
1590
return modified_paths
1592
def _inventory_altered(self):
1593
"""Determine which trans_ids need new Inventory entries.
1595
An new entry is needed when anything that would be reflected by an
1596
inventory entry changes, including file name, file_id, parent file_id,
1597
file kind, and the execute bit.
1599
Some care is taken to return entries with real changes, not cases
1600
where the value is deleted and then restored to its original value,
1601
but some actually unchanged values may be returned.
1603
:returns: A list of (path, trans_id) for all items requiring an
1604
inventory change. Ordered by path.
1607
# Find entries whose file_ids are new (or changed).
1608
new_file_id = set(t for t in self._new_id
1609
if self._new_id[t] != self.tree_file_id(t))
1610
for id_set in [self._new_name, self._new_parent, new_file_id,
1611
self._new_executability]:
1612
changed_ids.update(id_set)
1613
# removing implies a kind change
1614
changed_kind = set(self._removed_contents)
1616
changed_kind.intersection_update(self._new_contents)
1617
# Ignore entries that are already known to have changed.
1618
changed_kind.difference_update(changed_ids)
1619
# to keep only the truly changed ones
1620
changed_kind = (t for t in changed_kind
1621
if self.tree_kind(t) != self.final_kind(t))
1622
# all kind changes will alter the inventory
1623
changed_ids.update(changed_kind)
1624
# To find entries with changed parent_ids, find parents which existed,
1625
# but changed file_id.
1626
# Now add all their children to the set.
1627
for parent_trans_id in new_file_id:
1628
changed_ids.update(self.iter_tree_children(parent_trans_id))
1629
return sorted(FinalPaths(self).get_paths(changed_ids))
1631
def _generate_transform_changes(self):
1632
"""Generate an inventory delta for the current transform."""
1634
new_paths = self._inventory_altered()
1635
total_entries = len(new_paths) + len(self._removed_id)
1636
with ui.ui_factory.nested_progress_bar() as child_pb:
1637
for num, trans_id in enumerate(self._removed_id):
1639
child_pb.update(gettext('removing file'),
1641
if trans_id == self._new_root:
1642
file_id = self._tree.path2id('')
1644
file_id = self.tree_file_id(trans_id)
1645
# File-id isn't really being deleted, just moved
1646
if file_id in self._r_new_id:
1648
path = self._tree_id_paths[trans_id]
1649
changes.append((path, None, None))
1650
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1652
for num, (path, trans_id) in enumerate(new_paths):
1654
child_pb.update(gettext('adding file'),
1655
num + len(self._removed_id), total_entries)
1656
file_id = new_path_file_ids[trans_id]
1659
kind = self.final_kind(trans_id)
1661
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1662
parent_trans_id = self.final_parent(trans_id)
1663
parent_file_id = new_path_file_ids.get(parent_trans_id)
1664
if parent_file_id is None:
1665
parent_file_id = self.final_file_id(parent_trans_id)
1666
if trans_id in self._new_reference_revision:
1667
new_entry = inventory.TreeReference(
1669
self._new_name[trans_id],
1670
self.final_file_id(self._new_parent[trans_id]),
1671
None, self._new_reference_revision[trans_id])
1673
new_entry = inventory.make_entry(kind,
1674
self.final_name(trans_id),
1675
parent_file_id, file_id)
1677
old_path = self._tree.id2path(new_entry.file_id)
1678
except errors.NoSuchId:
1680
new_executability = self._new_executability.get(trans_id)
1681
if new_executability is not None:
1682
new_entry.executable = new_executability
1684
(old_path, path, new_entry))