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 conflicts, errors, multiparent, osutils, trace, ui, urlutils
26
from ..i18n import gettext
27
from ..mutabletree import MutableTree
28
from ..transform import (
35
TransformRenameFailed,
41
from ..bzr.inventorytree import InventoryTreeChange
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 self._new_parent.items()
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 self._non_present_ids.items():
281
if value == trans_id:
284
def find_raw_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._parent_loops())
294
conflicts.extend(self._duplicate_entries(by_parent))
295
conflicts.extend(self._parent_type_conflicts(by_parent))
296
conflicts.extend(self._improper_versioning())
297
conflicts.extend(self._executability_conflicts())
298
conflicts.extend(self._overwrite_conflicts())
301
def _check_malformed(self):
302
conflicts = self.find_raw_conflicts()
303
if len(conflicts) != 0:
304
raise MalformedTransform(conflicts=conflicts)
306
def _add_tree_children(self):
307
"""Add all the children of all active parents to the known paths.
309
Active parents are those which gain children, and those which are
310
removed. This is a necessary first step in detecting conflicts.
312
parents = list(self.by_parent())
313
parents.extend([t for t in self._removed_contents if
314
self.tree_kind(t) == 'directory'])
315
for trans_id in self._removed_id:
316
path = self.tree_path(trans_id)
318
if self._tree.stored_kind(path) == 'directory':
319
parents.append(trans_id)
320
elif self.tree_kind(trans_id) == 'directory':
321
parents.append(trans_id)
323
for parent_id in parents:
324
# ensure that all children are registered with the transaction
325
list(self.iter_tree_children(parent_id))
327
def _has_named_child(self, name, parent_id, known_children):
328
"""Does a parent already have a name child.
330
:param name: The searched for name.
332
:param parent_id: The parent for which the check is made.
334
:param known_children: The already known children. This should have
335
been recently obtained from `self.by_parent.get(parent_id)`
336
(or will be if None is passed).
338
if known_children is None:
339
known_children = self.by_parent().get(parent_id, [])
340
for child in known_children:
341
if self.final_name(child) == name:
343
parent_path = self._tree_id_paths.get(parent_id, None)
344
if parent_path is None:
345
# No parent... no children
347
child_path = joinpath(parent_path, name)
348
child_id = self._tree_path_ids.get(child_path, None)
350
# Not known by the tree transform yet, check the filesystem
351
return osutils.lexists(self._tree.abspath(child_path))
353
raise AssertionError('child_id is missing: %s, %s, %s'
354
% (name, parent_id, child_id))
356
def _available_backup_name(self, name, target_id):
357
"""Find an available backup name.
359
:param name: The basename of the file.
361
:param target_id: The directory trans_id where the backup should
364
known_children = self.by_parent().get(target_id, [])
365
return osutils.available_backup_name(
367
lambda base: self._has_named_child(
368
base, target_id, known_children))
370
def _parent_loops(self):
371
"""No entry should be its own ancestor"""
373
for trans_id in self._new_parent:
376
while parent_id != ROOT_PARENT:
379
parent_id = self.final_parent(parent_id)
382
if parent_id == trans_id:
383
conflicts.append(('parent loop', trans_id))
384
if parent_id in seen:
388
def _improper_versioning(self):
389
"""Cannot version a file with no contents, or a bad type.
391
However, existing entries with no contents are okay.
394
for trans_id in self._new_id:
395
kind = self.final_kind(trans_id)
396
if kind == 'symlink' and not self._tree.supports_symlinks():
397
# Ignore symlinks as they are not supported on this platform
400
conflicts.append(('versioning no contents', trans_id))
402
if not self._tree.versionable_kind(kind):
403
conflicts.append(('versioning bad kind', trans_id, kind))
406
def _executability_conflicts(self):
407
"""Check for bad executability changes.
409
Only versioned files may have their executability set, because
410
1. only versioned entries can have executability under windows
411
2. only files can be executable. (The execute bit on a directory
412
does not indicate searchability)
415
for trans_id in self._new_executability:
416
if not self.final_is_versioned(trans_id):
417
conflicts.append(('unversioned executability', trans_id))
419
if self.final_kind(trans_id) != "file":
420
conflicts.append(('non-file executability', trans_id))
423
def _overwrite_conflicts(self):
424
"""Check for overwrites (not permitted on Win32)"""
426
for trans_id in self._new_contents:
427
if self.tree_kind(trans_id) is None:
429
if trans_id not in self._removed_contents:
430
conflicts.append(('overwrite', trans_id,
431
self.final_name(trans_id)))
434
def _duplicate_entries(self, by_parent):
435
"""No directory may have two entries with the same name."""
437
if (self._new_name, self._new_parent) == ({}, {}):
439
for children in by_parent.values():
441
for child_tid in children:
442
name = self.final_name(child_tid)
444
# Keep children only if they still exist in the end
445
if not self._case_sensitive_target:
447
name_ids.append((name, child_tid))
451
for name, trans_id in name_ids:
452
kind = self.final_kind(trans_id)
453
if kind is None and not self.final_is_versioned(trans_id):
455
if name == last_name:
456
conflicts.append(('duplicate', last_trans_id, trans_id,
459
last_trans_id = trans_id
462
def _parent_type_conflicts(self, by_parent):
463
"""Children must have a directory parent"""
465
for parent_id, children in by_parent.items():
466
if parent_id == ROOT_PARENT:
469
for child_id in children:
470
if self.final_kind(child_id) is not None:
475
# There is at least a child, so we need an existing directory to
477
kind = self.final_kind(parent_id)
479
# The directory will be deleted
480
conflicts.append(('missing parent', parent_id))
481
elif kind != "directory":
482
# Meh, we need a *directory* to put something in it
483
conflicts.append(('non-directory parent', parent_id))
486
def _set_executability(self, path, trans_id):
487
"""Set the executability of versioned files """
488
if self._tree._supports_executable():
489
new_executability = self._new_executability[trans_id]
490
abspath = self._tree.abspath(path)
491
current_mode = os.stat(abspath).st_mode
492
if new_executability:
495
to_mode = current_mode | (0o100 & ~umask)
496
# Enable x-bit for others only if they can read it.
497
if current_mode & 0o004:
498
to_mode |= 0o001 & ~umask
499
if current_mode & 0o040:
500
to_mode |= 0o010 & ~umask
502
to_mode = current_mode & ~0o111
503
osutils.chmod_if_possible(abspath, to_mode)
505
def _new_entry(self, name, parent_id, file_id):
506
"""Helper function to create a new filesystem entry."""
507
trans_id = self.create_path(name, parent_id)
508
if file_id is not None:
509
self.version_file(trans_id, file_id=file_id)
512
def new_file(self, name, parent_id, contents, file_id=None,
513
executable=None, sha1=None):
514
"""Convenience method to create files.
516
name is the name of the file to create.
517
parent_id is the transaction id of the parent directory of the file.
518
contents is an iterator of bytestrings, which will be used to produce
520
:param file_id: The inventory ID of the file, if it is to be versioned.
521
:param executable: Only valid when a file_id has been supplied.
523
trans_id = self._new_entry(name, parent_id, file_id)
524
# TODO: rather than scheduling a set_executable call,
525
# have create_file create the file with the right mode.
526
self.create_file(contents, trans_id, sha1=sha1)
527
if executable is not None:
528
self.set_executability(executable, trans_id)
531
def new_directory(self, name, parent_id, file_id=None):
532
"""Convenience method to create directories.
534
name is the name of the directory to create.
535
parent_id is the transaction id of the parent directory of the
537
file_id is the inventory ID of the directory, if it is to be versioned.
539
trans_id = self._new_entry(name, parent_id, file_id)
540
self.create_directory(trans_id)
543
def new_symlink(self, name, parent_id, target, file_id=None):
544
"""Convenience method to create symbolic link.
546
name is the name of the symlink to create.
547
parent_id is the transaction id of the parent directory of the symlink.
548
target is a bytestring of the target of the symlink.
549
file_id is the inventory ID of the file, if it is to be versioned.
551
trans_id = self._new_entry(name, parent_id, file_id)
552
self.create_symlink(target, trans_id)
555
def new_orphan(self, trans_id, parent_id):
556
"""Schedule an item to be orphaned.
558
When a directory is about to be removed, its children, if they are not
559
versioned are moved out of the way: they don't have a parent anymore.
561
:param trans_id: The trans_id of the existing item.
562
:param parent_id: The parent trans_id of the item.
564
raise NotImplementedError(self.new_orphan)
566
def _get_potential_orphans(self, dir_id):
567
"""Find the potential orphans in a directory.
569
A directory can't be safely deleted if there are versioned files in it.
570
If all the contained files are unversioned then they can be orphaned.
572
The 'None' return value means that the directory contains at least one
573
versioned file and should not be deleted.
575
:param dir_id: The directory trans id.
577
:return: A list of the orphan trans ids or None if at least one
578
versioned file is present.
581
# Find the potential orphans, stop if one item should be kept
582
for child_tid in self.by_parent()[dir_id]:
583
if child_tid in self._removed_contents:
584
# The child is removed as part of the transform. Since it was
585
# versioned before, it's not an orphan
587
if not self.final_is_versioned(child_tid):
588
# The child is not versioned
589
orphans.append(child_tid)
591
# We have a versioned file here, searching for orphans is
597
def _affected_ids(self):
598
"""Return the set of transform ids affected by the transform"""
599
trans_ids = set(self._removed_id)
600
trans_ids.update(self._new_id)
601
trans_ids.update(self._removed_contents)
602
trans_ids.update(self._new_contents)
603
trans_ids.update(self._new_executability)
604
trans_ids.update(self._new_name)
605
trans_ids.update(self._new_parent)
608
def _get_file_id_maps(self):
609
"""Return mapping of file_ids to trans_ids in the to and from states"""
610
trans_ids = self._affected_ids()
613
# Build up two dicts: trans_ids associated with file ids in the
614
# FROM state, vs the TO state.
615
for trans_id in trans_ids:
616
from_file_id = self.tree_file_id(trans_id)
617
if from_file_id is not None:
618
from_trans_ids[from_file_id] = trans_id
619
to_file_id = self.final_file_id(trans_id)
620
if to_file_id is not None:
621
to_trans_ids[to_file_id] = trans_id
622
return from_trans_ids, to_trans_ids
624
def _from_file_data(self, from_trans_id, from_versioned, from_path):
625
"""Get data about a file in the from (tree) state
627
Return a (name, parent, kind, executable) tuple
629
from_path = self._tree_id_paths.get(from_trans_id)
631
# get data from working tree if versioned
632
from_entry = next(self._tree.iter_entries_by_dir(
633
specific_files=[from_path]))[1]
634
from_name = from_entry.name
635
from_parent = from_entry.parent_id
638
if from_path is None:
639
# File does not exist in FROM state
643
# File exists, but is not versioned. Have to use path-
645
from_name = os.path.basename(from_path)
646
tree_parent = self.get_tree_parent(from_trans_id)
647
from_parent = self.tree_file_id(tree_parent)
648
if from_path is not None:
649
from_kind, from_executable, from_stats = \
650
self._tree._comparison_data(from_entry, from_path)
653
from_executable = False
654
return from_name, from_parent, from_kind, from_executable
656
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
657
"""Get data about a file in the to (target) state
659
Return a (name, parent, kind, executable) tuple
661
to_name = self.final_name(to_trans_id)
662
to_kind = self.final_kind(to_trans_id)
663
to_parent = self.final_file_id(self.final_parent(to_trans_id))
664
if to_trans_id in self._new_executability:
665
to_executable = self._new_executability[to_trans_id]
666
elif to_trans_id == from_trans_id:
667
to_executable = from_executable
669
to_executable = False
670
return to_name, to_parent, to_kind, to_executable
672
def iter_changes(self):
673
"""Produce output in the same format as Tree.iter_changes.
675
Will produce nonsensical results if invoked while inventory/filesystem
676
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
678
This reads the Transform, but only reproduces changes involving a
679
file_id. Files that are not versioned in either of the FROM or TO
680
states are not reflected.
682
final_paths = FinalPaths(self)
683
from_trans_ids, to_trans_ids = self._get_file_id_maps()
685
# Now iterate through all active file_ids
686
for file_id in set(from_trans_ids).union(to_trans_ids):
688
from_trans_id = from_trans_ids.get(file_id)
689
# find file ids, and determine versioning state
690
if from_trans_id is None:
691
from_versioned = False
692
from_trans_id = to_trans_ids[file_id]
694
from_versioned = True
695
to_trans_id = to_trans_ids.get(file_id)
696
if to_trans_id is None:
698
to_trans_id = from_trans_id
702
if not from_versioned:
705
from_path = self._tree_id_paths.get(from_trans_id)
709
to_path = final_paths.get_path(to_trans_id)
711
from_name, from_parent, from_kind, from_executable = \
712
self._from_file_data(from_trans_id, from_versioned, from_path)
714
to_name, to_parent, to_kind, to_executable = \
715
self._to_file_data(to_trans_id, from_trans_id, from_executable)
717
if from_kind != to_kind:
719
elif to_kind in ('file', 'symlink') and (
720
to_trans_id != from_trans_id
721
or to_trans_id in self._new_contents):
723
if (not modified and from_versioned == to_versioned
724
and from_parent == to_parent and from_name == to_name
725
and from_executable == to_executable):
729
file_id, (from_path, to_path), modified,
730
(from_versioned, to_versioned),
731
(from_parent, to_parent),
732
(from_name, to_name),
733
(from_kind, to_kind),
734
(from_executable, to_executable)))
737
return (c.path[0] or '', c.path[1] or '')
738
return iter(sorted(results, key=path_key))
740
def get_preview_tree(self):
741
"""Return a tree representing the result of the transform.
743
The tree is a snapshot, and altering the TreeTransform will invalidate
746
raise NotImplementedError(self.get_preview_tree)
748
def commit(self, branch, message, merge_parents=None, strict=False,
749
timestamp=None, timezone=None, committer=None, authors=None,
750
revprops=None, revision_id=None):
751
"""Commit the result of this TreeTransform to a branch.
753
:param branch: The branch to commit to.
754
:param message: The message to attach to the commit.
755
:param merge_parents: Additional parent revision-ids specified by
757
:param strict: If True, abort the commit if there are unversioned
759
:param timestamp: if not None, seconds-since-epoch for the time and
760
date. (May be a float.)
761
:param timezone: Optional timezone for timestamp, as an offset in
763
:param committer: Optional committer in email-id format.
764
(e.g. "J Random Hacker <jrandom@example.com>")
765
:param authors: Optional list of authors in email-id format.
766
:param revprops: Optional dictionary of revision properties.
767
:param revision_id: Optional revision id. (Specifying a revision-id
768
may reduce performance for some non-native formats.)
769
:return: The revision_id of the revision committed.
771
self._check_malformed()
773
unversioned = set(self._new_contents).difference(set(self._new_id))
774
for trans_id in unversioned:
775
if not self.final_is_versioned(trans_id):
776
raise errors.StrictCommitFailed()
778
revno, last_rev_id = branch.last_revision_info()
779
if last_rev_id == _mod_revision.NULL_REVISION:
780
if merge_parents is not None:
781
raise ValueError('Cannot supply merge parents for first'
785
parent_ids = [last_rev_id]
786
if merge_parents is not None:
787
parent_ids.extend(merge_parents)
788
if self._tree.get_revision_id() != last_rev_id:
789
raise ValueError('TreeTransform not based on branch basis: %s' %
790
self._tree.get_revision_id().decode('utf-8'))
791
from .. import commit
792
revprops = commit.Commit.update_revprops(revprops, branch, authors)
793
builder = branch.get_commit_builder(parent_ids,
798
revision_id=revision_id)
799
preview = self.get_preview_tree()
800
list(builder.record_iter_changes(preview, last_rev_id,
801
self.iter_changes()))
802
builder.finish_inventory()
803
revision_id = builder.commit(message)
804
branch.set_last_revision_info(revno + 1, revision_id)
807
def _text_parent(self, trans_id):
808
path = self.tree_path(trans_id)
810
if path is None or self._tree.kind(path) != 'file':
812
except errors.NoSuchFile:
816
def _get_parents_texts(self, trans_id):
817
"""Get texts for compression parents of this file."""
818
path = self._text_parent(trans_id)
821
return (self._tree.get_file_text(path),)
823
def _get_parents_lines(self, trans_id):
824
"""Get lines for compression parents of this file."""
825
path = self._text_parent(trans_id)
828
return (self._tree.get_file_lines(path),)
830
def serialize(self, serializer):
831
"""Serialize this TreeTransform.
833
:param serializer: A Serialiser like pack.ContainerSerializer.
835
from .. import bencode
836
new_name = {k.encode('utf-8'): v.encode('utf-8')
837
for k, v in self._new_name.items()}
838
new_parent = {k.encode('utf-8'): v.encode('utf-8')
839
for k, v in self._new_parent.items()}
840
new_id = {k.encode('utf-8'): v
841
for k, v in self._new_id.items()}
842
new_executability = {k.encode('utf-8'): int(v)
843
for k, v in self._new_executability.items()}
844
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
845
for k, v in self._tree_path_ids.items()}
846
non_present_ids = {k: v.encode('utf-8')
847
for k, v in self._non_present_ids.items()}
848
removed_contents = [trans_id.encode('utf-8')
849
for trans_id in self._removed_contents]
850
removed_id = [trans_id.encode('utf-8')
851
for trans_id in self._removed_id]
853
b'_id_number': self._id_number,
854
b'_new_name': new_name,
855
b'_new_parent': new_parent,
856
b'_new_executability': new_executability,
858
b'_tree_path_ids': tree_path_ids,
859
b'_removed_id': removed_id,
860
b'_removed_contents': removed_contents,
861
b'_non_present_ids': non_present_ids,
863
yield serializer.bytes_record(bencode.bencode(attribs),
865
for trans_id, kind in sorted(self._new_contents.items()):
867
with open(self._limbo_name(trans_id), 'rb') as cur_file:
868
lines = cur_file.readlines()
869
parents = self._get_parents_lines(trans_id)
870
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
871
content = b''.join(mpdiff.to_patch())
872
if kind == 'directory':
874
if kind == 'symlink':
875
content = self._read_symlink_target(trans_id)
876
if not isinstance(content, bytes):
877
content = content.encode('utf-8')
878
yield serializer.bytes_record(
879
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
881
def deserialize(self, records):
882
"""Deserialize a stored TreeTransform.
884
:param records: An iterable of (names, content) tuples, as per
885
pack.ContainerPushParser.
887
from .. import bencode
888
names, content = next(records)
889
attribs = bencode.bdecode(content)
890
self._id_number = attribs[b'_id_number']
891
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
892
for k, v in attribs[b'_new_name'].items()}
893
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
894
for k, v in attribs[b'_new_parent'].items()}
895
self._new_executability = {
896
k.decode('utf-8'): bool(v)
897
for k, v in attribs[b'_new_executability'].items()}
898
self._new_id = {k.decode('utf-8'): v
899
for k, v in attribs[b'_new_id'].items()}
900
self._r_new_id = {v: k for k, v in self._new_id.items()}
901
self._tree_path_ids = {}
902
self._tree_id_paths = {}
903
for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
904
path = bytepath.decode('utf-8')
905
trans_id = trans_id.decode('utf-8')
906
self._tree_path_ids[path] = trans_id
907
self._tree_id_paths[trans_id] = path
908
self._removed_id = {trans_id.decode('utf-8')
909
for trans_id in attribs[b'_removed_id']}
910
self._removed_contents = set(
911
trans_id.decode('utf-8')
912
for trans_id in attribs[b'_removed_contents'])
913
self._non_present_ids = {
915
for k, v in attribs[b'_non_present_ids'].items()}
916
for ((trans_id, kind),), content in records:
917
trans_id = trans_id.decode('utf-8')
918
kind = kind.decode('ascii')
920
mpdiff = multiparent.MultiParent.from_patch(content)
921
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
922
self.create_file(lines, trans_id)
923
if kind == 'directory':
924
self.create_directory(trans_id)
925
if kind == 'symlink':
926
self.create_symlink(content.decode('utf-8'), trans_id)
928
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
929
"""Schedule creation of a new file.
933
:param contents: an iterator of strings, all of which will be written
934
to the target destination.
935
:param trans_id: TreeTransform handle
936
:param mode_id: If not None, force the mode of the target file to match
937
the mode of the object referenced by mode_id.
938
Otherwise, we will try to preserve mode bits of an existing file.
939
:param sha1: If the sha1 of this content is already known, pass it in.
940
We can use it to prevent future sha1 computations.
942
raise NotImplementedError(self.create_file)
944
def create_directory(self, trans_id):
945
"""Schedule creation of a new directory.
947
See also new_directory.
949
raise NotImplementedError(self.create_directory)
951
def create_symlink(self, target, trans_id):
952
"""Schedule creation of a new symbolic link.
954
target is a bytestring.
955
See also new_symlink.
957
raise NotImplementedError(self.create_symlink)
959
def create_hardlink(self, path, trans_id):
960
"""Schedule creation of a hard link"""
961
raise NotImplementedError(self.create_hardlink)
963
def cancel_creation(self, trans_id):
964
"""Cancel the creation of new file contents."""
965
raise NotImplementedError(self.cancel_creation)
967
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
968
"""Apply all changes to the inventory and filesystem.
970
If filesystem or inventory conflicts are present, MalformedTransform
973
If apply succeeds, finalize is not necessary.
975
:param no_conflicts: if True, the caller guarantees there are no
976
conflicts, so no check is made.
977
:param precomputed_delta: An inventory delta to use instead of
979
:param _mover: Supply an alternate FileMover, for testing
981
raise NotImplementedError(self.apply)
983
def cook_conflicts(self, raw_conflicts):
984
"""Generate a list of cooked conflicts, sorted by file path"""
985
if not raw_conflicts:
987
fp = FinalPaths(self)
988
from .workingtree import TextConflict
989
for c in raw_conflicts:
990
if c[0] == 'text conflict':
991
yield TextConflict(fp.get_path(c[1]))
992
elif c[0] == 'duplicate':
993
yield TextConflict(fp.get_path(c[2]))
994
elif c[0] == 'contents conflict':
995
yield TextConflict(fp.get_path(c[1][0]))
996
elif c[0] == 'missing parent':
997
# TODO(jelmer): This should not make it to here
998
yield TextConflict(fp.get_path(c[2]))
1000
raise AssertionError('unknown conflict %s' % c[0])
1003
class DiskTreeTransform(TreeTransformBase):
1004
"""Tree transform storing its contents on disk."""
1006
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1008
:param tree: The tree that will be transformed, but not necessarily
1010
:param limbodir: A directory where new files can be stored until
1011
they are installed in their proper places
1013
:param case_sensitive: If True, the target of the transform is
1014
case sensitive, not just case preserving.
1016
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1017
self._limbodir = limbodir
1018
self._deletiondir = None
1019
# A mapping of transform ids to their limbo filename
1020
self._limbo_files = {}
1021
self._possibly_stale_limbo_files = set()
1022
# A mapping of transform ids to a set of the transform ids of children
1023
# that their limbo directory has
1024
self._limbo_children = {}
1025
# Map transform ids to maps of child filename to child transform id
1026
self._limbo_children_names = {}
1027
# List of transform ids that need to be renamed from limbo into place
1028
self._needs_rename = set()
1029
self._creation_mtime = None
1030
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1033
"""Release the working tree lock, if held, clean up limbo dir.
1035
This is required if apply has not been invoked, but can be invoked
1038
if self._tree is None:
1041
limbo_paths = list(self._limbo_files.values())
1042
limbo_paths.extend(self._possibly_stale_limbo_files)
1043
limbo_paths.sort(reverse=True)
1044
for path in limbo_paths:
1046
osutils.delete_any(path)
1047
except OSError as e:
1048
if e.errno != errno.ENOENT:
1050
# XXX: warn? perhaps we just got interrupted at an
1051
# inconvenient moment, but perhaps files are disappearing
1054
osutils.delete_any(self._limbodir)
1056
# We don't especially care *why* the dir is immortal.
1057
raise ImmortalLimbo(self._limbodir)
1059
if self._deletiondir is not None:
1060
osutils.delete_any(self._deletiondir)
1062
raise errors.ImmortalPendingDeletion(self._deletiondir)
1064
TreeTransformBase.finalize(self)
1066
def _limbo_supports_executable(self):
1067
"""Check if the limbo path supports the executable bit."""
1068
return osutils.supports_executable(self._limbodir)
1070
def _limbo_name(self, trans_id):
1071
"""Generate the limbo name of a file"""
1072
limbo_name = self._limbo_files.get(trans_id)
1073
if limbo_name is None:
1074
limbo_name = self._generate_limbo_path(trans_id)
1075
self._limbo_files[trans_id] = limbo_name
1078
def _generate_limbo_path(self, trans_id):
1079
"""Generate a limbo path using the trans_id as the relative path.
1081
This is suitable as a fallback, and when the transform should not be
1082
sensitive to the path encoding of the limbo directory.
1084
self._needs_rename.add(trans_id)
1085
return osutils.pathjoin(self._limbodir, trans_id)
1087
def adjust_path(self, name, parent, trans_id):
1088
previous_parent = self._new_parent.get(trans_id)
1089
previous_name = self._new_name.get(trans_id)
1090
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1091
if (trans_id in self._limbo_files
1092
and trans_id not in self._needs_rename):
1093
self._rename_in_limbo([trans_id])
1094
if previous_parent != parent:
1095
self._limbo_children[previous_parent].remove(trans_id)
1096
if previous_parent != parent or previous_name != name:
1097
del self._limbo_children_names[previous_parent][previous_name]
1099
def _rename_in_limbo(self, trans_ids):
1100
"""Fix limbo names so that the right final path is produced.
1102
This means we outsmarted ourselves-- we tried to avoid renaming
1103
these files later by creating them with their final names in their
1104
final parents. But now the previous name or parent is no longer
1105
suitable, so we have to rename them.
1107
Even for trans_ids that have no new contents, we must remove their
1108
entries from _limbo_files, because they are now stale.
1110
for trans_id in trans_ids:
1111
old_path = self._limbo_files[trans_id]
1112
self._possibly_stale_limbo_files.add(old_path)
1113
del self._limbo_files[trans_id]
1114
if trans_id not in self._new_contents:
1116
new_path = self._limbo_name(trans_id)
1117
os.rename(old_path, new_path)
1118
self._possibly_stale_limbo_files.remove(old_path)
1119
for descendant in self._limbo_descendants(trans_id):
1120
desc_path = self._limbo_files[descendant]
1121
desc_path = new_path + desc_path[len(old_path):]
1122
self._limbo_files[descendant] = desc_path
1124
def _limbo_descendants(self, trans_id):
1125
"""Return the set of trans_ids whose limbo paths descend from this."""
1126
descendants = set(self._limbo_children.get(trans_id, []))
1127
for descendant in list(descendants):
1128
descendants.update(self._limbo_descendants(descendant))
1131
def _set_mode(self, trans_id, mode_id, typefunc):
1132
raise NotImplementedError(self._set_mode)
1134
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1135
"""Schedule creation of a new file.
1139
:param contents: an iterator of strings, all of which will be written
1140
to the target destination.
1141
:param trans_id: TreeTransform handle
1142
:param mode_id: If not None, force the mode of the target file to match
1143
the mode of the object referenced by mode_id.
1144
Otherwise, we will try to preserve mode bits of an existing file.
1145
:param sha1: If the sha1 of this content is already known, pass it in.
1146
We can use it to prevent future sha1 computations.
1148
name = self._limbo_name(trans_id)
1149
with open(name, 'wb') as f:
1150
unique_add(self._new_contents, trans_id, 'file')
1151
f.writelines(contents)
1152
self._set_mtime(name)
1153
self._set_mode(trans_id, mode_id, S_ISREG)
1154
# It is unfortunate we have to use lstat instead of fstat, but we just
1155
# used utime and chmod on the file, so we need the accurate final
1157
if sha1 is not None:
1158
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1160
def _read_symlink_target(self, trans_id):
1161
return os.readlink(self._limbo_name(trans_id))
1163
def _set_mtime(self, path):
1164
"""All files that are created get the same mtime.
1166
This time is set by the first object to be created.
1168
if self._creation_mtime is None:
1169
self._creation_mtime = time.time()
1170
os.utime(path, (self._creation_mtime, self._creation_mtime))
1172
def create_hardlink(self, path, trans_id):
1173
"""Schedule creation of a hard link"""
1174
name = self._limbo_name(trans_id)
1177
except OSError as e:
1178
if e.errno != errno.EPERM:
1180
raise errors.HardLinkNotSupported(path)
1182
unique_add(self._new_contents, trans_id, 'file')
1183
except BaseException:
1184
# Clean up the file, it never got registered so
1185
# TreeTransform.finalize() won't clean it up.
1189
def create_directory(self, trans_id):
1190
"""Schedule creation of a new directory.
1192
See also new_directory.
1194
os.mkdir(self._limbo_name(trans_id))
1195
unique_add(self._new_contents, trans_id, 'directory')
1197
def create_symlink(self, target, trans_id):
1198
"""Schedule creation of a new symbolic link.
1200
target is a bytestring.
1201
See also new_symlink.
1203
if self._create_symlinks:
1204
os.symlink(target, self._limbo_name(trans_id))
1207
path = FinalPaths(self).get_path(trans_id)
1211
'Unable to create symlink "%s" on this filesystem.' % (path,))
1212
# We add symlink to _new_contents even if they are unsupported
1213
# and not created. These entries are subsequently used to avoid
1214
# conflicts on platforms that don't support symlink
1215
unique_add(self._new_contents, trans_id, 'symlink')
1217
def cancel_creation(self, trans_id):
1218
"""Cancel the creation of new file contents."""
1219
del self._new_contents[trans_id]
1220
if trans_id in self._observed_sha1s:
1221
del self._observed_sha1s[trans_id]
1222
children = self._limbo_children.get(trans_id)
1223
# if this is a limbo directory with children, move them before removing
1225
if children is not None:
1226
self._rename_in_limbo(children)
1227
del self._limbo_children[trans_id]
1228
del self._limbo_children_names[trans_id]
1229
osutils.delete_any(self._limbo_name(trans_id))
1231
def new_orphan(self, trans_id, parent_id):
1232
conf = self._tree.get_config_stack()
1233
handle_orphan = conf.get('transform.orphan_policy')
1234
handle_orphan(self, trans_id, parent_id)
1237
class GitTreeTransform(DiskTreeTransform):
1238
"""Represent a tree transformation.
1240
This object is designed to support incremental generation of the transform,
1243
However, it gives optimum performance when parent directories are created
1244
before their contents. The transform is then able to put child files
1245
directly in their parent directory, avoiding later renames.
1247
It is easy to produce malformed transforms, but they are generally
1248
harmless. Attempting to apply a malformed transform will cause an
1249
exception to be raised before any modifications are made to the tree.
1251
Many kinds of malformed transforms can be corrected with the
1252
resolve_conflicts function. The remaining ones indicate programming error,
1253
such as trying to create a file with no path.
1255
Two sets of file creation methods are supplied. Convenience methods are:
1260
These are composed of the low-level methods:
1262
* create_file or create_directory or create_symlink
1266
Transform/Transaction ids
1267
-------------------------
1268
trans_ids are temporary ids assigned to all files involved in a transform.
1269
It's possible, even common, that not all files in the Tree have trans_ids.
1271
trans_ids are used because filenames and file_ids are not good enough
1272
identifiers; filenames change.
1274
trans_ids are only valid for the TreeTransform that generated them.
1278
Limbo is a temporary directory use to hold new versions of files.
1279
Files are added to limbo by create_file, create_directory, create_symlink,
1280
and their convenience variants (new_*). Files may be removed from limbo
1281
using cancel_creation. Files are renamed from limbo into their final
1282
location as part of TreeTransform.apply
1284
Limbo must be cleaned up, by either calling TreeTransform.apply or
1285
calling TreeTransform.finalize.
1287
Files are placed into limbo inside their parent directories, where
1288
possible. This reduces subsequent renames, and makes operations involving
1289
lots of files faster. This optimization is only possible if the parent
1290
directory is created *before* creating any of its children, so avoid
1291
creating children before parents, where possible.
1295
This temporary directory is used by _FileMover for storing files that are
1296
about to be deleted. In case of rollback, the files will be restored.
1297
FileMover does not delete files until it is sure that a rollback will not
1301
def __init__(self, tree, pb=None):
1302
"""Note: a tree_write lock is taken on the tree.
1304
Use TreeTransform.finalize() to release the lock (can be omitted if
1305
TreeTransform.apply() called).
1307
tree.lock_tree_write()
1309
limbodir = urlutils.local_path_from_url(
1310
tree._transport.abspath('limbo'))
1311
osutils.ensure_empty_directory_exists(
1313
errors.ExistingLimbo)
1314
deletiondir = urlutils.local_path_from_url(
1315
tree._transport.abspath('pending-deletion'))
1316
osutils.ensure_empty_directory_exists(
1318
errors.ExistingPendingDeletion)
1319
except BaseException:
1323
# Cache of realpath results, to speed up canonical_path
1324
self._realpaths = {}
1325
# Cache of relpath results, to speed up canonical_path
1327
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1328
tree.case_sensitive)
1329
self._deletiondir = deletiondir
1331
def canonical_path(self, path):
1332
"""Get the canonical tree-relative path"""
1333
# don't follow final symlinks
1334
abs = self._tree.abspath(path)
1335
if abs in self._relpaths:
1336
return self._relpaths[abs]
1337
dirname, basename = os.path.split(abs)
1338
if dirname not in self._realpaths:
1339
self._realpaths[dirname] = os.path.realpath(dirname)
1340
dirname = self._realpaths[dirname]
1341
abs = osutils.pathjoin(dirname, basename)
1342
if dirname in self._relpaths:
1343
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1344
relpath = relpath.rstrip('/\\')
1346
relpath = self._tree.relpath(abs)
1347
self._relpaths[abs] = relpath
1350
def tree_kind(self, trans_id):
1351
"""Determine the file kind in the working tree.
1353
:returns: The file kind or None if the file does not exist
1355
path = self._tree_id_paths.get(trans_id)
1359
return osutils.file_kind(self._tree.abspath(path))
1360
except errors.NoSuchFile:
1363
def _set_mode(self, trans_id, mode_id, typefunc):
1364
"""Set the mode of new file contents.
1365
The mode_id is the existing file to get the mode from (often the same
1366
as trans_id). The operation is only performed if there's a mode match
1367
according to typefunc.
1372
old_path = self._tree_id_paths[mode_id]
1376
mode = os.stat(self._tree.abspath(old_path)).st_mode
1377
except OSError as e:
1378
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1379
# Either old_path doesn't exist, or the parent of the
1380
# target is not a directory (but will be one eventually)
1381
# Either way, we know it doesn't exist *right now*
1382
# See also bug #248448
1387
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1389
def iter_tree_children(self, parent_id):
1390
"""Iterate through the entry's tree children, if any"""
1392
path = self._tree_id_paths[parent_id]
1396
children = os.listdir(self._tree.abspath(path))
1397
except OSError as e:
1398
if not (osutils._is_error_enotdir(e) or
1399
e.errno in (errno.ENOENT, errno.ESRCH)):
1403
for child in children:
1404
childpath = joinpath(path, child)
1405
if self._tree.is_control_filename(childpath):
1407
yield self.trans_id_tree_path(childpath)
1409
def _generate_limbo_path(self, trans_id):
1410
"""Generate a limbo path using the final path if possible.
1412
This optimizes the performance of applying the tree transform by
1413
avoiding renames. These renames can be avoided only when the parent
1414
directory is already scheduled for creation.
1416
If the final path cannot be used, falls back to using the trans_id as
1419
parent = self._new_parent.get(trans_id)
1420
# if the parent directory is already in limbo (e.g. when building a
1421
# tree), choose a limbo name inside the parent, to reduce further
1423
use_direct_path = False
1424
if self._new_contents.get(parent) == 'directory':
1425
filename = self._new_name.get(trans_id)
1426
if filename is not None:
1427
if parent not in self._limbo_children:
1428
self._limbo_children[parent] = set()
1429
self._limbo_children_names[parent] = {}
1430
use_direct_path = True
1431
# the direct path can only be used if no other file has
1432
# already taken this pathname, i.e. if the name is unused, or
1433
# if it is already associated with this trans_id.
1434
elif self._case_sensitive_target:
1435
if (self._limbo_children_names[parent].get(filename)
1436
in (trans_id, None)):
1437
use_direct_path = True
1439
for l_filename, l_trans_id in (
1440
self._limbo_children_names[parent].items()):
1441
if l_trans_id == trans_id:
1443
if l_filename.lower() == filename.lower():
1446
use_direct_path = True
1448
if not use_direct_path:
1449
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1451
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1452
self._limbo_children[parent].add(trans_id)
1453
self._limbo_children_names[parent][filename] = trans_id
1456
def version_file(self, trans_id, file_id=None):
1457
"""Schedule a file to become versioned."""
1460
unique_add(self._new_id, trans_id, file_id)
1461
unique_add(self._r_new_id, file_id, trans_id)
1463
def cancel_versioning(self, trans_id):
1464
"""Undo a previous versioning of a file"""
1465
file_id = self._new_id[trans_id]
1466
del self._new_id[trans_id]
1467
del self._r_new_id[file_id]
1469
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1470
"""Apply all changes to the inventory and filesystem.
1472
If filesystem or inventory conflicts are present, MalformedTransform
1475
If apply succeeds, finalize is not necessary.
1477
:param no_conflicts: if True, the caller guarantees there are no
1478
conflicts, so no check is made.
1479
:param precomputed_delta: An inventory delta to use instead of
1481
:param _mover: Supply an alternate FileMover, for testing
1483
for hook in MutableTree.hooks['pre_transform']:
1484
hook(self._tree, self)
1485
if not no_conflicts:
1486
self._check_malformed()
1487
self.rename_count = 0
1488
with ui.ui_factory.nested_progress_bar() as child_pb:
1489
if precomputed_delta is None:
1490
child_pb.update(gettext('Apply phase'), 0, 2)
1491
changes = self._generate_transform_changes()
1495
(op, np, ie) for (op, np, fid, ie) in precomputed_delta]
1498
mover = _FileMover()
1502
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1503
self._apply_removals(mover)
1504
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1505
modified_paths = self._apply_insertions(mover)
1506
except BaseException:
1510
mover.apply_deletions()
1511
if self.final_file_id(self.root) is None:
1512
changes = [e for e in changes if e[0] != '']
1513
self._tree._apply_transform_delta(changes)
1516
return _TransformResults(modified_paths, self.rename_count)
1518
def _apply_removals(self, mover):
1519
"""Perform tree operations that remove directory/inventory names.
1521
That is, delete files that are to be deleted, and put any files that
1522
need renaming into limbo. This must be done in strict child-to-parent
1525
If inventory_delta is None, no inventory delta generation is performed.
1527
tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1528
with ui.ui_factory.nested_progress_bar() as child_pb:
1529
for num, (path, trans_id) in enumerate(tree_paths):
1530
# do not attempt to move root into a subdirectory of itself.
1533
child_pb.update(gettext('removing file'), num, len(tree_paths))
1534
full_path = self._tree.abspath(path)
1535
if trans_id in self._removed_contents:
1536
delete_path = os.path.join(self._deletiondir, trans_id)
1537
mover.pre_delete(full_path, delete_path)
1538
elif (trans_id in self._new_name or
1539
trans_id in self._new_parent):
1541
mover.rename(full_path, self._limbo_name(trans_id))
1542
except TransformRenameFailed as e:
1543
if e.errno != errno.ENOENT:
1546
self.rename_count += 1
1548
def _apply_insertions(self, mover):
1549
"""Perform tree operations that insert directory/inventory names.
1551
That is, create any files that need to be created, and restore from
1552
limbo any files that needed renaming. This must be done in strict
1553
parent-to-child order.
1555
If inventory_delta is None, no inventory delta is calculated, and
1556
no list of modified paths is returned.
1558
new_paths = self.new_paths(filesystem_only=True)
1560
with ui.ui_factory.nested_progress_bar() as child_pb:
1561
for num, (path, trans_id) in enumerate(new_paths):
1563
child_pb.update(gettext('adding file'),
1564
num, len(new_paths))
1565
full_path = self._tree.abspath(path)
1566
if trans_id in self._needs_rename:
1568
mover.rename(self._limbo_name(trans_id), full_path)
1569
except TransformRenameFailed as e:
1570
# We may be renaming a dangling inventory id
1571
if e.errno != errno.ENOENT:
1574
self.rename_count += 1
1575
# TODO: if trans_id in self._observed_sha1s, we should
1576
# re-stat the final target, since ctime will be
1577
# updated by the change.
1578
if (trans_id in self._new_contents
1579
or self.path_changed(trans_id)):
1580
if trans_id in self._new_contents:
1581
modified_paths.append(full_path)
1582
if trans_id in self._new_executability:
1583
self._set_executability(path, trans_id)
1584
if trans_id in self._observed_sha1s:
1585
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1586
st = osutils.lstat(full_path)
1587
self._observed_sha1s[trans_id] = (o_sha1, st)
1588
for path, trans_id in new_paths:
1589
# new_paths includes stuff like workingtree conflicts. Only the
1590
# stuff in new_contents actually comes from limbo.
1591
if trans_id in self._limbo_files:
1592
del self._limbo_files[trans_id]
1593
self._new_contents.clear()
1594
return modified_paths
1596
def _inventory_altered(self):
1597
"""Determine which trans_ids need new Inventory entries.
1599
An new entry is needed when anything that would be reflected by an
1600
inventory entry changes, including file name, file_id, parent file_id,
1601
file kind, and the execute bit.
1603
Some care is taken to return entries with real changes, not cases
1604
where the value is deleted and then restored to its original value,
1605
but some actually unchanged values may be returned.
1607
:returns: A list of (path, trans_id) for all items requiring an
1608
inventory change. Ordered by path.
1611
# Find entries whose file_ids are new (or changed).
1612
new_file_id = set(t for t in self._new_id
1613
if self._new_id[t] != self.tree_file_id(t))
1614
for id_set in [self._new_name, self._new_parent, new_file_id,
1615
self._new_executability]:
1616
changed_ids.update(id_set)
1617
# removing implies a kind change
1618
changed_kind = set(self._removed_contents)
1620
changed_kind.intersection_update(self._new_contents)
1621
# Ignore entries that are already known to have changed.
1622
changed_kind.difference_update(changed_ids)
1623
# to keep only the truly changed ones
1624
changed_kind = (t for t in changed_kind
1625
if self.tree_kind(t) != self.final_kind(t))
1626
# all kind changes will alter the inventory
1627
changed_ids.update(changed_kind)
1628
# To find entries with changed parent_ids, find parents which existed,
1629
# but changed file_id.
1630
# Now add all their children to the set.
1631
for parent_trans_id in new_file_id:
1632
changed_ids.update(self.iter_tree_children(parent_trans_id))
1633
return sorted(FinalPaths(self).get_paths(changed_ids))
1635
def _generate_transform_changes(self):
1636
"""Generate an inventory delta for the current transform."""
1638
new_paths = self._inventory_altered()
1639
total_entries = len(new_paths) + len(self._removed_id)
1640
with ui.ui_factory.nested_progress_bar() as child_pb:
1641
for num, trans_id in enumerate(self._removed_id):
1643
child_pb.update(gettext('removing file'),
1645
if trans_id == self._new_root:
1646
file_id = self._tree.path2id('')
1648
file_id = self.tree_file_id(trans_id)
1649
# File-id isn't really being deleted, just moved
1650
if file_id in self._r_new_id:
1652
path = self._tree_id_paths[trans_id]
1653
changes.append((path, None, None))
1654
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1656
for num, (path, trans_id) in enumerate(new_paths):
1658
child_pb.update(gettext('adding file'),
1659
num + len(self._removed_id), total_entries)
1660
file_id = new_path_file_ids[trans_id]
1663
kind = self.final_kind(trans_id)
1665
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1666
parent_trans_id = self.final_parent(trans_id)
1667
parent_file_id = new_path_file_ids.get(parent_trans_id)
1668
if parent_file_id is None:
1669
parent_file_id = self.final_file_id(parent_trans_id)
1670
if trans_id in self._new_reference_revision:
1671
new_entry = inventory.TreeReference(
1673
self._new_name[trans_id],
1674
self.final_file_id(self._new_parent[trans_id]),
1675
None, self._new_reference_revision[trans_id])
1677
new_entry = inventory.make_entry(kind,
1678
self.final_name(trans_id),
1679
parent_file_id, file_id)
1681
old_path = self._tree.id2path(new_entry.file_id)
1682
except errors.NoSuchId:
1684
new_executability = self._new_executability.get(trans_id)
1685
if new_executability is not None:
1686
new_entry.executable = new_executability
1688
(old_path, path, new_entry))