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"""
305
for trans_id in self._new_parent:
308
while parent_id != ROOT_PARENT:
311
parent_id = self.final_parent(parent_id)
314
if parent_id == trans_id:
315
conflicts.append(('parent loop', trans_id))
316
if parent_id in seen:
320
def _improper_versioning(self):
321
"""Cannot version a file with no contents, or a bad type.
323
However, existing entries with no contents are okay.
326
for trans_id in self._versioned:
327
kind = self.final_kind(trans_id)
328
if kind == 'symlink' and not self._tree.supports_symlinks():
329
# Ignore symlinks as they are not supported on this platform
332
conflicts.append(('versioning no contents', trans_id))
334
if not self._tree.versionable_kind(kind):
335
conflicts.append(('versioning bad kind', trans_id, kind))
338
def _executability_conflicts(self):
339
"""Check for bad executability changes.
341
Only versioned files may have their executability set, because
342
1. only versioned entries can have executability under windows
343
2. only files can be executable. (The execute bit on a directory
344
does not indicate searchability)
347
for trans_id in self._new_executability:
348
if not self.final_is_versioned(trans_id):
349
conflicts.append(('unversioned executability', trans_id))
351
if self.final_kind(trans_id) != "file":
352
conflicts.append(('non-file executability', trans_id))
355
def _overwrite_conflicts(self):
356
"""Check for overwrites (not permitted on Win32)"""
358
for trans_id in self._new_contents:
359
if self.tree_kind(trans_id) is None:
361
if trans_id not in self._removed_contents:
362
conflicts.append(('overwrite', trans_id,
363
self.final_name(trans_id)))
366
def _duplicate_entries(self, by_parent):
367
"""No directory may have two entries with the same name."""
369
if (self._new_name, self._new_parent) == ({}, {}):
371
for children in viewvalues(by_parent):
373
for child_tid in children:
374
name = self.final_name(child_tid)
376
# Keep children only if they still exist in the end
377
if not self._case_sensitive_target:
379
name_ids.append((name, child_tid))
383
for name, trans_id in name_ids:
384
kind = self.final_kind(trans_id)
385
if kind is None and not self.final_is_versioned(trans_id):
387
if name == last_name:
388
conflicts.append(('duplicate', last_trans_id, trans_id,
391
last_trans_id = trans_id
394
def _parent_type_conflicts(self, by_parent):
395
"""Children must have a directory parent"""
397
for parent_id, children in viewitems(by_parent):
398
if parent_id == ROOT_PARENT:
401
for child_id in children:
402
if self.final_kind(child_id) is not None:
407
# There is at least a child, so we need an existing directory to
409
kind = self.final_kind(parent_id)
411
# The directory will be deleted
412
conflicts.append(('missing parent', parent_id))
413
elif kind != "directory":
414
# Meh, we need a *directory* to put something in it
415
conflicts.append(('non-directory parent', parent_id))
418
def _set_executability(self, path, trans_id):
419
"""Set the executability of versioned files """
420
if self._tree._supports_executable():
421
new_executability = self._new_executability[trans_id]
422
abspath = self._tree.abspath(path)
423
current_mode = os.stat(abspath).st_mode
424
if new_executability:
427
to_mode = current_mode | (0o100 & ~umask)
428
# Enable x-bit for others only if they can read it.
429
if current_mode & 0o004:
430
to_mode |= 0o001 & ~umask
431
if current_mode & 0o040:
432
to_mode |= 0o010 & ~umask
434
to_mode = current_mode & ~0o111
435
osutils.chmod_if_possible(abspath, to_mode)
437
def _new_entry(self, name, parent_id, file_id):
438
"""Helper function to create a new filesystem entry."""
439
trans_id = self.create_path(name, parent_id)
440
if file_id is not None:
441
self.version_file(trans_id, file_id=file_id)
444
def new_file(self, name, parent_id, contents, file_id=None,
445
executable=None, sha1=None):
446
"""Convenience method to create files.
448
name is the name of the file to create.
449
parent_id is the transaction id of the parent directory of the file.
450
contents is an iterator of bytestrings, which will be used to produce
452
:param file_id: The inventory ID of the file, if it is to be versioned.
453
:param executable: Only valid when a file_id has been supplied.
455
trans_id = self._new_entry(name, parent_id, file_id)
456
# TODO: rather than scheduling a set_executable call,
457
# have create_file create the file with the right mode.
458
self.create_file(contents, trans_id, sha1=sha1)
459
if executable is not None:
460
self.set_executability(executable, trans_id)
463
def new_directory(self, name, parent_id, file_id=None):
464
"""Convenience method to create directories.
466
name is the name of the directory to create.
467
parent_id is the transaction id of the parent directory of the
469
file_id is the inventory ID of the directory, if it is to be versioned.
471
trans_id = self._new_entry(name, parent_id, file_id)
472
self.create_directory(trans_id)
475
def new_symlink(self, name, parent_id, target, file_id=None):
476
"""Convenience method to create symbolic link.
478
name is the name of the symlink to create.
479
parent_id is the transaction id of the parent directory of the symlink.
480
target is a bytestring of the target of the symlink.
481
file_id is the inventory ID of the file, if it is to be versioned.
483
trans_id = self._new_entry(name, parent_id, file_id)
484
self.create_symlink(target, trans_id)
487
def new_orphan(self, trans_id, parent_id):
488
"""Schedule an item to be orphaned.
490
When a directory is about to be removed, its children, if they are not
491
versioned are moved out of the way: they don't have a parent anymore.
493
:param trans_id: The trans_id of the existing item.
494
:param parent_id: The parent trans_id of the item.
496
raise NotImplementedError(self.new_orphan)
498
def _get_potential_orphans(self, dir_id):
499
"""Find the potential orphans in a directory.
501
A directory can't be safely deleted if there are versioned files in it.
502
If all the contained files are unversioned then they can be orphaned.
504
The 'None' return value means that the directory contains at least one
505
versioned file and should not be deleted.
507
:param dir_id: The directory trans id.
509
:return: A list of the orphan trans ids or None if at least one
510
versioned file is present.
513
# Find the potential orphans, stop if one item should be kept
514
for child_tid in self.by_parent()[dir_id]:
515
if child_tid in self._removed_contents:
516
# The child is removed as part of the transform. Since it was
517
# versioned before, it's not an orphan
519
if not self.final_is_versioned(child_tid):
520
# The child is not versioned
521
orphans.append(child_tid)
523
# We have a versioned file here, searching for orphans is
529
def _affected_ids(self):
530
"""Return the set of transform ids affected by the transform"""
531
trans_ids = set(self._removed_id)
532
trans_ids.update(self._versioned)
533
trans_ids.update(self._removed_contents)
534
trans_ids.update(self._new_contents)
535
trans_ids.update(self._new_executability)
536
trans_ids.update(self._new_name)
537
trans_ids.update(self._new_parent)
540
def iter_changes(self, want_unversioned=False):
541
"""Produce output in the same format as Tree.iter_changes.
543
Will produce nonsensical results if invoked while inventory/filesystem
544
conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
546
final_paths = FinalPaths(self)
547
trans_ids = self._affected_ids()
549
# Now iterate through all active paths
550
for trans_id in trans_ids:
551
from_path = self.tree_path(trans_id)
553
# find file ids, and determine versioning state
554
if from_path is None:
555
from_versioned = False
557
from_versioned = self._tree.is_versioned(from_path)
558
if not want_unversioned and not from_versioned:
560
to_path = final_paths.get_path(trans_id)
564
to_versioned = self.final_is_versioned(trans_id)
565
if not want_unversioned and not to_versioned:
569
# get data from working tree if versioned
570
from_entry = next(self._tree.iter_entries_by_dir(
571
specific_files=[from_path]))[1]
572
from_name = from_entry.name
575
if from_path is None:
576
# File does not exist in FROM state
579
# File exists, but is not versioned. Have to use path-
581
from_name = os.path.basename(from_path)
582
if from_path is not None:
583
from_kind, from_executable, from_stats = \
584
self._tree._comparison_data(from_entry, from_path)
587
from_executable = False
589
to_name = self.final_name(trans_id)
590
to_kind = self.final_kind(trans_id)
591
if trans_id in self._new_executability:
592
to_executable = self._new_executability[trans_id]
594
to_executable = from_executable
596
if from_versioned and from_kind != to_kind:
598
elif to_kind in ('file', 'symlink') and (
599
trans_id in self._new_contents):
601
if (not modified and from_versioned == to_versioned
602
and from_path == to_path
603
and from_name == to_name
604
and from_executable == to_executable):
606
if (from_path, to_path) == (None, None):
610
(from_path, to_path), modified,
611
(from_versioned, to_versioned),
612
(from_name, to_name),
613
(from_kind, to_kind),
614
(from_executable, to_executable)))
617
return (c.path[0] or '', c.path[1] or '')
618
return iter(sorted(results, key=path_key))
620
def get_preview_tree(self):
621
"""Return a tree representing the result of the transform.
623
The tree is a snapshot, and altering the TreeTransform will invalidate
626
return GitPreviewTree(self)
628
def commit(self, branch, message, merge_parents=None, strict=False,
629
timestamp=None, timezone=None, committer=None, authors=None,
630
revprops=None, revision_id=None):
631
"""Commit the result of this TreeTransform to a branch.
633
:param branch: The branch to commit to.
634
:param message: The message to attach to the commit.
635
:param merge_parents: Additional parent revision-ids specified by
637
:param strict: If True, abort the commit if there are unversioned
639
:param timestamp: if not None, seconds-since-epoch for the time and
640
date. (May be a float.)
641
:param timezone: Optional timezone for timestamp, as an offset in
643
:param committer: Optional committer in email-id format.
644
(e.g. "J Random Hacker <jrandom@example.com>")
645
:param authors: Optional list of authors in email-id format.
646
:param revprops: Optional dictionary of revision properties.
647
:param revision_id: Optional revision id. (Specifying a revision-id
648
may reduce performance for some non-native formats.)
649
:return: The revision_id of the revision committed.
651
self._check_malformed()
653
unversioned = set(self._new_contents).difference(set(self._versioned))
654
for trans_id in unversioned:
655
if not self.final_is_versioned(trans_id):
656
raise errors.StrictCommitFailed()
658
revno, last_rev_id = branch.last_revision_info()
659
if last_rev_id == _mod_revision.NULL_REVISION:
660
if merge_parents is not None:
661
raise ValueError('Cannot supply merge parents for first'
665
parent_ids = [last_rev_id]
666
if merge_parents is not None:
667
parent_ids.extend(merge_parents)
668
if self._tree.get_revision_id() != last_rev_id:
669
raise ValueError('TreeTransform not based on branch basis: %s' %
670
self._tree.get_revision_id().decode('utf-8'))
671
from .. import commit
672
revprops = commit.Commit.update_revprops(revprops, branch, authors)
673
builder = branch.get_commit_builder(parent_ids,
678
revision_id=revision_id)
679
preview = self.get_preview_tree()
680
list(builder.record_iter_changes(preview, last_rev_id,
681
self.iter_changes()))
682
builder.finish_inventory()
683
revision_id = builder.commit(message)
684
branch.set_last_revision_info(revno + 1, revision_id)
687
def _text_parent(self, trans_id):
688
path = self.tree_path(trans_id)
690
if path is None or self._tree.kind(path) != 'file':
692
except errors.NoSuchFile:
696
def _get_parents_texts(self, trans_id):
697
"""Get texts for compression parents of this file."""
698
path = self._text_parent(trans_id)
701
return (self._tree.get_file_text(path),)
703
def _get_parents_lines(self, trans_id):
704
"""Get lines for compression parents of this file."""
705
path = self._text_parent(trans_id)
708
return (self._tree.get_file_lines(path),)
710
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
711
"""Schedule creation of a new file.
715
:param contents: an iterator of strings, all of which will be written
716
to the target destination.
717
:param trans_id: TreeTransform handle
718
:param mode_id: If not None, force the mode of the target file to match
719
the mode of the object referenced by mode_id.
720
Otherwise, we will try to preserve mode bits of an existing file.
721
:param sha1: If the sha1 of this content is already known, pass it in.
722
We can use it to prevent future sha1 computations.
724
raise NotImplementedError(self.create_file)
726
def create_directory(self, trans_id):
727
"""Schedule creation of a new directory.
729
See also new_directory.
731
raise NotImplementedError(self.create_directory)
733
def create_symlink(self, target, trans_id):
734
"""Schedule creation of a new symbolic link.
736
target is a bytestring.
737
See also new_symlink.
739
raise NotImplementedError(self.create_symlink)
741
def create_hardlink(self, path, trans_id):
742
"""Schedule creation of a hard link"""
743
raise NotImplementedError(self.create_hardlink)
745
def cancel_creation(self, trans_id):
746
"""Cancel the creation of new file contents."""
747
raise NotImplementedError(self.cancel_creation)
749
def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
750
"""Apply all changes to the inventory and filesystem.
752
If filesystem or inventory conflicts are present, MalformedTransform
755
If apply succeeds, finalize is not necessary.
757
:param no_conflicts: if True, the caller guarantees there are no
758
conflicts, so no check is made.
759
:param precomputed_delta: An inventory delta to use instead of
761
:param _mover: Supply an alternate FileMover, for testing
763
raise NotImplementedError(self.apply)
765
def cook_conflicts(self, raw_conflicts):
766
"""Generate a list of cooked conflicts, sorted by file path"""
767
if not raw_conflicts:
769
fp = FinalPaths(self)
770
from .workingtree import TextConflict
771
for c in raw_conflicts:
772
if c[0] == 'text conflict':
773
yield TextConflict(fp.get_path(c[1]))
774
elif c[0] == 'duplicate':
775
yield TextConflict(fp.get_path(c[2]))
776
elif c[0] == 'contents conflict':
777
yield TextConflict(fp.get_path(c[1][0]))
778
elif c[0] == 'missing parent':
779
# TODO(jelmer): This should not make it to here
780
yield TextConflict(fp.get_path(c[2]))
781
elif c[0] == 'non-directory parent':
782
yield TextConflict(fp.get_path(c[2]))
783
elif c[0] == 'deleting parent':
784
# TODO(jelmer): This should not make it to here
785
yield TextConflict(fp.get_path(c[2]))
786
elif c[0] == 'parent loop':
787
# TODO(jelmer): This should not make it to here
788
yield TextConflict(fp.get_path(c[2]))
789
elif c[0] == 'path conflict':
790
yield TextConflict(fp.get_path(c[1]))
792
raise AssertionError('unknown conflict %s' % c[0])
795
class DiskTreeTransform(TreeTransformBase):
796
"""Tree transform storing its contents on disk."""
798
def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
800
:param tree: The tree that will be transformed, but not necessarily
802
:param limbodir: A directory where new files can be stored until
803
they are installed in their proper places
805
:param case_sensitive: If True, the target of the transform is
806
case sensitive, not just case preserving.
808
TreeTransformBase.__init__(self, tree, pb, case_sensitive)
809
self._limbodir = limbodir
810
self._deletiondir = None
811
# A mapping of transform ids to their limbo filename
812
self._limbo_files = {}
813
self._possibly_stale_limbo_files = set()
814
# A mapping of transform ids to a set of the transform ids of children
815
# that their limbo directory has
816
self._limbo_children = {}
817
# Map transform ids to maps of child filename to child transform id
818
self._limbo_children_names = {}
819
# List of transform ids that need to be renamed from limbo into place
820
self._needs_rename = set()
821
self._creation_mtime = None
822
self._create_symlinks = osutils.supports_symlinks(self._limbodir)
825
"""Release the working tree lock, if held, clean up limbo dir.
827
This is required if apply has not been invoked, but can be invoked
830
if self._tree is None:
833
limbo_paths = list(viewvalues(self._limbo_files))
834
limbo_paths.extend(self._possibly_stale_limbo_files)
835
limbo_paths.sort(reverse=True)
836
for path in limbo_paths:
838
osutils.delete_any(path)
840
if e.errno != errno.ENOENT:
842
# XXX: warn? perhaps we just got interrupted at an
843
# inconvenient moment, but perhaps files are disappearing
846
osutils.delete_any(self._limbodir)
848
# We don't especially care *why* the dir is immortal.
849
raise ImmortalLimbo(self._limbodir)
851
if self._deletiondir is not None:
852
osutils.delete_any(self._deletiondir)
854
raise errors.ImmortalPendingDeletion(self._deletiondir)
856
TreeTransformBase.finalize(self)
858
def _limbo_supports_executable(self):
859
"""Check if the limbo path supports the executable bit."""
860
return osutils.supports_executable(self._limbodir)
862
def _limbo_name(self, trans_id):
863
"""Generate the limbo name of a file"""
864
limbo_name = self._limbo_files.get(trans_id)
865
if limbo_name is None:
866
limbo_name = self._generate_limbo_path(trans_id)
867
self._limbo_files[trans_id] = limbo_name
870
def _generate_limbo_path(self, trans_id):
871
"""Generate a limbo path using the trans_id as the relative path.
873
This is suitable as a fallback, and when the transform should not be
874
sensitive to the path encoding of the limbo directory.
876
self._needs_rename.add(trans_id)
877
return osutils.pathjoin(self._limbodir, trans_id)
879
def adjust_path(self, name, parent, trans_id):
880
previous_parent = self._new_parent.get(trans_id)
881
previous_name = self._new_name.get(trans_id)
882
super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
883
if (trans_id in self._limbo_files
884
and trans_id not in self._needs_rename):
885
self._rename_in_limbo([trans_id])
886
if previous_parent != parent:
887
self._limbo_children[previous_parent].remove(trans_id)
888
if previous_parent != parent or previous_name != name:
889
del self._limbo_children_names[previous_parent][previous_name]
891
def _rename_in_limbo(self, trans_ids):
892
"""Fix limbo names so that the right final path is produced.
894
This means we outsmarted ourselves-- we tried to avoid renaming
895
these files later by creating them with their final names in their
896
final parents. But now the previous name or parent is no longer
897
suitable, so we have to rename them.
899
Even for trans_ids that have no new contents, we must remove their
900
entries from _limbo_files, because they are now stale.
902
for trans_id in trans_ids:
903
old_path = self._limbo_files[trans_id]
904
self._possibly_stale_limbo_files.add(old_path)
905
del self._limbo_files[trans_id]
906
if trans_id not in self._new_contents:
908
new_path = self._limbo_name(trans_id)
909
os.rename(old_path, new_path)
910
self._possibly_stale_limbo_files.remove(old_path)
911
for descendant in self._limbo_descendants(trans_id):
912
desc_path = self._limbo_files[descendant]
913
desc_path = new_path + desc_path[len(old_path):]
914
self._limbo_files[descendant] = desc_path
916
def _limbo_descendants(self, trans_id):
917
"""Return the set of trans_ids whose limbo paths descend from this."""
918
descendants = set(self._limbo_children.get(trans_id, []))
919
for descendant in list(descendants):
920
descendants.update(self._limbo_descendants(descendant))
923
def _set_mode(self, trans_id, mode_id, typefunc):
924
raise NotImplementedError(self._set_mode)
926
def create_file(self, contents, trans_id, mode_id=None, sha1=None):
927
"""Schedule creation of a new file.
931
:param contents: an iterator of strings, all of which will be written
932
to the target destination.
933
:param trans_id: TreeTransform handle
934
:param mode_id: If not None, force the mode of the target file to match
935
the mode of the object referenced by mode_id.
936
Otherwise, we will try to preserve mode bits of an existing file.
937
:param sha1: If the sha1 of this content is already known, pass it in.
938
We can use it to prevent future sha1 computations.
940
name = self._limbo_name(trans_id)
941
with open(name, 'wb') as f:
942
unique_add(self._new_contents, trans_id, 'file')
943
f.writelines(contents)
944
self._set_mtime(name)
945
self._set_mode(trans_id, mode_id, S_ISREG)
946
# It is unfortunate we have to use lstat instead of fstat, but we just
947
# used utime and chmod on the file, so we need the accurate final
950
self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
952
def _read_symlink_target(self, trans_id):
953
return os.readlink(self._limbo_name(trans_id))
955
def _set_mtime(self, path):
956
"""All files that are created get the same mtime.
958
This time is set by the first object to be created.
960
if self._creation_mtime is None:
961
self._creation_mtime = time.time()
962
os.utime(path, (self._creation_mtime, self._creation_mtime))
964
def create_hardlink(self, path, trans_id):
965
"""Schedule creation of a hard link"""
966
name = self._limbo_name(trans_id)
970
if e.errno != errno.EPERM:
972
raise errors.HardLinkNotSupported(path)
974
unique_add(self._new_contents, trans_id, 'file')
975
except BaseException:
976
# Clean up the file, it never got registered so
977
# TreeTransform.finalize() won't clean it up.
981
def create_directory(self, trans_id):
982
"""Schedule creation of a new directory.
984
See also new_directory.
986
os.mkdir(self._limbo_name(trans_id))
987
unique_add(self._new_contents, trans_id, 'directory')
989
def create_symlink(self, target, trans_id):
990
"""Schedule creation of a new symbolic link.
992
target is a bytestring.
993
See also new_symlink.
995
if self._create_symlinks:
996
os.symlink(target, self._limbo_name(trans_id))
999
path = FinalPaths(self).get_path(trans_id)
1003
'Unable to create symlink "%s" on this filesystem.' % (path,))
1004
self._symlink_target[trans_id] = target
1005
# We add symlink to _new_contents even if they are unsupported
1006
# and not created. These entries are subsequently used to avoid
1007
# conflicts on platforms that don't support symlink
1008
unique_add(self._new_contents, trans_id, 'symlink')
1010
def cancel_creation(self, trans_id):
1011
"""Cancel the creation of new file contents."""
1012
del self._new_contents[trans_id]
1013
if trans_id in self._observed_sha1s:
1014
del self._observed_sha1s[trans_id]
1015
children = self._limbo_children.get(trans_id)
1016
# if this is a limbo directory with children, move them before removing
1018
if children is not None:
1019
self._rename_in_limbo(children)
1020
del self._limbo_children[trans_id]
1021
del self._limbo_children_names[trans_id]
1022
osutils.delete_any(self._limbo_name(trans_id))
1024
def new_orphan(self, trans_id, parent_id):
1025
conf = self._tree.get_config_stack()
1026
handle_orphan = conf.get('transform.orphan_policy')
1027
handle_orphan(self, trans_id, parent_id)
1029
def final_entry(self, trans_id):
1030
is_versioned = self.final_is_versioned(trans_id)
1031
fp = FinalPaths(self)
1032
tree_path = fp.get_path(trans_id)
1033
if trans_id in self._new_contents:
1034
path = self._limbo_name(trans_id)
1036
kind = mode_kind(st.st_mode)
1037
name = self.final_name(trans_id)
1038
file_id = self._tree.mapping.generate_file_id(tree_path)
1039
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1040
if kind == 'directory':
1041
return GitTreeDirectory(
1042
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1043
executable = mode_is_executable(st.st_mode)
1044
mode = object_mode(kind, executable)
1045
blob = blob_from_path_and_stat(encode_git_path(path), st)
1046
if kind == 'symlink':
1047
return GitTreeSymlink(
1048
file_id, name, parent_id,
1049
decode_git_path(blob.data)), is_versioned
1050
elif kind == 'file':
1052
file_id, name, executable=executable, parent_id=parent_id,
1053
git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1055
raise AssertionError(kind)
1056
elif trans_id in self._removed_contents:
1059
orig_path = self.tree_path(trans_id)
1060
if orig_path is None:
1062
file_id = self._tree.mapping.generate_file_id(tree_path)
1066
parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1068
ie = next(self._tree.iter_entries_by_dir(
1069
specific_files=[orig_path]))[1]
1070
ie.file_id = file_id
1071
ie.parent_id = parent_id
1072
return ie, is_versioned
1073
except StopIteration:
1075
if self.tree_kind(trans_id) == 'directory':
1076
return GitTreeDirectory(
1077
file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1078
except OSError as e:
1079
if e.errno != errno.ENOTDIR:
1083
def final_git_entry(self, trans_id):
1084
if trans_id in self._new_contents:
1085
path = self._limbo_name(trans_id)
1087
kind = mode_kind(st.st_mode)
1088
if kind == 'directory':
1090
executable = mode_is_executable(st.st_mode)
1091
mode = object_mode(kind, executable)
1092
blob = blob_from_path_and_stat(encode_git_path(path), st)
1093
elif trans_id in self._removed_contents:
1096
orig_path = self.tree_path(trans_id)
1097
kind = self._tree.kind(orig_path)
1098
executable = self._tree.is_executable(orig_path)
1099
mode = object_mode(kind, executable)
1100
if kind == 'symlink':
1101
contents = self._tree.get_symlink_target(orig_path)
1102
elif kind == 'file':
1103
contents = self._tree.get_file_text(orig_path)
1104
elif kind == 'directory':
1107
raise AssertionError(kind)
1108
blob = Blob.from_string(contents)
1112
class GitTreeTransform(DiskTreeTransform):
1113
"""Represent a tree transformation.
1115
This object is designed to support incremental generation of the transform,
1118
However, it gives optimum performance when parent directories are created
1119
before their contents. The transform is then able to put child files
1120
directly in their parent directory, avoiding later renames.
1122
It is easy to produce malformed transforms, but they are generally
1123
harmless. Attempting to apply a malformed transform will cause an
1124
exception to be raised before any modifications are made to the tree.
1126
Many kinds of malformed transforms can be corrected with the
1127
resolve_conflicts function. The remaining ones indicate programming error,
1128
such as trying to create a file with no path.
1130
Two sets of file creation methods are supplied. Convenience methods are:
1135
These are composed of the low-level methods:
1137
* create_file or create_directory or create_symlink
1141
Transform/Transaction ids
1142
-------------------------
1143
trans_ids are temporary ids assigned to all files involved in a transform.
1144
It's possible, even common, that not all files in the Tree have trans_ids.
1146
trans_ids are used because filenames and file_ids are not good enough
1147
identifiers; filenames change.
1149
trans_ids are only valid for the TreeTransform that generated them.
1153
Limbo is a temporary directory use to hold new versions of files.
1154
Files are added to limbo by create_file, create_directory, create_symlink,
1155
and their convenience variants (new_*). Files may be removed from limbo
1156
using cancel_creation. Files are renamed from limbo into their final
1157
location as part of TreeTransform.apply
1159
Limbo must be cleaned up, by either calling TreeTransform.apply or
1160
calling TreeTransform.finalize.
1162
Files are placed into limbo inside their parent directories, where
1163
possible. This reduces subsequent renames, and makes operations involving
1164
lots of files faster. This optimization is only possible if the parent
1165
directory is created *before* creating any of its children, so avoid
1166
creating children before parents, where possible.
1170
This temporary directory is used by _FileMover for storing files that are
1171
about to be deleted. In case of rollback, the files will be restored.
1172
FileMover does not delete files until it is sure that a rollback will not
1176
def __init__(self, tree, pb=None):
1177
"""Note: a tree_write lock is taken on the tree.
1179
Use TreeTransform.finalize() to release the lock (can be omitted if
1180
TreeTransform.apply() called).
1182
tree.lock_tree_write()
1184
limbodir = urlutils.local_path_from_url(
1185
tree._transport.abspath('limbo'))
1186
osutils.ensure_empty_directory_exists(
1188
errors.ExistingLimbo)
1189
deletiondir = urlutils.local_path_from_url(
1190
tree._transport.abspath('pending-deletion'))
1191
osutils.ensure_empty_directory_exists(
1193
errors.ExistingPendingDeletion)
1194
except BaseException:
1198
# Cache of realpath results, to speed up canonical_path
1199
self._realpaths = {}
1200
# Cache of relpath results, to speed up canonical_path
1202
DiskTreeTransform.__init__(self, tree, limbodir, pb,
1203
tree.case_sensitive)
1204
self._deletiondir = deletiondir
1206
def canonical_path(self, path):
1207
"""Get the canonical tree-relative path"""
1208
# don't follow final symlinks
1209
abs = self._tree.abspath(path)
1210
if abs in self._relpaths:
1211
return self._relpaths[abs]
1212
dirname, basename = os.path.split(abs)
1213
if dirname not in self._realpaths:
1214
self._realpaths[dirname] = os.path.realpath(dirname)
1215
dirname = self._realpaths[dirname]
1216
abs = osutils.pathjoin(dirname, basename)
1217
if dirname in self._relpaths:
1218
relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1219
relpath = relpath.rstrip('/\\')
1221
relpath = self._tree.relpath(abs)
1222
self._relpaths[abs] = relpath
1225
def tree_kind(self, trans_id):
1226
"""Determine the file kind in the working tree.
1228
:returns: The file kind or None if the file does not exist
1230
path = self._tree_id_paths.get(trans_id)
1234
return osutils.file_kind(self._tree.abspath(path))
1235
except errors.NoSuchFile:
1238
def _set_mode(self, trans_id, mode_id, typefunc):
1239
"""Set the mode of new file contents.
1240
The mode_id is the existing file to get the mode from (often the same
1241
as trans_id). The operation is only performed if there's a mode match
1242
according to typefunc.
1247
old_path = self._tree_id_paths[mode_id]
1251
mode = os.stat(self._tree.abspath(old_path)).st_mode
1252
except OSError as e:
1253
if e.errno in (errno.ENOENT, errno.ENOTDIR):
1254
# Either old_path doesn't exist, or the parent of the
1255
# target is not a directory (but will be one eventually)
1256
# Either way, we know it doesn't exist *right now*
1257
# See also bug #248448
1262
osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1264
def iter_tree_children(self, parent_id):
1265
"""Iterate through the entry's tree children, if any"""
1267
path = self._tree_id_paths[parent_id]
1271
children = os.listdir(self._tree.abspath(path))
1272
except OSError as e:
1273
if not (osutils._is_error_enotdir(e) or
1274
e.errno in (errno.ENOENT, errno.ESRCH)):
1278
for child in children:
1279
childpath = joinpath(path, child)
1280
if self._tree.is_control_filename(childpath):
1282
yield self.trans_id_tree_path(childpath)
1284
def _generate_limbo_path(self, trans_id):
1285
"""Generate a limbo path using the final path if possible.
1287
This optimizes the performance of applying the tree transform by
1288
avoiding renames. These renames can be avoided only when the parent
1289
directory is already scheduled for creation.
1291
If the final path cannot be used, falls back to using the trans_id as
1294
parent = self._new_parent.get(trans_id)
1295
# if the parent directory is already in limbo (e.g. when building a
1296
# tree), choose a limbo name inside the parent, to reduce further
1298
use_direct_path = False
1299
if self._new_contents.get(parent) == 'directory':
1300
filename = self._new_name.get(trans_id)
1301
if filename is not None:
1302
if parent not in self._limbo_children:
1303
self._limbo_children[parent] = set()
1304
self._limbo_children_names[parent] = {}
1305
use_direct_path = True
1306
# the direct path can only be used if no other file has
1307
# already taken this pathname, i.e. if the name is unused, or
1308
# if it is already associated with this trans_id.
1309
elif self._case_sensitive_target:
1310
if (self._limbo_children_names[parent].get(filename)
1311
in (trans_id, None)):
1312
use_direct_path = True
1314
for l_filename, l_trans_id in viewitems(
1315
self._limbo_children_names[parent]):
1316
if l_trans_id == trans_id:
1318
if l_filename.lower() == filename.lower():
1321
use_direct_path = True
1323
if not use_direct_path:
1324
return DiskTreeTransform._generate_limbo_path(self, trans_id)
1326
limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1327
self._limbo_children[parent].add(trans_id)
1328
self._limbo_children_names[parent][filename] = trans_id
1331
def cancel_versioning(self, trans_id):
1332
"""Undo a previous versioning of a file"""
1333
self._versioned.remove(trans_id)
1335
def apply(self, no_conflicts=False, _mover=None):
1336
"""Apply all changes to the inventory and filesystem.
1338
If filesystem or inventory conflicts are present, MalformedTransform
1341
If apply succeeds, finalize is not necessary.
1343
:param no_conflicts: if True, the caller guarantees there are no
1344
conflicts, so no check is made.
1345
:param _mover: Supply an alternate FileMover, for testing
1347
for hook in MutableTree.hooks['pre_transform']:
1348
hook(self._tree, self)
1349
if not no_conflicts:
1350
self._check_malformed()
1351
self.rename_count = 0
1352
with ui.ui_factory.nested_progress_bar() as child_pb:
1353
child_pb.update(gettext('Apply phase'), 0, 2)
1354
index_changes = self._generate_index_changes()
1357
mover = _FileMover()
1361
child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1362
self._apply_removals(mover)
1363
child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1364
modified_paths = self._apply_insertions(mover)
1365
except BaseException:
1369
mover.apply_deletions()
1370
self._tree._apply_index_changes(index_changes)
1373
return _TransformResults(modified_paths, self.rename_count)
1375
def _apply_removals(self, mover):
1376
"""Perform tree operations that remove directory/inventory names.
1378
That is, delete files that are to be deleted, and put any files that
1379
need renaming into limbo. This must be done in strict child-to-parent
1382
If inventory_delta is None, no inventory delta generation is performed.
1384
tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1385
with ui.ui_factory.nested_progress_bar() as child_pb:
1386
for num, (path, trans_id) in enumerate(tree_paths):
1387
# do not attempt to move root into a subdirectory of itself.
1390
child_pb.update(gettext('removing file'), num, len(tree_paths))
1391
full_path = self._tree.abspath(path)
1392
if trans_id in self._removed_contents:
1393
delete_path = os.path.join(self._deletiondir, trans_id)
1394
mover.pre_delete(full_path, delete_path)
1395
elif (trans_id in self._new_name or
1396
trans_id in self._new_parent):
1398
mover.rename(full_path, self._limbo_name(trans_id))
1399
except TransformRenameFailed as e:
1400
if e.errno != errno.ENOENT:
1403
self.rename_count += 1
1405
def _apply_insertions(self, mover):
1406
"""Perform tree operations that insert directory/inventory names.
1408
That is, create any files that need to be created, and restore from
1409
limbo any files that needed renaming. This must be done in strict
1410
parent-to-child order.
1412
If inventory_delta is None, no inventory delta is calculated, and
1413
no list of modified paths is returned.
1415
new_paths = self.new_paths(filesystem_only=True)
1417
with ui.ui_factory.nested_progress_bar() as child_pb:
1418
for num, (path, trans_id) in enumerate(new_paths):
1420
child_pb.update(gettext('adding file'),
1421
num, len(new_paths))
1422
full_path = self._tree.abspath(path)
1423
if trans_id in self._needs_rename:
1425
mover.rename(self._limbo_name(trans_id), full_path)
1426
except TransformRenameFailed as e:
1427
# We may be renaming a dangling inventory id
1428
if e.errno != errno.ENOENT:
1431
self.rename_count += 1
1432
# TODO: if trans_id in self._observed_sha1s, we should
1433
# re-stat the final target, since ctime will be
1434
# updated by the change.
1435
if (trans_id in self._new_contents
1436
or self.path_changed(trans_id)):
1437
if trans_id in self._new_contents:
1438
modified_paths.append(full_path)
1439
if trans_id in self._new_executability:
1440
self._set_executability(path, trans_id)
1441
if trans_id in self._observed_sha1s:
1442
o_sha1, o_st_val = self._observed_sha1s[trans_id]
1443
st = osutils.lstat(full_path)
1444
self._observed_sha1s[trans_id] = (o_sha1, st)
1445
for path, trans_id in new_paths:
1446
# new_paths includes stuff like workingtree conflicts. Only the
1447
# stuff in new_contents actually comes from limbo.
1448
if trans_id in self._limbo_files:
1449
del self._limbo_files[trans_id]
1450
self._new_contents.clear()
1451
return modified_paths
1453
def _generate_index_changes(self):
1454
"""Generate an inventory delta for the current transform."""
1455
removed_id = set(self._removed_id)
1456
removed_id.update(self._removed_contents)
1459
for id_set in [self._new_name, self._new_parent,
1460
self._new_executability]:
1461
changed_ids.update(id_set)
1462
for id_set in [self._new_name, self._new_parent]:
1463
removed_id.update(id_set)
1465
changed_kind = set(self._new_contents)
1466
# Ignore entries that are already known to have changed.
1467
changed_kind.difference_update(changed_ids)
1468
# to keep only the truly changed ones
1469
changed_kind = (t for t in changed_kind
1470
if self.tree_kind(t) != self.final_kind(t))
1471
changed_ids.update(changed_kind)
1472
for t in changed_kind:
1473
if self.final_kind(t) == 'directory':
1475
changed_ids.remove(t)
1476
new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1477
total_entries = len(new_paths) + len(removed_id)
1478
with ui.ui_factory.nested_progress_bar() as child_pb:
1479
for num, trans_id in enumerate(removed_id):
1481
child_pb.update(gettext('removing file'),
1484
path = self._tree_id_paths[trans_id]
1487
changes[path] = (None, None, None, None)
1488
for num, (path, trans_id) in enumerate(new_paths):
1490
child_pb.update(gettext('adding file'),
1491
num + len(removed_id), total_entries)
1493
kind = self.final_kind(trans_id)
1496
versioned = self.final_is_versioned(trans_id)
1499
executability = self._new_executability.get(trans_id)
1500
reference_revision = self._new_reference_revision.get(trans_id)
1501
symlink_target = self._symlink_target.get(trans_id)
1503
kind, executability, reference_revision, symlink_target)
1504
return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1507
class GitTransformPreview(GitTreeTransform):
1508
"""A TreeTransform for generating preview trees.
1510
Unlike TreeTransform, this version works when the input tree is a
1511
RevisionTree, rather than a WorkingTree. As a result, it tends to ignore
1512
unversioned files in the input tree.
1515
def __init__(self, tree, pb=None, case_sensitive=True):
1517
limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1518
DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1520
def canonical_path(self, path):
1523
def tree_kind(self, trans_id):
1524
path = self.tree_path(trans_id)
1527
kind = self._tree.path_content_summary(path)[0]
1528
if kind == 'missing':
1532
def _set_mode(self, trans_id, mode_id, typefunc):
1533
"""Set the mode of new file contents.
1534
The mode_id is the existing file to get the mode from (often the same
1535
as trans_id). The operation is only performed if there's a mode match
1536
according to typefunc.
1538
# is it ok to ignore this? probably
1541
def iter_tree_children(self, parent_id):
1542
"""Iterate through the entry's tree children, if any"""
1544
path = self._tree_id_paths[parent_id]
1548
for child in self._tree.iter_child_entries(path):
1549
childpath = joinpath(path, child.name)
1550
yield self.trans_id_tree_path(childpath)
1551
except errors.NoSuchFile:
1554
def new_orphan(self, trans_id, parent_id):
1555
raise NotImplementedError(self.new_orphan)
1558
class GitPreviewTree(PreviewTree, GitTree):
1559
"""Partial implementation of Tree to support show_diff_trees"""
1561
def __init__(self, transform):
1562
PreviewTree.__init__(self, transform)
1563
self.store = transform._tree.store
1564
self.mapping = transform._tree.mapping
1565
self._final_paths = FinalPaths(transform)
1567
def supports_setting_file_ids(self):
1570
def _supports_executable(self):
1571
return self._transform._limbo_supports_executable()
1573
def walkdirs(self, prefix=''):
1574
pending = [self._transform.root]
1575
while len(pending) > 0:
1576
parent_id = pending.pop()
1579
prefix = prefix.rstrip('/')
1580
parent_path = self._final_paths.get_path(parent_id)
1581
for child_id in self._all_children(parent_id):
1582
path_from_root = self._final_paths.get_path(child_id)
1583
basename = self._transform.final_name(child_id)
1584
kind = self._transform.final_kind(child_id)
1585
if kind is not None:
1586
versioned_kind = kind
1589
versioned_kind = self._transform._tree.stored_kind(
1591
if versioned_kind == 'directory':
1592
subdirs.append(child_id)
1593
children.append((path_from_root, basename, kind, None,
1596
if parent_path.startswith(prefix):
1597
yield parent_path, children
1598
pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1601
def iter_changes(self, from_tree, include_unchanged=False,
1602
specific_files=None, pb=None, extra_trees=None,
1603
require_versioned=True, want_unversioned=False):
1604
"""See InterTree.iter_changes.
1606
This has a fast path that is only used when the from_tree matches
1607
the transform tree, and no fancy options are supplied.
1609
return InterTree.get(from_tree, self).iter_changes(
1610
include_unchanged=include_unchanged,
1611
specific_files=specific_files,
1613
extra_trees=extra_trees,
1614
require_versioned=require_versioned,
1615
want_unversioned=want_unversioned)
1617
def get_file(self, path):
1618
"""See Tree.get_file"""
1619
trans_id = self._path2trans_id(path)
1620
if trans_id is None:
1621
raise errors.NoSuchFile(path)
1622
if trans_id in self._transform._new_contents:
1623
name = self._transform._limbo_name(trans_id)
1624
return open(name, 'rb')
1625
if trans_id in self._transform._removed_contents:
1626
raise errors.NoSuchFile(path)
1627
orig_path = self._transform.tree_path(trans_id)
1628
return self._transform._tree.get_file(orig_path)
1630
def get_symlink_target(self, path):
1631
"""See Tree.get_symlink_target"""
1632
trans_id = self._path2trans_id(path)
1633
if trans_id is None:
1634
raise errors.NoSuchFile(path)
1635
if trans_id not in self._transform._new_contents:
1636
orig_path = self._transform.tree_path(trans_id)
1637
return self._transform._tree.get_symlink_target(orig_path)
1638
name = self._transform._limbo_name(trans_id)
1639
return osutils.readlink(name)
1641
def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1642
trans_id = self._path2trans_id(path)
1643
if trans_id is None:
1645
orig_path = self._transform.tree_path(trans_id)
1646
if orig_path is not None:
1647
old_annotation = self._transform._tree.annotate_iter(
1648
orig_path, default_revision=default_revision)
1652
lines = self.get_file_lines(path)
1653
except errors.NoSuchFile:
1655
return annotate.reannotate([old_annotation], lines, default_revision)
1657
def get_file_text(self, path):
1658
"""Return the byte content of a file.
1660
:param path: The path of the file.
1662
:returns: A single byte string for the whole file.
1664
with self.get_file(path) as my_file:
1665
return my_file.read()
1667
def get_file_lines(self, path):
1668
"""Return the content of a file, as lines.
1670
:param path: The path of the file.
1672
return osutils.split_lines(self.get_file_text(path))
1675
possible_extras = set(self._transform.trans_id_tree_path(p) for p
1676
in self._transform._tree.extras())
1677
possible_extras.update(self._transform._new_contents)
1678
possible_extras.update(self._transform._removed_id)
1679
for trans_id in possible_extras:
1680
if not self._transform.final_is_versioned(trans_id):
1681
yield self._final_paths._determine_path(trans_id)
1683
def path_content_summary(self, path):
1684
trans_id = self._path2trans_id(path)
1685
tt = self._transform
1686
tree_path = tt.tree_path(trans_id)
1687
kind = tt._new_contents.get(trans_id)
1689
if tree_path is None or trans_id in tt._removed_contents:
1690
return 'missing', None, None, None
1691
summary = tt._tree.path_content_summary(tree_path)
1692
kind, size, executable, link_or_sha1 = summary
1695
limbo_name = tt._limbo_name(trans_id)
1696
if trans_id in tt._new_reference_revision:
1697
kind = 'tree-reference'
1699
statval = os.lstat(limbo_name)
1700
size = statval.st_size
1701
if not tt._limbo_supports_executable():
1704
executable = statval.st_mode & S_IEXEC
1708
if kind == 'symlink':
1709
link_or_sha1 = os.readlink(limbo_name)
1710
if not isinstance(link_or_sha1, text_type):
1711
link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1712
executable = tt._new_executability.get(trans_id, executable)
1713
return kind, size, executable, link_or_sha1
1715
def get_file_mtime(self, path):
1716
"""See Tree.get_file_mtime"""
1717
trans_id = self._path2trans_id(path)
1718
if trans_id is None:
1719
raise errors.NoSuchFile(path)
1720
if trans_id not in self._transform._new_contents:
1721
return self._transform._tree.get_file_mtime(
1722
self._transform.tree_path(trans_id))
1723
name = self._transform._limbo_name(trans_id)
1724
statval = os.lstat(name)
1725
return statval.st_mtime
1727
def is_versioned(self, path):
1728
trans_id = self._path2trans_id(path)
1729
if trans_id is None:
1730
# It doesn't exist, so it's not versioned.
1732
if trans_id in self._transform._versioned:
1734
if trans_id in self._transform._removed_id:
1736
orig_path = self._transform.tree_path(trans_id)
1737
return self._transform._tree.is_versioned(orig_path)
1739
def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1741
raise NotImplementedError(
1742
'follow tree references not yet supported')
1744
# This may not be a maximally efficient implementation, but it is
1745
# reasonably straightforward. An implementation that grafts the
1746
# TreeTransform changes onto the tree's iter_entries_by_dir results
1747
# might be more efficient, but requires tricky inferences about stack
1749
for trans_id, path in self._list_files_by_dir():
1750
entry, is_versioned = self._transform.final_entry(trans_id)
1753
if not is_versioned and entry.kind != 'directory':
1755
if specific_files is not None and path not in specific_files:
1757
if entry is not None:
1760
def _list_files_by_dir(self):
1761
todo = [ROOT_PARENT]
1762
while len(todo) > 0:
1764
children = list(self._all_children(parent))
1765
paths = dict(zip(children, self._final_paths.get_paths(children)))
1766
children.sort(key=paths.get)
1767
todo.extend(reversed(children))
1768
for trans_id in children:
1769
yield trans_id, paths[trans_id][0]
1771
def revision_tree(self, revision_id):
1772
return self._transform._tree.revision_tree(revision_id)
1774
def _stat_limbo_file(self, trans_id):
1775
name = self._transform._limbo_name(trans_id)
1776
return os.lstat(name)
1778
def git_snapshot(self, want_unversioned=False):
1781
for trans_id, path in self._list_files_by_dir():
1782
if not self._transform.final_is_versioned(trans_id):
1783
if not want_unversioned:
1786
o, mode = self._transform.final_git_entry(trans_id)
1788
self.store.add_object(o)
1789
os.append((encode_git_path(path), o.id, mode))
1792
return commit_tree(self.store, os), extra
1794
def iter_child_entries(self, path):
1795
trans_id = self._path2trans_id(path)
1796
if trans_id is None:
1797
raise errors.NoSuchFile(path)
1798
for child_trans_id in self._all_children(trans_id):
1799
entry, is_versioned = self._transform.final_entry(trans_id)
1800
if not is_versioned:
1802
if entry is not None: