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 ..sixish import text_type, viewitems, viewvalues
44
from ..transform import (
52
TransformRenameFailed,
59
from dulwich.index import commit_tree, blob_from_path_and_stat
60
from dulwich.objects import Blob
63
class TreeTransformBase(TreeTransform):
64
"""The base class for TreeTransform and its kin."""
66
def __init__(self, tree, pb=None, case_sensitive=True):
69
:param tree: The tree that will be transformed, but not necessarily
72
:param case_sensitive: If True, the target of the transform is
73
case sensitive, not just case preserving.
75
super(TreeTransformBase, self).__init__(tree, pb=pb)
76
# mapping of trans_id => (sha1 of content, stat_value)
77
self._observed_sha1s = {}
78
# Set of versioned trans ids
79
self._versioned = set()
80
# The trans_id that will be used as the tree root
81
self.root = self.trans_id_tree_path('')
82
# Whether the target is case sensitive
83
self._case_sensitive_target = case_sensitive
84
self._symlink_target = {}
88
return self._tree.mapping
91
"""Release the working tree lock, if held.
93
This is required if apply has not been invoked, but can be invoked
96
if self._tree is None:
98
for hook in MutableTree.hooks['post_transform']:
99
hook(self._tree, self)
103
def create_path(self, name, parent):
104
"""Assign a transaction id to a new path"""
105
trans_id = self.assign_id()
106
unique_add(self._new_name, trans_id, name)
107
unique_add(self._new_parent, trans_id, parent)
110
def adjust_root_path(self, name, parent):
111
"""Emulate moving the root by moving all children, instead.
114
def fixup_new_roots(self):
115
"""Reinterpret requests to change the root directory
117
Instead of creating a root directory, or moving an existing directory,
118
all the attributes and children of the new root are applied to the
119
existing root directory.
121
This means that the old root trans-id becomes obsolete, so it is
122
recommended only to invoke this after the root trans-id has become
126
new_roots = [k for k, v in viewitems(self._new_parent)
128
if len(new_roots) < 1:
130
if len(new_roots) != 1:
131
raise ValueError('A tree cannot have two roots!')
132
old_new_root = new_roots[0]
133
# unversion the new root's directory.
134
if old_new_root in self._versioned:
135
self.cancel_versioning(old_new_root)
137
self.unversion_file(old_new_root)
139
# Now move children of new root into old root directory.
140
# Ensure all children are registered with the transaction, but don't
141
# use directly-- some tree children have new parents
142
list(self.iter_tree_children(old_new_root))
143
# Move all children of new root into old root directory.
144
for child in self.by_parent().get(old_new_root, []):
145
self.adjust_path(self.final_name(child), self.root, child)
147
# Ensure old_new_root has no directory.
148
if old_new_root in self._new_contents:
149
self.cancel_creation(old_new_root)
151
self.delete_contents(old_new_root)
153
# prevent deletion of root directory.
154
if self.root in self._removed_contents:
155
self.cancel_deletion(self.root)
157
# destroy path info for old_new_root.
158
del self._new_parent[old_new_root]
159
del self._new_name[old_new_root]
161
def trans_id_file_id(self, file_id):
162
"""Determine or set the transaction id associated with a file ID.
163
A new id is only created for file_ids that were never present. If
164
a transaction has been unversioned, it is deliberately still returned.
165
(this will likely lead to an unversioned parent conflict.)
168
raise ValueError('None is not a valid file id')
169
path = self.mapping.parse_file_id(file_id)
170
return self.trans_id_tree_path(path)
172
def version_file(self, trans_id, file_id=None):
173
"""Schedule a file to become versioned."""
174
if trans_id in self._versioned:
175
raise errors.DuplicateKey(key=trans_id)
176
self._versioned.add(trans_id)
178
def cancel_versioning(self, trans_id):
179
"""Undo a previous versioning of a file"""
180
raise NotImplementedError(self.cancel_versioning)
182
def new_paths(self, filesystem_only=False):
183
"""Determine the paths of all new and changed files.
185
:param filesystem_only: if True, only calculate values for files
186
that require renames or execute bit changes.
190
stale_ids = self._needs_rename.difference(self._new_name)
191
stale_ids.difference_update(self._new_parent)
192
stale_ids.difference_update(self._new_contents)
193
stale_ids.difference_update(self._versioned)
194
needs_rename = self._needs_rename.difference(stale_ids)
195
id_sets = (needs_rename, self._new_executability)
197
id_sets = (self._new_name, self._new_parent, self._new_contents,
198
self._versioned, self._new_executability)
199
for id_set in id_sets:
200
new_ids.update(id_set)
201
return sorted(FinalPaths(self).get_paths(new_ids))
203
def final_is_versioned(self, trans_id):
204
if trans_id in self._versioned:
206
if trans_id in self._removed_id:
208
orig_path = self.tree_path(trans_id)
209
if orig_path is None:
211
return self._tree.is_versioned(orig_path)
213
def find_raw_conflicts(self):
214
"""Find any violations of inventory or filesystem invariants"""
215
if self._done is True:
216
raise ReusingTransform()
218
# ensure all children of all existent parents are known
219
# all children of non-existent parents are known, by definition.
220
self._add_tree_children()
221
by_parent = self.by_parent()
222
conflicts.extend(self._parent_loops())
223
conflicts.extend(self._duplicate_entries(by_parent))
224
conflicts.extend(self._parent_type_conflicts(by_parent))
225
conflicts.extend(self._improper_versioning())
226
conflicts.extend(self._executability_conflicts())
227
conflicts.extend(self._overwrite_conflicts())
230
def _check_malformed(self):
231
conflicts = self.find_raw_conflicts()
232
if len(conflicts) != 0:
233
raise MalformedTransform(conflicts=conflicts)
235
def _add_tree_children(self):
236
"""Add all the children of all active parents to the known paths.
238
Active parents are those which gain children, and those which are
239
removed. This is a necessary first step in detecting conflicts.
241
parents = list(self.by_parent())
242
parents.extend([t for t in self._removed_contents if
243
self.tree_kind(t) == 'directory'])
244
for trans_id in self._removed_id:
245
path = self.tree_path(trans_id)
248
if self._tree.stored_kind(path) == 'directory':
249
parents.append(trans_id)
250
except errors.NoSuchFile:
252
elif self.tree_kind(trans_id) == 'directory':
253
parents.append(trans_id)
255
for parent_id in parents:
256
# ensure that all children are registered with the transaction
257
list(self.iter_tree_children(parent_id))
259
def _has_named_child(self, name, parent_id, known_children):
260
"""Does a parent already have a name child.
262
:param name: The searched for name.
264
:param parent_id: The parent for which the check is made.
266
:param known_children: The already known children. This should have
267
been recently obtained from `self.by_parent.get(parent_id)`
268
(or will be if None is passed).
270
if known_children is None:
271
known_children = self.by_parent().get(parent_id, [])
272
for child in known_children:
273
if self.final_name(child) == name:
275
parent_path = self._tree_id_paths.get(parent_id, None)
276
if parent_path is None:
277
# No parent... no children
279
child_path = joinpath(parent_path, name)
280
child_id = self._tree_path_ids.get(child_path, None)
282
# Not known by the tree transform yet, check the filesystem
283
return osutils.lexists(self._tree.abspath(child_path))
285
raise AssertionError('child_id is missing: %s, %s, %s'
286
% (name, parent_id, child_id))
288
def _available_backup_name(self, name, target_id):
289
"""Find an available backup name.
291
:param name: The basename of the file.
293
:param target_id: The directory trans_id where the backup should
296
known_children = self.by_parent().get(target_id, [])
297
return osutils.available_backup_name(
299
lambda base: self._has_named_child(
300
base, target_id, known_children))
302
def _parent_loops(self):
303
"""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
yield ('parent loop', trans_id)
315
if parent_id in seen:
318
def _improper_versioning(self):
319
"""Cannot version a file with no contents, or a bad type.
321
However, existing entries with no contents are okay.
323
for trans_id in self._versioned:
324
kind = self.final_kind(trans_id)
325
if kind == 'symlink' and not self._tree.supports_symlinks():
326
# Ignore symlinks as they are not supported on this platform
329
yield ('versioning no contents', trans_id)
331
if not self._tree.versionable_kind(kind):
332
yield ('versioning bad kind', trans_id, kind)
334
def _executability_conflicts(self):
335
"""Check for bad executability changes.
337
Only versioned files may have their executability set, because
338
1. only versioned entries can have executability under windows
339
2. only files can be executable. (The execute bit on a directory
340
does not indicate searchability)
342
for trans_id in self._new_executability:
343
if not self.final_is_versioned(trans_id):
344
yield ('unversioned executability', trans_id)
346
if self.final_kind(trans_id) != "file":
347
yield ('non-file executability', trans_id)
349
def _overwrite_conflicts(self):
350
"""Check for overwrites (not permitted on Win32)"""
351
for trans_id in self._new_contents:
352
if self.tree_kind(trans_id) is None:
354
if trans_id not in self._removed_contents:
355
yield ('overwrite', trans_id, self.final_name(trans_id))
357
def _duplicate_entries(self, by_parent):
358
"""No directory may have two entries with the same name."""
359
if (self._new_name, self._new_parent) == ({}, {}):
361
for children in viewvalues(by_parent):
363
for child_tid in children:
364
name = self.final_name(child_tid)
366
# Keep children only if they still exist in the end
367
if not self._case_sensitive_target:
369
name_ids.append((name, child_tid))
373
for name, trans_id in name_ids:
374
kind = self.final_kind(trans_id)
375
if kind is None and not self.final_is_versioned(trans_id):
377
if name == last_name:
378
yield ('duplicate', last_trans_id, trans_id, name)
380
last_trans_id = trans_id
382
def _parent_type_conflicts(self, by_parent):
383
"""Children must have a directory parent"""
384
for parent_id, children in viewitems(by_parent):
385
if parent_id == ROOT_PARENT:
388
for child_id in children:
389
if self.final_kind(child_id) is not None:
394
# There is at least a child, so we need an existing directory to
396
kind = self.final_kind(parent_id)
398
# The directory will be deleted
399
yield ('missing parent', parent_id)
400
elif kind != "directory":
401
# Meh, we need a *directory* to put something in it
402
yield ('non-directory parent', parent_id)
404
def _set_executability(self, path, trans_id):
405
"""Set the executability of versioned files """
406
if self._tree._supports_executable():
407
new_executability = self._new_executability[trans_id]
408
abspath = self._tree.abspath(path)
409
current_mode = os.stat(abspath).st_mode
410
if new_executability:
413
to_mode = current_mode | (0o100 & ~umask)
414
# Enable x-bit for others only if they can read it.
415
if current_mode & 0o004:
416
to_mode |= 0o001 & ~umask
417
if current_mode & 0o040:
418
to_mode |= 0o010 & ~umask
420
to_mode = current_mode & ~0o111
421
osutils.chmod_if_possible(abspath, to_mode)
423
def _new_entry(self, name, parent_id, file_id):
424
"""Helper function to create a new filesystem entry."""
425
trans_id = self.create_path(name, parent_id)
426
if file_id is not None:
427
self.version_file(trans_id, file_id=file_id)
430
def new_file(self, name, parent_id, contents, file_id=None,
431
executable=None, sha1=None):
432
"""Convenience method to create files.
434
name is the name of the file to create.
435
parent_id is the transaction id of the parent directory of the file.
436
contents is an iterator of bytestrings, which will be used to produce
438
:param file_id: The inventory ID of the file, if it is to be versioned.
439
:param executable: Only valid when a file_id has been supplied.
441
trans_id = self._new_entry(name, parent_id, file_id)
442
# TODO: rather than scheduling a set_executable call,
443
# have create_file create the file with the right mode.
444
self.create_file(contents, trans_id, sha1=sha1)
445
if executable is not None:
446
self.set_executability(executable, trans_id)
449
def new_directory(self, name, parent_id, file_id=None):
450
"""Convenience method to create directories.
452
name is the name of the directory to create.
453
parent_id is the transaction id of the parent directory of the
455
file_id is the inventory ID of the directory, if it is to be versioned.
457
trans_id = self._new_entry(name, parent_id, file_id)
458
self.create_directory(trans_id)
461
def new_symlink(self, name, parent_id, target, file_id=None):
462
"""Convenience method to create symbolic link.
464
name is the name of the symlink to create.
465
parent_id is the transaction id of the parent directory of the symlink.
466
target is a bytestring of the target of the symlink.
467
file_id is the inventory ID of the file, if it is to be versioned.
469
trans_id = self._new_entry(name, parent_id, file_id)
470
self.create_symlink(target, trans_id)
473
def new_orphan(self, trans_id, parent_id):
474
"""Schedule an item to be orphaned.
476
When a directory is about to be removed, its children, if they are not
477
versioned are moved out of the way: they don't have a parent anymore.
479
:param trans_id: The trans_id of the existing item.
480
:param parent_id: The parent trans_id of the item.
482
raise NotImplementedError(self.new_orphan)
484
def _get_potential_orphans(self, dir_id):
485
"""Find the potential orphans in a directory.
487
A directory can't be safely deleted if there are versioned files in it.
488
If all the contained files are unversioned then they can be orphaned.
490
The 'None' return value means that the directory contains at least one
491
versioned file and should not be deleted.
493
:param dir_id: The directory trans id.
495
:return: A list of the orphan trans ids or None if at least one
496
versioned file is present.
499
# Find the potential orphans, stop if one item should be kept
500
for child_tid in self.by_parent()[dir_id]:
501
if child_tid in self._removed_contents:
502
# The child is removed as part of the transform. Since it was
503
# versioned before, it's not an orphan
505
if not self.final_is_versioned(child_tid):
506
# The child is not versioned
507
orphans.append(child_tid)
509
# We have a versioned file here, searching for orphans is
515
def _affected_ids(self):
516
"""Return the set of transform ids affected by the transform"""
517
trans_ids = set(self._removed_id)
518
trans_ids.update(self._versioned)
519
trans_ids.update(self._removed_contents)
520
trans_ids.update(self._new_contents)
521
trans_ids.update(self._new_executability)
522
trans_ids.update(self._new_name)
523
trans_ids.update(self._new_parent)
526
def iter_changes(self, want_unversioned=False):
527
"""Produce output in the same format as Tree.iter_changes.
529
Will produce nonsensical results if invoked while inventory/filesystem
530
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
532
final_paths = FinalPaths(self)
533
trans_ids = self._affected_ids()
535
# Now iterate through all active paths
536
for trans_id in trans_ids:
537
from_path = self.tree_path(trans_id)
539
# find file ids, and determine versioning state
540
if from_path is None:
541
from_versioned = False
543
from_versioned = self._tree.is_versioned(from_path)
544
if not want_unversioned and not from_versioned:
546
to_path = final_paths.get_path(trans_id)
550
to_versioned = self.final_is_versioned(trans_id)
551
if not want_unversioned and not to_versioned:
555
# get data from working tree if versioned
556
from_entry = next(self._tree.iter_entries_by_dir(
557
specific_files=[from_path]))[1]
558
from_name = from_entry.name
561
if from_path is None:
562
# File does not exist in FROM state
565
# File exists, but is not versioned. Have to use path-
567
from_name = os.path.basename(from_path)
568
if from_path is not None:
569
from_kind, from_executable, from_stats = \
570
self._tree._comparison_data(from_entry, from_path)
573
from_executable = False
575
to_name = self.final_name(trans_id)
576
to_kind = self.final_kind(trans_id)
577
if trans_id in self._new_executability:
578
to_executable = self._new_executability[trans_id]
580
to_executable = from_executable
582
if from_versioned and from_kind != to_kind:
584
elif to_kind in ('file', 'symlink') and (
585
trans_id in self._new_contents):
587
if (not modified and from_versioned == to_versioned
588
and from_path == to_path
589
and from_name == to_name
590
and from_executable == to_executable):
592
if (from_path, to_path) == (None, None):
596
(from_path, to_path), modified,
597
(from_versioned, to_versioned),
598
(from_name, to_name),
599
(from_kind, to_kind),
600
(from_executable, to_executable)))
603
return (c.path[0] or '', c.path[1] or '')
604
return iter(sorted(results, key=path_key))
606
def get_preview_tree(self):
607
"""Return a tree representing the result of the transform.
609
The tree is a snapshot, and altering the TreeTransform will invalidate
612
return GitPreviewTree(self)
614
def commit(self, branch, message, merge_parents=None, strict=False,
615
timestamp=None, timezone=None, committer=None, authors=None,
616
revprops=None, revision_id=None):
617
"""Commit the result of this TreeTransform to a branch.
619
:param branch: The branch to commit to.
620
:param message: The message to attach to the commit.
621
:param merge_parents: Additional parent revision-ids specified by
623
:param strict: If True, abort the commit if there are unversioned
625
:param timestamp: if not None, seconds-since-epoch for the time and
626
date. (May be a float.)
627
:param timezone: Optional timezone for timestamp, as an offset in
629
:param committer: Optional committer in email-id format.
630
(e.g. "J Random Hacker <jrandom@example.com>")
631
:param authors: Optional list of authors in email-id format.
632
:param revprops: Optional dictionary of revision properties.
633
:param revision_id: Optional revision id. (Specifying a revision-id
634
may reduce performance for some non-native formats.)
635
:return: The revision_id of the revision committed.
637
self._check_malformed()
639
unversioned = set(self._new_contents).difference(set(self._versioned))
640
for trans_id in unversioned:
641
if not self.final_is_versioned(trans_id):
642
raise errors.StrictCommitFailed()
644
revno, last_rev_id = branch.last_revision_info()
645
if last_rev_id == _mod_revision.NULL_REVISION:
646
if merge_parents is not None:
647
raise ValueError('Cannot supply merge parents for first'
651
parent_ids = [last_rev_id]
652
if merge_parents is not None:
653
parent_ids.extend(merge_parents)
654
if self._tree.get_revision_id() != last_rev_id:
655
raise ValueError('TreeTransform not based on branch basis: %s' %
656
self._tree.get_revision_id().decode('utf-8'))
657
from .. import commit
658
revprops = commit.Commit.update_revprops(revprops, branch, authors)
659
builder = branch.get_commit_builder(parent_ids,
664
revision_id=revision_id)
665
preview = self.get_preview_tree()
666
list(builder.record_iter_changes(preview, last_rev_id,
667
self.iter_changes()))
668
builder.finish_inventory()
669
revision_id = builder.commit(message)
670
branch.set_last_revision_info(revno + 1, revision_id)
673
def _text_parent(self, trans_id):
674
path = self.tree_path(trans_id)
676
if path is None or self._tree.kind(path) != 'file':
678
except errors.NoSuchFile:
682
def _get_parents_texts(self, trans_id):
683
"""Get texts for compression parents of this file."""
684
path = self._text_parent(trans_id)
687
return (self._tree.get_file_text(path),)
689
def _get_parents_lines(self, trans_id):
690
"""Get lines for compression parents of this file."""
691
path = self._text_parent(trans_id)
694
return (self._tree.get_file_lines(path),)
696
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
697
"""Schedule creation of a new file.
701
:param contents: an iterator of strings, all of which will be written
702
to the target destination.
703
:param trans_id: TreeTransform handle
704
:param mode_id: If not None, force the mode of the target file to match
705
the mode of the object referenced by mode_id.
706
Otherwise, we will try to preserve mode bits of an existing file.
707
:param sha1: If the sha1 of this content is already known, pass it in.
708
We can use it to prevent future sha1 computations.
710
raise NotImplementedError(self.create_file)
712
def create_directory(self, trans_id):
713
"""Schedule creation of a new directory.
715
See also new_directory.
717
raise NotImplementedError(self.create_directory)
719
def create_symlink(self, target, trans_id):
720
"""Schedule creation of a new symbolic link.
722
target is a bytestring.
723
See also new_symlink.
725
raise NotImplementedError(self.create_symlink)
727
def create_hardlink(self, path, trans_id):
728
"""Schedule creation of a hard link"""
729
raise NotImplementedError(self.create_hardlink)
731
def cancel_creation(self, trans_id):
732
"""Cancel the creation of new file contents."""
733
raise NotImplementedError(self.cancel_creation)
735
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
736
"""Apply all changes to the inventory and filesystem.
738
If filesystem or inventory conflicts are present, MalformedTransform
741
If apply succeeds, finalize is not necessary.
743
:param no_conflicts: if True, the caller guarantees there are no
744
conflicts, so no check is made.
745
:param precomputed_delta: An inventory delta to use instead of
747
:param _mover: Supply an alternate FileMover, for testing
749
raise NotImplementedError(self.apply)
751
def cook_conflicts(self, raw_conflicts):
752
"""Generate a list of cooked conflicts, sorted by file path"""
753
if not raw_conflicts:
755
fp = FinalPaths(self)
756
from .workingtree import TextConflict
757
for c in raw_conflicts:
758
if c[0] == 'text conflict':
759
yield TextConflict(fp.get_path(c[1]))
760
elif c[0] == 'duplicate':
761
yield TextConflict(fp.get_path(c[2]))
762
elif c[0] == 'contents conflict':
763
yield TextConflict(fp.get_path(c[1][0]))
764
elif c[0] == 'missing parent':
765
# TODO(jelmer): This should not make it to here
766
yield TextConflict(fp.get_path(c[2]))
767
elif c[0] == 'non-directory parent':
768
yield TextConflict(fp.get_path(c[2]))
769
elif c[0] == 'deleting parent':
770
# TODO(jelmer): This should not make it to here
771
yield TextConflict(fp.get_path(c[2]))
772
elif c[0] == 'parent loop':
773
# TODO(jelmer): This should not make it to here
774
yield TextConflict(fp.get_path(c[2]))
775
elif c[0] == 'path conflict':
776
yield TextConflict(fp.get_path(c[1]))
778
raise AssertionError('unknown conflict %s' % c[0])
781
class DiskTreeTransform(TreeTransformBase):
782
"""Tree transform storing its contents on disk."""
784
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
786
:param tree: The tree that will be transformed, but not necessarily
788
:param limbodir: A directory where new files can be stored until
789
they are installed in their proper places
791
:param case_sensitive: If True, the target of the transform is
792
case sensitive, not just case preserving.
794
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
795
self._limbodir = limbodir
796
self._deletiondir = None
797
# A mapping of transform ids to their limbo filename
798
self._limbo_files = {}
799
self._possibly_stale_limbo_files = set()
800
# A mapping of transform ids to a set of the transform ids of children
801
# that their limbo directory has
802
self._limbo_children = {}
803
# Map transform ids to maps of child filename to child transform id
804
self._limbo_children_names = {}
805
# List of transform ids that need to be renamed from limbo into place
806
self._needs_rename = set()
807
self._creation_mtime = None
808
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
811
"""Release the working tree lock, if held, clean up limbo dir.
813
This is required if apply has not been invoked, but can be invoked
816
if self._tree is None:
819
limbo_paths = list(viewvalues(self._limbo_files))
820
limbo_paths.extend(self._possibly_stale_limbo_files)
821
limbo_paths.sort(reverse=True)
822
for path in limbo_paths:
824
osutils.delete_any(path)
826
if e.errno != errno.ENOENT:
828
# XXX: warn? perhaps we just got interrupted at an
829
# inconvenient moment, but perhaps files are disappearing
832
osutils.delete_any(self._limbodir)
834
# We don't especially care *why* the dir is immortal.
835
raise ImmortalLimbo(self._limbodir)
837
if self._deletiondir is not None:
838
osutils.delete_any(self._deletiondir)
840
raise errors.ImmortalPendingDeletion(self._deletiondir)
842
TreeTransformBase.finalize(self)
844
def _limbo_supports_executable(self):
845
"""Check if the limbo path supports the executable bit."""
846
return osutils.supports_executable(self._limbodir)
848
def _limbo_name(self, trans_id):
849
"""Generate the limbo name of a file"""
850
limbo_name = self._limbo_files.get(trans_id)
851
if limbo_name is None:
852
limbo_name = self._generate_limbo_path(trans_id)
853
self._limbo_files[trans_id] = limbo_name
856
def _generate_limbo_path(self, trans_id):
857
"""Generate a limbo path using the trans_id as the relative path.
859
This is suitable as a fallback, and when the transform should not be
860
sensitive to the path encoding of the limbo directory.
862
self._needs_rename.add(trans_id)
863
return osutils.pathjoin(self._limbodir, trans_id)
865
def adjust_path(self, name, parent, trans_id):
866
previous_parent = self._new_parent.get(trans_id)
867
previous_name = self._new_name.get(trans_id)
868
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
869
if (trans_id in self._limbo_files
870
and trans_id not in self._needs_rename):
871
self._rename_in_limbo([trans_id])
872
if previous_parent != parent:
873
self._limbo_children[previous_parent].remove(trans_id)
874
if previous_parent != parent or previous_name != name:
875
del self._limbo_children_names[previous_parent][previous_name]
877
def _rename_in_limbo(self, trans_ids):
878
"""Fix limbo names so that the right final path is produced.
880
This means we outsmarted ourselves-- we tried to avoid renaming
881
these files later by creating them with their final names in their
882
final parents. But now the previous name or parent is no longer
883
suitable, so we have to rename them.
885
Even for trans_ids that have no new contents, we must remove their
886
entries from _limbo_files, because they are now stale.
888
for trans_id in trans_ids:
889
old_path = self._limbo_files[trans_id]
890
self._possibly_stale_limbo_files.add(old_path)
891
del self._limbo_files[trans_id]
892
if trans_id not in self._new_contents:
894
new_path = self._limbo_name(trans_id)
895
os.rename(old_path, new_path)
896
self._possibly_stale_limbo_files.remove(old_path)
897
for descendant in self._limbo_descendants(trans_id):
898
desc_path = self._limbo_files[descendant]
899
desc_path = new_path + desc_path[len(old_path):]
900
self._limbo_files[descendant] = desc_path
902
def _limbo_descendants(self, trans_id):
903
"""Return the set of trans_ids whose limbo paths descend from this."""
904
descendants = set(self._limbo_children.get(trans_id, []))
905
for descendant in list(descendants):
906
descendants.update(self._limbo_descendants(descendant))
909
def _set_mode(self, trans_id, mode_id, typefunc):
910
raise NotImplementedError(self._set_mode)
912
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
913
"""Schedule creation of a new file.
917
:param contents: an iterator of strings, all of which will be written
918
to the target destination.
919
:param trans_id: TreeTransform handle
920
:param mode_id: If not None, force the mode of the target file to match
921
the mode of the object referenced by mode_id.
922
Otherwise, we will try to preserve mode bits of an existing file.
923
:param sha1: If the sha1 of this content is already known, pass it in.
924
We can use it to prevent future sha1 computations.
926
name = self._limbo_name(trans_id)
927
with open(name, 'wb') as f:
928
unique_add(self._new_contents, trans_id, 'file')
929
f.writelines(contents)
930
self._set_mtime(name)
931
self._set_mode(trans_id, mode_id, S_ISREG)
932
# It is unfortunate we have to use lstat instead of fstat, but we just
933
# used utime and chmod on the file, so we need the accurate final
936
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
938
def _read_symlink_target(self, trans_id):
939
return os.readlink(self._limbo_name(trans_id))
941
def _set_mtime(self, path):
942
"""All files that are created get the same mtime.
944
This time is set by the first object to be created.
946
if self._creation_mtime is None:
947
self._creation_mtime = time.time()
948
os.utime(path, (self._creation_mtime, self._creation_mtime))
950
def create_hardlink(self, path, trans_id):
951
"""Schedule creation of a hard link"""
952
name = self._limbo_name(trans_id)
956
if e.errno != errno.EPERM:
958
raise errors.HardLinkNotSupported(path)
960
unique_add(self._new_contents, trans_id, 'file')
961
except BaseException:
962
# Clean up the file, it never got registered so
963
# TreeTransform.finalize() won't clean it up.
967
def create_directory(self, trans_id):
968
"""Schedule creation of a new directory.
970
See also new_directory.
972
os.mkdir(self._limbo_name(trans_id))
973
unique_add(self._new_contents, trans_id, 'directory')
975
def create_symlink(self, target, trans_id):
976
"""Schedule creation of a new symbolic link.
978
target is a bytestring.
979
See also new_symlink.
981
if self._create_symlinks:
982
os.symlink(target, self._limbo_name(trans_id))
985
path = FinalPaths(self).get_path(trans_id)
989
'Unable to create symlink "%s" on this filesystem.' % (path,))
990
self._symlink_target[trans_id] = target
991
# We add symlink to _new_contents even if they are unsupported
992
# and not created. These entries are subsequently used to avoid
993
# conflicts on platforms that don't support symlink
994
unique_add(self._new_contents, trans_id, 'symlink')
996
def cancel_creation(self, trans_id):
997
"""Cancel the creation of new file contents."""
998
del self._new_contents[trans_id]
999
if trans_id in self._observed_sha1s:
1000
del self._observed_sha1s[trans_id]
1001
children = self._limbo_children.get(trans_id)
1002
# if this is a limbo directory with children, move them before removing
1004
if children is not None:
1005
self._rename_in_limbo(children)
1006
del self._limbo_children[trans_id]
1007
del self._limbo_children_names[trans_id]
1008
osutils.delete_any(self._limbo_name(trans_id))
1010
def new_orphan(self, trans_id, parent_id):
1011
conf = self._tree.get_config_stack()
1012
handle_orphan = conf.get('transform.orphan_policy')
1013
handle_orphan(self, trans_id, parent_id)
1015
def final_entry(self, trans_id):
1016
is_versioned = self.final_is_versioned(trans_id)
1017
fp = FinalPaths(self)
1018
tree_path = fp.get_path(trans_id)
1019
if trans_id in self._new_contents:
1020
path = self._limbo_name(trans_id)
1022
kind = mode_kind(st.st_mode)
1023
name = self.final_name(trans_id)
1024
file_id = self._tree.mapping.generate_file_id(tree_path)
1025
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1026
if kind == 'directory':
1027
return GitTreeDirectory(
1028
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1029
executable = mode_is_executable(st.st_mode)
1030
mode = object_mode(kind, executable)
1031
blob = blob_from_path_and_stat(encode_git_path(path), st)
1032
if kind == 'symlink':
1033
return GitTreeSymlink(
1034
file_id, name, parent_id,
1035
decode_git_path(blob.data)), is_versioned
1036
elif kind == 'file':
1038
file_id, name, executable=executable, parent_id=parent_id,
1039
git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1041
raise AssertionError(kind)
1042
elif trans_id in self._removed_contents:
1045
orig_path = self.tree_path(trans_id)
1046
if orig_path is None:
1048
file_id = self._tree.mapping.generate_file_id(tree_path)
1052
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1054
ie = next(self._tree.iter_entries_by_dir(
1055
specific_files=[orig_path]))[1]
1056
ie.file_id = file_id
1057
ie.parent_id = parent_id
1058
return ie, is_versioned
1059
except StopIteration:
1061
if self.tree_kind(trans_id) == 'directory':
1062
return GitTreeDirectory(
1063
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1064
except OSError as e:
1065
if e.errno != errno.ENOTDIR:
1069
def final_git_entry(self, trans_id):
1070
if trans_id in self._new_contents:
1071
path = self._limbo_name(trans_id)
1073
kind = mode_kind(st.st_mode)
1074
if kind == 'directory':
1076
executable = mode_is_executable(st.st_mode)
1077
mode = object_mode(kind, executable)
1078
blob = blob_from_path_and_stat(encode_git_path(path), st)
1079
elif trans_id in self._removed_contents:
1082
orig_path = self.tree_path(trans_id)
1083
kind = self._tree.kind(orig_path)
1084
executable = self._tree.is_executable(orig_path)
1085
mode = object_mode(kind, executable)
1086
if kind == 'symlink':
1087
contents = self._tree.get_symlink_target(orig_path)
1088
elif kind == 'file':
1089
contents = self._tree.get_file_text(orig_path)
1090
elif kind == 'directory':
1093
raise AssertionError(kind)
1094
blob = Blob.from_string(contents)
1098
class GitTreeTransform(DiskTreeTransform):
1099
"""Represent a tree transformation.
1101
This object is designed to support incremental generation of the transform,
1104
However, it gives optimum performance when parent directories are created
1105
before their contents. The transform is then able to put child files
1106
directly in their parent directory, avoiding later renames.
1108
It is easy to produce malformed transforms, but they are generally
1109
harmless. Attempting to apply a malformed transform will cause an
1110
exception to be raised before any modifications are made to the tree.
1112
Many kinds of malformed transforms can be corrected with the
1113
resolve_conflicts function. The remaining ones indicate programming error,
1114
such as trying to create a file with no path.
1116
Two sets of file creation methods are supplied. Convenience methods are:
1121
These are composed of the low-level methods:
1123
* create_file or create_directory or create_symlink
1127
Transform/Transaction ids
1128
-------------------------
1129
trans_ids are temporary ids assigned to all files involved in a transform.
1130
It's possible, even common, that not all files in the Tree have trans_ids.
1132
trans_ids are used because filenames and file_ids are not good enough
1133
identifiers; filenames change.
1135
trans_ids are only valid for the TreeTransform that generated them.
1139
Limbo is a temporary directory use to hold new versions of files.
1140
Files are added to limbo by create_file, create_directory, create_symlink,
1141
and their convenience variants (new_*). Files may be removed from limbo
1142
using cancel_creation. Files are renamed from limbo into their final
1143
location as part of TreeTransform.apply
1145
Limbo must be cleaned up, by either calling TreeTransform.apply or
1146
calling TreeTransform.finalize.
1148
Files are placed into limbo inside their parent directories, where
1149
possible. This reduces subsequent renames, and makes operations involving
1150
lots of files faster. This optimization is only possible if the parent
1151
directory is created *before* creating any of its children, so avoid
1152
creating children before parents, where possible.
1156
This temporary directory is used by _FileMover for storing files that are
1157
about to be deleted. In case of rollback, the files will be restored.
1158
FileMover does not delete files until it is sure that a rollback will not
1162
def __init__(self, tree, pb=None):
1163
"""Note: a tree_write lock is taken on the tree.
1165
Use TreeTransform.finalize() to release the lock (can be omitted if
1166
TreeTransform.apply() called).
1168
tree.lock_tree_write()
1170
limbodir = urlutils.local_path_from_url(
1171
tree._transport.abspath('limbo'))
1172
osutils.ensure_empty_directory_exists(
1174
errors.ExistingLimbo)
1175
deletiondir = urlutils.local_path_from_url(
1176
tree._transport.abspath('pending-deletion'))
1177
osutils.ensure_empty_directory_exists(
1179
errors.ExistingPendingDeletion)
1180
except BaseException:
1184
# Cache of realpath results, to speed up canonical_path
1185
self._realpaths = {}
1186
# Cache of relpath results, to speed up canonical_path
1188
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1189
tree.case_sensitive)
1190
self._deletiondir = deletiondir
1192
def canonical_path(self, path):
1193
"""Get the canonical tree-relative path"""
1194
# don't follow final symlinks
1195
abs = self._tree.abspath(path)
1196
if abs in self._relpaths:
1197
return self._relpaths[abs]
1198
dirname, basename = os.path.split(abs)
1199
if dirname not in self._realpaths:
1200
self._realpaths[dirname] = os.path.realpath(dirname)
1201
dirname = self._realpaths[dirname]
1202
abs = osutils.pathjoin(dirname, basename)
1203
if dirname in self._relpaths:
1204
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1205
relpath = relpath.rstrip('/\\')
1207
relpath = self._tree.relpath(abs)
1208
self._relpaths[abs] = relpath
1211
def tree_kind(self, trans_id):
1212
"""Determine the file kind in the working tree.
1214
:returns: The file kind or None if the file does not exist
1216
path = self._tree_id_paths.get(trans_id)
1220
return osutils.file_kind(self._tree.abspath(path))
1221
except errors.NoSuchFile:
1224
def _set_mode(self, trans_id, mode_id, typefunc):
1225
"""Set the mode of new file contents.
1226
The mode_id is the existing file to get the mode from (often the same
1227
as trans_id). The operation is only performed if there's a mode match
1228
according to typefunc.
1233
old_path = self._tree_id_paths[mode_id]
1237
mode = os.stat(self._tree.abspath(old_path)).st_mode
1238
except OSError as e:
1239
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1240
# Either old_path doesn't exist, or the parent of the
1241
# target is not a directory (but will be one eventually)
1242
# Either way, we know it doesn't exist *right now*
1243
# See also bug #248448
1248
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1250
def iter_tree_children(self, parent_id):
1251
"""Iterate through the entry's tree children, if any"""
1253
path = self._tree_id_paths[parent_id]
1257
children = os.listdir(self._tree.abspath(path))
1258
except OSError as e:
1259
if not (osutils._is_error_enotdir(e) or
1260
e.errno in (errno.ENOENT, errno.ESRCH)):
1264
for child in children:
1265
childpath = joinpath(path, child)
1266
if self._tree.is_control_filename(childpath):
1268
yield self.trans_id_tree_path(childpath)
1270
def _generate_limbo_path(self, trans_id):
1271
"""Generate a limbo path using the final path if possible.
1273
This optimizes the performance of applying the tree transform by
1274
avoiding renames. These renames can be avoided only when the parent
1275
directory is already scheduled for creation.
1277
If the final path cannot be used, falls back to using the trans_id as
1280
parent = self._new_parent.get(trans_id)
1281
# if the parent directory is already in limbo (e.g. when building a
1282
# tree), choose a limbo name inside the parent, to reduce further
1284
use_direct_path = False
1285
if self._new_contents.get(parent) == 'directory':
1286
filename = self._new_name.get(trans_id)
1287
if filename is not None:
1288
if parent not in self._limbo_children:
1289
self._limbo_children[parent] = set()
1290
self._limbo_children_names[parent] = {}
1291
use_direct_path = True
1292
# the direct path can only be used if no other file has
1293
# already taken this pathname, i.e. if the name is unused, or
1294
# if it is already associated with this trans_id.
1295
elif self._case_sensitive_target:
1296
if (self._limbo_children_names[parent].get(filename)
1297
in (trans_id, None)):
1298
use_direct_path = True
1300
for l_filename, l_trans_id in viewitems(
1301
self._limbo_children_names[parent]):
1302
if l_trans_id == trans_id:
1304
if l_filename.lower() == filename.lower():
1307
use_direct_path = True
1309
if not use_direct_path:
1310
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1312
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1313
self._limbo_children[parent].add(trans_id)
1314
self._limbo_children_names[parent][filename] = trans_id
1317
def cancel_versioning(self, trans_id):
1318
"""Undo a previous versioning of a file"""
1319
self._versioned.remove(trans_id)
1321
def apply(self, no_conflicts=False, _mover=None):
1322
"""Apply all changes to the inventory and filesystem.
1324
If filesystem or inventory conflicts are present, MalformedTransform
1327
If apply succeeds, finalize is not necessary.
1329
:param no_conflicts: if True, the caller guarantees there are no
1330
conflicts, so no check is made.
1331
:param _mover: Supply an alternate FileMover, for testing
1333
for hook in MutableTree.hooks['pre_transform']:
1334
hook(self._tree, self)
1335
if not no_conflicts:
1336
self._check_malformed()
1337
self.rename_count = 0
1338
with ui.ui_factory.nested_progress_bar() as child_pb:
1339
child_pb.update(gettext('Apply phase'), 0, 2)
1340
index_changes = self._generate_index_changes()
1343
mover = _FileMover()
1347
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1348
self._apply_removals(mover)
1349
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1350
modified_paths = self._apply_insertions(mover)
1351
except BaseException:
1355
mover.apply_deletions()
1356
self._tree._apply_index_changes(index_changes)
1359
return _TransformResults(modified_paths, self.rename_count)
1361
def _apply_removals(self, mover):
1362
"""Perform tree operations that remove directory/inventory names.
1364
That is, delete files that are to be deleted, and put any files that
1365
need renaming into limbo. This must be done in strict child-to-parent
1368
If inventory_delta is None, no inventory delta generation is performed.
1370
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1371
with ui.ui_factory.nested_progress_bar() as child_pb:
1372
for num, (path, trans_id) in enumerate(tree_paths):
1373
# do not attempt to move root into a subdirectory of itself.
1376
child_pb.update(gettext('removing file'), num, len(tree_paths))
1377
full_path = self._tree.abspath(path)
1378
if trans_id in self._removed_contents:
1379
delete_path = os.path.join(self._deletiondir, trans_id)
1380
mover.pre_delete(full_path, delete_path)
1381
elif (trans_id in self._new_name or
1382
trans_id in self._new_parent):
1384
mover.rename(full_path, self._limbo_name(trans_id))
1385
except TransformRenameFailed as e:
1386
if e.errno != errno.ENOENT:
1389
self.rename_count += 1
1391
def _apply_insertions(self, mover):
1392
"""Perform tree operations that insert directory/inventory names.
1394
That is, create any files that need to be created, and restore from
1395
limbo any files that needed renaming. This must be done in strict
1396
parent-to-child order.
1398
If inventory_delta is None, no inventory delta is calculated, and
1399
no list of modified paths is returned.
1401
new_paths = self.new_paths(filesystem_only=True)
1403
with ui.ui_factory.nested_progress_bar() as child_pb:
1404
for num, (path, trans_id) in enumerate(new_paths):
1406
child_pb.update(gettext('adding file'),
1407
num, len(new_paths))
1408
full_path = self._tree.abspath(path)
1409
if trans_id in self._needs_rename:
1411
mover.rename(self._limbo_name(trans_id), full_path)
1412
except TransformRenameFailed as e:
1413
# We may be renaming a dangling inventory id
1414
if e.errno != errno.ENOENT:
1417
self.rename_count += 1
1418
# TODO: if trans_id in self._observed_sha1s, we should
1419
# re-stat the final target, since ctime will be
1420
# updated by the change.
1421
if (trans_id in self._new_contents
1422
or self.path_changed(trans_id)):
1423
if trans_id in self._new_contents:
1424
modified_paths.append(full_path)
1425
if trans_id in self._new_executability:
1426
self._set_executability(path, trans_id)
1427
if trans_id in self._observed_sha1s:
1428
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1429
st = osutils.lstat(full_path)
1430
self._observed_sha1s[trans_id] = (o_sha1, st)
1431
for path, trans_id in new_paths:
1432
# new_paths includes stuff like workingtree conflicts. Only the
1433
# stuff in new_contents actually comes from limbo.
1434
if trans_id in self._limbo_files:
1435
del self._limbo_files[trans_id]
1436
self._new_contents.clear()
1437
return modified_paths
1439
def _generate_index_changes(self):
1440
"""Generate an inventory delta for the current transform."""
1441
removed_id = set(self._removed_id)
1442
removed_id.update(self._removed_contents)
1445
for id_set in [self._new_name, self._new_parent,
1446
self._new_executability]:
1447
changed_ids.update(id_set)
1448
for id_set in [self._new_name, self._new_parent]:
1449
removed_id.update(id_set)
1451
changed_kind = set(self._new_contents)
1452
# Ignore entries that are already known to have changed.
1453
changed_kind.difference_update(changed_ids)
1454
# to keep only the truly changed ones
1455
changed_kind = (t for t in changed_kind
1456
if self.tree_kind(t) != self.final_kind(t))
1457
changed_ids.update(changed_kind)
1458
for t in changed_kind:
1459
if self.final_kind(t) == 'directory':
1461
changed_ids.remove(t)
1462
new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1463
total_entries = len(new_paths) + len(removed_id)
1464
with ui.ui_factory.nested_progress_bar() as child_pb:
1465
for num, trans_id in enumerate(removed_id):
1467
child_pb.update(gettext('removing file'),
1470
path = self._tree_id_paths[trans_id]
1473
changes[path] = (None, None, None, None)
1474
for num, (path, trans_id) in enumerate(new_paths):
1476
child_pb.update(gettext('adding file'),
1477
num + len(removed_id), total_entries)
1479
kind = self.final_kind(trans_id)
1482
versioned = self.final_is_versioned(trans_id)
1485
executability = self._new_executability.get(trans_id)
1486
reference_revision = self._new_reference_revision.get(trans_id)
1487
symlink_target = self._symlink_target.get(trans_id)
1489
kind, executability, reference_revision, symlink_target)
1490
return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1493
class GitTransformPreview(GitTreeTransform):
1494
"""A TreeTransform for generating preview trees.
1496
Unlike TreeTransform, this version works when the input tree is a
1497
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1498
unversioned files in the input tree.
1501
def __init__(self, tree, pb=None, case_sensitive=True):
1503
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1504
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1506
def canonical_path(self, path):
1509
def tree_kind(self, trans_id):
1510
path = self.tree_path(trans_id)
1513
kind = self._tree.path_content_summary(path)[0]
1514
if kind == 'missing':
1518
def _set_mode(self, trans_id, mode_id, typefunc):
1519
"""Set the mode of new file contents.
1520
The mode_id is the existing file to get the mode from (often the same
1521
as trans_id). The operation is only performed if there's a mode match
1522
according to typefunc.
1524
# is it ok to ignore this? probably
1527
def iter_tree_children(self, parent_id):
1528
"""Iterate through the entry's tree children, if any"""
1530
path = self._tree_id_paths[parent_id]
1534
for child in self._tree.iter_child_entries(path):
1535
childpath = joinpath(path, child.name)
1536
yield self.trans_id_tree_path(childpath)
1537
except errors.NoSuchFile:
1540
def new_orphan(self, trans_id, parent_id):
1541
raise NotImplementedError(self.new_orphan)
1544
class GitPreviewTree(PreviewTree, GitTree):
1545
"""Partial implementation of Tree to support show_diff_trees"""
1547
def __init__(self, transform):
1548
PreviewTree.__init__(self, transform)
1549
self.store = transform._tree.store
1550
self.mapping = transform._tree.mapping
1551
self._final_paths = FinalPaths(transform)
1553
def supports_setting_file_ids(self):
1556
def _supports_executable(self):
1557
return self._transform._limbo_supports_executable()
1559
def walkdirs(self, prefix=''):
1560
pending = [self._transform.root]
1561
while len(pending) > 0:
1562
parent_id = pending.pop()
1565
prefix = prefix.rstrip('/')
1566
parent_path = self._final_paths.get_path(parent_id)
1567
for child_id in self._all_children(parent_id):
1568
path_from_root = self._final_paths.get_path(child_id)
1569
basename = self._transform.final_name(child_id)
1570
kind = self._transform.final_kind(child_id)
1571
if kind is not None:
1572
versioned_kind = kind
1575
versioned_kind = self._transform._tree.stored_kind(
1577
if versioned_kind == 'directory':
1578
subdirs.append(child_id)
1579
children.append((path_from_root, basename, kind, None,
1582
if parent_path.startswith(prefix):
1583
yield parent_path, children
1584
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1587
def iter_changes(self, from_tree, include_unchanged=False,
1588
specific_files=None, pb=None, extra_trees=None,
1589
require_versioned=True, want_unversioned=False):
1590
"""See InterTree.iter_changes.
1592
This has a fast path that is only used when the from_tree matches
1593
the transform tree, and no fancy options are supplied.
1595
return InterTree.get(from_tree, self).iter_changes(
1596
include_unchanged=include_unchanged,
1597
specific_files=specific_files,
1599
extra_trees=extra_trees,
1600
require_versioned=require_versioned,
1601
want_unversioned=want_unversioned)
1603
def get_file(self, path):
1604
"""See Tree.get_file"""
1605
trans_id = self._path2trans_id(path)
1606
if trans_id is None:
1607
raise errors.NoSuchFile(path)
1608
if trans_id in self._transform._new_contents:
1609
name = self._transform._limbo_name(trans_id)
1610
return open(name, 'rb')
1611
if trans_id in self._transform._removed_contents:
1612
raise errors.NoSuchFile(path)
1613
orig_path = self._transform.tree_path(trans_id)
1614
return self._transform._tree.get_file(orig_path)
1616
def get_symlink_target(self, path):
1617
"""See Tree.get_symlink_target"""
1618
trans_id = self._path2trans_id(path)
1619
if trans_id is None:
1620
raise errors.NoSuchFile(path)
1621
if trans_id not in self._transform._new_contents:
1622
orig_path = self._transform.tree_path(trans_id)
1623
return self._transform._tree.get_symlink_target(orig_path)
1624
name = self._transform._limbo_name(trans_id)
1625
return osutils.readlink(name)
1627
def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1628
trans_id = self._path2trans_id(path)
1629
if trans_id is None:
1631
orig_path = self._transform.tree_path(trans_id)
1632
if orig_path is not None:
1633
old_annotation = self._transform._tree.annotate_iter(
1634
orig_path, default_revision=default_revision)
1638
lines = self.get_file_lines(path)
1639
except errors.NoSuchFile:
1641
return annotate.reannotate([old_annotation], lines, default_revision)
1643
def get_file_text(self, path):
1644
"""Return the byte content of a file.
1646
:param path: The path of the file.
1648
:returns: A single byte string for the whole file.
1650
with self.get_file(path) as my_file:
1651
return my_file.read()
1653
def get_file_lines(self, path):
1654
"""Return the content of a file, as lines.
1656
:param path: The path of the file.
1658
return osutils.split_lines(self.get_file_text(path))
1661
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1662
in self._transform._tree.extras())
1663
possible_extras.update(self._transform._new_contents)
1664
possible_extras.update(self._transform._removed_id)
1665
for trans_id in possible_extras:
1666
if not self._transform.final_is_versioned(trans_id):
1667
yield self._final_paths._determine_path(trans_id)
1669
def path_content_summary(self, path):
1670
trans_id = self._path2trans_id(path)
1671
tt = self._transform
1672
tree_path = tt.tree_path(trans_id)
1673
kind = tt._new_contents.get(trans_id)
1675
if tree_path is None or trans_id in tt._removed_contents:
1676
return 'missing', None, None, None
1677
summary = tt._tree.path_content_summary(tree_path)
1678
kind, size, executable, link_or_sha1 = summary
1681
limbo_name = tt._limbo_name(trans_id)
1682
if trans_id in tt._new_reference_revision:
1683
kind = 'tree-reference'
1685
statval = os.lstat(limbo_name)
1686
size = statval.st_size
1687
if not tt._limbo_supports_executable():
1690
executable = statval.st_mode & S_IEXEC
1694
if kind == 'symlink':
1695
link_or_sha1 = os.readlink(limbo_name)
1696
if not isinstance(link_or_sha1, text_type):
1697
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1698
executable = tt._new_executability.get(trans_id, executable)
1699
return kind, size, executable, link_or_sha1
1701
def get_file_mtime(self, path):
1702
"""See Tree.get_file_mtime"""
1703
trans_id = self._path2trans_id(path)
1704
if trans_id is None:
1705
raise errors.NoSuchFile(path)
1706
if trans_id not in self._transform._new_contents:
1707
return self._transform._tree.get_file_mtime(
1708
self._transform.tree_path(trans_id))
1709
name = self._transform._limbo_name(trans_id)
1710
statval = os.lstat(name)
1711
return statval.st_mtime
1713
def is_versioned(self, path):
1714
trans_id = self._path2trans_id(path)
1715
if trans_id is None:
1716
# It doesn't exist, so it's not versioned.
1718
if trans_id in self._transform._versioned:
1720
if trans_id in self._transform._removed_id:
1722
orig_path = self._transform.tree_path(trans_id)
1723
return self._transform._tree.is_versioned(orig_path)
1725
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1727
raise NotImplementedError(
1728
'follow tree references not yet supported')
1730
# This may not be a maximally efficient implementation, but it is
1731
# reasonably straightforward. An implementation that grafts the
1732
# TreeTransform changes onto the tree's iter_entries_by_dir results
1733
# might be more efficient, but requires tricky inferences about stack
1735
for trans_id, path in self._list_files_by_dir():
1736
entry, is_versioned = self._transform.final_entry(trans_id)
1739
if not is_versioned and entry.kind != 'directory':
1741
if specific_files is not None and path not in specific_files:
1743
if entry is not None:
1746
def _list_files_by_dir(self):
1747
todo = [ROOT_PARENT]
1748
while len(todo) > 0:
1750
children = list(self._all_children(parent))
1751
paths = dict(zip(children, self._final_paths.get_paths(children)))
1752
children.sort(key=paths.get)
1753
todo.extend(reversed(children))
1754
for trans_id in children:
1755
yield trans_id, paths[trans_id][0]
1757
def revision_tree(self, revision_id):
1758
return self._transform._tree.revision_tree(revision_id)
1760
def _stat_limbo_file(self, trans_id):
1761
name = self._transform._limbo_name(trans_id)
1762
return os.lstat(name)
1764
def git_snapshot(self, want_unversioned=False):
1767
for trans_id, path in self._list_files_by_dir():
1768
if not self._transform.final_is_versioned(trans_id):
1769
if not want_unversioned:
1772
o, mode = self._transform.final_git_entry(trans_id)
1774
self.store.add_object(o)
1775
os.append((encode_git_path(path), o.id, mode))
1778
return commit_tree(self.store, os), extra
1780
def iter_child_entries(self, path):
1781
trans_id = self._path2trans_id(path)
1782
if trans_id is None:
1783
raise errors.NoSuchFile(path)
1784
for child_trans_id in self._all_children(trans_id):
1785
entry, is_versioned = self._transform.final_entry(trans_id)
1786
if not is_versioned:
1788
if entry is not None: