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 ..transform import (
35
TransformRenameFailed,
42
from ..bzr import inventory
43
from ..bzr.transform import TransformPreview as GitTransformPreview
46
class TreeTransformBase(TreeTransform):
47
"""The base class for TreeTransform and its kin."""
49
def __init__(self, tree, pb=None, case_sensitive=True):
52
:param tree: The tree that will be transformed, but not necessarily
55
:param case_sensitive: If True, the target of the transform is
56
case sensitive, not just case preserving.
58
super(TreeTransformBase, self).__init__(tree, pb=pb)
59
# mapping of trans_id => (sha1 of content, stat_value)
60
self._observed_sha1s = {}
61
# Mapping of trans_id -> new file_id
63
# Mapping of old file-id -> trans_id
64
self._non_present_ids = {}
65
# Mapping of new file_id -> trans_id
67
# The trans_id that will be used as the tree root
68
if tree.is_versioned(''):
69
self._new_root = self.trans_id_tree_path('')
72
# Whether the target is case sensitive
73
self._case_sensitive_target = case_sensitive
76
"""Release the working tree lock, if held.
78
This is required if apply has not been invoked, but can be invoked
81
if self._tree is None:
83
for hook in MutableTree.hooks['post_transform']:
84
hook(self._tree, self)
91
root = property(__get_root)
93
def create_path(self, name, parent):
94
"""Assign a transaction id to a new path"""
95
trans_id = self._assign_id()
96
unique_add(self._new_name, trans_id, name)
97
unique_add(self._new_parent, trans_id, parent)
100
def adjust_root_path(self, name, parent):
101
"""Emulate moving the root by moving all children, instead.
103
We do this by undoing the association of root's transaction id with the
104
current tree. This allows us to create a new directory with that
105
transaction id. We unversion the root directory and version the
106
physically new directory, and hope someone versions the tree root
109
old_root = self._new_root
110
old_root_file_id = self.final_file_id(old_root)
111
# force moving all children of root
112
for child_id in self.iter_tree_children(old_root):
113
if child_id != parent:
114
self.adjust_path(self.final_name(child_id),
115
self.final_parent(child_id), child_id)
116
file_id = self.final_file_id(child_id)
117
if file_id is not None:
118
self.unversion_file(child_id)
119
self.version_file(child_id, file_id=file_id)
121
# the physical root needs a new transaction id
122
self._tree_path_ids.pop("")
123
self._tree_id_paths.pop(old_root)
124
self._new_root = self.trans_id_tree_path('')
125
if parent == old_root:
126
parent = self._new_root
127
self.adjust_path(name, parent, old_root)
128
self.create_directory(old_root)
129
self.version_file(old_root, file_id=old_root_file_id)
130
self.unversion_file(self._new_root)
132
def fixup_new_roots(self):
133
"""Reinterpret requests to change the root directory
135
Instead of creating a root directory, or moving an existing directory,
136
all the attributes and children of the new root are applied to the
137
existing root directory.
139
This means that the old root trans-id becomes obsolete, so it is
140
recommended only to invoke this after the root trans-id has become
144
new_roots = [k for k, v in self._new_parent.items()
146
if len(new_roots) < 1:
148
if len(new_roots) != 1:
149
raise ValueError('A tree cannot have two roots!')
150
if self._new_root is None:
151
self._new_root = new_roots[0]
153
old_new_root = new_roots[0]
154
# unversion the new root's directory.
155
if self.final_kind(self._new_root) is None:
156
file_id = self.final_file_id(old_new_root)
158
file_id = self.final_file_id(self._new_root)
159
if old_new_root in self._new_id:
160
self.cancel_versioning(old_new_root)
162
self.unversion_file(old_new_root)
163
# if, at this stage, root still has an old file_id, zap it so we can
164
# stick a new one in.
165
if (self.tree_file_id(self._new_root) is not None
166
and self._new_root not in self._removed_id):
167
self.unversion_file(self._new_root)
168
if file_id is not None:
169
self.version_file(self._new_root, file_id=file_id)
171
# Now move children of new root into old root directory.
172
# Ensure all children are registered with the transaction, but don't
173
# use directly-- some tree children have new parents
174
list(self.iter_tree_children(old_new_root))
175
# Move all children of new root into old root directory.
176
for child in self.by_parent().get(old_new_root, []):
177
self.adjust_path(self.final_name(child), self._new_root, child)
179
# Ensure old_new_root has no directory.
180
if old_new_root in self._new_contents:
181
self.cancel_creation(old_new_root)
183
self.delete_contents(old_new_root)
185
# prevent deletion of root directory.
186
if self._new_root in self._removed_contents:
187
self.cancel_deletion(self._new_root)
189
# destroy path info for old_new_root.
190
del self._new_parent[old_new_root]
191
del self._new_name[old_new_root]
193
def trans_id_file_id(self, file_id):
194
"""Determine or set the transaction id associated with a file ID.
195
A new id is only created for file_ids that were never present. If
196
a transaction has been unversioned, it is deliberately still returned.
197
(this will likely lead to an unversioned parent conflict.)
200
raise ValueError('None is not a valid file id')
201
if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
202
return self._r_new_id[file_id]
205
path = self._tree.id2path(file_id)
206
except errors.NoSuchId:
207
if file_id in self._non_present_ids:
208
return self._non_present_ids[file_id]
210
trans_id = self._assign_id()
211
self._non_present_ids[file_id] = trans_id
214
return self.trans_id_tree_path(path)
216
def version_file(self, trans_id, file_id=None):
217
"""Schedule a file to become versioned."""
218
raise NotImplementedError(self.version_file)
220
def cancel_versioning(self, trans_id):
221
"""Undo a previous versioning of a file"""
222
raise NotImplementedError(self.cancel_versioning)
224
def new_paths(self, filesystem_only=False):
225
"""Determine the paths of all new and changed files.
227
:param filesystem_only: if True, only calculate values for files
228
that require renames or execute bit changes.
232
stale_ids = self._needs_rename.difference(self._new_name)
233
stale_ids.difference_update(self._new_parent)
234
stale_ids.difference_update(self._new_contents)
235
stale_ids.difference_update(self._new_id)
236
needs_rename = self._needs_rename.difference(stale_ids)
237
id_sets = (needs_rename, self._new_executability)
239
id_sets = (self._new_name, self._new_parent, self._new_contents,
240
self._new_id, self._new_executability)
241
for id_set in id_sets:
242
new_ids.update(id_set)
243
return sorted(FinalPaths(self).get_paths(new_ids))
245
def tree_file_id(self, trans_id):
246
"""Determine the file id associated with the trans_id in the tree"""
247
path = self.tree_path(trans_id)
250
# the file is old; the old id is still valid
251
if self._new_root == trans_id:
252
return self._tree.path2id('')
253
return self._tree.path2id(path)
255
def final_is_versioned(self, trans_id):
256
return self.final_file_id(trans_id) is not None
258
def final_file_id(self, trans_id):
259
"""Determine the file id after any changes are applied, or None.
261
None indicates that the file will not be versioned after changes are
265
return self._new_id[trans_id]
267
if trans_id in self._removed_id:
269
return self.tree_file_id(trans_id)
271
def inactive_file_id(self, trans_id):
272
"""Return the inactive file_id associated with a transaction id.
273
That is, the one in the tree or in non_present_ids.
274
The file_id may actually be active, too.
276
file_id = self.tree_file_id(trans_id)
277
if file_id is not None:
279
for key, value in self._non_present_ids.items():
280
if value == trans_id:
283
def find_conflicts(self):
284
"""Find any violations of inventory or filesystem invariants"""
285
if self._done is True:
286
raise ReusingTransform()
288
# ensure all children of all existent parents are known
289
# all children of non-existent parents are known, by definition.
290
self._add_tree_children()
291
by_parent = self.by_parent()
292
conflicts.extend(self._unversioned_parents(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_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 _unversioned_parents(self, by_parent):
389
"""If parent directories are versioned, children must be versioned."""
391
for parent_id, children in by_parent.items():
392
if parent_id == ROOT_PARENT:
394
if self.final_is_versioned(parent_id):
396
for child_id in children:
397
if self.final_is_versioned(child_id):
398
conflicts.append(('unversioned parent', parent_id))
402
def _improper_versioning(self):
403
"""Cannot version a file with no contents, or a bad type.
405
However, existing entries with no contents are okay.
408
for trans_id in self._new_id:
409
kind = self.final_kind(trans_id)
410
if kind == 'symlink' and not self._tree.supports_symlinks():
411
# Ignore symlinks as they are not supported on this platform
414
conflicts.append(('versioning no contents', trans_id))
416
if not self._tree.versionable_kind(kind):
417
conflicts.append(('versioning bad kind', trans_id, kind))
420
def _executability_conflicts(self):
421
"""Check for bad executability changes.
423
Only versioned files may have their executability set, because
424
1. only versioned entries can have executability under windows
425
2. only files can be executable. (The execute bit on a directory
426
does not indicate searchability)
429
for trans_id in self._new_executability:
430
if not self.final_is_versioned(trans_id):
431
conflicts.append(('unversioned executability', trans_id))
433
if self.final_kind(trans_id) != "file":
434
conflicts.append(('non-file executability', trans_id))
437
def _overwrite_conflicts(self):
438
"""Check for overwrites (not permitted on Win32)"""
440
for trans_id in self._new_contents:
441
if self.tree_kind(trans_id) is None:
443
if trans_id not in self._removed_contents:
444
conflicts.append(('overwrite', trans_id,
445
self.final_name(trans_id)))
448
def _duplicate_entries(self, by_parent):
449
"""No directory may have two entries with the same name."""
451
if (self._new_name, self._new_parent) == ({}, {}):
453
for children in by_parent.values():
455
for child_tid in children:
456
name = self.final_name(child_tid)
458
# Keep children only if they still exist in the end
459
if not self._case_sensitive_target:
461
name_ids.append((name, child_tid))
465
for name, trans_id in name_ids:
466
kind = self.final_kind(trans_id)
467
if kind is None and not self.final_is_versioned(trans_id):
469
if name == last_name:
470
conflicts.append(('duplicate', last_trans_id, trans_id,
473
last_trans_id = trans_id
476
def _parent_type_conflicts(self, by_parent):
477
"""Children must have a directory parent"""
479
for parent_id, children in by_parent.items():
480
if parent_id == ROOT_PARENT:
483
for child_id in children:
484
if self.final_kind(child_id) is not None:
489
# There is at least a child, so we need an existing directory to
491
kind = self.final_kind(parent_id)
493
# The directory will be deleted
494
conflicts.append(('missing parent', parent_id))
495
elif kind != "directory":
496
# Meh, we need a *directory* to put something in it
497
conflicts.append(('non-directory parent', parent_id))
500
def _set_executability(self, path, trans_id):
501
"""Set the executability of versioned files """
502
if self._tree._supports_executable():
503
new_executability = self._new_executability[trans_id]
504
abspath = self._tree.abspath(path)
505
current_mode = os.stat(abspath).st_mode
506
if new_executability:
509
to_mode = current_mode | (0o100 & ~umask)
510
# Enable x-bit for others only if they can read it.
511
if current_mode & 0o004:
512
to_mode |= 0o001 & ~umask
513
if current_mode & 0o040:
514
to_mode |= 0o010 & ~umask
516
to_mode = current_mode & ~0o111
517
osutils.chmod_if_possible(abspath, to_mode)
519
def _new_entry(self, name, parent_id, file_id):
520
"""Helper function to create a new filesystem entry."""
521
trans_id = self.create_path(name, parent_id)
522
if file_id is not None:
523
self.version_file(trans_id, file_id=file_id)
526
def new_file(self, name, parent_id, contents, file_id=None,
527
executable=None, sha1=None):
528
"""Convenience method to create files.
530
name is the name of the file to create.
531
parent_id is the transaction id of the parent directory of the file.
532
contents is an iterator of bytestrings, which will be used to produce
534
:param file_id: The inventory ID of the file, if it is to be versioned.
535
:param executable: Only valid when a file_id has been supplied.
537
trans_id = self._new_entry(name, parent_id, file_id)
538
# TODO: rather than scheduling a set_executable call,
539
# have create_file create the file with the right mode.
540
self.create_file(contents, trans_id, sha1=sha1)
541
if executable is not None:
542
self.set_executability(executable, trans_id)
545
def new_directory(self, name, parent_id, file_id=None):
546
"""Convenience method to create directories.
548
name is the name of the directory to create.
549
parent_id is the transaction id of the parent directory of the
551
file_id is the inventory ID of the directory, if it is to be versioned.
553
trans_id = self._new_entry(name, parent_id, file_id)
554
self.create_directory(trans_id)
557
def new_symlink(self, name, parent_id, target, file_id=None):
558
"""Convenience method to create symbolic link.
560
name is the name of the symlink to create.
561
parent_id is the transaction id of the parent directory of the symlink.
562
target is a bytestring of the target of the symlink.
563
file_id is the inventory ID of the file, if it is to be versioned.
565
trans_id = self._new_entry(name, parent_id, file_id)
566
self.create_symlink(target, trans_id)
569
def new_orphan(self, trans_id, parent_id):
570
"""Schedule an item to be orphaned.
572
When a directory is about to be removed, its children, if they are not
573
versioned are moved out of the way: they don't have a parent anymore.
575
:param trans_id: The trans_id of the existing item.
576
:param parent_id: The parent trans_id of the item.
578
raise NotImplementedError(self.new_orphan)
580
def _get_potential_orphans(self, dir_id):
581
"""Find the potential orphans in a directory.
583
A directory can't be safely deleted if there are versioned files in it.
584
If all the contained files are unversioned then they can be orphaned.
586
The 'None' return value means that the directory contains at least one
587
versioned file and should not be deleted.
589
:param dir_id: The directory trans id.
591
:return: A list of the orphan trans ids or None if at least one
592
versioned file is present.
595
# Find the potential orphans, stop if one item should be kept
596
for child_tid in self.by_parent()[dir_id]:
597
if child_tid in self._removed_contents:
598
# The child is removed as part of the transform. Since it was
599
# versioned before, it's not an orphan
601
if not self.final_is_versioned(child_tid):
602
# The child is not versioned
603
orphans.append(child_tid)
605
# We have a versioned file here, searching for orphans is
611
def _affected_ids(self):
612
"""Return the set of transform ids affected by the transform"""
613
trans_ids = set(self._removed_id)
614
trans_ids.update(self._new_id)
615
trans_ids.update(self._removed_contents)
616
trans_ids.update(self._new_contents)
617
trans_ids.update(self._new_executability)
618
trans_ids.update(self._new_name)
619
trans_ids.update(self._new_parent)
622
def _get_file_id_maps(self):
623
"""Return mapping of file_ids to trans_ids in the to and from states"""
624
trans_ids = self._affected_ids()
627
# Build up two dicts: trans_ids associated with file ids in the
628
# FROM state, vs the TO state.
629
for trans_id in trans_ids:
630
from_file_id = self.tree_file_id(trans_id)
631
if from_file_id is not None:
632
from_trans_ids[from_file_id] = trans_id
633
to_file_id = self.final_file_id(trans_id)
634
if to_file_id is not None:
635
to_trans_ids[to_file_id] = trans_id
636
return from_trans_ids, to_trans_ids
638
def _from_file_data(self, from_trans_id, from_versioned, from_path):
639
"""Get data about a file in the from (tree) state
641
Return a (name, parent, kind, executable) tuple
643
from_path = self._tree_id_paths.get(from_trans_id)
645
# get data from working tree if versioned
646
from_entry = next(self._tree.iter_entries_by_dir(
647
specific_files=[from_path]))[1]
648
from_name = from_entry.name
649
from_parent = from_entry.parent_id
652
if from_path is None:
653
# File does not exist in FROM state
657
# File exists, but is not versioned. Have to use path-
659
from_name = os.path.basename(from_path)
660
tree_parent = self.get_tree_parent(from_trans_id)
661
from_parent = self.tree_file_id(tree_parent)
662
if from_path is not None:
663
from_kind, from_executable, from_stats = \
664
self._tree._comparison_data(from_entry, from_path)
667
from_executable = False
668
return from_name, from_parent, from_kind, from_executable
670
def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
671
"""Get data about a file in the to (target) state
673
Return a (name, parent, kind, executable) tuple
675
to_name = self.final_name(to_trans_id)
676
to_kind = self.final_kind(to_trans_id)
677
to_parent = self.final_file_id(self.final_parent(to_trans_id))
678
if to_trans_id in self._new_executability:
679
to_executable = self._new_executability[to_trans_id]
680
elif to_trans_id == from_trans_id:
681
to_executable = from_executable
683
to_executable = False
684
return to_name, to_parent, to_kind, to_executable
686
def iter_changes(self):
687
"""Produce output in the same format as Tree.iter_changes.
689
Will produce nonsensical results if invoked while inventory/filesystem
690
conflicts (as reported by TreeTransform.find_conflicts()) are present.
692
This reads the Transform, but only reproduces changes involving a
693
file_id. Files that are not versioned in either of the FROM or TO
694
states are not reflected.
696
final_paths = FinalPaths(self)
697
from_trans_ids, to_trans_ids = self._get_file_id_maps()
699
# Now iterate through all active file_ids
700
for file_id in set(from_trans_ids).union(to_trans_ids):
702
from_trans_id = from_trans_ids.get(file_id)
703
# find file ids, and determine versioning state
704
if from_trans_id is None:
705
from_versioned = False
706
from_trans_id = to_trans_ids[file_id]
708
from_versioned = True
709
to_trans_id = to_trans_ids.get(file_id)
710
if to_trans_id is None:
712
to_trans_id = from_trans_id
716
if not from_versioned:
719
from_path = self._tree_id_paths.get(from_trans_id)
723
to_path = final_paths.get_path(to_trans_id)
725
from_name, from_parent, from_kind, from_executable = \
726
self._from_file_data(from_trans_id, from_versioned, from_path)
728
to_name, to_parent, to_kind, to_executable = \
729
self._to_file_data(to_trans_id, from_trans_id, from_executable)
731
if from_kind != to_kind:
733
elif to_kind in ('file', 'symlink') and (
734
to_trans_id != from_trans_id
735
or to_trans_id in self._new_contents):
737
if (not modified and from_versioned == to_versioned
738
and from_parent == to_parent and from_name == to_name
739
and from_executable == to_executable):
743
file_id, (from_path, to_path), modified,
744
(from_versioned, to_versioned),
745
(from_parent, to_parent),
746
(from_name, to_name),
747
(from_kind, to_kind),
748
(from_executable, to_executable)))
751
return (c.path[0] or '', c.path[1] or '')
752
return iter(sorted(results, key=path_key))
754
def get_preview_tree(self):
755
"""Return a tree representing the result of the transform.
757
The tree is a snapshot, and altering the TreeTransform will invalidate
760
raise NotImplementedError(self.get_preview_tree)
762
def commit(self, branch, message, merge_parents=None, strict=False,
763
timestamp=None, timezone=None, committer=None, authors=None,
764
revprops=None, revision_id=None):
765
"""Commit the result of this TreeTransform to a branch.
767
:param branch: The branch to commit to.
768
:param message: The message to attach to the commit.
769
:param merge_parents: Additional parent revision-ids specified by
771
:param strict: If True, abort the commit if there are unversioned
773
:param timestamp: if not None, seconds-since-epoch for the time and
774
date. (May be a float.)
775
:param timezone: Optional timezone for timestamp, as an offset in
777
:param committer: Optional committer in email-id format.
778
(e.g. "J Random Hacker <jrandom@example.com>")
779
:param authors: Optional list of authors in email-id format.
780
:param revprops: Optional dictionary of revision properties.
781
:param revision_id: Optional revision id. (Specifying a revision-id
782
may reduce performance for some non-native formats.)
783
:return: The revision_id of the revision committed.
785
self._check_malformed()
787
unversioned = set(self._new_contents).difference(set(self._new_id))
788
for trans_id in unversioned:
789
if not self.final_is_versioned(trans_id):
790
raise errors.StrictCommitFailed()
792
revno, last_rev_id = branch.last_revision_info()
793
if last_rev_id == _mod_revision.NULL_REVISION:
794
if merge_parents is not None:
795
raise ValueError('Cannot supply merge parents for first'
799
parent_ids = [last_rev_id]
800
if merge_parents is not None:
801
parent_ids.extend(merge_parents)
802
if self._tree.get_revision_id() != last_rev_id:
803
raise ValueError('TreeTransform not based on branch basis: %s' %
804
self._tree.get_revision_id().decode('utf-8'))
805
from .. import commit
806
revprops = commit.Commit.update_revprops(revprops, branch, authors)
807
builder = branch.get_commit_builder(parent_ids,
812
revision_id=revision_id)
813
preview = self.get_preview_tree()
814
list(builder.record_iter_changes(preview, last_rev_id,
815
self.iter_changes()))
816
builder.finish_inventory()
817
revision_id = builder.commit(message)
818
branch.set_last_revision_info(revno + 1, revision_id)
821
def _text_parent(self, trans_id):
822
path = self.tree_path(trans_id)
824
if path is None or self._tree.kind(path) != 'file':
826
except errors.NoSuchFile:
830
def _get_parents_texts(self, trans_id):
831
"""Get texts for compression parents of this file."""
832
path = self._text_parent(trans_id)
835
return (self._tree.get_file_text(path),)
837
def _get_parents_lines(self, trans_id):
838
"""Get lines for compression parents of this file."""
839
path = self._text_parent(trans_id)
842
return (self._tree.get_file_lines(path),)
844
def serialize(self, serializer):
845
"""Serialize this TreeTransform.
847
:param serializer: A Serialiser like pack.ContainerSerializer.
849
from .. import bencode
850
new_name = {k.encode('utf-8'): v.encode('utf-8')
851
for k, v in self._new_name.items()}
852
new_parent = {k.encode('utf-8'): v.encode('utf-8')
853
for k, v in self._new_parent.items()}
854
new_id = {k.encode('utf-8'): v
855
for k, v in self._new_id.items()}
856
new_executability = {k.encode('utf-8'): int(v)
857
for k, v in self._new_executability.items()}
858
tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
859
for k, v in self._tree_path_ids.items()}
860
non_present_ids = {k: v.encode('utf-8')
861
for k, v in self._non_present_ids.items()}
862
removed_contents = [trans_id.encode('utf-8')
863
for trans_id in self._removed_contents]
864
removed_id = [trans_id.encode('utf-8')
865
for trans_id in self._removed_id]
867
b'_id_number': self._id_number,
868
b'_new_name': new_name,
869
b'_new_parent': new_parent,
870
b'_new_executability': new_executability,
872
b'_tree_path_ids': tree_path_ids,
873
b'_removed_id': removed_id,
874
b'_removed_contents': removed_contents,
875
b'_non_present_ids': non_present_ids,
877
yield serializer.bytes_record(bencode.bencode(attribs),
879
for trans_id, kind in sorted(self._new_contents.items()):
881
with open(self._limbo_name(trans_id), 'rb') as cur_file:
882
lines = cur_file.readlines()
883
parents = self._get_parents_lines(trans_id)
884
mpdiff = multiparent.MultiParent.from_lines(lines, parents)
885
content = b''.join(mpdiff.to_patch())
886
if kind == 'directory':
888
if kind == 'symlink':
889
content = self._read_symlink_target(trans_id)
890
if not isinstance(content, bytes):
891
content = content.encode('utf-8')
892
yield serializer.bytes_record(
893
content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
895
def deserialize(self, records):
896
"""Deserialize a stored TreeTransform.
898
:param records: An iterable of (names, content) tuples, as per
899
pack.ContainerPushParser.
901
from .. import bencode
902
names, content = next(records)
903
attribs = bencode.bdecode(content)
904
self._id_number = attribs[b'_id_number']
905
self._new_name = {k.decode('utf-8'): v.decode('utf-8')
906
for k, v in attribs[b'_new_name'].items()}
907
self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
908
for k, v in attribs[b'_new_parent'].items()}
909
self._new_executability = {
910
k.decode('utf-8'): bool(v)
911
for k, v in attribs[b'_new_executability'].items()}
912
self._new_id = {k.decode('utf-8'): v
913
for k, v in attribs[b'_new_id'].items()}
914
self._r_new_id = {v: k for k, v in self._new_id.items()}
915
self._tree_path_ids = {}
916
self._tree_id_paths = {}
917
for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
918
path = bytepath.decode('utf-8')
919
trans_id = trans_id.decode('utf-8')
920
self._tree_path_ids[path] = trans_id
921
self._tree_id_paths[trans_id] = path
922
self._removed_id = {trans_id.decode('utf-8')
923
for trans_id in attribs[b'_removed_id']}
924
self._removed_contents = set(
925
trans_id.decode('utf-8')
926
for trans_id in attribs[b'_removed_contents'])
927
self._non_present_ids = {
929
for k, v in attribs[b'_non_present_ids'].items()}
930
for ((trans_id, kind),), content in records:
931
trans_id = trans_id.decode('utf-8')
932
kind = kind.decode('ascii')
934
mpdiff = multiparent.MultiParent.from_patch(content)
935
lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
936
self.create_file(lines, trans_id)
937
if kind == 'directory':
938
self.create_directory(trans_id)
939
if kind == 'symlink':
940
self.create_symlink(content.decode('utf-8'), trans_id)
942
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
943
"""Schedule creation of a new file.
947
:param contents: an iterator of strings, all of which will be written
948
to the target destination.
949
:param trans_id: TreeTransform handle
950
:param mode_id: If not None, force the mode of the target file to match
951
the mode of the object referenced by mode_id.
952
Otherwise, we will try to preserve mode bits of an existing file.
953
:param sha1: If the sha1 of this content is already known, pass it in.
954
We can use it to prevent future sha1 computations.
956
raise NotImplementedError(self.create_file)
958
def create_directory(self, trans_id):
959
"""Schedule creation of a new directory.
961
See also new_directory.
963
raise NotImplementedError(self.create_directory)
965
def create_symlink(self, target, trans_id):
966
"""Schedule creation of a new symbolic link.
968
target is a bytestring.
969
See also new_symlink.
971
raise NotImplementedError(self.create_symlink)
973
def create_hardlink(self, path, trans_id):
974
"""Schedule creation of a hard link"""
975
raise NotImplementedError(self.create_hardlink)
977
def cancel_creation(self, trans_id):
978
"""Cancel the creation of new file contents."""
979
raise NotImplementedError(self.cancel_creation)
981
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
982
"""Apply all changes to the inventory and filesystem.
984
If filesystem or inventory conflicts are present, MalformedTransform
987
If apply succeeds, finalize is not necessary.
989
:param no_conflicts: if True, the caller guarantees there are no
990
conflicts, so no check is made.
991
:param precomputed_delta: An inventory delta to use instead of
993
:param _mover: Supply an alternate FileMover, for testing
995
raise NotImplementedError(self.apply)
998
class DiskTreeTransform(TreeTransformBase):
999
"""Tree transform storing its contents on disk."""
1001
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1003
:param tree: The tree that will be transformed, but not necessarily
1005
:param limbodir: A directory where new files can be stored until
1006
they are installed in their proper places
1008
:param case_sensitive: If True, the target of the transform is
1009
case sensitive, not just case preserving.
1011
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1012
self._limbodir = limbodir
1013
self._deletiondir = None
1014
# A mapping of transform ids to their limbo filename
1015
self._limbo_files = {}
1016
self._possibly_stale_limbo_files = set()
1017
# A mapping of transform ids to a set of the transform ids of children
1018
# that their limbo directory has
1019
self._limbo_children = {}
1020
# Map transform ids to maps of child filename to child transform id
1021
self._limbo_children_names = {}
1022
# List of transform ids that need to be renamed from limbo into place
1023
self._needs_rename = set()
1024
self._creation_mtime = None
1025
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1028
"""Release the working tree lock, if held, clean up limbo dir.
1030
This is required if apply has not been invoked, but can be invoked
1033
if self._tree is None:
1036
limbo_paths = list(self._limbo_files.values())
1037
limbo_paths.extend(self._possibly_stale_limbo_files)
1038
limbo_paths.sort(reverse=True)
1039
for path in limbo_paths:
1041
osutils.delete_any(path)
1042
except OSError as e:
1043
if e.errno != errno.ENOENT:
1045
# XXX: warn? perhaps we just got interrupted at an
1046
# inconvenient moment, but perhaps files are disappearing
1049
osutils.delete_any(self._limbodir)
1051
# We don't especially care *why* the dir is immortal.
1052
raise ImmortalLimbo(self._limbodir)
1054
if self._deletiondir is not None:
1055
osutils.delete_any(self._deletiondir)
1057
raise errors.ImmortalPendingDeletion(self._deletiondir)
1059
TreeTransformBase.finalize(self)
1061
def _limbo_supports_executable(self):
1062
"""Check if the limbo path supports the executable bit."""
1063
return osutils.supports_executable(self._limbodir)
1065
def _limbo_name(self, trans_id):
1066
"""Generate the limbo name of a file"""
1067
limbo_name = self._limbo_files.get(trans_id)
1068
if limbo_name is None:
1069
limbo_name = self._generate_limbo_path(trans_id)
1070
self._limbo_files[trans_id] = limbo_name
1073
def _generate_limbo_path(self, trans_id):
1074
"""Generate a limbo path using the trans_id as the relative path.
1076
This is suitable as a fallback, and when the transform should not be
1077
sensitive to the path encoding of the limbo directory.
1079
self._needs_rename.add(trans_id)
1080
return osutils.pathjoin(self._limbodir, trans_id)
1082
def adjust_path(self, name, parent, trans_id):
1083
previous_parent = self._new_parent.get(trans_id)
1084
previous_name = self._new_name.get(trans_id)
1085
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
1086
if (trans_id in self._limbo_files
1087
and trans_id not in self._needs_rename):
1088
self._rename_in_limbo([trans_id])
1089
if previous_parent != parent:
1090
self._limbo_children[previous_parent].remove(trans_id)
1091
if previous_parent != parent or previous_name != name:
1092
del self._limbo_children_names[previous_parent][previous_name]
1094
def _rename_in_limbo(self, trans_ids):
1095
"""Fix limbo names so that the right final path is produced.
1097
This means we outsmarted ourselves-- we tried to avoid renaming
1098
these files later by creating them with their final names in their
1099
final parents. But now the previous name or parent is no longer
1100
suitable, so we have to rename them.
1102
Even for trans_ids that have no new contents, we must remove their
1103
entries from _limbo_files, because they are now stale.
1105
for trans_id in trans_ids:
1106
old_path = self._limbo_files[trans_id]
1107
self._possibly_stale_limbo_files.add(old_path)
1108
del self._limbo_files[trans_id]
1109
if trans_id not in self._new_contents:
1111
new_path = self._limbo_name(trans_id)
1112
os.rename(old_path, new_path)
1113
self._possibly_stale_limbo_files.remove(old_path)
1114
for descendant in self._limbo_descendants(trans_id):
1115
desc_path = self._limbo_files[descendant]
1116
desc_path = new_path + desc_path[len(old_path):]
1117
self._limbo_files[descendant] = desc_path
1119
def _limbo_descendants(self, trans_id):
1120
"""Return the set of trans_ids whose limbo paths descend from this."""
1121
descendants = set(self._limbo_children.get(trans_id, []))
1122
for descendant in list(descendants):
1123
descendants.update(self._limbo_descendants(descendant))
1126
def _set_mode(self, trans_id, mode_id, typefunc):
1127
raise NotImplementedError(self._set_mode)
1129
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1130
"""Schedule creation of a new file.
1134
:param contents: an iterator of strings, all of which will be written
1135
to the target destination.
1136
:param trans_id: TreeTransform handle
1137
:param mode_id: If not None, force the mode of the target file to match
1138
the mode of the object referenced by mode_id.
1139
Otherwise, we will try to preserve mode bits of an existing file.
1140
:param sha1: If the sha1 of this content is already known, pass it in.
1141
We can use it to prevent future sha1 computations.
1143
name = self._limbo_name(trans_id)
1144
with open(name, 'wb') as f:
1145
unique_add(self._new_contents, trans_id, 'file')
1146
f.writelines(contents)
1147
self._set_mtime(name)
1148
self._set_mode(trans_id, mode_id, S_ISREG)
1149
# It is unfortunate we have to use lstat instead of fstat, but we just
1150
# used utime and chmod on the file, so we need the accurate final
1152
if sha1 is not None:
1153
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1155
def _read_symlink_target(self, trans_id):
1156
return os.readlink(self._limbo_name(trans_id))
1158
def _set_mtime(self, path):
1159
"""All files that are created get the same mtime.
1161
This time is set by the first object to be created.
1163
if self._creation_mtime is None:
1164
self._creation_mtime = time.time()
1165
os.utime(path, (self._creation_mtime, self._creation_mtime))
1167
def create_hardlink(self, path, trans_id):
1168
"""Schedule creation of a hard link"""
1169
name = self._limbo_name(trans_id)
1172
except OSError as e:
1173
if e.errno != errno.EPERM:
1175
raise errors.HardLinkNotSupported(path)
1177
unique_add(self._new_contents, trans_id, 'file')
1178
except BaseException:
1179
# Clean up the file, it never got registered so
1180
# TreeTransform.finalize() won't clean it up.
1184
def create_directory(self, trans_id):
1185
"""Schedule creation of a new directory.
1187
See also new_directory.
1189
os.mkdir(self._limbo_name(trans_id))
1190
unique_add(self._new_contents, trans_id, 'directory')
1192
def create_symlink(self, target, trans_id):
1193
"""Schedule creation of a new symbolic link.
1195
target is a bytestring.
1196
See also new_symlink.
1198
if self._create_symlinks:
1199
os.symlink(target, self._limbo_name(trans_id))
1202
path = FinalPaths(self).get_path(trans_id)
1206
'Unable to create symlink "%s" on this filesystem.' % (path,))
1207
# We add symlink to _new_contents even if they are unsupported
1208
# and not created. These entries are subsequently used to avoid
1209
# conflicts on platforms that don't support symlink
1210
unique_add(self._new_contents, trans_id, 'symlink')
1212
def cancel_creation(self, trans_id):
1213
"""Cancel the creation of new file contents."""
1214
del self._new_contents[trans_id]
1215
if trans_id in self._observed_sha1s:
1216
del self._observed_sha1s[trans_id]
1217
children = self._limbo_children.get(trans_id)
1218
# if this is a limbo directory with children, move them before removing
1220
if children is not None:
1221
self._rename_in_limbo(children)
1222
del self._limbo_children[trans_id]
1223
del self._limbo_children_names[trans_id]
1224
osutils.delete_any(self._limbo_name(trans_id))
1226
def new_orphan(self, trans_id, parent_id):
1227
conf = self._tree.get_config_stack()
1228
handle_orphan = conf.get('transform.orphan_policy')
1229
handle_orphan(self, trans_id, parent_id)
1232
class GitTreeTransform(DiskTreeTransform):
1233
"""Represent a tree transformation.
1235
This object is designed to support incremental generation of the transform,
1238
However, it gives optimum performance when parent directories are created
1239
before their contents. The transform is then able to put child files
1240
directly in their parent directory, avoiding later renames.
1242
It is easy to produce malformed transforms, but they are generally
1243
harmless. Attempting to apply a malformed transform will cause an
1244
exception to be raised before any modifications are made to the tree.
1246
Many kinds of malformed transforms can be corrected with the
1247
resolve_conflicts function. The remaining ones indicate programming error,
1248
such as trying to create a file with no path.
1250
Two sets of file creation methods are supplied. Convenience methods are:
1255
These are composed of the low-level methods:
1257
* create_file or create_directory or create_symlink
1261
Transform/Transaction ids
1262
-------------------------
1263
trans_ids are temporary ids assigned to all files involved in a transform.
1264
It's possible, even common, that not all files in the Tree have trans_ids.
1266
trans_ids are used because filenames and file_ids are not good enough
1267
identifiers; filenames change.
1269
trans_ids are only valid for the TreeTransform that generated them.
1273
Limbo is a temporary directory use to hold new versions of files.
1274
Files are added to limbo by create_file, create_directory, create_symlink,
1275
and their convenience variants (new_*). Files may be removed from limbo
1276
using cancel_creation. Files are renamed from limbo into their final
1277
location as part of TreeTransform.apply
1279
Limbo must be cleaned up, by either calling TreeTransform.apply or
1280
calling TreeTransform.finalize.
1282
Files are placed into limbo inside their parent directories, where
1283
possible. This reduces subsequent renames, and makes operations involving
1284
lots of files faster. This optimization is only possible if the parent
1285
directory is created *before* creating any of its children, so avoid
1286
creating children before parents, where possible.
1290
This temporary directory is used by _FileMover for storing files that are
1291
about to be deleted. In case of rollback, the files will be restored.
1292
FileMover does not delete files until it is sure that a rollback will not
1296
def __init__(self, tree, pb=None):
1297
"""Note: a tree_write lock is taken on the tree.
1299
Use TreeTransform.finalize() to release the lock (can be omitted if
1300
TreeTransform.apply() called).
1302
tree.lock_tree_write()
1304
limbodir = urlutils.local_path_from_url(
1305
tree._transport.abspath('limbo'))
1306
osutils.ensure_empty_directory_exists(
1308
errors.ExistingLimbo)
1309
deletiondir = urlutils.local_path_from_url(
1310
tree._transport.abspath('pending-deletion'))
1311
osutils.ensure_empty_directory_exists(
1313
errors.ExistingPendingDeletion)
1314
except BaseException:
1318
# Cache of realpath results, to speed up canonical_path
1319
self._realpaths = {}
1320
# Cache of relpath results, to speed up canonical_path
1322
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1323
tree.case_sensitive)
1324
self._deletiondir = deletiondir
1326
def canonical_path(self, path):
1327
"""Get the canonical tree-relative path"""
1328
# don't follow final symlinks
1329
abs = self._tree.abspath(path)
1330
if abs in self._relpaths:
1331
return self._relpaths[abs]
1332
dirname, basename = os.path.split(abs)
1333
if dirname not in self._realpaths:
1334
self._realpaths[dirname] = os.path.realpath(dirname)
1335
dirname = self._realpaths[dirname]
1336
abs = osutils.pathjoin(dirname, basename)
1337
if dirname in self._relpaths:
1338
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1339
relpath = relpath.rstrip('/\\')
1341
relpath = self._tree.relpath(abs)
1342
self._relpaths[abs] = relpath
1345
def tree_kind(self, trans_id):
1346
"""Determine the file kind in the working tree.
1348
:returns: The file kind or None if the file does not exist
1350
path = self._tree_id_paths.get(trans_id)
1354
return osutils.file_kind(self._tree.abspath(path))
1355
except errors.NoSuchFile:
1358
def _set_mode(self, trans_id, mode_id, typefunc):
1359
"""Set the mode of new file contents.
1360
The mode_id is the existing file to get the mode from (often the same
1361
as trans_id). The operation is only performed if there's a mode match
1362
according to typefunc.
1367
old_path = self._tree_id_paths[mode_id]
1371
mode = os.stat(self._tree.abspath(old_path)).st_mode
1372
except OSError as e:
1373
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1374
# Either old_path doesn't exist, or the parent of the
1375
# target is not a directory (but will be one eventually)
1376
# Either way, we know it doesn't exist *right now*
1377
# See also bug #248448
1382
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1384
def iter_tree_children(self, parent_id):
1385
"""Iterate through the entry's tree children, if any"""
1387
path = self._tree_id_paths[parent_id]
1391
children = os.listdir(self._tree.abspath(path))
1392
except OSError as e:
1393
if not (osutils._is_error_enotdir(e) or
1394
e.errno in (errno.ENOENT, errno.ESRCH)):
1398
for child in children:
1399
childpath = joinpath(path, child)
1400
if self._tree.is_control_filename(childpath):
1402
yield self.trans_id_tree_path(childpath)
1404
def _generate_limbo_path(self, trans_id):
1405
"""Generate a limbo path using the final path if possible.
1407
This optimizes the performance of applying the tree transform by
1408
avoiding renames. These renames can be avoided only when the parent
1409
directory is already scheduled for creation.
1411
If the final path cannot be used, falls back to using the trans_id as
1414
parent = self._new_parent.get(trans_id)
1415
# if the parent directory is already in limbo (e.g. when building a
1416
# tree), choose a limbo name inside the parent, to reduce further
1418
use_direct_path = False
1419
if self._new_contents.get(parent) == 'directory':
1420
filename = self._new_name.get(trans_id)
1421
if filename is not None:
1422
if parent not in self._limbo_children:
1423
self._limbo_children[parent] = set()
1424
self._limbo_children_names[parent] = {}
1425
use_direct_path = True
1426
# the direct path can only be used if no other file has
1427
# already taken this pathname, i.e. if the name is unused, or
1428
# if it is already associated with this trans_id.
1429
elif self._case_sensitive_target:
1430
if (self._limbo_children_names[parent].get(filename)
1431
in (trans_id, None)):
1432
use_direct_path = True
1434
for l_filename, l_trans_id in (
1435
self._limbo_children_names[parent].items()):
1436
if l_trans_id == trans_id:
1438
if l_filename.lower() == filename.lower():
1441
use_direct_path = True
1443
if not use_direct_path:
1444
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1446
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1447
self._limbo_children[parent].add(trans_id)
1448
self._limbo_children_names[parent][filename] = trans_id
1451
def version_file(self, trans_id, file_id=None):
1452
"""Schedule a file to become versioned."""
1455
unique_add(self._new_id, trans_id, file_id)
1456
unique_add(self._r_new_id, file_id, trans_id)
1458
def cancel_versioning(self, trans_id):
1459
"""Undo a previous versioning of a file"""
1460
file_id = self._new_id[trans_id]
1461
del self._new_id[trans_id]
1462
del self._r_new_id[file_id]
1464
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1465
"""Apply all changes to the inventory and filesystem.
1467
If filesystem or inventory conflicts are present, MalformedTransform
1470
If apply succeeds, finalize is not necessary.
1472
:param no_conflicts: if True, the caller guarantees there are no
1473
conflicts, so no check is made.
1474
:param precomputed_delta: An inventory delta to use instead of
1476
:param _mover: Supply an alternate FileMover, for testing
1478
for hook in MutableTree.hooks['pre_transform']:
1479
hook(self._tree, self)
1480
if not no_conflicts:
1481
self._check_malformed()
1482
self.rename_count = 0
1483
with ui.ui_factory.nested_progress_bar() as child_pb:
1484
if precomputed_delta is None:
1485
child_pb.update(gettext('Apply phase'), 0, 2)
1486
changes = self._generate_transform_changes()
1490
(op, np, ie) for (op, np, fid, ie) in precomputed_delta]
1493
mover = _FileMover()
1497
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1498
self._apply_removals(mover)
1499
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1500
modified_paths = self._apply_insertions(mover)
1501
except BaseException:
1505
mover.apply_deletions()
1506
if self.final_file_id(self.root) is None:
1507
changes = [e for e in changes if e[0] != '']
1508
self._tree._apply_transform_delta(changes)
1511
return _TransformResults(modified_paths, self.rename_count)
1513
def _apply_removals(self, mover):
1514
"""Perform tree operations that remove directory/inventory names.
1516
That is, delete files that are to be deleted, and put any files that
1517
need renaming into limbo. This must be done in strict child-to-parent
1520
If inventory_delta is None, no inventory delta generation is performed.
1522
tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1523
with ui.ui_factory.nested_progress_bar() as child_pb:
1524
for num, (path, trans_id) in enumerate(tree_paths):
1525
# do not attempt to move root into a subdirectory of itself.
1528
child_pb.update(gettext('removing file'), num, len(tree_paths))
1529
full_path = self._tree.abspath(path)
1530
if trans_id in self._removed_contents:
1531
delete_path = os.path.join(self._deletiondir, trans_id)
1532
mover.pre_delete(full_path, delete_path)
1533
elif (trans_id in self._new_name or
1534
trans_id in self._new_parent):
1536
mover.rename(full_path, self._limbo_name(trans_id))
1537
except TransformRenameFailed as e:
1538
if e.errno != errno.ENOENT:
1541
self.rename_count += 1
1543
def _apply_insertions(self, mover):
1544
"""Perform tree operations that insert directory/inventory names.
1546
That is, create any files that need to be created, and restore from
1547
limbo any files that needed renaming. This must be done in strict
1548
parent-to-child order.
1550
If inventory_delta is None, no inventory delta is calculated, and
1551
no list of modified paths is returned.
1553
new_paths = self.new_paths(filesystem_only=True)
1555
with ui.ui_factory.nested_progress_bar() as child_pb:
1556
for num, (path, trans_id) in enumerate(new_paths):
1558
child_pb.update(gettext('adding file'),
1559
num, len(new_paths))
1560
full_path = self._tree.abspath(path)
1561
if trans_id in self._needs_rename:
1563
mover.rename(self._limbo_name(trans_id), full_path)
1564
except TransformRenameFailed as e:
1565
# We may be renaming a dangling inventory id
1566
if e.errno != errno.ENOENT:
1569
self.rename_count += 1
1570
# TODO: if trans_id in self._observed_sha1s, we should
1571
# re-stat the final target, since ctime will be
1572
# updated by the change.
1573
if (trans_id in self._new_contents
1574
or self.path_changed(trans_id)):
1575
if trans_id in self._new_contents:
1576
modified_paths.append(full_path)
1577
if trans_id in self._new_executability:
1578
self._set_executability(path, trans_id)
1579
if trans_id in self._observed_sha1s:
1580
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1581
st = osutils.lstat(full_path)
1582
self._observed_sha1s[trans_id] = (o_sha1, st)
1583
for path, trans_id in new_paths:
1584
# new_paths includes stuff like workingtree conflicts. Only the
1585
# stuff in new_contents actually comes from limbo.
1586
if trans_id in self._limbo_files:
1587
del self._limbo_files[trans_id]
1588
self._new_contents.clear()
1589
return modified_paths
1591
def _inventory_altered(self):
1592
"""Determine which trans_ids need new Inventory entries.
1594
An new entry is needed when anything that would be reflected by an
1595
inventory entry changes, including file name, file_id, parent file_id,
1596
file kind, and the execute bit.
1598
Some care is taken to return entries with real changes, not cases
1599
where the value is deleted and then restored to its original value,
1600
but some actually unchanged values may be returned.
1602
:returns: A list of (path, trans_id) for all items requiring an
1603
inventory change. Ordered by path.
1606
# Find entries whose file_ids are new (or changed).
1607
new_file_id = set(t for t in self._new_id
1608
if self._new_id[t] != self.tree_file_id(t))
1609
for id_set in [self._new_name, self._new_parent, new_file_id,
1610
self._new_executability]:
1611
changed_ids.update(id_set)
1612
# removing implies a kind change
1613
changed_kind = set(self._removed_contents)
1615
changed_kind.intersection_update(self._new_contents)
1616
# Ignore entries that are already known to have changed.
1617
changed_kind.difference_update(changed_ids)
1618
# to keep only the truly changed ones
1619
changed_kind = (t for t in changed_kind
1620
if self.tree_kind(t) != self.final_kind(t))
1621
# all kind changes will alter the inventory
1622
changed_ids.update(changed_kind)
1623
# To find entries with changed parent_ids, find parents which existed,
1624
# but changed file_id.
1625
# Now add all their children to the set.
1626
for parent_trans_id in new_file_id:
1627
changed_ids.update(self.iter_tree_children(parent_trans_id))
1628
return sorted(FinalPaths(self).get_paths(changed_ids))
1630
def _generate_transform_changes(self):
1631
"""Generate an inventory delta for the current transform."""
1633
new_paths = self._inventory_altered()
1634
total_entries = len(new_paths) + len(self._removed_id)
1635
with ui.ui_factory.nested_progress_bar() as child_pb:
1636
for num, trans_id in enumerate(self._removed_id):
1638
child_pb.update(gettext('removing file'),
1640
if trans_id == self._new_root:
1641
file_id = self._tree.path2id('')
1643
file_id = self.tree_file_id(trans_id)
1644
# File-id isn't really being deleted, just moved
1645
if file_id in self._r_new_id:
1647
path = self._tree_id_paths[trans_id]
1648
changes.append((path, None, None))
1649
new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1651
for num, (path, trans_id) in enumerate(new_paths):
1653
child_pb.update(gettext('adding file'),
1654
num + len(self._removed_id), total_entries)
1655
file_id = new_path_file_ids[trans_id]
1658
kind = self.final_kind(trans_id)
1660
kind = self._tree.stored_kind(self._tree.id2path(file_id))
1661
parent_trans_id = self.final_parent(trans_id)
1662
parent_file_id = new_path_file_ids.get(parent_trans_id)
1663
if parent_file_id is None:
1664
parent_file_id = self.final_file_id(parent_trans_id)
1665
if trans_id in self._new_reference_revision:
1666
new_entry = inventory.TreeReference(
1668
self._new_name[trans_id],
1669
self.final_file_id(self._new_parent[trans_id]),
1670
None, self._new_reference_revision[trans_id])
1672
new_entry = inventory.make_entry(kind,
1673
self.final_name(trans_id),
1674
parent_file_id, file_id)
1676
old_path = self._tree.id2path(new_entry.file_id)
1677
except errors.NoSuchId:
1679
new_executability = self._new_executability.get(trans_id)
1680
if new_executability is not None:
1681
new_entry.executable = new_executability
1683
(old_path, path, new_entry))