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
23
from stat import S_IEXEC, S_ISREG
26
from .mapping import encode_git_path, mode_kind, mode_is_executable, object_mode
27
from .tree import GitTree, GitTreeDirectory, GitTreeSymlink, GitTreeFile
35
revision as _mod_revision,
40
from ..i18n import gettext
41
from ..mutabletree import MutableTree
42
from ..tree import InterTree, TreeChange
43
from ..transform import (
51
TransformRenameFailed,
58
from dulwich.index import commit_tree, blob_from_path_and_stat
59
from dulwich.objects import Blob
62
class TreeTransformBase(TreeTransform):
63
"""The base class for TreeTransform and its kin."""
65
def __init__(self, tree, pb=None, case_sensitive=True):
68
:param tree: The tree that will be transformed, but not necessarily
71
:param case_sensitive: If True, the target of the transform is
72
case sensitive, not just case preserving.
74
super(TreeTransformBase, self).__init__(tree, pb=pb)
75
# mapping of trans_id => (sha1 of content, stat_value)
76
self._observed_sha1s = {}
77
# Set of versioned trans ids
78
self._versioned = set()
79
# The trans_id that will be used as the tree root
80
self.root = self.trans_id_tree_path('')
81
# Whether the target is case sensitive
82
self._case_sensitive_target = case_sensitive
83
self._symlink_target = {}
87
return self._tree.mapping
90
"""Release the working tree lock, if held.
92
This is required if apply has not been invoked, but can be invoked
95
if self._tree is None:
97
for hook in MutableTree.hooks['post_transform']:
98
hook(self._tree, self)
102
def create_path(self, name, parent):
103
"""Assign a transaction id to a new path"""
104
trans_id = self.assign_id()
105
unique_add(self._new_name, trans_id, name)
106
unique_add(self._new_parent, trans_id, parent)
109
def adjust_root_path(self, name, parent):
110
"""Emulate moving the root by moving all children, instead.
113
def fixup_new_roots(self):
114
"""Reinterpret requests to change the root directory
116
Instead of creating a root directory, or moving an existing directory,
117
all the attributes and children of the new root are applied to the
118
existing root directory.
120
This means that the old root trans-id becomes obsolete, so it is
121
recommended only to invoke this after the root trans-id has become
125
new_roots = [k for k, v in self._new_parent.items()
127
if len(new_roots) < 1:
129
if len(new_roots) != 1:
130
raise ValueError('A tree cannot have two roots!')
131
old_new_root = new_roots[0]
132
# unversion the new root's directory.
133
if old_new_root in self._versioned:
134
self.cancel_versioning(old_new_root)
136
self.unversion_file(old_new_root)
138
# Now move children of new root into old root directory.
139
# Ensure all children are registered with the transaction, but don't
140
# use directly-- some tree children have new parents
141
list(self.iter_tree_children(old_new_root))
142
# Move all children of new root into old root directory.
143
for child in self.by_parent().get(old_new_root, []):
144
self.adjust_path(self.final_name(child), self.root, child)
146
# Ensure old_new_root has no directory.
147
if old_new_root in self._new_contents:
148
self.cancel_creation(old_new_root)
150
self.delete_contents(old_new_root)
152
# prevent deletion of root directory.
153
if self.root in self._removed_contents:
154
self.cancel_deletion(self.root)
156
# destroy path info for old_new_root.
157
del self._new_parent[old_new_root]
158
del self._new_name[old_new_root]
160
def trans_id_file_id(self, file_id):
161
"""Determine or set the transaction id associated with a file ID.
162
A new id is only created for file_ids that were never present. If
163
a transaction has been unversioned, it is deliberately still returned.
164
(this will likely lead to an unversioned parent conflict.)
167
raise ValueError('None is not a valid file id')
168
path = self.mapping.parse_file_id(file_id)
169
return self.trans_id_tree_path(path)
171
def version_file(self, trans_id, file_id=None):
172
"""Schedule a file to become versioned."""
173
if trans_id in self._versioned:
174
raise errors.DuplicateKey(key=trans_id)
175
self._versioned.add(trans_id)
177
def cancel_versioning(self, trans_id):
178
"""Undo a previous versioning of a file"""
179
raise NotImplementedError(self.cancel_versioning)
181
def new_paths(self, filesystem_only=False):
182
"""Determine the paths of all new and changed files.
184
:param filesystem_only: if True, only calculate values for files
185
that require renames or execute bit changes.
189
stale_ids = self._needs_rename.difference(self._new_name)
190
stale_ids.difference_update(self._new_parent)
191
stale_ids.difference_update(self._new_contents)
192
stale_ids.difference_update(self._versioned)
193
needs_rename = self._needs_rename.difference(stale_ids)
194
id_sets = (needs_rename, self._new_executability)
196
id_sets = (self._new_name, self._new_parent, self._new_contents,
197
self._versioned, self._new_executability)
198
for id_set in id_sets:
199
new_ids.update(id_set)
200
return sorted(FinalPaths(self).get_paths(new_ids))
202
def final_is_versioned(self, trans_id):
203
if trans_id in self._versioned:
205
if trans_id in self._removed_id:
207
orig_path = self.tree_path(trans_id)
208
if orig_path is None:
210
return self._tree.is_versioned(orig_path)
212
def find_raw_conflicts(self):
213
"""Find any violations of inventory or filesystem invariants"""
214
if self._done is True:
215
raise ReusingTransform()
217
# ensure all children of all existent parents are known
218
# all children of non-existent parents are known, by definition.
219
self._add_tree_children()
220
by_parent = self.by_parent()
221
conflicts.extend(self._parent_loops())
222
conflicts.extend(self._duplicate_entries(by_parent))
223
conflicts.extend(self._parent_type_conflicts(by_parent))
224
conflicts.extend(self._improper_versioning())
225
conflicts.extend(self._executability_conflicts())
226
conflicts.extend(self._overwrite_conflicts())
229
def _check_malformed(self):
230
conflicts = self.find_raw_conflicts()
231
if len(conflicts) != 0:
232
raise MalformedTransform(conflicts=conflicts)
234
def _add_tree_children(self):
235
"""Add all the children of all active parents to the known paths.
237
Active parents are those which gain children, and those which are
238
removed. This is a necessary first step in detecting conflicts.
240
parents = list(self.by_parent())
241
parents.extend([t for t in self._removed_contents if
242
self.tree_kind(t) == 'directory'])
243
for trans_id in self._removed_id:
244
path = self.tree_path(trans_id)
247
if self._tree.stored_kind(path) == 'directory':
248
parents.append(trans_id)
249
except errors.NoSuchFile:
251
elif self.tree_kind(trans_id) == 'directory':
252
parents.append(trans_id)
254
for parent_id in parents:
255
# ensure that all children are registered with the transaction
256
list(self.iter_tree_children(parent_id))
258
def _has_named_child(self, name, parent_id, known_children):
259
"""Does a parent already have a name child.
261
:param name: The searched for name.
263
:param parent_id: The parent for which the check is made.
265
:param known_children: The already known children. This should have
266
been recently obtained from `self.by_parent.get(parent_id)`
267
(or will be if None is passed).
269
if known_children is None:
270
known_children = self.by_parent().get(parent_id, [])
271
for child in known_children:
272
if self.final_name(child) == name:
274
parent_path = self._tree_id_paths.get(parent_id, None)
275
if parent_path is None:
276
# No parent... no children
278
child_path = joinpath(parent_path, name)
279
child_id = self._tree_path_ids.get(child_path, None)
281
# Not known by the tree transform yet, check the filesystem
282
return osutils.lexists(self._tree.abspath(child_path))
284
raise AssertionError('child_id is missing: %s, %s, %s'
285
% (name, parent_id, child_id))
287
def _available_backup_name(self, name, target_id):
288
"""Find an available backup name.
290
:param name: The basename of the file.
292
:param target_id: The directory trans_id where the backup should
295
known_children = self.by_parent().get(target_id, [])
296
return osutils.available_backup_name(
298
lambda base: self._has_named_child(
299
base, target_id, known_children))
301
def _parent_loops(self):
302
"""No entry should be its own ancestor"""
303
for trans_id in self._new_parent:
306
while parent_id != ROOT_PARENT:
309
parent_id = self.final_parent(parent_id)
312
if parent_id == trans_id:
313
yield ('parent loop', trans_id)
314
if parent_id in seen:
317
def _improper_versioning(self):
318
"""Cannot version a file with no contents, or a bad type.
320
However, existing entries with no contents are okay.
322
for trans_id in self._versioned:
323
kind = self.final_kind(trans_id)
324
if kind == 'symlink' and not self._tree.supports_symlinks():
325
# Ignore symlinks as they are not supported on this platform
328
yield ('versioning no contents', trans_id)
330
if not self._tree.versionable_kind(kind):
331
yield ('versioning bad kind', trans_id, kind)
333
def _executability_conflicts(self):
334
"""Check for bad executability changes.
336
Only versioned files may have their executability set, because
337
1. only versioned entries can have executability under windows
338
2. only files can be executable. (The execute bit on a directory
339
does not indicate searchability)
341
for trans_id in self._new_executability:
342
if not self.final_is_versioned(trans_id):
343
yield ('unversioned executability', trans_id)
345
if self.final_kind(trans_id) != "file":
346
yield ('non-file executability', trans_id)
348
def _overwrite_conflicts(self):
349
"""Check for overwrites (not permitted on Win32)"""
350
for trans_id in self._new_contents:
351
if self.tree_kind(trans_id) is None:
353
if trans_id not in self._removed_contents:
354
yield ('overwrite', trans_id, self.final_name(trans_id))
356
def _duplicate_entries(self, by_parent):
357
"""No directory may have two entries with the same name."""
358
if (self._new_name, self._new_parent) == ({}, {}):
360
for children in by_parent.values():
362
for child_tid in children:
363
name = self.final_name(child_tid)
365
# Keep children only if they still exist in the end
366
if not self._case_sensitive_target:
368
name_ids.append((name, child_tid))
372
for name, trans_id in name_ids:
373
kind = self.final_kind(trans_id)
374
if kind is None and not self.final_is_versioned(trans_id):
376
if name == last_name:
377
yield ('duplicate', last_trans_id, trans_id, name)
379
last_trans_id = trans_id
381
def _parent_type_conflicts(self, by_parent):
382
"""Children must have a directory parent"""
383
for parent_id, children in by_parent.items():
384
if parent_id == ROOT_PARENT:
387
for child_id in children:
388
if self.final_kind(child_id) is not None:
393
# There is at least a child, so we need an existing directory to
395
kind = self.final_kind(parent_id)
397
# The directory will be deleted
398
yield ('missing parent', parent_id)
399
elif kind != "directory":
400
# Meh, we need a *directory* to put something in it
401
yield ('non-directory parent', parent_id)
403
def _set_executability(self, path, trans_id):
404
"""Set the executability of versioned files """
405
if self._tree._supports_executable():
406
new_executability = self._new_executability[trans_id]
407
abspath = self._tree.abspath(path)
408
current_mode = os.stat(abspath).st_mode
409
if new_executability:
412
to_mode = current_mode | (0o100 & ~umask)
413
# Enable x-bit for others only if they can read it.
414
if current_mode & 0o004:
415
to_mode |= 0o001 & ~umask
416
if current_mode & 0o040:
417
to_mode |= 0o010 & ~umask
419
to_mode = current_mode & ~0o111
420
osutils.chmod_if_possible(abspath, to_mode)
422
def _new_entry(self, name, parent_id, file_id):
423
"""Helper function to create a new filesystem entry."""
424
trans_id = self.create_path(name, parent_id)
425
if file_id is not None:
426
self.version_file(trans_id, file_id=file_id)
429
def new_file(self, name, parent_id, contents, file_id=None,
430
executable=None, sha1=None):
431
"""Convenience method to create files.
433
name is the name of the file to create.
434
parent_id is the transaction id of the parent directory of the file.
435
contents is an iterator of bytestrings, which will be used to produce
437
:param file_id: The inventory ID of the file, if it is to be versioned.
438
:param executable: Only valid when a file_id has been supplied.
440
trans_id = self._new_entry(name, parent_id, file_id)
441
# TODO: rather than scheduling a set_executable call,
442
# have create_file create the file with the right mode.
443
self.create_file(contents, trans_id, sha1=sha1)
444
if executable is not None:
445
self.set_executability(executable, trans_id)
448
def new_directory(self, name, parent_id, file_id=None):
449
"""Convenience method to create directories.
451
name is the name of the directory to create.
452
parent_id is the transaction id of the parent directory of the
454
file_id is the inventory ID of the directory, if it is to be versioned.
456
trans_id = self._new_entry(name, parent_id, file_id)
457
self.create_directory(trans_id)
460
def new_symlink(self, name, parent_id, target, file_id=None):
461
"""Convenience method to create symbolic link.
463
name is the name of the symlink to create.
464
parent_id is the transaction id of the parent directory of the symlink.
465
target is a bytestring of the target of the symlink.
466
file_id is the inventory ID of the file, if it is to be versioned.
468
trans_id = self._new_entry(name, parent_id, file_id)
469
self.create_symlink(target, trans_id)
472
def new_orphan(self, trans_id, parent_id):
473
"""Schedule an item to be orphaned.
475
When a directory is about to be removed, its children, if they are not
476
versioned are moved out of the way: they don't have a parent anymore.
478
:param trans_id: The trans_id of the existing item.
479
:param parent_id: The parent trans_id of the item.
481
raise NotImplementedError(self.new_orphan)
483
def _get_potential_orphans(self, dir_id):
484
"""Find the potential orphans in a directory.
486
A directory can't be safely deleted if there are versioned files in it.
487
If all the contained files are unversioned then they can be orphaned.
489
The 'None' return value means that the directory contains at least one
490
versioned file and should not be deleted.
492
:param dir_id: The directory trans id.
494
:return: A list of the orphan trans ids or None if at least one
495
versioned file is present.
498
# Find the potential orphans, stop if one item should be kept
499
for child_tid in self.by_parent()[dir_id]:
500
if child_tid in self._removed_contents:
501
# The child is removed as part of the transform. Since it was
502
# versioned before, it's not an orphan
504
if not self.final_is_versioned(child_tid):
505
# The child is not versioned
506
orphans.append(child_tid)
508
# We have a versioned file here, searching for orphans is
514
def _affected_ids(self):
515
"""Return the set of transform ids affected by the transform"""
516
trans_ids = set(self._removed_id)
517
trans_ids.update(self._versioned)
518
trans_ids.update(self._removed_contents)
519
trans_ids.update(self._new_contents)
520
trans_ids.update(self._new_executability)
521
trans_ids.update(self._new_name)
522
trans_ids.update(self._new_parent)
525
def iter_changes(self, want_unversioned=False):
526
"""Produce output in the same format as Tree.iter_changes.
528
Will produce nonsensical results if invoked while inventory/filesystem
529
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
531
final_paths = FinalPaths(self)
532
trans_ids = self._affected_ids()
534
# Now iterate through all active paths
535
for trans_id in trans_ids:
536
from_path = self.tree_path(trans_id)
538
# find file ids, and determine versioning state
539
if from_path is None:
540
from_versioned = False
542
from_versioned = self._tree.is_versioned(from_path)
543
if not want_unversioned and not from_versioned:
545
to_path = final_paths.get_path(trans_id)
549
to_versioned = self.final_is_versioned(trans_id)
550
if not want_unversioned and not to_versioned:
554
# get data from working tree if versioned
555
from_entry = next(self._tree.iter_entries_by_dir(
556
specific_files=[from_path]))[1]
557
from_name = from_entry.name
560
if from_path is None:
561
# File does not exist in FROM state
564
# File exists, but is not versioned. Have to use path-
566
from_name = os.path.basename(from_path)
567
if from_path is not None:
568
from_kind, from_executable, from_stats = \
569
self._tree._comparison_data(from_entry, from_path)
572
from_executable = False
574
to_name = self.final_name(trans_id)
575
to_kind = self.final_kind(trans_id)
576
if trans_id in self._new_executability:
577
to_executable = self._new_executability[trans_id]
579
to_executable = from_executable
581
if from_versioned and from_kind != to_kind:
583
elif to_kind in ('file', 'symlink') and (
584
trans_id in self._new_contents):
586
if (not modified and from_versioned == to_versioned
587
and from_path == to_path
588
and from_name == to_name
589
and from_executable == to_executable):
591
if (from_path, to_path) == (None, None):
595
(from_path, to_path), modified,
596
(from_versioned, to_versioned),
597
(from_name, to_name),
598
(from_kind, to_kind),
599
(from_executable, to_executable)))
602
return (c.path[0] or '', c.path[1] or '')
603
return iter(sorted(results, key=path_key))
605
def get_preview_tree(self):
606
"""Return a tree representing the result of the transform.
608
The tree is a snapshot, and altering the TreeTransform will invalidate
611
return GitPreviewTree(self)
613
def commit(self, branch, message, merge_parents=None, strict=False,
614
timestamp=None, timezone=None, committer=None, authors=None,
615
revprops=None, revision_id=None):
616
"""Commit the result of this TreeTransform to a branch.
618
:param branch: The branch to commit to.
619
:param message: The message to attach to the commit.
620
:param merge_parents: Additional parent revision-ids specified by
622
:param strict: If True, abort the commit if there are unversioned
624
:param timestamp: if not None, seconds-since-epoch for the time and
625
date. (May be a float.)
626
:param timezone: Optional timezone for timestamp, as an offset in
628
:param committer: Optional committer in email-id format.
629
(e.g. "J Random Hacker <jrandom@example.com>")
630
:param authors: Optional list of authors in email-id format.
631
:param revprops: Optional dictionary of revision properties.
632
:param revision_id: Optional revision id. (Specifying a revision-id
633
may reduce performance for some non-native formats.)
634
:return: The revision_id of the revision committed.
636
self._check_malformed()
638
unversioned = set(self._new_contents).difference(set(self._versioned))
639
for trans_id in unversioned:
640
if not self.final_is_versioned(trans_id):
641
raise errors.StrictCommitFailed()
643
revno, last_rev_id = branch.last_revision_info()
644
if last_rev_id == _mod_revision.NULL_REVISION:
645
if merge_parents is not None:
646
raise ValueError('Cannot supply merge parents for first'
650
parent_ids = [last_rev_id]
651
if merge_parents is not None:
652
parent_ids.extend(merge_parents)
653
if self._tree.get_revision_id() != last_rev_id:
654
raise ValueError('TreeTransform not based on branch basis: %s' %
655
self._tree.get_revision_id().decode('utf-8'))
656
from .. import commit
657
revprops = commit.Commit.update_revprops(revprops, branch, authors)
658
builder = branch.get_commit_builder(parent_ids,
663
revision_id=revision_id)
664
preview = self.get_preview_tree()
665
list(builder.record_iter_changes(preview, last_rev_id,
666
self.iter_changes()))
667
builder.finish_inventory()
668
revision_id = builder.commit(message)
669
branch.set_last_revision_info(revno + 1, revision_id)
672
def _text_parent(self, trans_id):
673
path = self.tree_path(trans_id)
675
if path is None or self._tree.kind(path) != 'file':
677
except errors.NoSuchFile:
681
def _get_parents_texts(self, trans_id):
682
"""Get texts for compression parents of this file."""
683
path = self._text_parent(trans_id)
686
return (self._tree.get_file_text(path),)
688
def _get_parents_lines(self, trans_id):
689
"""Get lines for compression parents of this file."""
690
path = self._text_parent(trans_id)
693
return (self._tree.get_file_lines(path),)
695
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
696
"""Schedule creation of a new file.
700
:param contents: an iterator of strings, all of which will be written
701
to the target destination.
702
:param trans_id: TreeTransform handle
703
:param mode_id: If not None, force the mode of the target file to match
704
the mode of the object referenced by mode_id.
705
Otherwise, we will try to preserve mode bits of an existing file.
706
:param sha1: If the sha1 of this content is already known, pass it in.
707
We can use it to prevent future sha1 computations.
709
raise NotImplementedError(self.create_file)
711
def create_directory(self, trans_id):
712
"""Schedule creation of a new directory.
714
See also new_directory.
716
raise NotImplementedError(self.create_directory)
718
def create_symlink(self, target, trans_id):
719
"""Schedule creation of a new symbolic link.
721
target is a bytestring.
722
See also new_symlink.
724
raise NotImplementedError(self.create_symlink)
726
def create_hardlink(self, path, trans_id):
727
"""Schedule creation of a hard link"""
728
raise NotImplementedError(self.create_hardlink)
730
def cancel_creation(self, trans_id):
731
"""Cancel the creation of new file contents."""
732
raise NotImplementedError(self.cancel_creation)
734
def apply(self, no_conflicts=False, _mover=None):
735
"""Apply all changes to the inventory and filesystem.
737
If filesystem or inventory conflicts are present, MalformedTransform
740
If apply succeeds, finalize is not necessary.
742
:param no_conflicts: if True, the caller guarantees there are no
743
conflicts, so no check is made.
744
:param _mover: Supply an alternate FileMover, for testing
746
raise NotImplementedError(self.apply)
748
def cook_conflicts(self, raw_conflicts):
749
"""Generate a list of cooked conflicts, sorted by file path"""
750
if not raw_conflicts:
752
fp = FinalPaths(self)
753
from .workingtree import TextConflict
754
for c in raw_conflicts:
755
if c[0] == 'text conflict':
756
yield TextConflict(fp.get_path(c[1]))
757
elif c[0] == 'duplicate':
758
yield TextConflict(fp.get_path(c[2]))
759
elif c[0] == 'contents conflict':
760
yield TextConflict(fp.get_path(c[1][0]))
761
elif c[0] == 'missing parent':
762
# TODO(jelmer): This should not make it to here
763
yield TextConflict(fp.get_path(c[2]))
764
elif c[0] == 'non-directory parent':
765
yield TextConflict(fp.get_path(c[2]))
766
elif c[0] == 'deleting parent':
767
# TODO(jelmer): This should not make it to here
768
yield TextConflict(fp.get_path(c[2]))
769
elif c[0] == 'parent loop':
770
# TODO(jelmer): This should not make it to here
771
yield TextConflict(fp.get_path(c[2]))
772
elif c[0] == 'path conflict':
773
yield TextConflict(fp.get_path(c[1]))
775
raise AssertionError('unknown conflict %s' % c[0])
778
class DiskTreeTransform(TreeTransformBase):
779
"""Tree transform storing its contents on disk."""
781
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
783
:param tree: The tree that will be transformed, but not necessarily
785
:param limbodir: A directory where new files can be stored until
786
they are installed in their proper places
788
:param case_sensitive: If True, the target of the transform is
789
case sensitive, not just case preserving.
791
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
792
self._limbodir = limbodir
793
self._deletiondir = None
794
# A mapping of transform ids to their limbo filename
795
self._limbo_files = {}
796
self._possibly_stale_limbo_files = set()
797
# A mapping of transform ids to a set of the transform ids of children
798
# that their limbo directory has
799
self._limbo_children = {}
800
# Map transform ids to maps of child filename to child transform id
801
self._limbo_children_names = {}
802
# List of transform ids that need to be renamed from limbo into place
803
self._needs_rename = set()
804
self._creation_mtime = None
805
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
808
"""Release the working tree lock, if held, clean up limbo dir.
810
This is required if apply has not been invoked, but can be invoked
813
if self._tree is None:
816
limbo_paths = list(self._limbo_files.values())
817
limbo_paths.extend(self._possibly_stale_limbo_files)
818
limbo_paths.sort(reverse=True)
819
for path in limbo_paths:
821
osutils.delete_any(path)
823
if e.errno != errno.ENOENT:
825
# XXX: warn? perhaps we just got interrupted at an
826
# inconvenient moment, but perhaps files are disappearing
829
osutils.delete_any(self._limbodir)
831
# We don't especially care *why* the dir is immortal.
832
raise ImmortalLimbo(self._limbodir)
834
if self._deletiondir is not None:
835
osutils.delete_any(self._deletiondir)
837
raise errors.ImmortalPendingDeletion(self._deletiondir)
839
TreeTransformBase.finalize(self)
841
def _limbo_supports_executable(self):
842
"""Check if the limbo path supports the executable bit."""
843
return osutils.supports_executable(self._limbodir)
845
def _limbo_name(self, trans_id):
846
"""Generate the limbo name of a file"""
847
limbo_name = self._limbo_files.get(trans_id)
848
if limbo_name is None:
849
limbo_name = self._generate_limbo_path(trans_id)
850
self._limbo_files[trans_id] = limbo_name
853
def _generate_limbo_path(self, trans_id):
854
"""Generate a limbo path using the trans_id as the relative path.
856
This is suitable as a fallback, and when the transform should not be
857
sensitive to the path encoding of the limbo directory.
859
self._needs_rename.add(trans_id)
860
return osutils.pathjoin(self._limbodir, trans_id)
862
def adjust_path(self, name, parent, trans_id):
863
previous_parent = self._new_parent.get(trans_id)
864
previous_name = self._new_name.get(trans_id)
865
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
866
if (trans_id in self._limbo_files
867
and trans_id not in self._needs_rename):
868
self._rename_in_limbo([trans_id])
869
if previous_parent != parent:
870
self._limbo_children[previous_parent].remove(trans_id)
871
if previous_parent != parent or previous_name != name:
872
del self._limbo_children_names[previous_parent][previous_name]
874
def _rename_in_limbo(self, trans_ids):
875
"""Fix limbo names so that the right final path is produced.
877
This means we outsmarted ourselves-- we tried to avoid renaming
878
these files later by creating them with their final names in their
879
final parents. But now the previous name or parent is no longer
880
suitable, so we have to rename them.
882
Even for trans_ids that have no new contents, we must remove their
883
entries from _limbo_files, because they are now stale.
885
for trans_id in trans_ids:
886
old_path = self._limbo_files[trans_id]
887
self._possibly_stale_limbo_files.add(old_path)
888
del self._limbo_files[trans_id]
889
if trans_id not in self._new_contents:
891
new_path = self._limbo_name(trans_id)
892
os.rename(old_path, new_path)
893
self._possibly_stale_limbo_files.remove(old_path)
894
for descendant in self._limbo_descendants(trans_id):
895
desc_path = self._limbo_files[descendant]
896
desc_path = new_path + desc_path[len(old_path):]
897
self._limbo_files[descendant] = desc_path
899
def _limbo_descendants(self, trans_id):
900
"""Return the set of trans_ids whose limbo paths descend from this."""
901
descendants = set(self._limbo_children.get(trans_id, []))
902
for descendant in list(descendants):
903
descendants.update(self._limbo_descendants(descendant))
906
def _set_mode(self, trans_id, mode_id, typefunc):
907
raise NotImplementedError(self._set_mode)
909
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
910
"""Schedule creation of a new file.
914
:param contents: an iterator of strings, all of which will be written
915
to the target destination.
916
:param trans_id: TreeTransform handle
917
:param mode_id: If not None, force the mode of the target file to match
918
the mode of the object referenced by mode_id.
919
Otherwise, we will try to preserve mode bits of an existing file.
920
:param sha1: If the sha1 of this content is already known, pass it in.
921
We can use it to prevent future sha1 computations.
923
name = self._limbo_name(trans_id)
924
with open(name, 'wb') as f:
925
unique_add(self._new_contents, trans_id, 'file')
926
f.writelines(contents)
927
self._set_mtime(name)
928
self._set_mode(trans_id, mode_id, S_ISREG)
929
# It is unfortunate we have to use lstat instead of fstat, but we just
930
# used utime and chmod on the file, so we need the accurate final
933
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
935
def _read_symlink_target(self, trans_id):
936
return os.readlink(self._limbo_name(trans_id))
938
def _set_mtime(self, path):
939
"""All files that are created get the same mtime.
941
This time is set by the first object to be created.
943
if self._creation_mtime is None:
944
self._creation_mtime = time.time()
945
os.utime(path, (self._creation_mtime, self._creation_mtime))
947
def create_hardlink(self, path, trans_id):
948
"""Schedule creation of a hard link"""
949
name = self._limbo_name(trans_id)
953
if e.errno != errno.EPERM:
955
raise errors.HardLinkNotSupported(path)
957
unique_add(self._new_contents, trans_id, 'file')
958
except BaseException:
959
# Clean up the file, it never got registered so
960
# TreeTransform.finalize() won't clean it up.
964
def create_directory(self, trans_id):
965
"""Schedule creation of a new directory.
967
See also new_directory.
969
os.mkdir(self._limbo_name(trans_id))
970
unique_add(self._new_contents, trans_id, 'directory')
972
def create_symlink(self, target, trans_id):
973
"""Schedule creation of a new symbolic link.
975
target is a bytestring.
976
See also new_symlink.
978
if self._create_symlinks:
979
os.symlink(target, self._limbo_name(trans_id))
982
path = FinalPaths(self).get_path(trans_id)
986
'Unable to create symlink "%s" on this filesystem.' % (path,))
987
self._symlink_target[trans_id] = target
988
# We add symlink to _new_contents even if they are unsupported
989
# and not created. These entries are subsequently used to avoid
990
# conflicts on platforms that don't support symlink
991
unique_add(self._new_contents, trans_id, 'symlink')
993
def cancel_creation(self, trans_id):
994
"""Cancel the creation of new file contents."""
995
del self._new_contents[trans_id]
996
if trans_id in self._observed_sha1s:
997
del self._observed_sha1s[trans_id]
998
children = self._limbo_children.get(trans_id)
999
# if this is a limbo directory with children, move them before removing
1001
if children is not None:
1002
self._rename_in_limbo(children)
1003
del self._limbo_children[trans_id]
1004
del self._limbo_children_names[trans_id]
1005
osutils.delete_any(self._limbo_name(trans_id))
1007
def new_orphan(self, trans_id, parent_id):
1008
conf = self._tree.get_config_stack()
1009
handle_orphan = conf.get('transform.orphan_policy')
1010
handle_orphan(self, trans_id, parent_id)
1012
def final_entry(self, trans_id):
1013
is_versioned = self.final_is_versioned(trans_id)
1014
fp = FinalPaths(self)
1015
tree_path = fp.get_path(trans_id)
1016
if trans_id in self._new_contents:
1017
path = self._limbo_name(trans_id)
1019
kind = mode_kind(st.st_mode)
1020
name = self.final_name(trans_id)
1021
file_id = self._tree.mapping.generate_file_id(tree_path)
1022
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1023
if kind == 'directory':
1024
return GitTreeDirectory(
1025
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1026
executable = mode_is_executable(st.st_mode)
1027
mode = object_mode(kind, executable)
1028
blob = blob_from_path_and_stat(encode_git_path(path), st)
1029
if kind == 'symlink':
1030
return GitTreeSymlink(
1031
file_id, name, parent_id,
1032
decode_git_path(blob.data)), is_versioned
1033
elif kind == 'file':
1035
file_id, name, executable=executable, parent_id=parent_id,
1036
git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1038
raise AssertionError(kind)
1039
elif trans_id in self._removed_contents:
1042
orig_path = self.tree_path(trans_id)
1043
if orig_path is None:
1045
file_id = self._tree.mapping.generate_file_id(tree_path)
1049
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1051
ie = next(self._tree.iter_entries_by_dir(
1052
specific_files=[orig_path]))[1]
1053
ie.file_id = file_id
1054
ie.parent_id = parent_id
1055
return ie, is_versioned
1056
except StopIteration:
1058
if self.tree_kind(trans_id) == 'directory':
1059
return GitTreeDirectory(
1060
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1061
except OSError as e:
1062
if e.errno != errno.ENOTDIR:
1066
def final_git_entry(self, trans_id):
1067
if trans_id in self._new_contents:
1068
path = self._limbo_name(trans_id)
1070
kind = mode_kind(st.st_mode)
1071
if kind == 'directory':
1073
executable = mode_is_executable(st.st_mode)
1074
mode = object_mode(kind, executable)
1075
blob = blob_from_path_and_stat(encode_git_path(path), st)
1076
elif trans_id in self._removed_contents:
1079
orig_path = self.tree_path(trans_id)
1080
kind = self._tree.kind(orig_path)
1081
executable = self._tree.is_executable(orig_path)
1082
mode = object_mode(kind, executable)
1083
if kind == 'symlink':
1084
contents = self._tree.get_symlink_target(orig_path)
1085
elif kind == 'file':
1086
contents = self._tree.get_file_text(orig_path)
1087
elif kind == 'directory':
1090
raise AssertionError(kind)
1091
blob = Blob.from_string(contents)
1095
class GitTreeTransform(DiskTreeTransform):
1096
"""Represent a tree transformation.
1098
This object is designed to support incremental generation of the transform,
1101
However, it gives optimum performance when parent directories are created
1102
before their contents. The transform is then able to put child files
1103
directly in their parent directory, avoiding later renames.
1105
It is easy to produce malformed transforms, but they are generally
1106
harmless. Attempting to apply a malformed transform will cause an
1107
exception to be raised before any modifications are made to the tree.
1109
Many kinds of malformed transforms can be corrected with the
1110
resolve_conflicts function. The remaining ones indicate programming error,
1111
such as trying to create a file with no path.
1113
Two sets of file creation methods are supplied. Convenience methods are:
1118
These are composed of the low-level methods:
1120
* create_file or create_directory or create_symlink
1124
Transform/Transaction ids
1125
-------------------------
1126
trans_ids are temporary ids assigned to all files involved in a transform.
1127
It's possible, even common, that not all files in the Tree have trans_ids.
1129
trans_ids are used because filenames and file_ids are not good enough
1130
identifiers; filenames change.
1132
trans_ids are only valid for the TreeTransform that generated them.
1136
Limbo is a temporary directory use to hold new versions of files.
1137
Files are added to limbo by create_file, create_directory, create_symlink,
1138
and their convenience variants (new_*). Files may be removed from limbo
1139
using cancel_creation. Files are renamed from limbo into their final
1140
location as part of TreeTransform.apply
1142
Limbo must be cleaned up, by either calling TreeTransform.apply or
1143
calling TreeTransform.finalize.
1145
Files are placed into limbo inside their parent directories, where
1146
possible. This reduces subsequent renames, and makes operations involving
1147
lots of files faster. This optimization is only possible if the parent
1148
directory is created *before* creating any of its children, so avoid
1149
creating children before parents, where possible.
1153
This temporary directory is used by _FileMover for storing files that are
1154
about to be deleted. In case of rollback, the files will be restored.
1155
FileMover does not delete files until it is sure that a rollback will not
1159
def __init__(self, tree, pb=None):
1160
"""Note: a tree_write lock is taken on the tree.
1162
Use TreeTransform.finalize() to release the lock (can be omitted if
1163
TreeTransform.apply() called).
1165
tree.lock_tree_write()
1167
limbodir = urlutils.local_path_from_url(
1168
tree._transport.abspath('limbo'))
1169
osutils.ensure_empty_directory_exists(
1171
errors.ExistingLimbo)
1172
deletiondir = urlutils.local_path_from_url(
1173
tree._transport.abspath('pending-deletion'))
1174
osutils.ensure_empty_directory_exists(
1176
errors.ExistingPendingDeletion)
1177
except BaseException:
1181
# Cache of realpath results, to speed up canonical_path
1182
self._realpaths = {}
1183
# Cache of relpath results, to speed up canonical_path
1185
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1186
tree.case_sensitive)
1187
self._deletiondir = deletiondir
1189
def canonical_path(self, path):
1190
"""Get the canonical tree-relative path"""
1191
# don't follow final symlinks
1192
abs = self._tree.abspath(path)
1193
if abs in self._relpaths:
1194
return self._relpaths[abs]
1195
dirname, basename = os.path.split(abs)
1196
if dirname not in self._realpaths:
1197
self._realpaths[dirname] = os.path.realpath(dirname)
1198
dirname = self._realpaths[dirname]
1199
abs = osutils.pathjoin(dirname, basename)
1200
if dirname in self._relpaths:
1201
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1202
relpath = relpath.rstrip('/\\')
1204
relpath = self._tree.relpath(abs)
1205
self._relpaths[abs] = relpath
1208
def tree_kind(self, trans_id):
1209
"""Determine the file kind in the working tree.
1211
:returns: The file kind or None if the file does not exist
1213
path = self._tree_id_paths.get(trans_id)
1217
return osutils.file_kind(self._tree.abspath(path))
1218
except errors.NoSuchFile:
1221
def _set_mode(self, trans_id, mode_id, typefunc):
1222
"""Set the mode of new file contents.
1223
The mode_id is the existing file to get the mode from (often the same
1224
as trans_id). The operation is only performed if there's a mode match
1225
according to typefunc.
1230
old_path = self._tree_id_paths[mode_id]
1234
mode = os.stat(self._tree.abspath(old_path)).st_mode
1235
except OSError as e:
1236
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1237
# Either old_path doesn't exist, or the parent of the
1238
# target is not a directory (but will be one eventually)
1239
# Either way, we know it doesn't exist *right now*
1240
# See also bug #248448
1245
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1247
def iter_tree_children(self, parent_id):
1248
"""Iterate through the entry's tree children, if any"""
1250
path = self._tree_id_paths[parent_id]
1254
children = os.listdir(self._tree.abspath(path))
1255
except OSError as e:
1256
if not (osutils._is_error_enotdir(e) or
1257
e.errno in (errno.ENOENT, errno.ESRCH)):
1261
for child in children:
1262
childpath = joinpath(path, child)
1263
if self._tree.is_control_filename(childpath):
1265
yield self.trans_id_tree_path(childpath)
1267
def _generate_limbo_path(self, trans_id):
1268
"""Generate a limbo path using the final path if possible.
1270
This optimizes the performance of applying the tree transform by
1271
avoiding renames. These renames can be avoided only when the parent
1272
directory is already scheduled for creation.
1274
If the final path cannot be used, falls back to using the trans_id as
1277
parent = self._new_parent.get(trans_id)
1278
# if the parent directory is already in limbo (e.g. when building a
1279
# tree), choose a limbo name inside the parent, to reduce further
1281
use_direct_path = False
1282
if self._new_contents.get(parent) == 'directory':
1283
filename = self._new_name.get(trans_id)
1284
if filename is not None:
1285
if parent not in self._limbo_children:
1286
self._limbo_children[parent] = set()
1287
self._limbo_children_names[parent] = {}
1288
use_direct_path = True
1289
# the direct path can only be used if no other file has
1290
# already taken this pathname, i.e. if the name is unused, or
1291
# if it is already associated with this trans_id.
1292
elif self._case_sensitive_target:
1293
if (self._limbo_children_names[parent].get(filename)
1294
in (trans_id, None)):
1295
use_direct_path = True
1297
for l_filename, l_trans_id in (
1298
self._limbo_children_names[parent].items()):
1299
if l_trans_id == trans_id:
1301
if l_filename.lower() == filename.lower():
1304
use_direct_path = True
1306
if not use_direct_path:
1307
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1309
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1310
self._limbo_children[parent].add(trans_id)
1311
self._limbo_children_names[parent][filename] = trans_id
1314
def cancel_versioning(self, trans_id):
1315
"""Undo a previous versioning of a file"""
1316
self._versioned.remove(trans_id)
1318
def apply(self, no_conflicts=False, _mover=None):
1319
"""Apply all changes to the inventory and filesystem.
1321
If filesystem or inventory conflicts are present, MalformedTransform
1324
If apply succeeds, finalize is not necessary.
1326
:param no_conflicts: if True, the caller guarantees there are no
1327
conflicts, so no check is made.
1328
:param _mover: Supply an alternate FileMover, for testing
1330
for hook in MutableTree.hooks['pre_transform']:
1331
hook(self._tree, self)
1332
if not no_conflicts:
1333
self._check_malformed()
1334
self.rename_count = 0
1335
with ui.ui_factory.nested_progress_bar() as child_pb:
1336
child_pb.update(gettext('Apply phase'), 0, 2)
1337
index_changes = self._generate_index_changes()
1340
mover = _FileMover()
1344
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1345
self._apply_removals(mover)
1346
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1347
modified_paths = self._apply_insertions(mover)
1348
except BaseException:
1352
mover.apply_deletions()
1353
self._tree._apply_index_changes(index_changes)
1356
return _TransformResults(modified_paths, self.rename_count)
1358
def _apply_removals(self, mover):
1359
"""Perform tree operations that remove directory/inventory names.
1361
That is, delete files that are to be deleted, and put any files that
1362
need renaming into limbo. This must be done in strict child-to-parent
1365
If inventory_delta is None, no inventory delta generation is performed.
1367
tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1368
with ui.ui_factory.nested_progress_bar() as child_pb:
1369
for num, (path, trans_id) in enumerate(tree_paths):
1370
# do not attempt to move root into a subdirectory of itself.
1373
child_pb.update(gettext('removing file'), num, len(tree_paths))
1374
full_path = self._tree.abspath(path)
1375
if trans_id in self._removed_contents:
1376
delete_path = os.path.join(self._deletiondir, trans_id)
1377
mover.pre_delete(full_path, delete_path)
1378
elif (trans_id in self._new_name or
1379
trans_id in self._new_parent):
1381
mover.rename(full_path, self._limbo_name(trans_id))
1382
except TransformRenameFailed as e:
1383
if e.errno != errno.ENOENT:
1386
self.rename_count += 1
1388
def _apply_insertions(self, mover):
1389
"""Perform tree operations that insert directory/inventory names.
1391
That is, create any files that need to be created, and restore from
1392
limbo any files that needed renaming. This must be done in strict
1393
parent-to-child order.
1395
If inventory_delta is None, no inventory delta is calculated, and
1396
no list of modified paths is returned.
1398
new_paths = self.new_paths(filesystem_only=True)
1400
with ui.ui_factory.nested_progress_bar() as child_pb:
1401
for num, (path, trans_id) in enumerate(new_paths):
1403
child_pb.update(gettext('adding file'),
1404
num, len(new_paths))
1405
full_path = self._tree.abspath(path)
1406
if trans_id in self._needs_rename:
1408
mover.rename(self._limbo_name(trans_id), full_path)
1409
except TransformRenameFailed as e:
1410
# We may be renaming a dangling inventory id
1411
if e.errno != errno.ENOENT:
1414
self.rename_count += 1
1415
# TODO: if trans_id in self._observed_sha1s, we should
1416
# re-stat the final target, since ctime will be
1417
# updated by the change.
1418
if (trans_id in self._new_contents
1419
or self.path_changed(trans_id)):
1420
if trans_id in self._new_contents:
1421
modified_paths.append(full_path)
1422
if trans_id in self._new_executability:
1423
self._set_executability(path, trans_id)
1424
if trans_id in self._observed_sha1s:
1425
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1426
st = osutils.lstat(full_path)
1427
self._observed_sha1s[trans_id] = (o_sha1, st)
1428
for path, trans_id in new_paths:
1429
# new_paths includes stuff like workingtree conflicts. Only the
1430
# stuff in new_contents actually comes from limbo.
1431
if trans_id in self._limbo_files:
1432
del self._limbo_files[trans_id]
1433
self._new_contents.clear()
1434
return modified_paths
1436
def _generate_index_changes(self):
1437
"""Generate an inventory delta for the current transform."""
1438
removed_id = set(self._removed_id)
1439
removed_id.update(self._removed_contents)
1442
for id_set in [self._new_name, self._new_parent,
1443
self._new_executability]:
1444
changed_ids.update(id_set)
1445
for id_set in [self._new_name, self._new_parent]:
1446
removed_id.update(id_set)
1448
changed_kind = set(self._new_contents)
1449
# Ignore entries that are already known to have changed.
1450
changed_kind.difference_update(changed_ids)
1451
# to keep only the truly changed ones
1452
changed_kind = (t for t in changed_kind
1453
if self.tree_kind(t) != self.final_kind(t))
1454
changed_ids.update(changed_kind)
1455
for t in changed_kind:
1456
if self.final_kind(t) == 'directory':
1458
changed_ids.remove(t)
1459
new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1460
total_entries = len(new_paths) + len(removed_id)
1461
with ui.ui_factory.nested_progress_bar() as child_pb:
1462
for num, trans_id in enumerate(removed_id):
1464
child_pb.update(gettext('removing file'),
1467
path = self._tree_id_paths[trans_id]
1470
changes[path] = (None, None, None, None)
1471
for num, (path, trans_id) in enumerate(new_paths):
1473
child_pb.update(gettext('adding file'),
1474
num + len(removed_id), total_entries)
1476
kind = self.final_kind(trans_id)
1479
versioned = self.final_is_versioned(trans_id)
1482
executability = self._new_executability.get(trans_id)
1483
reference_revision = self._new_reference_revision.get(trans_id)
1484
symlink_target = self._symlink_target.get(trans_id)
1486
kind, executability, reference_revision, symlink_target)
1487
return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1490
class GitTransformPreview(GitTreeTransform):
1491
"""A TreeTransform for generating preview trees.
1493
Unlike TreeTransform, this version works when the input tree is a
1494
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1495
unversioned files in the input tree.
1498
def __init__(self, tree, pb=None, case_sensitive=True):
1500
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1501
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1503
def canonical_path(self, path):
1506
def tree_kind(self, trans_id):
1507
path = self.tree_path(trans_id)
1510
kind = self._tree.path_content_summary(path)[0]
1511
if kind == 'missing':
1515
def _set_mode(self, trans_id, mode_id, typefunc):
1516
"""Set the mode of new file contents.
1517
The mode_id is the existing file to get the mode from (often the same
1518
as trans_id). The operation is only performed if there's a mode match
1519
according to typefunc.
1521
# is it ok to ignore this? probably
1524
def iter_tree_children(self, parent_id):
1525
"""Iterate through the entry's tree children, if any"""
1527
path = self._tree_id_paths[parent_id]
1531
for child in self._tree.iter_child_entries(path):
1532
childpath = joinpath(path, child.name)
1533
yield self.trans_id_tree_path(childpath)
1534
except errors.NoSuchFile:
1537
def new_orphan(self, trans_id, parent_id):
1538
raise NotImplementedError(self.new_orphan)
1541
class GitPreviewTree(PreviewTree, GitTree):
1542
"""Partial implementation of Tree to support show_diff_trees"""
1544
def __init__(self, transform):
1545
PreviewTree.__init__(self, transform)
1546
self.store = transform._tree.store
1547
self.mapping = transform._tree.mapping
1548
self._final_paths = FinalPaths(transform)
1550
def supports_setting_file_ids(self):
1553
def _supports_executable(self):
1554
return self._transform._limbo_supports_executable()
1556
def walkdirs(self, prefix=''):
1557
pending = [self._transform.root]
1558
while len(pending) > 0:
1559
parent_id = pending.pop()
1562
prefix = prefix.rstrip('/')
1563
parent_path = self._final_paths.get_path(parent_id)
1564
for child_id in self._all_children(parent_id):
1565
path_from_root = self._final_paths.get_path(child_id)
1566
basename = self._transform.final_name(child_id)
1567
kind = self._transform.final_kind(child_id)
1568
if kind is not None:
1569
versioned_kind = kind
1572
versioned_kind = self._transform._tree.stored_kind(
1574
if versioned_kind == 'directory':
1575
subdirs.append(child_id)
1576
children.append((path_from_root, basename, kind, None,
1579
if parent_path.startswith(prefix):
1580
yield parent_path, children
1581
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1584
def iter_changes(self, from_tree, include_unchanged=False,
1585
specific_files=None, pb=None, extra_trees=None,
1586
require_versioned=True, want_unversioned=False):
1587
"""See InterTree.iter_changes.
1589
This has a fast path that is only used when the from_tree matches
1590
the transform tree, and no fancy options are supplied.
1592
return InterTree.get(from_tree, self).iter_changes(
1593
include_unchanged=include_unchanged,
1594
specific_files=specific_files,
1596
extra_trees=extra_trees,
1597
require_versioned=require_versioned,
1598
want_unversioned=want_unversioned)
1600
def get_file(self, path):
1601
"""See Tree.get_file"""
1602
trans_id = self._path2trans_id(path)
1603
if trans_id is None:
1604
raise errors.NoSuchFile(path)
1605
if trans_id in self._transform._new_contents:
1606
name = self._transform._limbo_name(trans_id)
1607
return open(name, 'rb')
1608
if trans_id in self._transform._removed_contents:
1609
raise errors.NoSuchFile(path)
1610
orig_path = self._transform.tree_path(trans_id)
1611
return self._transform._tree.get_file(orig_path)
1613
def get_symlink_target(self, path):
1614
"""See Tree.get_symlink_target"""
1615
trans_id = self._path2trans_id(path)
1616
if trans_id is None:
1617
raise errors.NoSuchFile(path)
1618
if trans_id not in self._transform._new_contents:
1619
orig_path = self._transform.tree_path(trans_id)
1620
return self._transform._tree.get_symlink_target(orig_path)
1621
name = self._transform._limbo_name(trans_id)
1622
return osutils.readlink(name)
1624
def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1625
trans_id = self._path2trans_id(path)
1626
if trans_id is None:
1628
orig_path = self._transform.tree_path(trans_id)
1629
if orig_path is not None:
1630
old_annotation = self._transform._tree.annotate_iter(
1631
orig_path, default_revision=default_revision)
1635
lines = self.get_file_lines(path)
1636
except errors.NoSuchFile:
1638
return annotate.reannotate([old_annotation], lines, default_revision)
1640
def get_file_text(self, path):
1641
"""Return the byte content of a file.
1643
:param path: The path of the file.
1645
:returns: A single byte string for the whole file.
1647
with self.get_file(path) as my_file:
1648
return my_file.read()
1650
def get_file_lines(self, path):
1651
"""Return the content of a file, as lines.
1653
:param path: The path of the file.
1655
return osutils.split_lines(self.get_file_text(path))
1658
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1659
in self._transform._tree.extras())
1660
possible_extras.update(self._transform._new_contents)
1661
possible_extras.update(self._transform._removed_id)
1662
for trans_id in possible_extras:
1663
if not self._transform.final_is_versioned(trans_id):
1664
yield self._final_paths._determine_path(trans_id)
1666
def path_content_summary(self, path):
1667
trans_id = self._path2trans_id(path)
1668
tt = self._transform
1669
tree_path = tt.tree_path(trans_id)
1670
kind = tt._new_contents.get(trans_id)
1672
if tree_path is None or trans_id in tt._removed_contents:
1673
return 'missing', None, None, None
1674
summary = tt._tree.path_content_summary(tree_path)
1675
kind, size, executable, link_or_sha1 = summary
1678
limbo_name = tt._limbo_name(trans_id)
1679
if trans_id in tt._new_reference_revision:
1680
kind = 'tree-reference'
1682
statval = os.lstat(limbo_name)
1683
size = statval.st_size
1684
if not tt._limbo_supports_executable():
1687
executable = statval.st_mode & S_IEXEC
1691
if kind == 'symlink':
1692
link_or_sha1 = os.readlink(limbo_name)
1693
if not isinstance(link_or_sha1, str):
1694
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1695
executable = tt._new_executability.get(trans_id, executable)
1696
return kind, size, executable, link_or_sha1
1698
def get_file_mtime(self, path):
1699
"""See Tree.get_file_mtime"""
1700
trans_id = self._path2trans_id(path)
1701
if trans_id is None:
1702
raise errors.NoSuchFile(path)
1703
if trans_id not in self._transform._new_contents:
1704
return self._transform._tree.get_file_mtime(
1705
self._transform.tree_path(trans_id))
1706
name = self._transform._limbo_name(trans_id)
1707
statval = os.lstat(name)
1708
return statval.st_mtime
1710
def is_versioned(self, path):
1711
trans_id = self._path2trans_id(path)
1712
if trans_id is None:
1713
# It doesn't exist, so it's not versioned.
1715
if trans_id in self._transform._versioned:
1717
if trans_id in self._transform._removed_id:
1719
orig_path = self._transform.tree_path(trans_id)
1720
return self._transform._tree.is_versioned(orig_path)
1722
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1724
raise NotImplementedError(
1725
'follow tree references not yet supported')
1727
# This may not be a maximally efficient implementation, but it is
1728
# reasonably straightforward. An implementation that grafts the
1729
# TreeTransform changes onto the tree's iter_entries_by_dir results
1730
# might be more efficient, but requires tricky inferences about stack
1732
for trans_id, path in self._list_files_by_dir():
1733
entry, is_versioned = self._transform.final_entry(trans_id)
1736
if not is_versioned and entry.kind != 'directory':
1738
if specific_files is not None and path not in specific_files:
1740
if entry is not None:
1743
def _list_files_by_dir(self):
1744
todo = [ROOT_PARENT]
1745
while len(todo) > 0:
1747
children = list(self._all_children(parent))
1748
paths = dict(zip(children, self._final_paths.get_paths(children)))
1749
children.sort(key=paths.get)
1750
todo.extend(reversed(children))
1751
for trans_id in children:
1752
yield trans_id, paths[trans_id][0]
1754
def revision_tree(self, revision_id):
1755
return self._transform._tree.revision_tree(revision_id)
1757
def _stat_limbo_file(self, trans_id):
1758
name = self._transform._limbo_name(trans_id)
1759
return os.lstat(name)
1761
def git_snapshot(self, want_unversioned=False):
1764
for trans_id, path in self._list_files_by_dir():
1765
if not self._transform.final_is_versioned(trans_id):
1766
if not want_unversioned:
1769
o, mode = self._transform.final_git_entry(trans_id)
1771
self.store.add_object(o)
1772
os.append((encode_git_path(path), o.id, mode))
1775
return commit_tree(self.store, os), extra
1777
def iter_child_entries(self, path):
1778
trans_id = self._path2trans_id(path)
1779
if trans_id is None:
1780
raise errors.NoSuchFile(path)
1781
for child_trans_id in self._all_children(trans_id):
1782
entry, is_versioned = self._transform.final_entry(trans_id)
1783
if not is_versioned:
1785
if entry is not None: