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"""
304
for trans_id in self._new_parent:
307
while parent_id != ROOT_PARENT:
310
parent_id = self.final_parent(parent_id)
313
if parent_id == trans_id:
314
conflicts.append(('parent loop', trans_id))
315
if parent_id in seen:
319
def _improper_versioning(self):
320
"""Cannot version a file with no contents, or a bad type.
322
However, existing entries with no contents are okay.
325
for trans_id in self._versioned:
326
kind = self.final_kind(trans_id)
327
if kind == 'symlink' and not self._tree.supports_symlinks():
328
# Ignore symlinks as they are not supported on this platform
331
conflicts.append(('versioning no contents', trans_id))
333
if not self._tree.versionable_kind(kind):
334
conflicts.append(('versioning bad kind', trans_id, kind))
337
def _executability_conflicts(self):
338
"""Check for bad executability changes.
340
Only versioned files may have their executability set, because
341
1. only versioned entries can have executability under windows
342
2. only files can be executable. (The execute bit on a directory
343
does not indicate searchability)
346
for trans_id in self._new_executability:
347
if not self.final_is_versioned(trans_id):
348
conflicts.append(('unversioned executability', trans_id))
350
if self.final_kind(trans_id) != "file":
351
conflicts.append(('non-file executability', trans_id))
354
def _overwrite_conflicts(self):
355
"""Check for overwrites (not permitted on Win32)"""
357
for trans_id in self._new_contents:
358
if self.tree_kind(trans_id) is None:
360
if trans_id not in self._removed_contents:
361
conflicts.append(('overwrite', trans_id,
362
self.final_name(trans_id)))
365
def _duplicate_entries(self, by_parent):
366
"""No directory may have two entries with the same name."""
368
if (self._new_name, self._new_parent) == ({}, {}):
370
for children in by_parent.values():
372
for child_tid in children:
373
name = self.final_name(child_tid)
375
# Keep children only if they still exist in the end
376
if not self._case_sensitive_target:
378
name_ids.append((name, child_tid))
382
for name, trans_id in name_ids:
383
kind = self.final_kind(trans_id)
384
if kind is None and not self.final_is_versioned(trans_id):
386
if name == last_name:
387
conflicts.append(('duplicate', last_trans_id, trans_id,
390
last_trans_id = trans_id
393
def _parent_type_conflicts(self, by_parent):
394
"""Children must have a directory parent"""
396
for parent_id, children in by_parent.items():
397
if parent_id == ROOT_PARENT:
400
for child_id in children:
401
if self.final_kind(child_id) is not None:
406
# There is at least a child, so we need an existing directory to
408
kind = self.final_kind(parent_id)
410
# The directory will be deleted
411
conflicts.append(('missing parent', parent_id))
412
elif kind != "directory":
413
# Meh, we need a *directory* to put something in it
414
conflicts.append(('non-directory parent', parent_id))
417
def _set_executability(self, path, trans_id):
418
"""Set the executability of versioned files """
419
if self._tree._supports_executable():
420
new_executability = self._new_executability[trans_id]
421
abspath = self._tree.abspath(path)
422
current_mode = os.stat(abspath).st_mode
423
if new_executability:
426
to_mode = current_mode | (0o100 & ~umask)
427
# Enable x-bit for others only if they can read it.
428
if current_mode & 0o004:
429
to_mode |= 0o001 & ~umask
430
if current_mode & 0o040:
431
to_mode |= 0o010 & ~umask
433
to_mode = current_mode & ~0o111
434
osutils.chmod_if_possible(abspath, to_mode)
436
def _new_entry(self, name, parent_id, file_id):
437
"""Helper function to create a new filesystem entry."""
438
trans_id = self.create_path(name, parent_id)
439
if file_id is not None:
440
self.version_file(trans_id, file_id=file_id)
443
def new_file(self, name, parent_id, contents, file_id=None,
444
executable=None, sha1=None):
445
"""Convenience method to create files.
447
name is the name of the file to create.
448
parent_id is the transaction id of the parent directory of the file.
449
contents is an iterator of bytestrings, which will be used to produce
451
:param file_id: The inventory ID of the file, if it is to be versioned.
452
:param executable: Only valid when a file_id has been supplied.
454
trans_id = self._new_entry(name, parent_id, file_id)
455
# TODO: rather than scheduling a set_executable call,
456
# have create_file create the file with the right mode.
457
self.create_file(contents, trans_id, sha1=sha1)
458
if executable is not None:
459
self.set_executability(executable, trans_id)
462
def new_directory(self, name, parent_id, file_id=None):
463
"""Convenience method to create directories.
465
name is the name of the directory to create.
466
parent_id is the transaction id of the parent directory of the
468
file_id is the inventory ID of the directory, if it is to be versioned.
470
trans_id = self._new_entry(name, parent_id, file_id)
471
self.create_directory(trans_id)
474
def new_symlink(self, name, parent_id, target, file_id=None):
475
"""Convenience method to create symbolic link.
477
name is the name of the symlink to create.
478
parent_id is the transaction id of the parent directory of the symlink.
479
target is a bytestring of the target of the symlink.
480
file_id is the inventory ID of the file, if it is to be versioned.
482
trans_id = self._new_entry(name, parent_id, file_id)
483
self.create_symlink(target, trans_id)
486
def new_orphan(self, trans_id, parent_id):
487
"""Schedule an item to be orphaned.
489
When a directory is about to be removed, its children, if they are not
490
versioned are moved out of the way: they don't have a parent anymore.
492
:param trans_id: The trans_id of the existing item.
493
:param parent_id: The parent trans_id of the item.
495
raise NotImplementedError(self.new_orphan)
497
def _get_potential_orphans(self, dir_id):
498
"""Find the potential orphans in a directory.
500
A directory can't be safely deleted if there are versioned files in it.
501
If all the contained files are unversioned then they can be orphaned.
503
The 'None' return value means that the directory contains at least one
504
versioned file and should not be deleted.
506
:param dir_id: The directory trans id.
508
:return: A list of the orphan trans ids or None if at least one
509
versioned file is present.
512
# Find the potential orphans, stop if one item should be kept
513
for child_tid in self.by_parent()[dir_id]:
514
if child_tid in self._removed_contents:
515
# The child is removed as part of the transform. Since it was
516
# versioned before, it's not an orphan
518
if not self.final_is_versioned(child_tid):
519
# The child is not versioned
520
orphans.append(child_tid)
522
# We have a versioned file here, searching for orphans is
528
def _affected_ids(self):
529
"""Return the set of transform ids affected by the transform"""
530
trans_ids = set(self._removed_id)
531
trans_ids.update(self._versioned)
532
trans_ids.update(self._removed_contents)
533
trans_ids.update(self._new_contents)
534
trans_ids.update(self._new_executability)
535
trans_ids.update(self._new_name)
536
trans_ids.update(self._new_parent)
539
def iter_changes(self, want_unversioned=False):
540
"""Produce output in the same format as Tree.iter_changes.
542
Will produce nonsensical results if invoked while inventory/filesystem
543
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
545
final_paths = FinalPaths(self)
546
trans_ids = self._affected_ids()
548
# Now iterate through all active paths
549
for trans_id in trans_ids:
550
from_path = self.tree_path(trans_id)
552
# find file ids, and determine versioning state
553
if from_path is None:
554
from_versioned = False
556
from_versioned = self._tree.is_versioned(from_path)
557
if not want_unversioned and not from_versioned:
559
to_path = final_paths.get_path(trans_id)
563
to_versioned = self.final_is_versioned(trans_id)
564
if not want_unversioned and not to_versioned:
568
# get data from working tree if versioned
569
from_entry = next(self._tree.iter_entries_by_dir(
570
specific_files=[from_path]))[1]
571
from_name = from_entry.name
574
if from_path is None:
575
# File does not exist in FROM state
578
# File exists, but is not versioned. Have to use path-
580
from_name = os.path.basename(from_path)
581
if from_path is not None:
582
from_kind, from_executable, from_stats = \
583
self._tree._comparison_data(from_entry, from_path)
586
from_executable = False
588
to_name = self.final_name(trans_id)
589
to_kind = self.final_kind(trans_id)
590
if trans_id in self._new_executability:
591
to_executable = self._new_executability[trans_id]
593
to_executable = from_executable
595
if from_versioned and from_kind != to_kind:
597
elif to_kind in ('file', 'symlink') and (
598
trans_id in self._new_contents):
600
if (not modified and from_versioned == to_versioned
601
and from_path == to_path
602
and from_name == to_name
603
and from_executable == to_executable):
605
if (from_path, to_path) == (None, None):
609
(from_path, to_path), modified,
610
(from_versioned, to_versioned),
611
(from_name, to_name),
612
(from_kind, to_kind),
613
(from_executable, to_executable)))
616
return (c.path[0] or '', c.path[1] or '')
617
return iter(sorted(results, key=path_key))
619
def get_preview_tree(self):
620
"""Return a tree representing the result of the transform.
622
The tree is a snapshot, and altering the TreeTransform will invalidate
625
return GitPreviewTree(self)
627
def commit(self, branch, message, merge_parents=None, strict=False,
628
timestamp=None, timezone=None, committer=None, authors=None,
629
revprops=None, revision_id=None):
630
"""Commit the result of this TreeTransform to a branch.
632
:param branch: The branch to commit to.
633
:param message: The message to attach to the commit.
634
:param merge_parents: Additional parent revision-ids specified by
636
:param strict: If True, abort the commit if there are unversioned
638
:param timestamp: if not None, seconds-since-epoch for the time and
639
date. (May be a float.)
640
:param timezone: Optional timezone for timestamp, as an offset in
642
:param committer: Optional committer in email-id format.
643
(e.g. "J Random Hacker <jrandom@example.com>")
644
:param authors: Optional list of authors in email-id format.
645
:param revprops: Optional dictionary of revision properties.
646
:param revision_id: Optional revision id. (Specifying a revision-id
647
may reduce performance for some non-native formats.)
648
:return: The revision_id of the revision committed.
650
self._check_malformed()
652
unversioned = set(self._new_contents).difference(set(self._versioned))
653
for trans_id in unversioned:
654
if not self.final_is_versioned(trans_id):
655
raise errors.StrictCommitFailed()
657
revno, last_rev_id = branch.last_revision_info()
658
if last_rev_id == _mod_revision.NULL_REVISION:
659
if merge_parents is not None:
660
raise ValueError('Cannot supply merge parents for first'
664
parent_ids = [last_rev_id]
665
if merge_parents is not None:
666
parent_ids.extend(merge_parents)
667
if self._tree.get_revision_id() != last_rev_id:
668
raise ValueError('TreeTransform not based on branch basis: %s' %
669
self._tree.get_revision_id().decode('utf-8'))
670
from .. import commit
671
revprops = commit.Commit.update_revprops(revprops, branch, authors)
672
builder = branch.get_commit_builder(parent_ids,
677
revision_id=revision_id)
678
preview = self.get_preview_tree()
679
list(builder.record_iter_changes(preview, last_rev_id,
680
self.iter_changes()))
681
builder.finish_inventory()
682
revision_id = builder.commit(message)
683
branch.set_last_revision_info(revno + 1, revision_id)
686
def _text_parent(self, trans_id):
687
path = self.tree_path(trans_id)
689
if path is None or self._tree.kind(path) != 'file':
691
except errors.NoSuchFile:
695
def _get_parents_texts(self, trans_id):
696
"""Get texts for compression parents of this file."""
697
path = self._text_parent(trans_id)
700
return (self._tree.get_file_text(path),)
702
def _get_parents_lines(self, trans_id):
703
"""Get lines for compression parents of this file."""
704
path = self._text_parent(trans_id)
707
return (self._tree.get_file_lines(path),)
709
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
710
"""Schedule creation of a new file.
714
:param contents: an iterator of strings, all of which will be written
715
to the target destination.
716
:param trans_id: TreeTransform handle
717
:param mode_id: If not None, force the mode of the target file to match
718
the mode of the object referenced by mode_id.
719
Otherwise, we will try to preserve mode bits of an existing file.
720
:param sha1: If the sha1 of this content is already known, pass it in.
721
We can use it to prevent future sha1 computations.
723
raise NotImplementedError(self.create_file)
725
def create_directory(self, trans_id):
726
"""Schedule creation of a new directory.
728
See also new_directory.
730
raise NotImplementedError(self.create_directory)
732
def create_symlink(self, target, trans_id):
733
"""Schedule creation of a new symbolic link.
735
target is a bytestring.
736
See also new_symlink.
738
raise NotImplementedError(self.create_symlink)
740
def create_hardlink(self, path, trans_id):
741
"""Schedule creation of a hard link"""
742
raise NotImplementedError(self.create_hardlink)
744
def cancel_creation(self, trans_id):
745
"""Cancel the creation of new file contents."""
746
raise NotImplementedError(self.cancel_creation)
748
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
749
"""Apply all changes to the inventory and filesystem.
751
If filesystem or inventory conflicts are present, MalformedTransform
754
If apply succeeds, finalize is not necessary.
756
:param no_conflicts: if True, the caller guarantees there are no
757
conflicts, so no check is made.
758
:param precomputed_delta: An inventory delta to use instead of
760
:param _mover: Supply an alternate FileMover, for testing
762
raise NotImplementedError(self.apply)
764
def cook_conflicts(self, raw_conflicts):
765
"""Generate a list of cooked conflicts, sorted by file path"""
766
if not raw_conflicts:
768
fp = FinalPaths(self)
769
from .workingtree import TextConflict
770
for c in raw_conflicts:
771
if c[0] == 'text conflict':
772
yield TextConflict(fp.get_path(c[1]))
773
elif c[0] == 'duplicate':
774
yield TextConflict(fp.get_path(c[2]))
775
elif c[0] == 'contents conflict':
776
yield TextConflict(fp.get_path(c[1][0]))
777
elif c[0] == 'missing parent':
778
# TODO(jelmer): This should not make it to here
779
yield TextConflict(fp.get_path(c[2]))
780
elif c[0] == 'non-directory parent':
781
yield TextConflict(fp.get_path(c[2]))
782
elif c[0] == 'deleting parent':
783
# TODO(jelmer): This should not make it to here
784
yield TextConflict(fp.get_path(c[2]))
785
elif c[0] == 'parent loop':
786
# TODO(jelmer): This should not make it to here
787
yield TextConflict(fp.get_path(c[2]))
788
elif c[0] == 'path conflict':
789
yield TextConflict(fp.get_path(c[1]))
791
raise AssertionError('unknown conflict %s' % c[0])
794
class DiskTreeTransform(TreeTransformBase):
795
"""Tree transform storing its contents on disk."""
797
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
799
:param tree: The tree that will be transformed, but not necessarily
801
:param limbodir: A directory where new files can be stored until
802
they are installed in their proper places
804
:param case_sensitive: If True, the target of the transform is
805
case sensitive, not just case preserving.
807
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
808
self._limbodir = limbodir
809
self._deletiondir = None
810
# A mapping of transform ids to their limbo filename
811
self._limbo_files = {}
812
self._possibly_stale_limbo_files = set()
813
# A mapping of transform ids to a set of the transform ids of children
814
# that their limbo directory has
815
self._limbo_children = {}
816
# Map transform ids to maps of child filename to child transform id
817
self._limbo_children_names = {}
818
# List of transform ids that need to be renamed from limbo into place
819
self._needs_rename = set()
820
self._creation_mtime = None
821
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
824
"""Release the working tree lock, if held, clean up limbo dir.
826
This is required if apply has not been invoked, but can be invoked
829
if self._tree is None:
832
limbo_paths = list(self._limbo_files.values())
833
limbo_paths.extend(self._possibly_stale_limbo_files)
834
limbo_paths.sort(reverse=True)
835
for path in limbo_paths:
837
osutils.delete_any(path)
839
if e.errno != errno.ENOENT:
841
# XXX: warn? perhaps we just got interrupted at an
842
# inconvenient moment, but perhaps files are disappearing
845
osutils.delete_any(self._limbodir)
847
# We don't especially care *why* the dir is immortal.
848
raise ImmortalLimbo(self._limbodir)
850
if self._deletiondir is not None:
851
osutils.delete_any(self._deletiondir)
853
raise errors.ImmortalPendingDeletion(self._deletiondir)
855
TreeTransformBase.finalize(self)
857
def _limbo_supports_executable(self):
858
"""Check if the limbo path supports the executable bit."""
859
return osutils.supports_executable(self._limbodir)
861
def _limbo_name(self, trans_id):
862
"""Generate the limbo name of a file"""
863
limbo_name = self._limbo_files.get(trans_id)
864
if limbo_name is None:
865
limbo_name = self._generate_limbo_path(trans_id)
866
self._limbo_files[trans_id] = limbo_name
869
def _generate_limbo_path(self, trans_id):
870
"""Generate a limbo path using the trans_id as the relative path.
872
This is suitable as a fallback, and when the transform should not be
873
sensitive to the path encoding of the limbo directory.
875
self._needs_rename.add(trans_id)
876
return osutils.pathjoin(self._limbodir, trans_id)
878
def adjust_path(self, name, parent, trans_id):
879
previous_parent = self._new_parent.get(trans_id)
880
previous_name = self._new_name.get(trans_id)
881
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
882
if (trans_id in self._limbo_files
883
and trans_id not in self._needs_rename):
884
self._rename_in_limbo([trans_id])
885
if previous_parent != parent:
886
self._limbo_children[previous_parent].remove(trans_id)
887
if previous_parent != parent or previous_name != name:
888
del self._limbo_children_names[previous_parent][previous_name]
890
def _rename_in_limbo(self, trans_ids):
891
"""Fix limbo names so that the right final path is produced.
893
This means we outsmarted ourselves-- we tried to avoid renaming
894
these files later by creating them with their final names in their
895
final parents. But now the previous name or parent is no longer
896
suitable, so we have to rename them.
898
Even for trans_ids that have no new contents, we must remove their
899
entries from _limbo_files, because they are now stale.
901
for trans_id in trans_ids:
902
old_path = self._limbo_files[trans_id]
903
self._possibly_stale_limbo_files.add(old_path)
904
del self._limbo_files[trans_id]
905
if trans_id not in self._new_contents:
907
new_path = self._limbo_name(trans_id)
908
os.rename(old_path, new_path)
909
self._possibly_stale_limbo_files.remove(old_path)
910
for descendant in self._limbo_descendants(trans_id):
911
desc_path = self._limbo_files[descendant]
912
desc_path = new_path + desc_path[len(old_path):]
913
self._limbo_files[descendant] = desc_path
915
def _limbo_descendants(self, trans_id):
916
"""Return the set of trans_ids whose limbo paths descend from this."""
917
descendants = set(self._limbo_children.get(trans_id, []))
918
for descendant in list(descendants):
919
descendants.update(self._limbo_descendants(descendant))
922
def _set_mode(self, trans_id, mode_id, typefunc):
923
raise NotImplementedError(self._set_mode)
925
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
926
"""Schedule creation of a new file.
930
:param contents: an iterator of strings, all of which will be written
931
to the target destination.
932
:param trans_id: TreeTransform handle
933
:param mode_id: If not None, force the mode of the target file to match
934
the mode of the object referenced by mode_id.
935
Otherwise, we will try to preserve mode bits of an existing file.
936
:param sha1: If the sha1 of this content is already known, pass it in.
937
We can use it to prevent future sha1 computations.
939
name = self._limbo_name(trans_id)
940
with open(name, 'wb') as f:
941
unique_add(self._new_contents, trans_id, 'file')
942
f.writelines(contents)
943
self._set_mtime(name)
944
self._set_mode(trans_id, mode_id, S_ISREG)
945
# It is unfortunate we have to use lstat instead of fstat, but we just
946
# used utime and chmod on the file, so we need the accurate final
949
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
951
def _read_symlink_target(self, trans_id):
952
return os.readlink(self._limbo_name(trans_id))
954
def _set_mtime(self, path):
955
"""All files that are created get the same mtime.
957
This time is set by the first object to be created.
959
if self._creation_mtime is None:
960
self._creation_mtime = time.time()
961
os.utime(path, (self._creation_mtime, self._creation_mtime))
963
def create_hardlink(self, path, trans_id):
964
"""Schedule creation of a hard link"""
965
name = self._limbo_name(trans_id)
969
if e.errno != errno.EPERM:
971
raise errors.HardLinkNotSupported(path)
973
unique_add(self._new_contents, trans_id, 'file')
974
except BaseException:
975
# Clean up the file, it never got registered so
976
# TreeTransform.finalize() won't clean it up.
980
def create_directory(self, trans_id):
981
"""Schedule creation of a new directory.
983
See also new_directory.
985
os.mkdir(self._limbo_name(trans_id))
986
unique_add(self._new_contents, trans_id, 'directory')
988
def create_symlink(self, target, trans_id):
989
"""Schedule creation of a new symbolic link.
991
target is a bytestring.
992
See also new_symlink.
994
if self._create_symlinks:
995
os.symlink(target, self._limbo_name(trans_id))
998
path = FinalPaths(self).get_path(trans_id)
1002
'Unable to create symlink "%s" on this filesystem.' % (path,))
1003
self._symlink_target[trans_id] = target
1004
# We add symlink to _new_contents even if they are unsupported
1005
# and not created. These entries are subsequently used to avoid
1006
# conflicts on platforms that don't support symlink
1007
unique_add(self._new_contents, trans_id, 'symlink')
1009
def cancel_creation(self, trans_id):
1010
"""Cancel the creation of new file contents."""
1011
del self._new_contents[trans_id]
1012
if trans_id in self._observed_sha1s:
1013
del self._observed_sha1s[trans_id]
1014
children = self._limbo_children.get(trans_id)
1015
# if this is a limbo directory with children, move them before removing
1017
if children is not None:
1018
self._rename_in_limbo(children)
1019
del self._limbo_children[trans_id]
1020
del self._limbo_children_names[trans_id]
1021
osutils.delete_any(self._limbo_name(trans_id))
1023
def new_orphan(self, trans_id, parent_id):
1024
conf = self._tree.get_config_stack()
1025
handle_orphan = conf.get('transform.orphan_policy')
1026
handle_orphan(self, trans_id, parent_id)
1028
def final_entry(self, trans_id):
1029
is_versioned = self.final_is_versioned(trans_id)
1030
fp = FinalPaths(self)
1031
tree_path = fp.get_path(trans_id)
1032
if trans_id in self._new_contents:
1033
path = self._limbo_name(trans_id)
1035
kind = mode_kind(st.st_mode)
1036
name = self.final_name(trans_id)
1037
file_id = self._tree.mapping.generate_file_id(tree_path)
1038
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1039
if kind == 'directory':
1040
return GitTreeDirectory(
1041
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1042
executable = mode_is_executable(st.st_mode)
1043
mode = object_mode(kind, executable)
1044
blob = blob_from_path_and_stat(encode_git_path(path), st)
1045
if kind == 'symlink':
1046
return GitTreeSymlink(
1047
file_id, name, parent_id,
1048
decode_git_path(blob.data)), is_versioned
1049
elif kind == 'file':
1051
file_id, name, executable=executable, parent_id=parent_id,
1052
git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1054
raise AssertionError(kind)
1055
elif trans_id in self._removed_contents:
1058
orig_path = self.tree_path(trans_id)
1059
if orig_path is None:
1061
file_id = self._tree.mapping.generate_file_id(tree_path)
1065
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1067
ie = next(self._tree.iter_entries_by_dir(
1068
specific_files=[orig_path]))[1]
1069
ie.file_id = file_id
1070
ie.parent_id = parent_id
1071
return ie, is_versioned
1072
except StopIteration:
1074
if self.tree_kind(trans_id) == 'directory':
1075
return GitTreeDirectory(
1076
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1077
except OSError as e:
1078
if e.errno != errno.ENOTDIR:
1082
def final_git_entry(self, trans_id):
1083
if trans_id in self._new_contents:
1084
path = self._limbo_name(trans_id)
1086
kind = mode_kind(st.st_mode)
1087
if kind == 'directory':
1089
executable = mode_is_executable(st.st_mode)
1090
mode = object_mode(kind, executable)
1091
blob = blob_from_path_and_stat(encode_git_path(path), st)
1092
elif trans_id in self._removed_contents:
1095
orig_path = self.tree_path(trans_id)
1096
kind = self._tree.kind(orig_path)
1097
executable = self._tree.is_executable(orig_path)
1098
mode = object_mode(kind, executable)
1099
if kind == 'symlink':
1100
contents = self._tree.get_symlink_target(orig_path)
1101
elif kind == 'file':
1102
contents = self._tree.get_file_text(orig_path)
1103
elif kind == 'directory':
1106
raise AssertionError(kind)
1107
blob = Blob.from_string(contents)
1111
class GitTreeTransform(DiskTreeTransform):
1112
"""Represent a tree transformation.
1114
This object is designed to support incremental generation of the transform,
1117
However, it gives optimum performance when parent directories are created
1118
before their contents. The transform is then able to put child files
1119
directly in their parent directory, avoiding later renames.
1121
It is easy to produce malformed transforms, but they are generally
1122
harmless. Attempting to apply a malformed transform will cause an
1123
exception to be raised before any modifications are made to the tree.
1125
Many kinds of malformed transforms can be corrected with the
1126
resolve_conflicts function. The remaining ones indicate programming error,
1127
such as trying to create a file with no path.
1129
Two sets of file creation methods are supplied. Convenience methods are:
1134
These are composed of the low-level methods:
1136
* create_file or create_directory or create_symlink
1140
Transform/Transaction ids
1141
-------------------------
1142
trans_ids are temporary ids assigned to all files involved in a transform.
1143
It's possible, even common, that not all files in the Tree have trans_ids.
1145
trans_ids are used because filenames and file_ids are not good enough
1146
identifiers; filenames change.
1148
trans_ids are only valid for the TreeTransform that generated them.
1152
Limbo is a temporary directory use to hold new versions of files.
1153
Files are added to limbo by create_file, create_directory, create_symlink,
1154
and their convenience variants (new_*). Files may be removed from limbo
1155
using cancel_creation. Files are renamed from limbo into their final
1156
location as part of TreeTransform.apply
1158
Limbo must be cleaned up, by either calling TreeTransform.apply or
1159
calling TreeTransform.finalize.
1161
Files are placed into limbo inside their parent directories, where
1162
possible. This reduces subsequent renames, and makes operations involving
1163
lots of files faster. This optimization is only possible if the parent
1164
directory is created *before* creating any of its children, so avoid
1165
creating children before parents, where possible.
1169
This temporary directory is used by _FileMover for storing files that are
1170
about to be deleted. In case of rollback, the files will be restored.
1171
FileMover does not delete files until it is sure that a rollback will not
1175
def __init__(self, tree, pb=None):
1176
"""Note: a tree_write lock is taken on the tree.
1178
Use TreeTransform.finalize() to release the lock (can be omitted if
1179
TreeTransform.apply() called).
1181
tree.lock_tree_write()
1183
limbodir = urlutils.local_path_from_url(
1184
tree._transport.abspath('limbo'))
1185
osutils.ensure_empty_directory_exists(
1187
errors.ExistingLimbo)
1188
deletiondir = urlutils.local_path_from_url(
1189
tree._transport.abspath('pending-deletion'))
1190
osutils.ensure_empty_directory_exists(
1192
errors.ExistingPendingDeletion)
1193
except BaseException:
1197
# Cache of realpath results, to speed up canonical_path
1198
self._realpaths = {}
1199
# Cache of relpath results, to speed up canonical_path
1201
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1202
tree.case_sensitive)
1203
self._deletiondir = deletiondir
1205
def canonical_path(self, path):
1206
"""Get the canonical tree-relative path"""
1207
# don't follow final symlinks
1208
abs = self._tree.abspath(path)
1209
if abs in self._relpaths:
1210
return self._relpaths[abs]
1211
dirname, basename = os.path.split(abs)
1212
if dirname not in self._realpaths:
1213
self._realpaths[dirname] = os.path.realpath(dirname)
1214
dirname = self._realpaths[dirname]
1215
abs = osutils.pathjoin(dirname, basename)
1216
if dirname in self._relpaths:
1217
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1218
relpath = relpath.rstrip('/\\')
1220
relpath = self._tree.relpath(abs)
1221
self._relpaths[abs] = relpath
1224
def tree_kind(self, trans_id):
1225
"""Determine the file kind in the working tree.
1227
:returns: The file kind or None if the file does not exist
1229
path = self._tree_id_paths.get(trans_id)
1233
return osutils.file_kind(self._tree.abspath(path))
1234
except errors.NoSuchFile:
1237
def _set_mode(self, trans_id, mode_id, typefunc):
1238
"""Set the mode of new file contents.
1239
The mode_id is the existing file to get the mode from (often the same
1240
as trans_id). The operation is only performed if there's a mode match
1241
according to typefunc.
1246
old_path = self._tree_id_paths[mode_id]
1250
mode = os.stat(self._tree.abspath(old_path)).st_mode
1251
except OSError as e:
1252
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1253
# Either old_path doesn't exist, or the parent of the
1254
# target is not a directory (but will be one eventually)
1255
# Either way, we know it doesn't exist *right now*
1256
# See also bug #248448
1261
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1263
def iter_tree_children(self, parent_id):
1264
"""Iterate through the entry's tree children, if any"""
1266
path = self._tree_id_paths[parent_id]
1270
children = os.listdir(self._tree.abspath(path))
1271
except OSError as e:
1272
if not (osutils._is_error_enotdir(e) or
1273
e.errno in (errno.ENOENT, errno.ESRCH)):
1277
for child in children:
1278
childpath = joinpath(path, child)
1279
if self._tree.is_control_filename(childpath):
1281
yield self.trans_id_tree_path(childpath)
1283
def _generate_limbo_path(self, trans_id):
1284
"""Generate a limbo path using the final path if possible.
1286
This optimizes the performance of applying the tree transform by
1287
avoiding renames. These renames can be avoided only when the parent
1288
directory is already scheduled for creation.
1290
If the final path cannot be used, falls back to using the trans_id as
1293
parent = self._new_parent.get(trans_id)
1294
# if the parent directory is already in limbo (e.g. when building a
1295
# tree), choose a limbo name inside the parent, to reduce further
1297
use_direct_path = False
1298
if self._new_contents.get(parent) == 'directory':
1299
filename = self._new_name.get(trans_id)
1300
if filename is not None:
1301
if parent not in self._limbo_children:
1302
self._limbo_children[parent] = set()
1303
self._limbo_children_names[parent] = {}
1304
use_direct_path = True
1305
# the direct path can only be used if no other file has
1306
# already taken this pathname, i.e. if the name is unused, or
1307
# if it is already associated with this trans_id.
1308
elif self._case_sensitive_target:
1309
if (self._limbo_children_names[parent].get(filename)
1310
in (trans_id, None)):
1311
use_direct_path = True
1313
for l_filename, l_trans_id in (
1314
self._limbo_children_names[parent].items()):
1315
if l_trans_id == trans_id:
1317
if l_filename.lower() == filename.lower():
1320
use_direct_path = True
1322
if not use_direct_path:
1323
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1325
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1326
self._limbo_children[parent].add(trans_id)
1327
self._limbo_children_names[parent][filename] = trans_id
1330
def cancel_versioning(self, trans_id):
1331
"""Undo a previous versioning of a file"""
1332
self._versioned.remove(trans_id)
1334
def apply(self, no_conflicts=False, _mover=None):
1335
"""Apply all changes to the inventory and filesystem.
1337
If filesystem or inventory conflicts are present, MalformedTransform
1340
If apply succeeds, finalize is not necessary.
1342
:param no_conflicts: if True, the caller guarantees there are no
1343
conflicts, so no check is made.
1344
:param _mover: Supply an alternate FileMover, for testing
1346
for hook in MutableTree.hooks['pre_transform']:
1347
hook(self._tree, self)
1348
if not no_conflicts:
1349
self._check_malformed()
1350
self.rename_count = 0
1351
with ui.ui_factory.nested_progress_bar() as child_pb:
1352
child_pb.update(gettext('Apply phase'), 0, 2)
1353
index_changes = self._generate_index_changes()
1356
mover = _FileMover()
1360
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1361
self._apply_removals(mover)
1362
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1363
modified_paths = self._apply_insertions(mover)
1364
except BaseException:
1368
mover.apply_deletions()
1369
self._tree._apply_index_changes(index_changes)
1372
return _TransformResults(modified_paths, self.rename_count)
1374
def _apply_removals(self, mover):
1375
"""Perform tree operations that remove directory/inventory names.
1377
That is, delete files that are to be deleted, and put any files that
1378
need renaming into limbo. This must be done in strict child-to-parent
1381
If inventory_delta is None, no inventory delta generation is performed.
1383
tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1384
with ui.ui_factory.nested_progress_bar() as child_pb:
1385
for num, (path, trans_id) in enumerate(tree_paths):
1386
# do not attempt to move root into a subdirectory of itself.
1389
child_pb.update(gettext('removing file'), num, len(tree_paths))
1390
full_path = self._tree.abspath(path)
1391
if trans_id in self._removed_contents:
1392
delete_path = os.path.join(self._deletiondir, trans_id)
1393
mover.pre_delete(full_path, delete_path)
1394
elif (trans_id in self._new_name or
1395
trans_id in self._new_parent):
1397
mover.rename(full_path, self._limbo_name(trans_id))
1398
except TransformRenameFailed as e:
1399
if e.errno != errno.ENOENT:
1402
self.rename_count += 1
1404
def _apply_insertions(self, mover):
1405
"""Perform tree operations that insert directory/inventory names.
1407
That is, create any files that need to be created, and restore from
1408
limbo any files that needed renaming. This must be done in strict
1409
parent-to-child order.
1411
If inventory_delta is None, no inventory delta is calculated, and
1412
no list of modified paths is returned.
1414
new_paths = self.new_paths(filesystem_only=True)
1416
with ui.ui_factory.nested_progress_bar() as child_pb:
1417
for num, (path, trans_id) in enumerate(new_paths):
1419
child_pb.update(gettext('adding file'),
1420
num, len(new_paths))
1421
full_path = self._tree.abspath(path)
1422
if trans_id in self._needs_rename:
1424
mover.rename(self._limbo_name(trans_id), full_path)
1425
except TransformRenameFailed as e:
1426
# We may be renaming a dangling inventory id
1427
if e.errno != errno.ENOENT:
1430
self.rename_count += 1
1431
# TODO: if trans_id in self._observed_sha1s, we should
1432
# re-stat the final target, since ctime will be
1433
# updated by the change.
1434
if (trans_id in self._new_contents
1435
or self.path_changed(trans_id)):
1436
if trans_id in self._new_contents:
1437
modified_paths.append(full_path)
1438
if trans_id in self._new_executability:
1439
self._set_executability(path, trans_id)
1440
if trans_id in self._observed_sha1s:
1441
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1442
st = osutils.lstat(full_path)
1443
self._observed_sha1s[trans_id] = (o_sha1, st)
1444
for path, trans_id in new_paths:
1445
# new_paths includes stuff like workingtree conflicts. Only the
1446
# stuff in new_contents actually comes from limbo.
1447
if trans_id in self._limbo_files:
1448
del self._limbo_files[trans_id]
1449
self._new_contents.clear()
1450
return modified_paths
1452
def _generate_index_changes(self):
1453
"""Generate an inventory delta for the current transform."""
1454
removed_id = set(self._removed_id)
1455
removed_id.update(self._removed_contents)
1458
for id_set in [self._new_name, self._new_parent,
1459
self._new_executability]:
1460
changed_ids.update(id_set)
1461
for id_set in [self._new_name, self._new_parent]:
1462
removed_id.update(id_set)
1464
changed_kind = set(self._new_contents)
1465
# Ignore entries that are already known to have changed.
1466
changed_kind.difference_update(changed_ids)
1467
# to keep only the truly changed ones
1468
changed_kind = (t for t in changed_kind
1469
if self.tree_kind(t) != self.final_kind(t))
1470
changed_ids.update(changed_kind)
1471
for t in changed_kind:
1472
if self.final_kind(t) == 'directory':
1474
changed_ids.remove(t)
1475
new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1476
total_entries = len(new_paths) + len(removed_id)
1477
with ui.ui_factory.nested_progress_bar() as child_pb:
1478
for num, trans_id in enumerate(removed_id):
1480
child_pb.update(gettext('removing file'),
1483
path = self._tree_id_paths[trans_id]
1486
changes[path] = (None, None, None, None)
1487
for num, (path, trans_id) in enumerate(new_paths):
1489
child_pb.update(gettext('adding file'),
1490
num + len(removed_id), total_entries)
1492
kind = self.final_kind(trans_id)
1495
versioned = self.final_is_versioned(trans_id)
1498
executability = self._new_executability.get(trans_id)
1499
reference_revision = self._new_reference_revision.get(trans_id)
1500
symlink_target = self._symlink_target.get(trans_id)
1502
kind, executability, reference_revision, symlink_target)
1503
return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1506
class GitTransformPreview(GitTreeTransform):
1507
"""A TreeTransform for generating preview trees.
1509
Unlike TreeTransform, this version works when the input tree is a
1510
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1511
unversioned files in the input tree.
1514
def __init__(self, tree, pb=None, case_sensitive=True):
1516
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1517
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1519
def canonical_path(self, path):
1522
def tree_kind(self, trans_id):
1523
path = self.tree_path(trans_id)
1526
kind = self._tree.path_content_summary(path)[0]
1527
if kind == 'missing':
1531
def _set_mode(self, trans_id, mode_id, typefunc):
1532
"""Set the mode of new file contents.
1533
The mode_id is the existing file to get the mode from (often the same
1534
as trans_id). The operation is only performed if there's a mode match
1535
according to typefunc.
1537
# is it ok to ignore this? probably
1540
def iter_tree_children(self, parent_id):
1541
"""Iterate through the entry's tree children, if any"""
1543
path = self._tree_id_paths[parent_id]
1547
for child in self._tree.iter_child_entries(path):
1548
childpath = joinpath(path, child.name)
1549
yield self.trans_id_tree_path(childpath)
1550
except errors.NoSuchFile:
1553
def new_orphan(self, trans_id, parent_id):
1554
raise NotImplementedError(self.new_orphan)
1557
class GitPreviewTree(PreviewTree, GitTree):
1558
"""Partial implementation of Tree to support show_diff_trees"""
1560
def __init__(self, transform):
1561
PreviewTree.__init__(self, transform)
1562
self.store = transform._tree.store
1563
self.mapping = transform._tree.mapping
1564
self._final_paths = FinalPaths(transform)
1566
def supports_setting_file_ids(self):
1569
def _supports_executable(self):
1570
return self._transform._limbo_supports_executable()
1572
def walkdirs(self, prefix=''):
1573
pending = [self._transform.root]
1574
while len(pending) > 0:
1575
parent_id = pending.pop()
1578
prefix = prefix.rstrip('/')
1579
parent_path = self._final_paths.get_path(parent_id)
1580
for child_id in self._all_children(parent_id):
1581
path_from_root = self._final_paths.get_path(child_id)
1582
basename = self._transform.final_name(child_id)
1583
kind = self._transform.final_kind(child_id)
1584
if kind is not None:
1585
versioned_kind = kind
1588
versioned_kind = self._transform._tree.stored_kind(
1590
if versioned_kind == 'directory':
1591
subdirs.append(child_id)
1592
children.append((path_from_root, basename, kind, None,
1595
if parent_path.startswith(prefix):
1596
yield parent_path, children
1597
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1600
def iter_changes(self, from_tree, include_unchanged=False,
1601
specific_files=None, pb=None, extra_trees=None,
1602
require_versioned=True, want_unversioned=False):
1603
"""See InterTree.iter_changes.
1605
This has a fast path that is only used when the from_tree matches
1606
the transform tree, and no fancy options are supplied.
1608
return InterTree.get(from_tree, self).iter_changes(
1609
include_unchanged=include_unchanged,
1610
specific_files=specific_files,
1612
extra_trees=extra_trees,
1613
require_versioned=require_versioned,
1614
want_unversioned=want_unversioned)
1616
def get_file(self, path):
1617
"""See Tree.get_file"""
1618
trans_id = self._path2trans_id(path)
1619
if trans_id is None:
1620
raise errors.NoSuchFile(path)
1621
if trans_id in self._transform._new_contents:
1622
name = self._transform._limbo_name(trans_id)
1623
return open(name, 'rb')
1624
if trans_id in self._transform._removed_contents:
1625
raise errors.NoSuchFile(path)
1626
orig_path = self._transform.tree_path(trans_id)
1627
return self._transform._tree.get_file(orig_path)
1629
def get_symlink_target(self, path):
1630
"""See Tree.get_symlink_target"""
1631
trans_id = self._path2trans_id(path)
1632
if trans_id is None:
1633
raise errors.NoSuchFile(path)
1634
if trans_id not in self._transform._new_contents:
1635
orig_path = self._transform.tree_path(trans_id)
1636
return self._transform._tree.get_symlink_target(orig_path)
1637
name = self._transform._limbo_name(trans_id)
1638
return osutils.readlink(name)
1640
def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1641
trans_id = self._path2trans_id(path)
1642
if trans_id is None:
1644
orig_path = self._transform.tree_path(trans_id)
1645
if orig_path is not None:
1646
old_annotation = self._transform._tree.annotate_iter(
1647
orig_path, default_revision=default_revision)
1651
lines = self.get_file_lines(path)
1652
except errors.NoSuchFile:
1654
return annotate.reannotate([old_annotation], lines, default_revision)
1656
def get_file_text(self, path):
1657
"""Return the byte content of a file.
1659
:param path: The path of the file.
1661
:returns: A single byte string for the whole file.
1663
with self.get_file(path) as my_file:
1664
return my_file.read()
1666
def get_file_lines(self, path):
1667
"""Return the content of a file, as lines.
1669
:param path: The path of the file.
1671
return osutils.split_lines(self.get_file_text(path))
1674
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1675
in self._transform._tree.extras())
1676
possible_extras.update(self._transform._new_contents)
1677
possible_extras.update(self._transform._removed_id)
1678
for trans_id in possible_extras:
1679
if not self._transform.final_is_versioned(trans_id):
1680
yield self._final_paths._determine_path(trans_id)
1682
def path_content_summary(self, path):
1683
trans_id = self._path2trans_id(path)
1684
tt = self._transform
1685
tree_path = tt.tree_path(trans_id)
1686
kind = tt._new_contents.get(trans_id)
1688
if tree_path is None or trans_id in tt._removed_contents:
1689
return 'missing', None, None, None
1690
summary = tt._tree.path_content_summary(tree_path)
1691
kind, size, executable, link_or_sha1 = summary
1694
limbo_name = tt._limbo_name(trans_id)
1695
if trans_id in tt._new_reference_revision:
1696
kind = 'tree-reference'
1698
statval = os.lstat(limbo_name)
1699
size = statval.st_size
1700
if not tt._limbo_supports_executable():
1703
executable = statval.st_mode & S_IEXEC
1707
if kind == 'symlink':
1708
link_or_sha1 = os.readlink(limbo_name)
1709
if not isinstance(link_or_sha1, str):
1710
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1711
executable = tt._new_executability.get(trans_id, executable)
1712
return kind, size, executable, link_or_sha1
1714
def get_file_mtime(self, path):
1715
"""See Tree.get_file_mtime"""
1716
trans_id = self._path2trans_id(path)
1717
if trans_id is None:
1718
raise errors.NoSuchFile(path)
1719
if trans_id not in self._transform._new_contents:
1720
return self._transform._tree.get_file_mtime(
1721
self._transform.tree_path(trans_id))
1722
name = self._transform._limbo_name(trans_id)
1723
statval = os.lstat(name)
1724
return statval.st_mtime
1726
def is_versioned(self, path):
1727
trans_id = self._path2trans_id(path)
1728
if trans_id is None:
1729
# It doesn't exist, so it's not versioned.
1731
if trans_id in self._transform._versioned:
1733
if trans_id in self._transform._removed_id:
1735
orig_path = self._transform.tree_path(trans_id)
1736
return self._transform._tree.is_versioned(orig_path)
1738
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1740
raise NotImplementedError(
1741
'follow tree references not yet supported')
1743
# This may not be a maximally efficient implementation, but it is
1744
# reasonably straightforward. An implementation that grafts the
1745
# TreeTransform changes onto the tree's iter_entries_by_dir results
1746
# might be more efficient, but requires tricky inferences about stack
1748
for trans_id, path in self._list_files_by_dir():
1749
entry, is_versioned = self._transform.final_entry(trans_id)
1752
if not is_versioned and entry.kind != 'directory':
1754
if specific_files is not None and path not in specific_files:
1756
if entry is not None:
1759
def _list_files_by_dir(self):
1760
todo = [ROOT_PARENT]
1761
while len(todo) > 0:
1763
children = list(self._all_children(parent))
1764
paths = dict(zip(children, self._final_paths.get_paths(children)))
1765
children.sort(key=paths.get)
1766
todo.extend(reversed(children))
1767
for trans_id in children:
1768
yield trans_id, paths[trans_id][0]
1770
def revision_tree(self, revision_id):
1771
return self._transform._tree.revision_tree(revision_id)
1773
def _stat_limbo_file(self, trans_id):
1774
name = self._transform._limbo_name(trans_id)
1775
return os.lstat(name)
1777
def git_snapshot(self, want_unversioned=False):
1780
for trans_id, path in self._list_files_by_dir():
1781
if not self._transform.final_is_versioned(trans_id):
1782
if not want_unversioned:
1785
o, mode = self._transform.final_git_entry(trans_id)
1787
self.store.add_object(o)
1788
os.append((encode_git_path(path), o.id, mode))
1791
return commit_tree(self.store, os), extra
1793
def iter_child_entries(self, path):
1794
trans_id = self._path2trans_id(path)
1795
if trans_id is None:
1796
raise errors.NoSuchFile(path)
1797
for child_trans_id in self._all_children(trans_id):
1798
entry, is_versioned = self._transform.final_entry(trans_id)
1799
if not is_versioned:
1801
if entry is not None: