/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-06-01 21:57:00 UTC
  • mfrom: (7490.39.3 move-launchpad)
  • Revision ID: breezy.the.bot@gmail.com-20200601215700-joxuzo6w172gq74v
Move launchpad hoster support to the launchpad plugin.

Merged from https://code.launchpad.net/~jelmer/brz/move-launchpad/+merge/384931

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 ..transform import (
44
 
    PreviewTree,
45
 
    TreeTransform,
46
 
    _TransformResults,
47
 
    _FileMover,
48
 
    FinalPaths,
49
 
    joinpath,
50
 
    unique_add,
51
 
    TransformRenameFailed,
52
 
    ImmortalLimbo,
53
 
    ROOT_PARENT,
54
 
    ReusingTransform,
55
 
    MalformedTransform,
56
 
    )
57
 
 
58
 
from dulwich.index import commit_tree, blob_from_path_and_stat
59
 
from dulwich.objects import Blob
60
 
 
61
 
 
62
 
class TreeTransformBase(TreeTransform):
63
 
    """The base class for TreeTransform and its kin."""
64
 
 
65
 
    def __init__(self, tree, pb=None, case_sensitive=True):
66
 
        """Constructor.
67
 
 
68
 
        :param tree: The tree that will be transformed, but not necessarily
69
 
            the output tree.
70
 
        :param pb: ignored
71
 
        :param case_sensitive: If True, the target of the transform is
72
 
            case sensitive, not just case preserving.
73
 
        """
74
 
        super(TreeTransformBase, self).__init__(tree, pb=pb)
75
 
        # mapping of trans_id => (sha1 of content, stat_value)
76
 
        self._observed_sha1s = {}
77
 
        # Set of versioned trans ids
78
 
        self._versioned = set()
79
 
        # The trans_id that will be used as the tree root
80
 
        self.root = self.trans_id_tree_path('')
81
 
        # Whether the target is case sensitive
82
 
        self._case_sensitive_target = case_sensitive
83
 
        self._symlink_target = {}
84
 
 
85
 
    @property
86
 
    def mapping(self):
87
 
        return self._tree.mapping
88
 
 
89
 
    def finalize(self):
90
 
        """Release the working tree lock, if held.
91
 
 
92
 
        This is required if apply has not been invoked, but can be invoked
93
 
        even after apply.
94
 
        """
95
 
        if self._tree is None:
96
 
            return
97
 
        for hook in MutableTree.hooks['post_transform']:
98
 
            hook(self._tree, self)
99
 
        self._tree.unlock()
100
 
        self._tree = None
101
 
 
102
 
    def create_path(self, name, parent):
103
 
        """Assign a transaction id to a new path"""
104
 
        trans_id = self.assign_id()
105
 
        unique_add(self._new_name, trans_id, name)
106
 
        unique_add(self._new_parent, trans_id, parent)
107
 
        return trans_id
108
 
 
109
 
    def adjust_root_path(self, name, parent):
110
 
        """Emulate moving the root by moving all children, instead.
111
 
        """
112
 
 
113
 
    def fixup_new_roots(self):
114
 
        """Reinterpret requests to change the root directory
115
 
 
116
 
        Instead of creating a root directory, or moving an existing directory,
117
 
        all the attributes and children of the new root are applied to the
118
 
        existing root directory.
119
 
 
120
 
        This means that the old root trans-id becomes obsolete, so it is
121
 
        recommended only to invoke this after the root trans-id has become
122
 
        irrelevant.
123
 
 
124
 
        """
125
 
        new_roots = [k for k, v in self._new_parent.items()
126
 
                     if v == ROOT_PARENT]
127
 
        if len(new_roots) < 1:
128
 
            return
129
 
        if len(new_roots) != 1:
130
 
            raise ValueError('A tree cannot have two roots!')
131
 
        old_new_root = new_roots[0]
132
 
        # unversion the new root's directory.
133
 
        if old_new_root in self._versioned:
134
 
            self.cancel_versioning(old_new_root)
135
 
        else:
136
 
            self.unversion_file(old_new_root)
137
 
 
138
 
        # Now move children of new root into old root directory.
139
 
        # Ensure all children are registered with the transaction, but don't
140
 
        # use directly-- some tree children have new parents
141
 
        list(self.iter_tree_children(old_new_root))
142
 
        # Move all children of new root into old root directory.
143
 
        for child in self.by_parent().get(old_new_root, []):
144
 
            self.adjust_path(self.final_name(child), self.root, child)
145
 
 
146
 
        # Ensure old_new_root has no directory.
147
 
        if old_new_root in self._new_contents:
148
 
            self.cancel_creation(old_new_root)
149
 
        else:
150
 
            self.delete_contents(old_new_root)
151
 
 
152
 
        # prevent deletion of root directory.
153
 
        if self.root in self._removed_contents:
154
 
            self.cancel_deletion(self.root)
155
 
 
156
 
        # destroy path info for old_new_root.
157
 
        del self._new_parent[old_new_root]
158
 
        del self._new_name[old_new_root]
159
 
 
160
 
    def trans_id_file_id(self, file_id):
161
 
        """Determine or set the transaction id associated with a file ID.
162
 
        A new id is only created for file_ids that were never present.  If
163
 
        a transaction has been unversioned, it is deliberately still returned.
164
 
        (this will likely lead to an unversioned parent conflict.)
165
 
        """
166
 
        if file_id is None:
167
 
            raise ValueError('None is not a valid file id')
168
 
        path = self.mapping.parse_file_id(file_id)
169
 
        return self.trans_id_tree_path(path)
170
 
 
171
 
    def version_file(self, trans_id, file_id=None):
172
 
        """Schedule a file to become versioned."""
173
 
        if trans_id in self._versioned:
174
 
            raise errors.DuplicateKey(key=trans_id)
175
 
        self._versioned.add(trans_id)
176
 
 
177
 
    def cancel_versioning(self, trans_id):
178
 
        """Undo a previous versioning of a file"""
179
 
        raise NotImplementedError(self.cancel_versioning)
180
 
 
181
 
    def new_paths(self, filesystem_only=False):
182
 
        """Determine the paths of all new and changed files.
183
 
 
184
 
        :param filesystem_only: if True, only calculate values for files
185
 
            that require renames or execute bit changes.
186
 
        """
187
 
        new_ids = set()
188
 
        if filesystem_only:
189
 
            stale_ids = self._needs_rename.difference(self._new_name)
190
 
            stale_ids.difference_update(self._new_parent)
191
 
            stale_ids.difference_update(self._new_contents)
192
 
            stale_ids.difference_update(self._versioned)
193
 
            needs_rename = self._needs_rename.difference(stale_ids)
194
 
            id_sets = (needs_rename, self._new_executability)
195
 
        else:
196
 
            id_sets = (self._new_name, self._new_parent, self._new_contents,
197
 
                       self._versioned, self._new_executability)
198
 
        for id_set in id_sets:
199
 
            new_ids.update(id_set)
200
 
        return sorted(FinalPaths(self).get_paths(new_ids))
201
 
 
202
 
    def final_is_versioned(self, trans_id):
203
 
        if trans_id in self._versioned:
204
 
            return True
205
 
        if trans_id in self._removed_id:
206
 
            return False
207
 
        orig_path = self.tree_path(trans_id)
208
 
        if orig_path is None:
209
 
            return False
210
 
        return self._tree.is_versioned(orig_path)
211
 
 
212
 
    def find_raw_conflicts(self):
213
 
        """Find any violations of inventory or filesystem invariants"""
214
 
        if self._done is True:
215
 
            raise ReusingTransform()
216
 
        conflicts = []
217
 
        # ensure all children of all existent parents are known
218
 
        # all children of non-existent parents are known, by definition.
219
 
        self._add_tree_children()
220
 
        by_parent = self.by_parent()
221
 
        conflicts.extend(self._parent_loops())
222
 
        conflicts.extend(self._duplicate_entries(by_parent))
223
 
        conflicts.extend(self._parent_type_conflicts(by_parent))
224
 
        conflicts.extend(self._improper_versioning())
225
 
        conflicts.extend(self._executability_conflicts())
226
 
        conflicts.extend(self._overwrite_conflicts())
227
 
        return conflicts
228
 
 
229
 
    def _check_malformed(self):
230
 
        conflicts = self.find_raw_conflicts()
231
 
        if len(conflicts) != 0:
232
 
            raise MalformedTransform(conflicts=conflicts)
233
 
 
234
 
    def _add_tree_children(self):
235
 
        """Add all the children of all active parents to the known paths.
236
 
 
237
 
        Active parents are those which gain children, and those which are
238
 
        removed.  This is a necessary first step in detecting conflicts.
239
 
        """
240
 
        parents = list(self.by_parent())
241
 
        parents.extend([t for t in self._removed_contents if
242
 
                        self.tree_kind(t) == 'directory'])
243
 
        for trans_id in self._removed_id:
244
 
            path = self.tree_path(trans_id)
245
 
            if path is not None:
246
 
                try:
247
 
                    if self._tree.stored_kind(path) == 'directory':
248
 
                        parents.append(trans_id)
249
 
                except errors.NoSuchFile:
250
 
                    pass
251
 
            elif self.tree_kind(trans_id) == 'directory':
252
 
                parents.append(trans_id)
253
 
 
254
 
        for parent_id in parents:
255
 
            # ensure that all children are registered with the transaction
256
 
            list(self.iter_tree_children(parent_id))
257
 
 
258
 
    def _has_named_child(self, name, parent_id, known_children):
259
 
        """Does a parent already have a name child.
260
 
 
261
 
        :param name: The searched for name.
262
 
 
263
 
        :param parent_id: The parent for which the check is made.
264
 
 
265
 
        :param known_children: The already known children. This should have
266
 
            been recently obtained from `self.by_parent.get(parent_id)`
267
 
            (or will be if None is passed).
268
 
        """
269
 
        if known_children is None:
270
 
            known_children = self.by_parent().get(parent_id, [])
271
 
        for child in known_children:
272
 
            if self.final_name(child) == name:
273
 
                return True
274
 
        parent_path = self._tree_id_paths.get(parent_id, None)
275
 
        if parent_path is None:
276
 
            # No parent... no children
277
 
            return False
278
 
        child_path = joinpath(parent_path, name)
279
 
        child_id = self._tree_path_ids.get(child_path, None)
280
 
        if child_id is None:
281
 
            # Not known by the tree transform yet, check the filesystem
282
 
            return osutils.lexists(self._tree.abspath(child_path))
283
 
        else:
284
 
            raise AssertionError('child_id is missing: %s, %s, %s'
285
 
                                 % (name, parent_id, child_id))
286
 
 
287
 
    def _available_backup_name(self, name, target_id):
288
 
        """Find an available backup name.
289
 
 
290
 
        :param name: The basename of the file.
291
 
 
292
 
        :param target_id: The directory trans_id where the backup should
293
 
            be placed.
294
 
        """
295
 
        known_children = self.by_parent().get(target_id, [])
296
 
        return osutils.available_backup_name(
297
 
            name,
298
 
            lambda base: self._has_named_child(
299
 
                base, target_id, known_children))
300
 
 
301
 
    def _parent_loops(self):
302
 
        """No entry should be its own ancestor"""
303
 
        for trans_id in self._new_parent:
304
 
            seen = set()
305
 
            parent_id = trans_id
306
 
            while parent_id != ROOT_PARENT:
307
 
                seen.add(parent_id)
308
 
                try:
309
 
                    parent_id = self.final_parent(parent_id)
310
 
                except KeyError:
311
 
                    break
312
 
                if parent_id == trans_id:
313
 
                    yield ('parent loop', trans_id)
314
 
                if parent_id in seen:
315
 
                    break
316
 
 
317
 
    def _improper_versioning(self):
318
 
        """Cannot version a file with no contents, or a bad type.
319
 
 
320
 
        However, existing entries with no contents are okay.
321
 
        """
322
 
        for trans_id in self._versioned:
323
 
            kind = self.final_kind(trans_id)
324
 
            if kind == 'symlink' and not self._tree.supports_symlinks():
325
 
                # Ignore symlinks as they are not supported on this platform
326
 
                continue
327
 
            if kind is None:
328
 
                yield ('versioning no contents', trans_id)
329
 
                continue
330
 
            if not self._tree.versionable_kind(kind):
331
 
                yield ('versioning bad kind', trans_id, kind)
332
 
 
333
 
    def _executability_conflicts(self):
334
 
        """Check for bad executability changes.
335
 
 
336
 
        Only versioned files may have their executability set, because
337
 
        1. only versioned entries can have executability under windows
338
 
        2. only files can be executable.  (The execute bit on a directory
339
 
           does not indicate searchability)
340
 
        """
341
 
        for trans_id in self._new_executability:
342
 
            if not self.final_is_versioned(trans_id):
343
 
                yield ('unversioned executability', trans_id)
344
 
            else:
345
 
                if self.final_kind(trans_id) != "file":
346
 
                    yield ('non-file executability', trans_id)
347
 
 
348
 
    def _overwrite_conflicts(self):
349
 
        """Check for overwrites (not permitted on Win32)"""
350
 
        for trans_id in self._new_contents:
351
 
            if self.tree_kind(trans_id) is None:
352
 
                continue
353
 
            if trans_id not in self._removed_contents:
354
 
                yield ('overwrite', trans_id, self.final_name(trans_id))
355
 
 
356
 
    def _duplicate_entries(self, by_parent):
357
 
        """No directory may have two entries with the same name."""
358
 
        if (self._new_name, self._new_parent) == ({}, {}):
359
 
            return
360
 
        for children in by_parent.values():
361
 
            name_ids = []
362
 
            for child_tid in children:
363
 
                name = self.final_name(child_tid)
364
 
                if name is not None:
365
 
                    # Keep children only if they still exist in the end
366
 
                    if not self._case_sensitive_target:
367
 
                        name = name.lower()
368
 
                    name_ids.append((name, child_tid))
369
 
            name_ids.sort()
370
 
            last_name = None
371
 
            last_trans_id = None
372
 
            for name, trans_id in name_ids:
373
 
                kind = self.final_kind(trans_id)
374
 
                if kind is None and not self.final_is_versioned(trans_id):
375
 
                    continue
376
 
                if name == last_name:
377
 
                    yield ('duplicate', last_trans_id, trans_id, name)
378
 
                last_name = name
379
 
                last_trans_id = trans_id
380
 
 
381
 
    def _parent_type_conflicts(self, by_parent):
382
 
        """Children must have a directory parent"""
383
 
        for parent_id, children in by_parent.items():
384
 
            if parent_id == ROOT_PARENT:
385
 
                continue
386
 
            no_children = True
387
 
            for child_id in children:
388
 
                if self.final_kind(child_id) is not None:
389
 
                    no_children = False
390
 
                    break
391
 
            if no_children:
392
 
                continue
393
 
            # There is at least a child, so we need an existing directory to
394
 
            # contain it.
395
 
            kind = self.final_kind(parent_id)
396
 
            if kind is None:
397
 
                # The directory will be deleted
398
 
                yield ('missing parent', parent_id)
399
 
            elif kind != "directory":
400
 
                # Meh, we need a *directory* to put something in it
401
 
                yield ('non-directory parent', parent_id)
402
 
 
403
 
    def _set_executability(self, path, trans_id):
404
 
        """Set the executability of versioned files """
405
 
        if self._tree._supports_executable():
406
 
            new_executability = self._new_executability[trans_id]
407
 
            abspath = self._tree.abspath(path)
408
 
            current_mode = os.stat(abspath).st_mode
409
 
            if new_executability:
410
 
                umask = os.umask(0)
411
 
                os.umask(umask)
412
 
                to_mode = current_mode | (0o100 & ~umask)
413
 
                # Enable x-bit for others only if they can read it.
414
 
                if current_mode & 0o004:
415
 
                    to_mode |= 0o001 & ~umask
416
 
                if current_mode & 0o040:
417
 
                    to_mode |= 0o010 & ~umask
418
 
            else:
419
 
                to_mode = current_mode & ~0o111
420
 
            osutils.chmod_if_possible(abspath, to_mode)
421
 
 
422
 
    def _new_entry(self, name, parent_id, file_id):
423
 
        """Helper function to create a new filesystem entry."""
424
 
        trans_id = self.create_path(name, parent_id)
425
 
        if file_id is not None:
426
 
            self.version_file(trans_id, file_id=file_id)
427
 
        return trans_id
428
 
 
429
 
    def new_file(self, name, parent_id, contents, file_id=None,
430
 
                 executable=None, sha1=None):
431
 
        """Convenience method to create files.
432
 
 
433
 
        name is the name of the file to create.
434
 
        parent_id is the transaction id of the parent directory of the file.
435
 
        contents is an iterator of bytestrings, which will be used to produce
436
 
        the file.
437
 
        :param file_id: The inventory ID of the file, if it is to be versioned.
438
 
        :param executable: Only valid when a file_id has been supplied.
439
 
        """
440
 
        trans_id = self._new_entry(name, parent_id, file_id)
441
 
        # TODO: rather than scheduling a set_executable call,
442
 
        # have create_file create the file with the right mode.
443
 
        self.create_file(contents, trans_id, sha1=sha1)
444
 
        if executable is not None:
445
 
            self.set_executability(executable, trans_id)
446
 
        return trans_id
447
 
 
448
 
    def new_directory(self, name, parent_id, file_id=None):
449
 
        """Convenience method to create directories.
450
 
 
451
 
        name is the name of the directory to create.
452
 
        parent_id is the transaction id of the parent directory of the
453
 
        directory.
454
 
        file_id is the inventory ID of the directory, if it is to be versioned.
455
 
        """
456
 
        trans_id = self._new_entry(name, parent_id, file_id)
457
 
        self.create_directory(trans_id)
458
 
        return trans_id
459
 
 
460
 
    def new_symlink(self, name, parent_id, target, file_id=None):
461
 
        """Convenience method to create symbolic link.
462
 
 
463
 
        name is the name of the symlink to create.
464
 
        parent_id is the transaction id of the parent directory of the symlink.
465
 
        target is a bytestring of the target of the symlink.
466
 
        file_id is the inventory ID of the file, if it is to be versioned.
467
 
        """
468
 
        trans_id = self._new_entry(name, parent_id, file_id)
469
 
        self.create_symlink(target, trans_id)
470
 
        return trans_id
471
 
 
472
 
    def new_orphan(self, trans_id, parent_id):
473
 
        """Schedule an item to be orphaned.
474
 
 
475
 
        When a directory is about to be removed, its children, if they are not
476
 
        versioned are moved out of the way: they don't have a parent anymore.
477
 
 
478
 
        :param trans_id: The trans_id of the existing item.
479
 
        :param parent_id: The parent trans_id of the item.
480
 
        """
481
 
        raise NotImplementedError(self.new_orphan)
482
 
 
483
 
    def _get_potential_orphans(self, dir_id):
484
 
        """Find the potential orphans in a directory.
485
 
 
486
 
        A directory can't be safely deleted if there are versioned files in it.
487
 
        If all the contained files are unversioned then they can be orphaned.
488
 
 
489
 
        The 'None' return value means that the directory contains at least one
490
 
        versioned file and should not be deleted.
491
 
 
492
 
        :param dir_id: The directory trans id.
493
 
 
494
 
        :return: A list of the orphan trans ids or None if at least one
495
 
             versioned file is present.
496
 
        """
497
 
        orphans = []
498
 
        # Find the potential orphans, stop if one item should be kept
499
 
        for child_tid in self.by_parent()[dir_id]:
500
 
            if child_tid in self._removed_contents:
501
 
                # The child is removed as part of the transform. Since it was
502
 
                # versioned before, it's not an orphan
503
 
                continue
504
 
            if not self.final_is_versioned(child_tid):
505
 
                # The child is not versioned
506
 
                orphans.append(child_tid)
507
 
            else:
508
 
                # We have a versioned file here, searching for orphans is
509
 
                # meaningless.
510
 
                orphans = None
511
 
                break
512
 
        return orphans
513
 
 
514
 
    def _affected_ids(self):
515
 
        """Return the set of transform ids affected by the transform"""
516
 
        trans_ids = set(self._removed_id)
517
 
        trans_ids.update(self._versioned)
518
 
        trans_ids.update(self._removed_contents)
519
 
        trans_ids.update(self._new_contents)
520
 
        trans_ids.update(self._new_executability)
521
 
        trans_ids.update(self._new_name)
522
 
        trans_ids.update(self._new_parent)
523
 
        return trans_ids
524
 
 
525
 
    def iter_changes(self, want_unversioned=False):
526
 
        """Produce output in the same format as Tree.iter_changes.
527
 
 
528
 
        Will produce nonsensical results if invoked while inventory/filesystem
529
 
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
530
 
        """
531
 
        final_paths = FinalPaths(self)
532
 
        trans_ids = self._affected_ids()
533
 
        results = []
534
 
        # Now iterate through all active paths
535
 
        for trans_id in trans_ids:
536
 
            from_path = self.tree_path(trans_id)
537
 
            modified = False
538
 
            # find file ids, and determine versioning state
539
 
            if from_path is None:
540
 
                from_versioned = False
541
 
            else:
542
 
                from_versioned = self._tree.is_versioned(from_path)
543
 
            if not want_unversioned and not from_versioned:
544
 
                from_path = None
545
 
            to_path = final_paths.get_path(trans_id)
546
 
            if to_path is None:
547
 
                to_versioned = False
548
 
            else:
549
 
                to_versioned = self.final_is_versioned(trans_id)
550
 
            if not want_unversioned and not to_versioned:
551
 
                to_path = None
552
 
 
553
 
            if from_versioned:
554
 
                # get data from working tree if versioned
555
 
                from_entry = next(self._tree.iter_entries_by_dir(
556
 
                    specific_files=[from_path]))[1]
557
 
                from_name = from_entry.name
558
 
            else:
559
 
                from_entry = None
560
 
                if from_path is None:
561
 
                    # File does not exist in FROM state
562
 
                    from_name = None
563
 
                else:
564
 
                    # File exists, but is not versioned.  Have to use path-
565
 
                    # splitting stuff
566
 
                    from_name = os.path.basename(from_path)
567
 
            if from_path is not None:
568
 
                from_kind, from_executable, from_stats = \
569
 
                    self._tree._comparison_data(from_entry, from_path)
570
 
            else:
571
 
                from_kind = None
572
 
                from_executable = False
573
 
 
574
 
            to_name = self.final_name(trans_id)
575
 
            to_kind = self.final_kind(trans_id)
576
 
            if trans_id in self._new_executability:
577
 
                to_executable = self._new_executability[trans_id]
578
 
            else:
579
 
                to_executable = from_executable
580
 
 
581
 
            if from_versioned and from_kind != to_kind:
582
 
                modified = True
583
 
            elif to_kind in ('file', 'symlink') and (
584
 
                    trans_id in self._new_contents):
585
 
                modified = True
586
 
            if (not modified and from_versioned == to_versioned
587
 
                and from_path == to_path
588
 
                and from_name == to_name
589
 
                    and from_executable == to_executable):
590
 
                continue
591
 
            if (from_path, to_path) == (None, None):
592
 
                continue
593
 
            results.append(
594
 
                TreeChange(
595
 
                    (from_path, to_path), modified,
596
 
                    (from_versioned, to_versioned),
597
 
                    (from_name, to_name),
598
 
                    (from_kind, to_kind),
599
 
                    (from_executable, to_executable)))
600
 
 
601
 
        def path_key(c):
602
 
            return (c.path[0] or '', c.path[1] or '')
603
 
        return iter(sorted(results, key=path_key))
604
 
 
605
 
    def get_preview_tree(self):
606
 
        """Return a tree representing the result of the transform.
607
 
 
608
 
        The tree is a snapshot, and altering the TreeTransform will invalidate
609
 
        it.
610
 
        """
611
 
        return GitPreviewTree(self)
612
 
 
613
 
    def commit(self, branch, message, merge_parents=None, strict=False,
614
 
               timestamp=None, timezone=None, committer=None, authors=None,
615
 
               revprops=None, revision_id=None):
616
 
        """Commit the result of this TreeTransform to a branch.
617
 
 
618
 
        :param branch: The branch to commit to.
619
 
        :param message: The message to attach to the commit.
620
 
        :param merge_parents: Additional parent revision-ids specified by
621
 
            pending merges.
622
 
        :param strict: If True, abort the commit if there are unversioned
623
 
            files.
624
 
        :param timestamp: if not None, seconds-since-epoch for the time and
625
 
            date.  (May be a float.)
626
 
        :param timezone: Optional timezone for timestamp, as an offset in
627
 
            seconds.
628
 
        :param committer: Optional committer in email-id format.
629
 
            (e.g. "J Random Hacker <jrandom@example.com>")
630
 
        :param authors: Optional list of authors in email-id format.
631
 
        :param revprops: Optional dictionary of revision properties.
632
 
        :param revision_id: Optional revision id.  (Specifying a revision-id
633
 
            may reduce performance for some non-native formats.)
634
 
        :return: The revision_id of the revision committed.
635
 
        """
636
 
        self._check_malformed()
637
 
        if strict:
638
 
            unversioned = set(self._new_contents).difference(set(self._versioned))
639
 
            for trans_id in unversioned:
640
 
                if not self.final_is_versioned(trans_id):
641
 
                    raise errors.StrictCommitFailed()
642
 
 
643
 
        revno, last_rev_id = branch.last_revision_info()
644
 
        if last_rev_id == _mod_revision.NULL_REVISION:
645
 
            if merge_parents is not None:
646
 
                raise ValueError('Cannot supply merge parents for first'
647
 
                                 ' commit.')
648
 
            parent_ids = []
649
 
        else:
650
 
            parent_ids = [last_rev_id]
651
 
            if merge_parents is not None:
652
 
                parent_ids.extend(merge_parents)
653
 
        if self._tree.get_revision_id() != last_rev_id:
654
 
            raise ValueError('TreeTransform not based on branch basis: %s' %
655
 
                             self._tree.get_revision_id().decode('utf-8'))
656
 
        from .. import commit
657
 
        revprops = commit.Commit.update_revprops(revprops, branch, authors)
658
 
        builder = branch.get_commit_builder(parent_ids,
659
 
                                            timestamp=timestamp,
660
 
                                            timezone=timezone,
661
 
                                            committer=committer,
662
 
                                            revprops=revprops,
663
 
                                            revision_id=revision_id)
664
 
        preview = self.get_preview_tree()
665
 
        list(builder.record_iter_changes(preview, last_rev_id,
666
 
                                         self.iter_changes()))
667
 
        builder.finish_inventory()
668
 
        revision_id = builder.commit(message)
669
 
        branch.set_last_revision_info(revno + 1, revision_id)
670
 
        return revision_id
671
 
 
672
 
    def _text_parent(self, trans_id):
673
 
        path = self.tree_path(trans_id)
674
 
        try:
675
 
            if path is None or self._tree.kind(path) != 'file':
676
 
                return None
677
 
        except errors.NoSuchFile:
678
 
            return None
679
 
        return path
680
 
 
681
 
    def _get_parents_texts(self, trans_id):
682
 
        """Get texts for compression parents of this file."""
683
 
        path = self._text_parent(trans_id)
684
 
        if path is None:
685
 
            return ()
686
 
        return (self._tree.get_file_text(path),)
687
 
 
688
 
    def _get_parents_lines(self, trans_id):
689
 
        """Get lines for compression parents of this file."""
690
 
        path = self._text_parent(trans_id)
691
 
        if path is None:
692
 
            return ()
693
 
        return (self._tree.get_file_lines(path),)
694
 
 
695
 
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
696
 
        """Schedule creation of a new file.
697
 
 
698
 
        :seealso: new_file.
699
 
 
700
 
        :param contents: an iterator of strings, all of which will be written
701
 
            to the target destination.
702
 
        :param trans_id: TreeTransform handle
703
 
        :param mode_id: If not None, force the mode of the target file to match
704
 
            the mode of the object referenced by mode_id.
705
 
            Otherwise, we will try to preserve mode bits of an existing file.
706
 
        :param sha1: If the sha1 of this content is already known, pass it in.
707
 
            We can use it to prevent future sha1 computations.
708
 
        """
709
 
        raise NotImplementedError(self.create_file)
710
 
 
711
 
    def create_directory(self, trans_id):
712
 
        """Schedule creation of a new directory.
713
 
 
714
 
        See also new_directory.
715
 
        """
716
 
        raise NotImplementedError(self.create_directory)
717
 
 
718
 
    def create_symlink(self, target, trans_id):
719
 
        """Schedule creation of a new symbolic link.
720
 
 
721
 
        target is a bytestring.
722
 
        See also new_symlink.
723
 
        """
724
 
        raise NotImplementedError(self.create_symlink)
725
 
 
726
 
    def create_hardlink(self, path, trans_id):
727
 
        """Schedule creation of a hard link"""
728
 
        raise NotImplementedError(self.create_hardlink)
729
 
 
730
 
    def cancel_creation(self, trans_id):
731
 
        """Cancel the creation of new file contents."""
732
 
        raise NotImplementedError(self.cancel_creation)
733
 
 
734
 
    def apply(self, no_conflicts=False, _mover=None):
735
 
        """Apply all changes to the inventory and filesystem.
736
 
 
737
 
        If filesystem or inventory conflicts are present, MalformedTransform
738
 
        will be thrown.
739
 
 
740
 
        If apply succeeds, finalize is not necessary.
741
 
 
742
 
        :param no_conflicts: if True, the caller guarantees there are no
743
 
            conflicts, so no check is made.
744
 
        :param _mover: Supply an alternate FileMover, for testing
745
 
        """
746
 
        raise NotImplementedError(self.apply)
747
 
 
748
 
    def cook_conflicts(self, raw_conflicts):
749
 
        """Generate a list of cooked conflicts, sorted by file path"""
750
 
        if not raw_conflicts:
751
 
            return
752
 
        fp = FinalPaths(self)
753
 
        from .workingtree import TextConflict
754
 
        for c in raw_conflicts:
755
 
            if c[0] == 'text conflict':
756
 
                yield TextConflict(fp.get_path(c[1]))
757
 
            elif c[0] == 'duplicate':
758
 
                yield TextConflict(fp.get_path(c[2]))
759
 
            elif c[0] == 'contents conflict':
760
 
                yield TextConflict(fp.get_path(c[1][0]))
761
 
            elif c[0] == 'missing parent':
762
 
                # TODO(jelmer): This should not make it to here
763
 
                yield TextConflict(fp.get_path(c[2]))
764
 
            elif c[0] == 'non-directory parent':
765
 
                yield TextConflict(fp.get_path(c[2]))
766
 
            elif c[0] == 'deleting parent':
767
 
                # TODO(jelmer): This should not make it to here
768
 
                yield TextConflict(fp.get_path(c[2]))
769
 
            elif c[0] == 'parent loop':
770
 
                # TODO(jelmer): This should not make it to here
771
 
                yield TextConflict(fp.get_path(c[2]))
772
 
            elif c[0] == 'path conflict':
773
 
                yield TextConflict(fp.get_path(c[1]))
774
 
            else:
775
 
                raise AssertionError('unknown conflict %s' % c[0])
776
 
 
777
 
 
778
 
class DiskTreeTransform(TreeTransformBase):
779
 
    """Tree transform storing its contents on disk."""
780
 
 
781
 
    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
782
 
        """Constructor.
783
 
        :param tree: The tree that will be transformed, but not necessarily
784
 
            the output tree.
785
 
        :param limbodir: A directory where new files can be stored until
786
 
            they are installed in their proper places
787
 
        :param pb: ignored
788
 
        :param case_sensitive: If True, the target of the transform is
789
 
            case sensitive, not just case preserving.
790
 
        """
791
 
        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
792
 
        self._limbodir = limbodir
793
 
        self._deletiondir = None
794
 
        # A mapping of transform ids to their limbo filename
795
 
        self._limbo_files = {}
796
 
        self._possibly_stale_limbo_files = set()
797
 
        # A mapping of transform ids to a set of the transform ids of children
798
 
        # that their limbo directory has
799
 
        self._limbo_children = {}
800
 
        # Map transform ids to maps of child filename to child transform id
801
 
        self._limbo_children_names = {}
802
 
        # List of transform ids that need to be renamed from limbo into place
803
 
        self._needs_rename = set()
804
 
        self._creation_mtime = None
805
 
        self._create_symlinks = osutils.supports_symlinks(self._limbodir)
806
 
 
807
 
    def finalize(self):
808
 
        """Release the working tree lock, if held, clean up limbo dir.
809
 
 
810
 
        This is required if apply has not been invoked, but can be invoked
811
 
        even after apply.
812
 
        """
813
 
        if self._tree is None:
814
 
            return
815
 
        try:
816
 
            limbo_paths = list(self._limbo_files.values())
817
 
            limbo_paths.extend(self._possibly_stale_limbo_files)
818
 
            limbo_paths.sort(reverse=True)
819
 
            for path in limbo_paths:
820
 
                try:
821
 
                    osutils.delete_any(path)
822
 
                except OSError as e:
823
 
                    if e.errno != errno.ENOENT:
824
 
                        raise
825
 
                    # XXX: warn? perhaps we just got interrupted at an
826
 
                    # inconvenient moment, but perhaps files are disappearing
827
 
                    # from under us?
828
 
            try:
829
 
                osutils.delete_any(self._limbodir)
830
 
            except OSError:
831
 
                # We don't especially care *why* the dir is immortal.
832
 
                raise ImmortalLimbo(self._limbodir)
833
 
            try:
834
 
                if self._deletiondir is not None:
835
 
                    osutils.delete_any(self._deletiondir)
836
 
            except OSError:
837
 
                raise errors.ImmortalPendingDeletion(self._deletiondir)
838
 
        finally:
839
 
            TreeTransformBase.finalize(self)
840
 
 
841
 
    def _limbo_supports_executable(self):
842
 
        """Check if the limbo path supports the executable bit."""
843
 
        return osutils.supports_executable(self._limbodir)
844
 
 
845
 
    def _limbo_name(self, trans_id):
846
 
        """Generate the limbo name of a file"""
847
 
        limbo_name = self._limbo_files.get(trans_id)
848
 
        if limbo_name is None:
849
 
            limbo_name = self._generate_limbo_path(trans_id)
850
 
            self._limbo_files[trans_id] = limbo_name
851
 
        return limbo_name
852
 
 
853
 
    def _generate_limbo_path(self, trans_id):
854
 
        """Generate a limbo path using the trans_id as the relative path.
855
 
 
856
 
        This is suitable as a fallback, and when the transform should not be
857
 
        sensitive to the path encoding of the limbo directory.
858
 
        """
859
 
        self._needs_rename.add(trans_id)
860
 
        return osutils.pathjoin(self._limbodir, trans_id)
861
 
 
862
 
    def adjust_path(self, name, parent, trans_id):
863
 
        previous_parent = self._new_parent.get(trans_id)
864
 
        previous_name = self._new_name.get(trans_id)
865
 
        super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
866
 
        if (trans_id in self._limbo_files
867
 
                and trans_id not in self._needs_rename):
868
 
            self._rename_in_limbo([trans_id])
869
 
            if previous_parent != parent:
870
 
                self._limbo_children[previous_parent].remove(trans_id)
871
 
            if previous_parent != parent or previous_name != name:
872
 
                del self._limbo_children_names[previous_parent][previous_name]
873
 
 
874
 
    def _rename_in_limbo(self, trans_ids):
875
 
        """Fix limbo names so that the right final path is produced.
876
 
 
877
 
        This means we outsmarted ourselves-- we tried to avoid renaming
878
 
        these files later by creating them with their final names in their
879
 
        final parents.  But now the previous name or parent is no longer
880
 
        suitable, so we have to rename them.
881
 
 
882
 
        Even for trans_ids that have no new contents, we must remove their
883
 
        entries from _limbo_files, because they are now stale.
884
 
        """
885
 
        for trans_id in trans_ids:
886
 
            old_path = self._limbo_files[trans_id]
887
 
            self._possibly_stale_limbo_files.add(old_path)
888
 
            del self._limbo_files[trans_id]
889
 
            if trans_id not in self._new_contents:
890
 
                continue
891
 
            new_path = self._limbo_name(trans_id)
892
 
            os.rename(old_path, new_path)
893
 
            self._possibly_stale_limbo_files.remove(old_path)
894
 
            for descendant in self._limbo_descendants(trans_id):
895
 
                desc_path = self._limbo_files[descendant]
896
 
                desc_path = new_path + desc_path[len(old_path):]
897
 
                self._limbo_files[descendant] = desc_path
898
 
 
899
 
    def _limbo_descendants(self, trans_id):
900
 
        """Return the set of trans_ids whose limbo paths descend from this."""
901
 
        descendants = set(self._limbo_children.get(trans_id, []))
902
 
        for descendant in list(descendants):
903
 
            descendants.update(self._limbo_descendants(descendant))
904
 
        return descendants
905
 
 
906
 
    def _set_mode(self, trans_id, mode_id, typefunc):
907
 
        raise NotImplementedError(self._set_mode)
908
 
 
909
 
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
910
 
        """Schedule creation of a new file.
911
 
 
912
 
        :seealso: new_file.
913
 
 
914
 
        :param contents: an iterator of strings, all of which will be written
915
 
            to the target destination.
916
 
        :param trans_id: TreeTransform handle
917
 
        :param mode_id: If not None, force the mode of the target file to match
918
 
            the mode of the object referenced by mode_id.
919
 
            Otherwise, we will try to preserve mode bits of an existing file.
920
 
        :param sha1: If the sha1 of this content is already known, pass it in.
921
 
            We can use it to prevent future sha1 computations.
922
 
        """
923
 
        name = self._limbo_name(trans_id)
924
 
        with open(name, 'wb') as f:
925
 
            unique_add(self._new_contents, trans_id, 'file')
926
 
            f.writelines(contents)
927
 
        self._set_mtime(name)
928
 
        self._set_mode(trans_id, mode_id, S_ISREG)
929
 
        # It is unfortunate we have to use lstat instead of fstat, but we just
930
 
        # used utime and chmod on the file, so we need the accurate final
931
 
        # details.
932
 
        if sha1 is not None:
933
 
            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
934
 
 
935
 
    def _read_symlink_target(self, trans_id):
936
 
        return os.readlink(self._limbo_name(trans_id))
937
 
 
938
 
    def _set_mtime(self, path):
939
 
        """All files that are created get the same mtime.
940
 
 
941
 
        This time is set by the first object to be created.
942
 
        """
943
 
        if self._creation_mtime is None:
944
 
            self._creation_mtime = time.time()
945
 
        os.utime(path, (self._creation_mtime, self._creation_mtime))
946
 
 
947
 
    def create_hardlink(self, path, trans_id):
948
 
        """Schedule creation of a hard link"""
949
 
        name = self._limbo_name(trans_id)
950
 
        try:
951
 
            os.link(path, name)
952
 
        except OSError as e:
953
 
            if e.errno != errno.EPERM:
954
 
                raise
955
 
            raise errors.HardLinkNotSupported(path)
956
 
        try:
957
 
            unique_add(self._new_contents, trans_id, 'file')
958
 
        except BaseException:
959
 
            # Clean up the file, it never got registered so
960
 
            # TreeTransform.finalize() won't clean it up.
961
 
            os.unlink(name)
962
 
            raise
963
 
 
964
 
    def create_directory(self, trans_id):
965
 
        """Schedule creation of a new directory.
966
 
 
967
 
        See also new_directory.
968
 
        """
969
 
        os.mkdir(self._limbo_name(trans_id))
970
 
        unique_add(self._new_contents, trans_id, 'directory')
971
 
 
972
 
    def create_symlink(self, target, trans_id):
973
 
        """Schedule creation of a new symbolic link.
974
 
 
975
 
        target is a bytestring.
976
 
        See also new_symlink.
977
 
        """
978
 
        if self._create_symlinks:
979
 
            os.symlink(target, self._limbo_name(trans_id))
980
 
        else:
981
 
            try:
982
 
                path = FinalPaths(self).get_path(trans_id)
983
 
            except KeyError:
984
 
                path = None
985
 
            trace.warning(
986
 
                'Unable to create symlink "%s" on this filesystem.' % (path,))
987
 
            self._symlink_target[trans_id] = target
988
 
        # We add symlink to _new_contents even if they are unsupported
989
 
        # and not created. These entries are subsequently used to avoid
990
 
        # conflicts on platforms that don't support symlink
991
 
        unique_add(self._new_contents, trans_id, 'symlink')
992
 
 
993
 
    def cancel_creation(self, trans_id):
994
 
        """Cancel the creation of new file contents."""
995
 
        del self._new_contents[trans_id]
996
 
        if trans_id in self._observed_sha1s:
997
 
            del self._observed_sha1s[trans_id]
998
 
        children = self._limbo_children.get(trans_id)
999
 
        # if this is a limbo directory with children, move them before removing
1000
 
        # the directory
1001
 
        if children is not None:
1002
 
            self._rename_in_limbo(children)
1003
 
            del self._limbo_children[trans_id]
1004
 
            del self._limbo_children_names[trans_id]
1005
 
        osutils.delete_any(self._limbo_name(trans_id))
1006
 
 
1007
 
    def new_orphan(self, trans_id, parent_id):
1008
 
        conf = self._tree.get_config_stack()
1009
 
        handle_orphan = conf.get('transform.orphan_policy')
1010
 
        handle_orphan(self, trans_id, parent_id)
1011
 
 
1012
 
    def final_entry(self, trans_id):
1013
 
        is_versioned = self.final_is_versioned(trans_id)
1014
 
        fp = FinalPaths(self)
1015
 
        tree_path = fp.get_path(trans_id)
1016
 
        if trans_id in self._new_contents:
1017
 
            path = self._limbo_name(trans_id)
1018
 
            st = os.lstat(path)
1019
 
            kind = mode_kind(st.st_mode)
1020
 
            name = self.final_name(trans_id)
1021
 
            file_id = self._tree.mapping.generate_file_id(tree_path)
1022
 
            parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1023
 
            if kind == 'directory':
1024
 
                return GitTreeDirectory(
1025
 
                    file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1026
 
            executable = mode_is_executable(st.st_mode)
1027
 
            mode = object_mode(kind, executable)
1028
 
            blob = blob_from_path_and_stat(encode_git_path(path), st)
1029
 
            if kind == 'symlink':
1030
 
                return GitTreeSymlink(
1031
 
                    file_id, name, parent_id,
1032
 
                    decode_git_path(blob.data)), is_versioned
1033
 
            elif kind == 'file':
1034
 
                return GitTreeFile(
1035
 
                    file_id, name, executable=executable, parent_id=parent_id,
1036
 
                    git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1037
 
            else:
1038
 
                raise AssertionError(kind)
1039
 
        elif trans_id in self._removed_contents:
1040
 
            return None, None
1041
 
        else:
1042
 
            orig_path = self.tree_path(trans_id)
1043
 
            if orig_path is None:
1044
 
                return None, None
1045
 
            file_id = self._tree.mapping.generate_file_id(tree_path)
1046
 
            if tree_path == '':
1047
 
                parent_id = None
1048
 
            else:
1049
 
                parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1050
 
            try:
1051
 
                ie = next(self._tree.iter_entries_by_dir(
1052
 
                    specific_files=[orig_path]))[1]
1053
 
                ie.file_id = file_id
1054
 
                ie.parent_id = parent_id
1055
 
                return ie, is_versioned
1056
 
            except StopIteration:
1057
 
                try:
1058
 
                    if self.tree_kind(trans_id) == 'directory':
1059
 
                        return GitTreeDirectory(
1060
 
                            file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1061
 
                except OSError as e:
1062
 
                    if e.errno != errno.ENOTDIR:
1063
 
                        raise
1064
 
                return None, None
1065
 
 
1066
 
    def final_git_entry(self, trans_id):
1067
 
        if trans_id in self._new_contents:
1068
 
            path = self._limbo_name(trans_id)
1069
 
            st = os.lstat(path)
1070
 
            kind = mode_kind(st.st_mode)
1071
 
            if kind == 'directory':
1072
 
                return None, None
1073
 
            executable = mode_is_executable(st.st_mode)
1074
 
            mode = object_mode(kind, executable)
1075
 
            blob = blob_from_path_and_stat(encode_git_path(path), st)
1076
 
        elif trans_id in self._removed_contents:
1077
 
            return None, None
1078
 
        else:
1079
 
            orig_path = self.tree_path(trans_id)
1080
 
            kind = self._tree.kind(orig_path)
1081
 
            executable = self._tree.is_executable(orig_path)
1082
 
            mode = object_mode(kind, executable)
1083
 
            if kind == 'symlink':
1084
 
                contents = self._tree.get_symlink_target(orig_path)
1085
 
            elif kind == 'file':
1086
 
                contents = self._tree.get_file_text(orig_path)
1087
 
            elif kind == 'directory':
1088
 
                return None, None
1089
 
            else:
1090
 
                raise AssertionError(kind)
1091
 
            blob = Blob.from_string(contents)
1092
 
        return blob, mode
1093
 
 
1094
 
 
1095
 
class GitTreeTransform(DiskTreeTransform):
1096
 
    """Represent a tree transformation.
1097
 
 
1098
 
    This object is designed to support incremental generation of the transform,
1099
 
    in any order.
1100
 
 
1101
 
    However, it gives optimum performance when parent directories are created
1102
 
    before their contents.  The transform is then able to put child files
1103
 
    directly in their parent directory, avoiding later renames.
1104
 
 
1105
 
    It is easy to produce malformed transforms, but they are generally
1106
 
    harmless.  Attempting to apply a malformed transform will cause an
1107
 
    exception to be raised before any modifications are made to the tree.
1108
 
 
1109
 
    Many kinds of malformed transforms can be corrected with the
1110
 
    resolve_conflicts function.  The remaining ones indicate programming error,
1111
 
    such as trying to create a file with no path.
1112
 
 
1113
 
    Two sets of file creation methods are supplied.  Convenience methods are:
1114
 
     * new_file
1115
 
     * new_directory
1116
 
     * new_symlink
1117
 
 
1118
 
    These are composed of the low-level methods:
1119
 
     * create_path
1120
 
     * create_file or create_directory or create_symlink
1121
 
     * version_file
1122
 
     * set_executability
1123
 
 
1124
 
    Transform/Transaction ids
1125
 
    -------------------------
1126
 
    trans_ids are temporary ids assigned to all files involved in a transform.
1127
 
    It's possible, even common, that not all files in the Tree have trans_ids.
1128
 
 
1129
 
    trans_ids are used because filenames and file_ids are not good enough
1130
 
    identifiers; filenames change.
1131
 
 
1132
 
    trans_ids are only valid for the TreeTransform that generated them.
1133
 
 
1134
 
    Limbo
1135
 
    -----
1136
 
    Limbo is a temporary directory use to hold new versions of files.
1137
 
    Files are added to limbo by create_file, create_directory, create_symlink,
1138
 
    and their convenience variants (new_*).  Files may be removed from limbo
1139
 
    using cancel_creation.  Files are renamed from limbo into their final
1140
 
    location as part of TreeTransform.apply
1141
 
 
1142
 
    Limbo must be cleaned up, by either calling TreeTransform.apply or
1143
 
    calling TreeTransform.finalize.
1144
 
 
1145
 
    Files are placed into limbo inside their parent directories, where
1146
 
    possible.  This reduces subsequent renames, and makes operations involving
1147
 
    lots of files faster.  This optimization is only possible if the parent
1148
 
    directory is created *before* creating any of its children, so avoid
1149
 
    creating children before parents, where possible.
1150
 
 
1151
 
    Pending-deletion
1152
 
    ----------------
1153
 
    This temporary directory is used by _FileMover for storing files that are
1154
 
    about to be deleted.  In case of rollback, the files will be restored.
1155
 
    FileMover does not delete files until it is sure that a rollback will not
1156
 
    happen.
1157
 
    """
1158
 
 
1159
 
    def __init__(self, tree, pb=None):
1160
 
        """Note: a tree_write lock is taken on the tree.
1161
 
 
1162
 
        Use TreeTransform.finalize() to release the lock (can be omitted if
1163
 
        TreeTransform.apply() called).
1164
 
        """
1165
 
        tree.lock_tree_write()
1166
 
        try:
1167
 
            limbodir = urlutils.local_path_from_url(
1168
 
                tree._transport.abspath('limbo'))
1169
 
            osutils.ensure_empty_directory_exists(
1170
 
                limbodir,
1171
 
                errors.ExistingLimbo)
1172
 
            deletiondir = urlutils.local_path_from_url(
1173
 
                tree._transport.abspath('pending-deletion'))
1174
 
            osutils.ensure_empty_directory_exists(
1175
 
                deletiondir,
1176
 
                errors.ExistingPendingDeletion)
1177
 
        except BaseException:
1178
 
            tree.unlock()
1179
 
            raise
1180
 
 
1181
 
        # Cache of realpath results, to speed up canonical_path
1182
 
        self._realpaths = {}
1183
 
        # Cache of relpath results, to speed up canonical_path
1184
 
        self._relpaths = {}
1185
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
1186
 
                                   tree.case_sensitive)
1187
 
        self._deletiondir = deletiondir
1188
 
 
1189
 
    def canonical_path(self, path):
1190
 
        """Get the canonical tree-relative path"""
1191
 
        # don't follow final symlinks
1192
 
        abs = self._tree.abspath(path)
1193
 
        if abs in self._relpaths:
1194
 
            return self._relpaths[abs]
1195
 
        dirname, basename = os.path.split(abs)
1196
 
        if dirname not in self._realpaths:
1197
 
            self._realpaths[dirname] = os.path.realpath(dirname)
1198
 
        dirname = self._realpaths[dirname]
1199
 
        abs = osutils.pathjoin(dirname, basename)
1200
 
        if dirname in self._relpaths:
1201
 
            relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1202
 
            relpath = relpath.rstrip('/\\')
1203
 
        else:
1204
 
            relpath = self._tree.relpath(abs)
1205
 
        self._relpaths[abs] = relpath
1206
 
        return relpath
1207
 
 
1208
 
    def tree_kind(self, trans_id):
1209
 
        """Determine the file kind in the working tree.
1210
 
 
1211
 
        :returns: The file kind or None if the file does not exist
1212
 
        """
1213
 
        path = self._tree_id_paths.get(trans_id)
1214
 
        if path is None:
1215
 
            return None
1216
 
        try:
1217
 
            return osutils.file_kind(self._tree.abspath(path))
1218
 
        except errors.NoSuchFile:
1219
 
            return None
1220
 
 
1221
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1222
 
        """Set the mode of new file contents.
1223
 
        The mode_id is the existing file to get the mode from (often the same
1224
 
        as trans_id).  The operation is only performed if there's a mode match
1225
 
        according to typefunc.
1226
 
        """
1227
 
        if mode_id is None:
1228
 
            mode_id = trans_id
1229
 
        try:
1230
 
            old_path = self._tree_id_paths[mode_id]
1231
 
        except KeyError:
1232
 
            return
1233
 
        try:
1234
 
            mode = os.stat(self._tree.abspath(old_path)).st_mode
1235
 
        except OSError as e:
1236
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
1237
 
                # Either old_path doesn't exist, or the parent of the
1238
 
                # target is not a directory (but will be one eventually)
1239
 
                # Either way, we know it doesn't exist *right now*
1240
 
                # See also bug #248448
1241
 
                return
1242
 
            else:
1243
 
                raise
1244
 
        if typefunc(mode):
1245
 
            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1246
 
 
1247
 
    def iter_tree_children(self, parent_id):
1248
 
        """Iterate through the entry's tree children, if any"""
1249
 
        try:
1250
 
            path = self._tree_id_paths[parent_id]
1251
 
        except KeyError:
1252
 
            return
1253
 
        try:
1254
 
            children = os.listdir(self._tree.abspath(path))
1255
 
        except OSError as e:
1256
 
            if not (osutils._is_error_enotdir(e) or
1257
 
                    e.errno in (errno.ENOENT, errno.ESRCH)):
1258
 
                raise
1259
 
            return
1260
 
 
1261
 
        for child in children:
1262
 
            childpath = joinpath(path, child)
1263
 
            if self._tree.is_control_filename(childpath):
1264
 
                continue
1265
 
            yield self.trans_id_tree_path(childpath)
1266
 
 
1267
 
    def _generate_limbo_path(self, trans_id):
1268
 
        """Generate a limbo path using the final path if possible.
1269
 
 
1270
 
        This optimizes the performance of applying the tree transform by
1271
 
        avoiding renames.  These renames can be avoided only when the parent
1272
 
        directory is already scheduled for creation.
1273
 
 
1274
 
        If the final path cannot be used, falls back to using the trans_id as
1275
 
        the relpath.
1276
 
        """
1277
 
        parent = self._new_parent.get(trans_id)
1278
 
        # if the parent directory is already in limbo (e.g. when building a
1279
 
        # tree), choose a limbo name inside the parent, to reduce further
1280
 
        # renames.
1281
 
        use_direct_path = False
1282
 
        if self._new_contents.get(parent) == 'directory':
1283
 
            filename = self._new_name.get(trans_id)
1284
 
            if filename is not None:
1285
 
                if parent not in self._limbo_children:
1286
 
                    self._limbo_children[parent] = set()
1287
 
                    self._limbo_children_names[parent] = {}
1288
 
                    use_direct_path = True
1289
 
                # the direct path can only be used if no other file has
1290
 
                # already taken this pathname, i.e. if the name is unused, or
1291
 
                # if it is already associated with this trans_id.
1292
 
                elif self._case_sensitive_target:
1293
 
                    if (self._limbo_children_names[parent].get(filename)
1294
 
                            in (trans_id, None)):
1295
 
                        use_direct_path = True
1296
 
                else:
1297
 
                    for l_filename, l_trans_id in (
1298
 
                            self._limbo_children_names[parent].items()):
1299
 
                        if l_trans_id == trans_id:
1300
 
                            continue
1301
 
                        if l_filename.lower() == filename.lower():
1302
 
                            break
1303
 
                    else:
1304
 
                        use_direct_path = True
1305
 
 
1306
 
        if not use_direct_path:
1307
 
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
1308
 
 
1309
 
        limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1310
 
        self._limbo_children[parent].add(trans_id)
1311
 
        self._limbo_children_names[parent][filename] = trans_id
1312
 
        return limbo_name
1313
 
 
1314
 
    def cancel_versioning(self, trans_id):
1315
 
        """Undo a previous versioning of a file"""
1316
 
        self._versioned.remove(trans_id)
1317
 
 
1318
 
    def apply(self, no_conflicts=False, _mover=None):
1319
 
        """Apply all changes to the inventory and filesystem.
1320
 
 
1321
 
        If filesystem or inventory conflicts are present, MalformedTransform
1322
 
        will be thrown.
1323
 
 
1324
 
        If apply succeeds, finalize is not necessary.
1325
 
 
1326
 
        :param no_conflicts: if True, the caller guarantees there are no
1327
 
            conflicts, so no check is made.
1328
 
        :param _mover: Supply an alternate FileMover, for testing
1329
 
        """
1330
 
        for hook in MutableTree.hooks['pre_transform']:
1331
 
            hook(self._tree, self)
1332
 
        if not no_conflicts:
1333
 
            self._check_malformed()
1334
 
        self.rename_count = 0
1335
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1336
 
            child_pb.update(gettext('Apply phase'), 0, 2)
1337
 
            index_changes = self._generate_index_changes()
1338
 
            offset = 1
1339
 
            if _mover is None:
1340
 
                mover = _FileMover()
1341
 
            else:
1342
 
                mover = _mover
1343
 
            try:
1344
 
                child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1345
 
                self._apply_removals(mover)
1346
 
                child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1347
 
                modified_paths = self._apply_insertions(mover)
1348
 
            except BaseException:
1349
 
                mover.rollback()
1350
 
                raise
1351
 
            else:
1352
 
                mover.apply_deletions()
1353
 
        self._tree._apply_index_changes(index_changes)
1354
 
        self._done = True
1355
 
        self.finalize()
1356
 
        return _TransformResults(modified_paths, self.rename_count)
1357
 
 
1358
 
    def _apply_removals(self, mover):
1359
 
        """Perform tree operations that remove directory/inventory names.
1360
 
 
1361
 
        That is, delete files that are to be deleted, and put any files that
1362
 
        need renaming into limbo.  This must be done in strict child-to-parent
1363
 
        order.
1364
 
 
1365
 
        If inventory_delta is None, no inventory delta generation is performed.
1366
 
        """
1367
 
        tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
1368
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1369
 
            for num, (path, trans_id) in enumerate(tree_paths):
1370
 
                # do not attempt to move root into a subdirectory of itself.
1371
 
                if path == '':
1372
 
                    continue
1373
 
                child_pb.update(gettext('removing file'), num, len(tree_paths))
1374
 
                full_path = self._tree.abspath(path)
1375
 
                if trans_id in self._removed_contents:
1376
 
                    delete_path = os.path.join(self._deletiondir, trans_id)
1377
 
                    mover.pre_delete(full_path, delete_path)
1378
 
                elif (trans_id in self._new_name or
1379
 
                      trans_id in self._new_parent):
1380
 
                    try:
1381
 
                        mover.rename(full_path, self._limbo_name(trans_id))
1382
 
                    except TransformRenameFailed as e:
1383
 
                        if e.errno != errno.ENOENT:
1384
 
                            raise
1385
 
                    else:
1386
 
                        self.rename_count += 1
1387
 
 
1388
 
    def _apply_insertions(self, mover):
1389
 
        """Perform tree operations that insert directory/inventory names.
1390
 
 
1391
 
        That is, create any files that need to be created, and restore from
1392
 
        limbo any files that needed renaming.  This must be done in strict
1393
 
        parent-to-child order.
1394
 
 
1395
 
        If inventory_delta is None, no inventory delta is calculated, and
1396
 
        no list of modified paths is returned.
1397
 
        """
1398
 
        new_paths = self.new_paths(filesystem_only=True)
1399
 
        modified_paths = []
1400
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1401
 
            for num, (path, trans_id) in enumerate(new_paths):
1402
 
                if (num % 10) == 0:
1403
 
                    child_pb.update(gettext('adding file'),
1404
 
                                    num, len(new_paths))
1405
 
                full_path = self._tree.abspath(path)
1406
 
                if trans_id in self._needs_rename:
1407
 
                    try:
1408
 
                        mover.rename(self._limbo_name(trans_id), full_path)
1409
 
                    except TransformRenameFailed as e:
1410
 
                        # We may be renaming a dangling inventory id
1411
 
                        if e.errno != errno.ENOENT:
1412
 
                            raise
1413
 
                    else:
1414
 
                        self.rename_count += 1
1415
 
                    # TODO: if trans_id in self._observed_sha1s, we should
1416
 
                    #       re-stat the final target, since ctime will be
1417
 
                    #       updated by the change.
1418
 
                if (trans_id in self._new_contents
1419
 
                        or self.path_changed(trans_id)):
1420
 
                    if trans_id in self._new_contents:
1421
 
                        modified_paths.append(full_path)
1422
 
                if trans_id in self._new_executability:
1423
 
                    self._set_executability(path, trans_id)
1424
 
                if trans_id in self._observed_sha1s:
1425
 
                    o_sha1, o_st_val = self._observed_sha1s[trans_id]
1426
 
                    st = osutils.lstat(full_path)
1427
 
                    self._observed_sha1s[trans_id] = (o_sha1, st)
1428
 
        for path, trans_id in new_paths:
1429
 
            # new_paths includes stuff like workingtree conflicts. Only the
1430
 
            # stuff in new_contents actually comes from limbo.
1431
 
            if trans_id in self._limbo_files:
1432
 
                del self._limbo_files[trans_id]
1433
 
        self._new_contents.clear()
1434
 
        return modified_paths
1435
 
 
1436
 
    def _generate_index_changes(self):
1437
 
        """Generate an inventory delta for the current transform."""
1438
 
        removed_id = set(self._removed_id)
1439
 
        removed_id.update(self._removed_contents)
1440
 
        changes = {}
1441
 
        changed_ids = set()
1442
 
        for id_set in [self._new_name, self._new_parent,
1443
 
                       self._new_executability]:
1444
 
            changed_ids.update(id_set)
1445
 
        for id_set in [self._new_name, self._new_parent]:
1446
 
            removed_id.update(id_set)
1447
 
        # so does adding
1448
 
        changed_kind = set(self._new_contents)
1449
 
        # Ignore entries that are already known to have changed.
1450
 
        changed_kind.difference_update(changed_ids)
1451
 
        #  to keep only the truly changed ones
1452
 
        changed_kind = (t for t in changed_kind
1453
 
                        if self.tree_kind(t) != self.final_kind(t))
1454
 
        changed_ids.update(changed_kind)
1455
 
        for t in changed_kind:
1456
 
            if self.final_kind(t) == 'directory':
1457
 
                removed_id.add(t)
1458
 
                changed_ids.remove(t)
1459
 
        new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1460
 
        total_entries = len(new_paths) + len(removed_id)
1461
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1462
 
            for num, trans_id in enumerate(removed_id):
1463
 
                if (num % 10) == 0:
1464
 
                    child_pb.update(gettext('removing file'),
1465
 
                                    num, total_entries)
1466
 
                try:
1467
 
                    path = self._tree_id_paths[trans_id]
1468
 
                except KeyError:
1469
 
                    continue
1470
 
                changes[path] = (None, None, None, None)
1471
 
            for num, (path, trans_id) in enumerate(new_paths):
1472
 
                if (num % 10) == 0:
1473
 
                    child_pb.update(gettext('adding file'),
1474
 
                                    num + len(removed_id), total_entries)
1475
 
 
1476
 
                kind = self.final_kind(trans_id)
1477
 
                if kind is None:
1478
 
                    continue
1479
 
                versioned = self.final_is_versioned(trans_id)
1480
 
                if not versioned:
1481
 
                    continue
1482
 
                executability = self._new_executability.get(trans_id)
1483
 
                reference_revision = self._new_reference_revision.get(trans_id)
1484
 
                symlink_target = self._symlink_target.get(trans_id)
1485
 
                changes[path] = (
1486
 
                    kind, executability, reference_revision, symlink_target)
1487
 
        return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1488
 
 
1489
 
 
1490
 
class GitTransformPreview(GitTreeTransform):
1491
 
    """A TreeTransform for generating preview trees.
1492
 
 
1493
 
    Unlike TreeTransform, this version works when the input tree is a
1494
 
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
1495
 
    unversioned files in the input tree.
1496
 
    """
1497
 
 
1498
 
    def __init__(self, tree, pb=None, case_sensitive=True):
1499
 
        tree.lock_read()
1500
 
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1501
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1502
 
 
1503
 
    def canonical_path(self, path):
1504
 
        return path
1505
 
 
1506
 
    def tree_kind(self, trans_id):
1507
 
        path = self.tree_path(trans_id)
1508
 
        if path is None:
1509
 
            return None
1510
 
        kind = self._tree.path_content_summary(path)[0]
1511
 
        if kind == 'missing':
1512
 
            kind = None
1513
 
        return kind
1514
 
 
1515
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1516
 
        """Set the mode of new file contents.
1517
 
        The mode_id is the existing file to get the mode from (often the same
1518
 
        as trans_id).  The operation is only performed if there's a mode match
1519
 
        according to typefunc.
1520
 
        """
1521
 
        # is it ok to ignore this?  probably
1522
 
        pass
1523
 
 
1524
 
    def iter_tree_children(self, parent_id):
1525
 
        """Iterate through the entry's tree children, if any"""
1526
 
        try:
1527
 
            path = self._tree_id_paths[parent_id]
1528
 
        except KeyError:
1529
 
            return
1530
 
        try:
1531
 
            for child in self._tree.iter_child_entries(path):
1532
 
                childpath = joinpath(path, child.name)
1533
 
                yield self.trans_id_tree_path(childpath)
1534
 
        except errors.NoSuchFile:
1535
 
            return
1536
 
 
1537
 
    def new_orphan(self, trans_id, parent_id):
1538
 
        raise NotImplementedError(self.new_orphan)
1539
 
 
1540
 
 
1541
 
class GitPreviewTree(PreviewTree, GitTree):
1542
 
    """Partial implementation of Tree to support show_diff_trees"""
1543
 
 
1544
 
    def __init__(self, transform):
1545
 
        PreviewTree.__init__(self, transform)
1546
 
        self.store = transform._tree.store
1547
 
        self.mapping = transform._tree.mapping
1548
 
        self._final_paths = FinalPaths(transform)
1549
 
 
1550
 
    def supports_setting_file_ids(self):
1551
 
        return False
1552
 
 
1553
 
    def _supports_executable(self):
1554
 
        return self._transform._limbo_supports_executable()
1555
 
 
1556
 
    def walkdirs(self, prefix=''):
1557
 
        pending = [self._transform.root]
1558
 
        while len(pending) > 0:
1559
 
            parent_id = pending.pop()
1560
 
            children = []
1561
 
            subdirs = []
1562
 
            prefix = prefix.rstrip('/')
1563
 
            parent_path = self._final_paths.get_path(parent_id)
1564
 
            for child_id in self._all_children(parent_id):
1565
 
                path_from_root = self._final_paths.get_path(child_id)
1566
 
                basename = self._transform.final_name(child_id)
1567
 
                kind = self._transform.final_kind(child_id)
1568
 
                if kind is not None:
1569
 
                    versioned_kind = kind
1570
 
                else:
1571
 
                    kind = 'unknown'
1572
 
                    versioned_kind = self._transform._tree.stored_kind(
1573
 
                        path_from_root)
1574
 
                if versioned_kind == 'directory':
1575
 
                    subdirs.append(child_id)
1576
 
                children.append((path_from_root, basename, kind, None,
1577
 
                                 versioned_kind))
1578
 
            children.sort()
1579
 
            if parent_path.startswith(prefix):
1580
 
                yield parent_path, children
1581
 
            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1582
 
                                  reverse=True))
1583
 
 
1584
 
    def iter_changes(self, from_tree, include_unchanged=False,
1585
 
                     specific_files=None, pb=None, extra_trees=None,
1586
 
                     require_versioned=True, want_unversioned=False):
1587
 
        """See InterTree.iter_changes.
1588
 
 
1589
 
        This has a fast path that is only used when the from_tree matches
1590
 
        the transform tree, and no fancy options are supplied.
1591
 
        """
1592
 
        return InterTree.get(from_tree, self).iter_changes(
1593
 
            include_unchanged=include_unchanged,
1594
 
            specific_files=specific_files,
1595
 
            pb=pb,
1596
 
            extra_trees=extra_trees,
1597
 
            require_versioned=require_versioned,
1598
 
            want_unversioned=want_unversioned)
1599
 
 
1600
 
    def get_file(self, path):
1601
 
        """See Tree.get_file"""
1602
 
        trans_id = self._path2trans_id(path)
1603
 
        if trans_id is None:
1604
 
            raise errors.NoSuchFile(path)
1605
 
        if trans_id in self._transform._new_contents:
1606
 
            name = self._transform._limbo_name(trans_id)
1607
 
            return open(name, 'rb')
1608
 
        if trans_id in self._transform._removed_contents:
1609
 
            raise errors.NoSuchFile(path)
1610
 
        orig_path = self._transform.tree_path(trans_id)
1611
 
        return self._transform._tree.get_file(orig_path)
1612
 
 
1613
 
    def get_symlink_target(self, path):
1614
 
        """See Tree.get_symlink_target"""
1615
 
        trans_id = self._path2trans_id(path)
1616
 
        if trans_id is None:
1617
 
            raise errors.NoSuchFile(path)
1618
 
        if trans_id not in self._transform._new_contents:
1619
 
            orig_path = self._transform.tree_path(trans_id)
1620
 
            return self._transform._tree.get_symlink_target(orig_path)
1621
 
        name = self._transform._limbo_name(trans_id)
1622
 
        return osutils.readlink(name)
1623
 
 
1624
 
    def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1625
 
        trans_id = self._path2trans_id(path)
1626
 
        if trans_id is None:
1627
 
            return None
1628
 
        orig_path = self._transform.tree_path(trans_id)
1629
 
        if orig_path is not None:
1630
 
            old_annotation = self._transform._tree.annotate_iter(
1631
 
                orig_path, default_revision=default_revision)
1632
 
        else:
1633
 
            old_annotation = []
1634
 
        try:
1635
 
            lines = self.get_file_lines(path)
1636
 
        except errors.NoSuchFile:
1637
 
            return None
1638
 
        return annotate.reannotate([old_annotation], lines, default_revision)
1639
 
 
1640
 
    def get_file_text(self, path):
1641
 
        """Return the byte content of a file.
1642
 
 
1643
 
        :param path: The path of the file.
1644
 
 
1645
 
        :returns: A single byte string for the whole file.
1646
 
        """
1647
 
        with self.get_file(path) as my_file:
1648
 
            return my_file.read()
1649
 
 
1650
 
    def get_file_lines(self, path):
1651
 
        """Return the content of a file, as lines.
1652
 
 
1653
 
        :param path: The path of the file.
1654
 
        """
1655
 
        return osutils.split_lines(self.get_file_text(path))
1656
 
 
1657
 
    def extras(self):
1658
 
        possible_extras = set(self._transform.trans_id_tree_path(p) for p
1659
 
                              in self._transform._tree.extras())
1660
 
        possible_extras.update(self._transform._new_contents)
1661
 
        possible_extras.update(self._transform._removed_id)
1662
 
        for trans_id in possible_extras:
1663
 
            if not self._transform.final_is_versioned(trans_id):
1664
 
                yield self._final_paths._determine_path(trans_id)
1665
 
 
1666
 
    def path_content_summary(self, path):
1667
 
        trans_id = self._path2trans_id(path)
1668
 
        tt = self._transform
1669
 
        tree_path = tt.tree_path(trans_id)
1670
 
        kind = tt._new_contents.get(trans_id)
1671
 
        if kind is None:
1672
 
            if tree_path is None or trans_id in tt._removed_contents:
1673
 
                return 'missing', None, None, None
1674
 
            summary = tt._tree.path_content_summary(tree_path)
1675
 
            kind, size, executable, link_or_sha1 = summary
1676
 
        else:
1677
 
            link_or_sha1 = None
1678
 
            limbo_name = tt._limbo_name(trans_id)
1679
 
            if trans_id in tt._new_reference_revision:
1680
 
                kind = 'tree-reference'
1681
 
            if kind == 'file':
1682
 
                statval = os.lstat(limbo_name)
1683
 
                size = statval.st_size
1684
 
                if not tt._limbo_supports_executable():
1685
 
                    executable = False
1686
 
                else:
1687
 
                    executable = statval.st_mode & S_IEXEC
1688
 
            else:
1689
 
                size = None
1690
 
                executable = None
1691
 
            if kind == 'symlink':
1692
 
                link_or_sha1 = os.readlink(limbo_name)
1693
 
                if not isinstance(link_or_sha1, str):
1694
 
                    link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1695
 
        executable = tt._new_executability.get(trans_id, executable)
1696
 
        return kind, size, executable, link_or_sha1
1697
 
 
1698
 
    def get_file_mtime(self, path):
1699
 
        """See Tree.get_file_mtime"""
1700
 
        trans_id = self._path2trans_id(path)
1701
 
        if trans_id is None:
1702
 
            raise errors.NoSuchFile(path)
1703
 
        if trans_id not in self._transform._new_contents:
1704
 
            return self._transform._tree.get_file_mtime(
1705
 
                self._transform.tree_path(trans_id))
1706
 
        name = self._transform._limbo_name(trans_id)
1707
 
        statval = os.lstat(name)
1708
 
        return statval.st_mtime
1709
 
 
1710
 
    def is_versioned(self, path):
1711
 
        trans_id = self._path2trans_id(path)
1712
 
        if trans_id is None:
1713
 
            # It doesn't exist, so it's not versioned.
1714
 
            return False
1715
 
        if trans_id in self._transform._versioned:
1716
 
            return True
1717
 
        if trans_id in self._transform._removed_id:
1718
 
            return False
1719
 
        orig_path = self._transform.tree_path(trans_id)
1720
 
        return self._transform._tree.is_versioned(orig_path)
1721
 
 
1722
 
    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1723
 
        if recurse_nested:
1724
 
            raise NotImplementedError(
1725
 
                'follow tree references not yet supported')
1726
 
 
1727
 
        # This may not be a maximally efficient implementation, but it is
1728
 
        # reasonably straightforward.  An implementation that grafts the
1729
 
        # TreeTransform changes onto the tree's iter_entries_by_dir results
1730
 
        # might be more efficient, but requires tricky inferences about stack
1731
 
        # position.
1732
 
        for trans_id, path in self._list_files_by_dir():
1733
 
            entry, is_versioned = self._transform.final_entry(trans_id)
1734
 
            if entry is None:
1735
 
                continue
1736
 
            if not is_versioned and entry.kind != 'directory':
1737
 
                continue
1738
 
            if specific_files is not None and path not in specific_files:
1739
 
                continue
1740
 
            if entry is not None:
1741
 
                yield path, entry
1742
 
 
1743
 
    def _list_files_by_dir(self):
1744
 
        todo = [ROOT_PARENT]
1745
 
        while len(todo) > 0:
1746
 
            parent = todo.pop()
1747
 
            children = list(self._all_children(parent))
1748
 
            paths = dict(zip(children, self._final_paths.get_paths(children)))
1749
 
            children.sort(key=paths.get)
1750
 
            todo.extend(reversed(children))
1751
 
            for trans_id in children:
1752
 
                yield trans_id, paths[trans_id][0]
1753
 
 
1754
 
    def revision_tree(self, revision_id):
1755
 
        return self._transform._tree.revision_tree(revision_id)
1756
 
 
1757
 
    def _stat_limbo_file(self, trans_id):
1758
 
        name = self._transform._limbo_name(trans_id)
1759
 
        return os.lstat(name)
1760
 
 
1761
 
    def git_snapshot(self, want_unversioned=False):
1762
 
        extra = set()
1763
 
        os = []
1764
 
        for trans_id, path in self._list_files_by_dir():
1765
 
            if not self._transform.final_is_versioned(trans_id):
1766
 
                if not want_unversioned:
1767
 
                    continue
1768
 
                extra.add(path)
1769
 
            o, mode = self._transform.final_git_entry(trans_id)
1770
 
            if o is not None:
1771
 
                self.store.add_object(o)
1772
 
                os.append((encode_git_path(path), o.id, mode))
1773
 
        if not os:
1774
 
            return None, extra
1775
 
        return commit_tree(self.store, os), extra
1776
 
 
1777
 
    def iter_child_entries(self, path):
1778
 
        trans_id = self._path2trans_id(path)
1779
 
        if trans_id is None:
1780
 
            raise errors.NoSuchFile(path)
1781
 
        for child_trans_id in self._all_children(trans_id):
1782
 
            entry, is_versioned = self._transform.final_entry(trans_id)
1783
 
            if not is_versioned:
1784
 
                continue
1785
 
            if entry is not None:
1786
 
                yield entry