/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/transform.py

  • Committer: Jelmer Vernooij
  • Date: 2020-09-02 15:11:17 UTC
  • mto: (7490.40.109 work)
  • mto: This revision was merged to the branch mainline in revision 7526.
  • Revision ID: jelmer@jelmer.uk-20200902151117-jeu7tarbz3dkuju0
Move more transform code.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
# Copyright (C) 2020 Breezy Developers
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
import errno
 
21
import os
 
22
import posixpath
 
23
from stat import S_IEXEC, S_ISREG
 
24
import time
 
25
 
 
26
from .mapping import encode_git_path, mode_kind, mode_is_executable, object_mode
 
27
from .tree import GitTree, GitTreeDirectory, GitTreeSymlink, GitTreeFile
 
28
 
 
29
from .. import (
 
30
    annotate,
 
31
    conflicts,
 
32
    errors,
 
33
    multiparent,
 
34
    osutils,
 
35
    revision as _mod_revision,
 
36
    trace,
 
37
    ui,
 
38
    urlutils,
 
39
    )
 
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 (
 
45
    PreviewTree,
 
46
    TreeTransform,
 
47
    _TransformResults,
 
48
    _FileMover,
 
49
    FinalPaths,
 
50
    joinpath,
 
51
    unique_add,
 
52
    TransformRenameFailed,
 
53
    ImmortalLimbo,
 
54
    ROOT_PARENT,
 
55
    ReusingTransform,
 
56
    MalformedTransform,
 
57
    )
 
58
 
 
59
from dulwich.index import commit_tree, blob_from_path_and_stat
 
60
from dulwich.objects import Blob
 
61
 
 
62
 
 
63
class TreeTransformBase(TreeTransform):
 
64
    """The base class for TreeTransform and its kin."""
 
65
 
 
66
    def __init__(self, tree, pb=None, case_sensitive=True):
 
67
        """Constructor.
 
68
 
 
69
        :param tree: The tree that will be transformed, but not necessarily
 
70
            the output tree.
 
71
        :param pb: ignored
 
72
        :param case_sensitive: If True, the target of the transform is
 
73
            case sensitive, not just case preserving.
 
74
        """
 
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 = {}
 
85
 
 
86
    @property
 
87
    def mapping(self):
 
88
        return self._tree.mapping
 
89
 
 
90
    def finalize(self):
 
91
        """Release the working tree lock, if held.
 
92
 
 
93
        This is required if apply has not been invoked, but can be invoked
 
94
        even after apply.
 
95
        """
 
96
        if self._tree is None:
 
97
            return
 
98
        for hook in MutableTree.hooks['post_transform']:
 
99
            hook(self._tree, self)
 
100
        self._tree.unlock()
 
101
        self._tree = None
 
102
 
 
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)
 
108
        return trans_id
 
109
 
 
110
    def adjust_root_path(self, name, parent):
 
111
        """Emulate moving the root by moving all children, instead.
 
112
        """
 
113
 
 
114
    def fixup_new_roots(self):
 
115
        """Reinterpret requests to change the root directory
 
116
 
 
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.
 
120
 
 
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
 
123
        irrelevant.
 
124
 
 
125
        """
 
126
        new_roots = [k for k, v in viewitems(self._new_parent)
 
127
                     if v == ROOT_PARENT]
 
128
        if len(new_roots) < 1:
 
129
            return
 
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)
 
136
        else:
 
137
            self.unversion_file(old_new_root)
 
138
 
 
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)
 
146
 
 
147
        # Ensure old_new_root has no directory.
 
148
        if old_new_root in self._new_contents:
 
149
            self.cancel_creation(old_new_root)
 
150
        else:
 
151
            self.delete_contents(old_new_root)
 
152
 
 
153
        # prevent deletion of root directory.
 
154
        if self.root in self._removed_contents:
 
155
            self.cancel_deletion(self.root)
 
156
 
 
157
        # destroy path info for old_new_root.
 
158
        del self._new_parent[old_new_root]
 
159
        del self._new_name[old_new_root]
 
160
 
 
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.)
 
166
        """
 
167
        if file_id is None:
 
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)
 
171
 
 
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)
 
177
 
 
178
    def cancel_versioning(self, trans_id):
 
179
        """Undo a previous versioning of a file"""
 
180
        raise NotImplementedError(self.cancel_versioning)
 
181
 
 
182
    def new_paths(self, filesystem_only=False):
 
183
        """Determine the paths of all new and changed files.
 
184
 
 
185
        :param filesystem_only: if True, only calculate values for files
 
186
            that require renames or execute bit changes.
 
187
        """
 
188
        new_ids = set()
 
189
        if filesystem_only:
 
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)
 
196
        else:
 
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))
 
202
 
 
203
    def final_is_versioned(self, trans_id):
 
204
        if trans_id in self._versioned:
 
205
            return True
 
206
        if trans_id in self._removed_id:
 
207
            return False
 
208
        orig_path = self.tree_path(trans_id)
 
209
        if orig_path is None:
 
210
            return False
 
211
        return self._tree.is_versioned(orig_path)
 
212
 
 
213
    def find_raw_conflicts(self):
 
214
        """Find any violations of inventory or filesystem invariants"""
 
215
        if self._done is True:
 
216
            raise ReusingTransform()
 
217
        conflicts = []
 
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())
 
228
        return conflicts
 
229
 
 
230
    def _check_malformed(self):
 
231
        conflicts = self.find_raw_conflicts()
 
232
        if len(conflicts) != 0:
 
233
            raise MalformedTransform(conflicts=conflicts)
 
234
 
 
235
    def _add_tree_children(self):
 
236
        """Add all the children of all active parents to the known paths.
 
237
 
 
238
        Active parents are those which gain children, and those which are
 
239
        removed.  This is a necessary first step in detecting conflicts.
 
240
        """
 
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)
 
246
            if path is not None:
 
247
                try:
 
248
                    if self._tree.stored_kind(path) == 'directory':
 
249
                        parents.append(trans_id)
 
250
                except errors.NoSuchFile:
 
251
                    pass
 
252
            elif self.tree_kind(trans_id) == 'directory':
 
253
                parents.append(trans_id)
 
254
 
 
255
        for parent_id in parents:
 
256
            # ensure that all children are registered with the transaction
 
257
            list(self.iter_tree_children(parent_id))
 
258
 
 
259
    def _has_named_child(self, name, parent_id, known_children):
 
260
        """Does a parent already have a name child.
 
261
 
 
262
        :param name: The searched for name.
 
263
 
 
264
        :param parent_id: The parent for which the check is made.
 
265
 
 
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).
 
269
        """
 
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:
 
274
                return True
 
275
        parent_path = self._tree_id_paths.get(parent_id, None)
 
276
        if parent_path is None:
 
277
            # No parent... no children
 
278
            return False
 
279
        child_path = joinpath(parent_path, name)
 
280
        child_id = self._tree_path_ids.get(child_path, None)
 
281
        if child_id is None:
 
282
            # Not known by the tree transform yet, check the filesystem
 
283
            return osutils.lexists(self._tree.abspath(child_path))
 
284
        else:
 
285
            raise AssertionError('child_id is missing: %s, %s, %s'
 
286
                                 % (name, parent_id, child_id))
 
287
 
 
288
    def _available_backup_name(self, name, target_id):
 
289
        """Find an available backup name.
 
290
 
 
291
        :param name: The basename of the file.
 
292
 
 
293
        :param target_id: The directory trans_id where the backup should
 
294
            be placed.
 
295
        """
 
296
        known_children = self.by_parent().get(target_id, [])
 
297
        return osutils.available_backup_name(
 
298
            name,
 
299
            lambda base: self._has_named_child(
 
300
                base, target_id, known_children))
 
301
 
 
302
    def _parent_loops(self):
 
303
        """No entry should be its own ancestor"""
 
304
        for trans_id in self._new_parent:
 
305
            seen = set()
 
306
            parent_id = trans_id
 
307
            while parent_id != ROOT_PARENT:
 
308
                seen.add(parent_id)
 
309
                try:
 
310
                    parent_id = self.final_parent(parent_id)
 
311
                except KeyError:
 
312
                    break
 
313
                if parent_id == trans_id:
 
314
                    yield ('parent loop', trans_id)
 
315
                if parent_id in seen:
 
316
                    break
 
317
 
 
318
    def _improper_versioning(self):
 
319
        """Cannot version a file with no contents, or a bad type.
 
320
 
 
321
        However, existing entries with no contents are okay.
 
322
        """
 
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
 
327
                continue
 
328
            if kind is None:
 
329
                yield ('versioning no contents', trans_id)
 
330
                continue
 
331
            if not self._tree.versionable_kind(kind):
 
332
                yield ('versioning bad kind', trans_id, kind)
 
333
 
 
334
    def _executability_conflicts(self):
 
335
        """Check for bad executability changes.
 
336
 
 
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)
 
341
        """
 
342
        for trans_id in self._new_executability:
 
343
            if not self.final_is_versioned(trans_id):
 
344
                yield ('unversioned executability', trans_id)
 
345
            else:
 
346
                if self.final_kind(trans_id) != "file":
 
347
                    yield ('non-file executability', trans_id)
 
348
 
 
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:
 
353
                continue
 
354
            if trans_id not in self._removed_contents:
 
355
                yield ('overwrite', trans_id, self.final_name(trans_id))
 
356
 
 
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) == ({}, {}):
 
360
            return
 
361
        for children in viewvalues(by_parent):
 
362
            name_ids = []
 
363
            for child_tid in children:
 
364
                name = self.final_name(child_tid)
 
365
                if name is not None:
 
366
                    # Keep children only if they still exist in the end
 
367
                    if not self._case_sensitive_target:
 
368
                        name = name.lower()
 
369
                    name_ids.append((name, child_tid))
 
370
            name_ids.sort()
 
371
            last_name = None
 
372
            last_trans_id = None
 
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):
 
376
                    continue
 
377
                if name == last_name:
 
378
                    yield ('duplicate', last_trans_id, trans_id, name)
 
379
                last_name = name
 
380
                last_trans_id = trans_id
 
381
 
 
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:
 
386
                continue
 
387
            no_children = True
 
388
            for child_id in children:
 
389
                if self.final_kind(child_id) is not None:
 
390
                    no_children = False
 
391
                    break
 
392
            if no_children:
 
393
                continue
 
394
            # There is at least a child, so we need an existing directory to
 
395
            # contain it.
 
396
            kind = self.final_kind(parent_id)
 
397
            if kind is None:
 
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)
 
403
 
 
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:
 
411
                umask = os.umask(0)
 
412
                os.umask(umask)
 
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
 
419
            else:
 
420
                to_mode = current_mode & ~0o111
 
421
            osutils.chmod_if_possible(abspath, to_mode)
 
422
 
 
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)
 
428
        return trans_id
 
429
 
 
430
    def new_file(self, name, parent_id, contents, file_id=None,
 
431
                 executable=None, sha1=None):
 
432
        """Convenience method to create files.
 
433
 
 
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
 
437
        the file.
 
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.
 
440
        """
 
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)
 
447
        return trans_id
 
448
 
 
449
    def new_directory(self, name, parent_id, file_id=None):
 
450
        """Convenience method to create directories.
 
451
 
 
452
        name is the name of the directory to create.
 
453
        parent_id is the transaction id of the parent directory of the
 
454
        directory.
 
455
        file_id is the inventory ID of the directory, if it is to be versioned.
 
456
        """
 
457
        trans_id = self._new_entry(name, parent_id, file_id)
 
458
        self.create_directory(trans_id)
 
459
        return trans_id
 
460
 
 
461
    def new_symlink(self, name, parent_id, target, file_id=None):
 
462
        """Convenience method to create symbolic link.
 
463
 
 
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.
 
468
        """
 
469
        trans_id = self._new_entry(name, parent_id, file_id)
 
470
        self.create_symlink(target, trans_id)
 
471
        return trans_id
 
472
 
 
473
    def new_orphan(self, trans_id, parent_id):
 
474
        """Schedule an item to be orphaned.
 
475
 
 
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.
 
478
 
 
479
        :param trans_id: The trans_id of the existing item.
 
480
        :param parent_id: The parent trans_id of the item.
 
481
        """
 
482
        raise NotImplementedError(self.new_orphan)
 
483
 
 
484
    def _get_potential_orphans(self, dir_id):
 
485
        """Find the potential orphans in a directory.
 
486
 
 
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.
 
489
 
 
490
        The 'None' return value means that the directory contains at least one
 
491
        versioned file and should not be deleted.
 
492
 
 
493
        :param dir_id: The directory trans id.
 
494
 
 
495
        :return: A list of the orphan trans ids or None if at least one
 
496
             versioned file is present.
 
497
        """
 
498
        orphans = []
 
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
 
504
                continue
 
505
            if not self.final_is_versioned(child_tid):
 
506
                # The child is not versioned
 
507
                orphans.append(child_tid)
 
508
            else:
 
509
                # We have a versioned file here, searching for orphans is
 
510
                # meaningless.
 
511
                orphans = None
 
512
                break
 
513
        return orphans
 
514
 
 
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)
 
524
        return trans_ids
 
525
 
 
526
    def iter_changes(self, want_unversioned=False):
 
527
        """Produce output in the same format as Tree.iter_changes.
 
528
 
 
529
        Will produce nonsensical results if invoked while inventory/filesystem
 
530
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
 
531
        """
 
532
        final_paths = FinalPaths(self)
 
533
        trans_ids = self._affected_ids()
 
534
        results = []
 
535
        # Now iterate through all active paths
 
536
        for trans_id in trans_ids:
 
537
            from_path = self.tree_path(trans_id)
 
538
            modified = False
 
539
            # find file ids, and determine versioning state
 
540
            if from_path is None:
 
541
                from_versioned = False
 
542
            else:
 
543
                from_versioned = self._tree.is_versioned(from_path)
 
544
            if not want_unversioned and not from_versioned:
 
545
                from_path = None
 
546
            to_path = final_paths.get_path(trans_id)
 
547
            if to_path is None:
 
548
                to_versioned = False
 
549
            else:
 
550
                to_versioned = self.final_is_versioned(trans_id)
 
551
            if not want_unversioned and not to_versioned:
 
552
                to_path = None
 
553
 
 
554
            if from_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
 
559
            else:
 
560
                from_entry = None
 
561
                if from_path is None:
 
562
                    # File does not exist in FROM state
 
563
                    from_name = None
 
564
                else:
 
565
                    # File exists, but is not versioned.  Have to use path-
 
566
                    # splitting stuff
 
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)
 
571
            else:
 
572
                from_kind = None
 
573
                from_executable = False
 
574
 
 
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]
 
579
            else:
 
580
                to_executable = from_executable
 
581
 
 
582
            if from_versioned and from_kind != to_kind:
 
583
                modified = True
 
584
            elif to_kind in ('file', 'symlink') and (
 
585
                    trans_id in self._new_contents):
 
586
                modified = True
 
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):
 
591
                continue
 
592
            if (from_path, to_path) == (None, None):
 
593
                continue
 
594
            results.append(
 
595
                TreeChange(
 
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)))
 
601
 
 
602
        def path_key(c):
 
603
            return (c.path[0] or '', c.path[1] or '')
 
604
        return iter(sorted(results, key=path_key))
 
605
 
 
606
    def get_preview_tree(self):
 
607
        """Return a tree representing the result of the transform.
 
608
 
 
609
        The tree is a snapshot, and altering the TreeTransform will invalidate
 
610
        it.
 
611
        """
 
612
        return GitPreviewTree(self)
 
613
 
 
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.
 
618
 
 
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
 
622
            pending merges.
 
623
        :param strict: If True, abort the commit if there are unversioned
 
624
            files.
 
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
 
628
            seconds.
 
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.
 
636
        """
 
637
        self._check_malformed()
 
638
        if strict:
 
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()
 
643
 
 
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'
 
648
                                 ' commit.')
 
649
            parent_ids = []
 
650
        else:
 
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,
 
660
                                            timestamp=timestamp,
 
661
                                            timezone=timezone,
 
662
                                            committer=committer,
 
663
                                            revprops=revprops,
 
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)
 
671
        return revision_id
 
672
 
 
673
    def _text_parent(self, trans_id):
 
674
        path = self.tree_path(trans_id)
 
675
        try:
 
676
            if path is None or self._tree.kind(path) != 'file':
 
677
                return None
 
678
        except errors.NoSuchFile:
 
679
            return None
 
680
        return path
 
681
 
 
682
    def _get_parents_texts(self, trans_id):
 
683
        """Get texts for compression parents of this file."""
 
684
        path = self._text_parent(trans_id)
 
685
        if path is None:
 
686
            return ()
 
687
        return (self._tree.get_file_text(path),)
 
688
 
 
689
    def _get_parents_lines(self, trans_id):
 
690
        """Get lines for compression parents of this file."""
 
691
        path = self._text_parent(trans_id)
 
692
        if path is None:
 
693
            return ()
 
694
        return (self._tree.get_file_lines(path),)
 
695
 
 
696
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
 
697
        """Schedule creation of a new file.
 
698
 
 
699
        :seealso: new_file.
 
700
 
 
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.
 
709
        """
 
710
        raise NotImplementedError(self.create_file)
 
711
 
 
712
    def create_directory(self, trans_id):
 
713
        """Schedule creation of a new directory.
 
714
 
 
715
        See also new_directory.
 
716
        """
 
717
        raise NotImplementedError(self.create_directory)
 
718
 
 
719
    def create_symlink(self, target, trans_id):
 
720
        """Schedule creation of a new symbolic link.
 
721
 
 
722
        target is a bytestring.
 
723
        See also new_symlink.
 
724
        """
 
725
        raise NotImplementedError(self.create_symlink)
 
726
 
 
727
    def create_hardlink(self, path, trans_id):
 
728
        """Schedule creation of a hard link"""
 
729
        raise NotImplementedError(self.create_hardlink)
 
730
 
 
731
    def cancel_creation(self, trans_id):
 
732
        """Cancel the creation of new file contents."""
 
733
        raise NotImplementedError(self.cancel_creation)
 
734
 
 
735
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
736
        """Apply all changes to the inventory and filesystem.
 
737
 
 
738
        If filesystem or inventory conflicts are present, MalformedTransform
 
739
        will be thrown.
 
740
 
 
741
        If apply succeeds, finalize is not necessary.
 
742
 
 
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
 
746
            calculating one.
 
747
        :param _mover: Supply an alternate FileMover, for testing
 
748
        """
 
749
        raise NotImplementedError(self.apply)
 
750
 
 
751
    def cook_conflicts(self, raw_conflicts):
 
752
        """Generate a list of cooked conflicts, sorted by file path"""
 
753
        if not raw_conflicts:
 
754
            return
 
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]))
 
777
            else:
 
778
                raise AssertionError('unknown conflict %s' % c[0])
 
779
 
 
780
 
 
781
class DiskTreeTransform(TreeTransformBase):
 
782
    """Tree transform storing its contents on disk."""
 
783
 
 
784
    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
 
785
        """Constructor.
 
786
        :param tree: The tree that will be transformed, but not necessarily
 
787
            the output tree.
 
788
        :param limbodir: A directory where new files can be stored until
 
789
            they are installed in their proper places
 
790
        :param pb: ignored
 
791
        :param case_sensitive: If True, the target of the transform is
 
792
            case sensitive, not just case preserving.
 
793
        """
 
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)
 
809
 
 
810
    def finalize(self):
 
811
        """Release the working tree lock, if held, clean up limbo dir.
 
812
 
 
813
        This is required if apply has not been invoked, but can be invoked
 
814
        even after apply.
 
815
        """
 
816
        if self._tree is None:
 
817
            return
 
818
        try:
 
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:
 
823
                try:
 
824
                    osutils.delete_any(path)
 
825
                except OSError as e:
 
826
                    if e.errno != errno.ENOENT:
 
827
                        raise
 
828
                    # XXX: warn? perhaps we just got interrupted at an
 
829
                    # inconvenient moment, but perhaps files are disappearing
 
830
                    # from under us?
 
831
            try:
 
832
                osutils.delete_any(self._limbodir)
 
833
            except OSError:
 
834
                # We don't especially care *why* the dir is immortal.
 
835
                raise ImmortalLimbo(self._limbodir)
 
836
            try:
 
837
                if self._deletiondir is not None:
 
838
                    osutils.delete_any(self._deletiondir)
 
839
            except OSError:
 
840
                raise errors.ImmortalPendingDeletion(self._deletiondir)
 
841
        finally:
 
842
            TreeTransformBase.finalize(self)
 
843
 
 
844
    def _limbo_supports_executable(self):
 
845
        """Check if the limbo path supports the executable bit."""
 
846
        return osutils.supports_executable(self._limbodir)
 
847
 
 
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
 
854
        return limbo_name
 
855
 
 
856
    def _generate_limbo_path(self, trans_id):
 
857
        """Generate a limbo path using the trans_id as the relative path.
 
858
 
 
859
        This is suitable as a fallback, and when the transform should not be
 
860
        sensitive to the path encoding of the limbo directory.
 
861
        """
 
862
        self._needs_rename.add(trans_id)
 
863
        return osutils.pathjoin(self._limbodir, trans_id)
 
864
 
 
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]
 
876
 
 
877
    def _rename_in_limbo(self, trans_ids):
 
878
        """Fix limbo names so that the right final path is produced.
 
879
 
 
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.
 
884
 
 
885
        Even for trans_ids that have no new contents, we must remove their
 
886
        entries from _limbo_files, because they are now stale.
 
887
        """
 
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:
 
893
                continue
 
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
 
901
 
 
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))
 
907
        return descendants
 
908
 
 
909
    def _set_mode(self, trans_id, mode_id, typefunc):
 
910
        raise NotImplementedError(self._set_mode)
 
911
 
 
912
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
 
913
        """Schedule creation of a new file.
 
914
 
 
915
        :seealso: new_file.
 
916
 
 
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.
 
925
        """
 
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
 
934
        # details.
 
935
        if sha1 is not None:
 
936
            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
 
937
 
 
938
    def _read_symlink_target(self, trans_id):
 
939
        return os.readlink(self._limbo_name(trans_id))
 
940
 
 
941
    def _set_mtime(self, path):
 
942
        """All files that are created get the same mtime.
 
943
 
 
944
        This time is set by the first object to be created.
 
945
        """
 
946
        if self._creation_mtime is None:
 
947
            self._creation_mtime = time.time()
 
948
        os.utime(path, (self._creation_mtime, self._creation_mtime))
 
949
 
 
950
    def create_hardlink(self, path, trans_id):
 
951
        """Schedule creation of a hard link"""
 
952
        name = self._limbo_name(trans_id)
 
953
        try:
 
954
            os.link(path, name)
 
955
        except OSError as e:
 
956
            if e.errno != errno.EPERM:
 
957
                raise
 
958
            raise errors.HardLinkNotSupported(path)
 
959
        try:
 
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.
 
964
            os.unlink(name)
 
965
            raise
 
966
 
 
967
    def create_directory(self, trans_id):
 
968
        """Schedule creation of a new directory.
 
969
 
 
970
        See also new_directory.
 
971
        """
 
972
        os.mkdir(self._limbo_name(trans_id))
 
973
        unique_add(self._new_contents, trans_id, 'directory')
 
974
 
 
975
    def create_symlink(self, target, trans_id):
 
976
        """Schedule creation of a new symbolic link.
 
977
 
 
978
        target is a bytestring.
 
979
        See also new_symlink.
 
980
        """
 
981
        if self._create_symlinks:
 
982
            os.symlink(target, self._limbo_name(trans_id))
 
983
        else:
 
984
            try:
 
985
                path = FinalPaths(self).get_path(trans_id)
 
986
            except KeyError:
 
987
                path = None
 
988
            trace.warning(
 
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')
 
995
 
 
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
 
1003
        # the directory
 
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))
 
1009
 
 
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)
 
1014
 
 
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)
 
1021
            st = os.lstat(path)
 
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':
 
1037
                return GitTreeFile(
 
1038
                    file_id, name, executable=executable, parent_id=parent_id,
 
1039
                    git_sha1=blob.id, text_size=len(blob.data)), is_versioned
 
1040
            else:
 
1041
                raise AssertionError(kind)
 
1042
        elif trans_id in self._removed_contents:
 
1043
            return None, None
 
1044
        else:
 
1045
            orig_path = self.tree_path(trans_id)
 
1046
            if orig_path is None:
 
1047
                return None, None
 
1048
            file_id = self._tree.mapping.generate_file_id(tree_path)
 
1049
            if tree_path == '':
 
1050
                parent_id = None
 
1051
            else:
 
1052
                parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
 
1053
            try:
 
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:
 
1060
                try:
 
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:
 
1066
                        raise
 
1067
                return None, None
 
1068
 
 
1069
    def final_git_entry(self, trans_id):
 
1070
        if trans_id in self._new_contents:
 
1071
            path = self._limbo_name(trans_id)
 
1072
            st = os.lstat(path)
 
1073
            kind = mode_kind(st.st_mode)
 
1074
            if kind == 'directory':
 
1075
                return None, None
 
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:
 
1080
            return None, None
 
1081
        else:
 
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':
 
1091
                return None, None
 
1092
            else:
 
1093
                raise AssertionError(kind)
 
1094
            blob = Blob.from_string(contents)
 
1095
        return blob, mode
 
1096
 
 
1097
 
 
1098
class GitTreeTransform(DiskTreeTransform):
 
1099
    """Represent a tree transformation.
 
1100
 
 
1101
    This object is designed to support incremental generation of the transform,
 
1102
    in any order.
 
1103
 
 
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.
 
1107
 
 
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.
 
1111
 
 
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.
 
1115
 
 
1116
    Two sets of file creation methods are supplied.  Convenience methods are:
 
1117
     * new_file
 
1118
     * new_directory
 
1119
     * new_symlink
 
1120
 
 
1121
    These are composed of the low-level methods:
 
1122
     * create_path
 
1123
     * create_file or create_directory or create_symlink
 
1124
     * version_file
 
1125
     * set_executability
 
1126
 
 
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.
 
1131
 
 
1132
    trans_ids are used because filenames and file_ids are not good enough
 
1133
    identifiers; filenames change.
 
1134
 
 
1135
    trans_ids are only valid for the TreeTransform that generated them.
 
1136
 
 
1137
    Limbo
 
1138
    -----
 
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
 
1144
 
 
1145
    Limbo must be cleaned up, by either calling TreeTransform.apply or
 
1146
    calling TreeTransform.finalize.
 
1147
 
 
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.
 
1153
 
 
1154
    Pending-deletion
 
1155
    ----------------
 
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
 
1159
    happen.
 
1160
    """
 
1161
 
 
1162
    def __init__(self, tree, pb=None):
 
1163
        """Note: a tree_write lock is taken on the tree.
 
1164
 
 
1165
        Use TreeTransform.finalize() to release the lock (can be omitted if
 
1166
        TreeTransform.apply() called).
 
1167
        """
 
1168
        tree.lock_tree_write()
 
1169
        try:
 
1170
            limbodir = urlutils.local_path_from_url(
 
1171
                tree._transport.abspath('limbo'))
 
1172
            osutils.ensure_empty_directory_exists(
 
1173
                limbodir,
 
1174
                errors.ExistingLimbo)
 
1175
            deletiondir = urlutils.local_path_from_url(
 
1176
                tree._transport.abspath('pending-deletion'))
 
1177
            osutils.ensure_empty_directory_exists(
 
1178
                deletiondir,
 
1179
                errors.ExistingPendingDeletion)
 
1180
        except BaseException:
 
1181
            tree.unlock()
 
1182
            raise
 
1183
 
 
1184
        # Cache of realpath results, to speed up canonical_path
 
1185
        self._realpaths = {}
 
1186
        # Cache of relpath results, to speed up canonical_path
 
1187
        self._relpaths = {}
 
1188
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
 
1189
                                   tree.case_sensitive)
 
1190
        self._deletiondir = deletiondir
 
1191
 
 
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('/\\')
 
1206
        else:
 
1207
            relpath = self._tree.relpath(abs)
 
1208
        self._relpaths[abs] = relpath
 
1209
        return relpath
 
1210
 
 
1211
    def tree_kind(self, trans_id):
 
1212
        """Determine the file kind in the working tree.
 
1213
 
 
1214
        :returns: The file kind or None if the file does not exist
 
1215
        """
 
1216
        path = self._tree_id_paths.get(trans_id)
 
1217
        if path is None:
 
1218
            return None
 
1219
        try:
 
1220
            return osutils.file_kind(self._tree.abspath(path))
 
1221
        except errors.NoSuchFile:
 
1222
            return None
 
1223
 
 
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.
 
1229
        """
 
1230
        if mode_id is None:
 
1231
            mode_id = trans_id
 
1232
        try:
 
1233
            old_path = self._tree_id_paths[mode_id]
 
1234
        except KeyError:
 
1235
            return
 
1236
        try:
 
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
 
1244
                return
 
1245
            else:
 
1246
                raise
 
1247
        if typefunc(mode):
 
1248
            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
 
1249
 
 
1250
    def iter_tree_children(self, parent_id):
 
1251
        """Iterate through the entry's tree children, if any"""
 
1252
        try:
 
1253
            path = self._tree_id_paths[parent_id]
 
1254
        except KeyError:
 
1255
            return
 
1256
        try:
 
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)):
 
1261
                raise
 
1262
            return
 
1263
 
 
1264
        for child in children:
 
1265
            childpath = joinpath(path, child)
 
1266
            if self._tree.is_control_filename(childpath):
 
1267
                continue
 
1268
            yield self.trans_id_tree_path(childpath)
 
1269
 
 
1270
    def _generate_limbo_path(self, trans_id):
 
1271
        """Generate a limbo path using the final path if possible.
 
1272
 
 
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.
 
1276
 
 
1277
        If the final path cannot be used, falls back to using the trans_id as
 
1278
        the relpath.
 
1279
        """
 
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
 
1283
        # renames.
 
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
 
1299
                else:
 
1300
                    for l_filename, l_trans_id in viewitems(
 
1301
                            self._limbo_children_names[parent]):
 
1302
                        if l_trans_id == trans_id:
 
1303
                            continue
 
1304
                        if l_filename.lower() == filename.lower():
 
1305
                            break
 
1306
                    else:
 
1307
                        use_direct_path = True
 
1308
 
 
1309
        if not use_direct_path:
 
1310
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
 
1311
 
 
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
 
1315
        return limbo_name
 
1316
 
 
1317
    def cancel_versioning(self, trans_id):
 
1318
        """Undo a previous versioning of a file"""
 
1319
        self._versioned.remove(trans_id)
 
1320
 
 
1321
    def apply(self, no_conflicts=False, _mover=None):
 
1322
        """Apply all changes to the inventory and filesystem.
 
1323
 
 
1324
        If filesystem or inventory conflicts are present, MalformedTransform
 
1325
        will be thrown.
 
1326
 
 
1327
        If apply succeeds, finalize is not necessary.
 
1328
 
 
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
 
1332
        """
 
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()
 
1341
            offset = 1
 
1342
            if _mover is None:
 
1343
                mover = _FileMover()
 
1344
            else:
 
1345
                mover = _mover
 
1346
            try:
 
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:
 
1352
                mover.rollback()
 
1353
                raise
 
1354
            else:
 
1355
                mover.apply_deletions()
 
1356
        self._tree._apply_index_changes(index_changes)
 
1357
        self._done = True
 
1358
        self.finalize()
 
1359
        return _TransformResults(modified_paths, self.rename_count)
 
1360
 
 
1361
    def _apply_removals(self, mover):
 
1362
        """Perform tree operations that remove directory/inventory names.
 
1363
 
 
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
 
1366
        order.
 
1367
 
 
1368
        If inventory_delta is None, no inventory delta generation is performed.
 
1369
        """
 
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.
 
1374
                if path == '':
 
1375
                    continue
 
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):
 
1383
                    try:
 
1384
                        mover.rename(full_path, self._limbo_name(trans_id))
 
1385
                    except TransformRenameFailed as e:
 
1386
                        if e.errno != errno.ENOENT:
 
1387
                            raise
 
1388
                    else:
 
1389
                        self.rename_count += 1
 
1390
 
 
1391
    def _apply_insertions(self, mover):
 
1392
        """Perform tree operations that insert directory/inventory names.
 
1393
 
 
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.
 
1397
 
 
1398
        If inventory_delta is None, no inventory delta is calculated, and
 
1399
        no list of modified paths is returned.
 
1400
        """
 
1401
        new_paths = self.new_paths(filesystem_only=True)
 
1402
        modified_paths = []
 
1403
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
1404
            for num, (path, trans_id) in enumerate(new_paths):
 
1405
                if (num % 10) == 0:
 
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:
 
1410
                    try:
 
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:
 
1415
                            raise
 
1416
                    else:
 
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
 
1438
 
 
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)
 
1443
        changes = {}
 
1444
        changed_ids = set()
 
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)
 
1450
        # so does adding
 
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':
 
1460
                removed_id.add(t)
 
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):
 
1466
                if (num % 10) == 0:
 
1467
                    child_pb.update(gettext('removing file'),
 
1468
                                    num, total_entries)
 
1469
                try:
 
1470
                    path = self._tree_id_paths[trans_id]
 
1471
                except KeyError:
 
1472
                    continue
 
1473
                changes[path] = (None, None, None, None)
 
1474
            for num, (path, trans_id) in enumerate(new_paths):
 
1475
                if (num % 10) == 0:
 
1476
                    child_pb.update(gettext('adding file'),
 
1477
                                    num + len(removed_id), total_entries)
 
1478
 
 
1479
                kind = self.final_kind(trans_id)
 
1480
                if kind is None:
 
1481
                    continue
 
1482
                versioned = self.final_is_versioned(trans_id)
 
1483
                if not versioned:
 
1484
                    continue
 
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)
 
1488
                changes[path] = (
 
1489
                    kind, executability, reference_revision, symlink_target)
 
1490
        return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
 
1491
 
 
1492
 
 
1493
class GitTransformPreview(GitTreeTransform):
 
1494
    """A TreeTransform for generating preview trees.
 
1495
 
 
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.
 
1499
    """
 
1500
 
 
1501
    def __init__(self, tree, pb=None, case_sensitive=True):
 
1502
        tree.lock_read()
 
1503
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
 
1504
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
 
1505
 
 
1506
    def canonical_path(self, path):
 
1507
        return path
 
1508
 
 
1509
    def tree_kind(self, trans_id):
 
1510
        path = self.tree_path(trans_id)
 
1511
        if path is None:
 
1512
            return None
 
1513
        kind = self._tree.path_content_summary(path)[0]
 
1514
        if kind == 'missing':
 
1515
            kind = None
 
1516
        return kind
 
1517
 
 
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.
 
1523
        """
 
1524
        # is it ok to ignore this?  probably
 
1525
        pass
 
1526
 
 
1527
    def iter_tree_children(self, parent_id):
 
1528
        """Iterate through the entry's tree children, if any"""
 
1529
        try:
 
1530
            path = self._tree_id_paths[parent_id]
 
1531
        except KeyError:
 
1532
            return
 
1533
        try:
 
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:
 
1538
            return
 
1539
 
 
1540
    def new_orphan(self, trans_id, parent_id):
 
1541
        raise NotImplementedError(self.new_orphan)
 
1542
 
 
1543
 
 
1544
class GitPreviewTree(PreviewTree, GitTree):
 
1545
    """Partial implementation of Tree to support show_diff_trees"""
 
1546
 
 
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)
 
1552
 
 
1553
    def supports_setting_file_ids(self):
 
1554
        return False
 
1555
 
 
1556
    def _supports_executable(self):
 
1557
        return self._transform._limbo_supports_executable()
 
1558
 
 
1559
    def walkdirs(self, prefix=''):
 
1560
        pending = [self._transform.root]
 
1561
        while len(pending) > 0:
 
1562
            parent_id = pending.pop()
 
1563
            children = []
 
1564
            subdirs = []
 
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
 
1573
                else:
 
1574
                    kind = 'unknown'
 
1575
                    versioned_kind = self._transform._tree.stored_kind(
 
1576
                        path_from_root)
 
1577
                if versioned_kind == 'directory':
 
1578
                    subdirs.append(child_id)
 
1579
                children.append((path_from_root, basename, kind, None,
 
1580
                                 versioned_kind))
 
1581
            children.sort()
 
1582
            if parent_path.startswith(prefix):
 
1583
                yield parent_path, children
 
1584
            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
 
1585
                                  reverse=True))
 
1586
 
 
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.
 
1591
 
 
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.
 
1594
        """
 
1595
        return InterTree.get(from_tree, self).iter_changes(
 
1596
            include_unchanged=include_unchanged,
 
1597
            specific_files=specific_files,
 
1598
            pb=pb,
 
1599
            extra_trees=extra_trees,
 
1600
            require_versioned=require_versioned,
 
1601
            want_unversioned=want_unversioned)
 
1602
 
 
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)
 
1615
 
 
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)
 
1626
 
 
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:
 
1630
            return 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)
 
1635
        else:
 
1636
            old_annotation = []
 
1637
        try:
 
1638
            lines = self.get_file_lines(path)
 
1639
        except errors.NoSuchFile:
 
1640
            return None
 
1641
        return annotate.reannotate([old_annotation], lines, default_revision)
 
1642
 
 
1643
    def get_file_text(self, path):
 
1644
        """Return the byte content of a file.
 
1645
 
 
1646
        :param path: The path of the file.
 
1647
 
 
1648
        :returns: A single byte string for the whole file.
 
1649
        """
 
1650
        with self.get_file(path) as my_file:
 
1651
            return my_file.read()
 
1652
 
 
1653
    def get_file_lines(self, path):
 
1654
        """Return the content of a file, as lines.
 
1655
 
 
1656
        :param path: The path of the file.
 
1657
        """
 
1658
        return osutils.split_lines(self.get_file_text(path))
 
1659
 
 
1660
    def extras(self):
 
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)
 
1668
 
 
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)
 
1674
        if kind is None:
 
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
 
1679
        else:
 
1680
            link_or_sha1 = None
 
1681
            limbo_name = tt._limbo_name(trans_id)
 
1682
            if trans_id in tt._new_reference_revision:
 
1683
                kind = 'tree-reference'
 
1684
            if kind == 'file':
 
1685
                statval = os.lstat(limbo_name)
 
1686
                size = statval.st_size
 
1687
                if not tt._limbo_supports_executable():
 
1688
                    executable = False
 
1689
                else:
 
1690
                    executable = statval.st_mode & S_IEXEC
 
1691
            else:
 
1692
                size = None
 
1693
                executable = None
 
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
 
1700
 
 
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
 
1712
 
 
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.
 
1717
            return False
 
1718
        if trans_id in self._transform._versioned:
 
1719
            return True
 
1720
        if trans_id in self._transform._removed_id:
 
1721
            return False
 
1722
        orig_path = self._transform.tree_path(trans_id)
 
1723
        return self._transform._tree.is_versioned(orig_path)
 
1724
 
 
1725
    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
 
1726
        if recurse_nested:
 
1727
            raise NotImplementedError(
 
1728
                'follow tree references not yet supported')
 
1729
 
 
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
 
1734
        # position.
 
1735
        for trans_id, path in self._list_files_by_dir():
 
1736
            entry, is_versioned = self._transform.final_entry(trans_id)
 
1737
            if entry is None:
 
1738
                continue
 
1739
            if not is_versioned and entry.kind != 'directory':
 
1740
                continue
 
1741
            if specific_files is not None and path not in specific_files:
 
1742
                continue
 
1743
            if entry is not None:
 
1744
                yield path, entry
 
1745
 
 
1746
    def _list_files_by_dir(self):
 
1747
        todo = [ROOT_PARENT]
 
1748
        while len(todo) > 0:
 
1749
            parent = todo.pop()
 
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]
 
1756
 
 
1757
    def revision_tree(self, revision_id):
 
1758
        return self._transform._tree.revision_tree(revision_id)
 
1759
 
 
1760
    def _stat_limbo_file(self, trans_id):
 
1761
        name = self._transform._limbo_name(trans_id)
 
1762
        return os.lstat(name)
 
1763
 
 
1764
    def git_snapshot(self, want_unversioned=False):
 
1765
        extra = set()
 
1766
        os = []
 
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:
 
1770
                    continue
 
1771
                extra.add(path)
 
1772
            o, mode = self._transform.final_git_entry(trans_id)
 
1773
            if o is not None:
 
1774
                self.store.add_object(o)
 
1775
                os.append((encode_git_path(path), o.id, mode))
 
1776
        if not os:
 
1777
            return None, extra
 
1778
        return commit_tree(self.store, os), extra
 
1779
 
 
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:
 
1787
                continue
 
1788
            if entry is not None:
 
1789
                yield entry