/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: Robert Collins
  • Date: 2010-05-11 08:36:16 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100511083616-b8fjb19zomwupid0
Make all lock methods return Result objects, rather than lock_read returning self, as per John's review.

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, _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 _mover: Supply an alternate FileMover, for testing
746
 
        """
747
 
        raise NotImplementedError(self.apply)
748
 
 
749
 
    def cook_conflicts(self, raw_conflicts):
750
 
        """Generate a list of cooked conflicts, sorted by file path"""
751
 
        if not raw_conflicts:
752
 
            return
753
 
        fp = FinalPaths(self)
754
 
        from .workingtree import TextConflict
755
 
        for c in raw_conflicts:
756
 
            if c[0] == 'text conflict':
757
 
                yield TextConflict(fp.get_path(c[1]))
758
 
            elif c[0] == 'duplicate':
759
 
                yield TextConflict(fp.get_path(c[2]))
760
 
            elif c[0] == 'contents conflict':
761
 
                yield TextConflict(fp.get_path(c[1][0]))
762
 
            elif c[0] == 'missing parent':
763
 
                # TODO(jelmer): This should not make it to here
764
 
                yield TextConflict(fp.get_path(c[2]))
765
 
            elif c[0] == 'non-directory parent':
766
 
                yield TextConflict(fp.get_path(c[2]))
767
 
            elif c[0] == 'deleting parent':
768
 
                # TODO(jelmer): This should not make it to here
769
 
                yield TextConflict(fp.get_path(c[2]))
770
 
            elif c[0] == 'parent loop':
771
 
                # TODO(jelmer): This should not make it to here
772
 
                yield TextConflict(fp.get_path(c[2]))
773
 
            elif c[0] == 'path conflict':
774
 
                yield TextConflict(fp.get_path(c[1]))
775
 
            else:
776
 
                raise AssertionError('unknown conflict %s' % c[0])
777
 
 
778
 
 
779
 
class DiskTreeTransform(TreeTransformBase):
780
 
    """Tree transform storing its contents on disk."""
781
 
 
782
 
    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
783
 
        """Constructor.
784
 
        :param tree: The tree that will be transformed, but not necessarily
785
 
            the output tree.
786
 
        :param limbodir: A directory where new files can be stored until
787
 
            they are installed in their proper places
788
 
        :param pb: ignored
789
 
        :param case_sensitive: If True, the target of the transform is
790
 
            case sensitive, not just case preserving.
791
 
        """
792
 
        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
793
 
        self._limbodir = limbodir
794
 
        self._deletiondir = None
795
 
        # A mapping of transform ids to their limbo filename
796
 
        self._limbo_files = {}
797
 
        self._possibly_stale_limbo_files = set()
798
 
        # A mapping of transform ids to a set of the transform ids of children
799
 
        # that their limbo directory has
800
 
        self._limbo_children = {}
801
 
        # Map transform ids to maps of child filename to child transform id
802
 
        self._limbo_children_names = {}
803
 
        # List of transform ids that need to be renamed from limbo into place
804
 
        self._needs_rename = set()
805
 
        self._creation_mtime = None
806
 
        self._create_symlinks = osutils.supports_symlinks(self._limbodir)
807
 
 
808
 
    def finalize(self):
809
 
        """Release the working tree lock, if held, clean up limbo dir.
810
 
 
811
 
        This is required if apply has not been invoked, but can be invoked
812
 
        even after apply.
813
 
        """
814
 
        if self._tree is None:
815
 
            return
816
 
        try:
817
 
            limbo_paths = list(viewvalues(self._limbo_files))
818
 
            limbo_paths.extend(self._possibly_stale_limbo_files)
819
 
            limbo_paths.sort(reverse=True)
820
 
            for path in limbo_paths:
821
 
                try:
822
 
                    osutils.delete_any(path)
823
 
                except OSError as e:
824
 
                    if e.errno != errno.ENOENT:
825
 
                        raise
826
 
                    # XXX: warn? perhaps we just got interrupted at an
827
 
                    # inconvenient moment, but perhaps files are disappearing
828
 
                    # from under us?
829
 
            try:
830
 
                osutils.delete_any(self._limbodir)
831
 
            except OSError:
832
 
                # We don't especially care *why* the dir is immortal.
833
 
                raise ImmortalLimbo(self._limbodir)
834
 
            try:
835
 
                if self._deletiondir is not None:
836
 
                    osutils.delete_any(self._deletiondir)
837
 
            except OSError:
838
 
                raise errors.ImmortalPendingDeletion(self._deletiondir)
839
 
        finally:
840
 
            TreeTransformBase.finalize(self)
841
 
 
842
 
    def _limbo_supports_executable(self):
843
 
        """Check if the limbo path supports the executable bit."""
844
 
        return osutils.supports_executable(self._limbodir)
845
 
 
846
 
    def _limbo_name(self, trans_id):
847
 
        """Generate the limbo name of a file"""
848
 
        limbo_name = self._limbo_files.get(trans_id)
849
 
        if limbo_name is None:
850
 
            limbo_name = self._generate_limbo_path(trans_id)
851
 
            self._limbo_files[trans_id] = limbo_name
852
 
        return limbo_name
853
 
 
854
 
    def _generate_limbo_path(self, trans_id):
855
 
        """Generate a limbo path using the trans_id as the relative path.
856
 
 
857
 
        This is suitable as a fallback, and when the transform should not be
858
 
        sensitive to the path encoding of the limbo directory.
859
 
        """
860
 
        self._needs_rename.add(trans_id)
861
 
        return osutils.pathjoin(self._limbodir, trans_id)
862
 
 
863
 
    def adjust_path(self, name, parent, trans_id):
864
 
        previous_parent = self._new_parent.get(trans_id)
865
 
        previous_name = self._new_name.get(trans_id)
866
 
        super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
867
 
        if (trans_id in self._limbo_files
868
 
                and trans_id not in self._needs_rename):
869
 
            self._rename_in_limbo([trans_id])
870
 
            if previous_parent != parent:
871
 
                self._limbo_children[previous_parent].remove(trans_id)
872
 
            if previous_parent != parent or previous_name != name:
873
 
                del self._limbo_children_names[previous_parent][previous_name]
874
 
 
875
 
    def _rename_in_limbo(self, trans_ids):
876
 
        """Fix limbo names so that the right final path is produced.
877
 
 
878
 
        This means we outsmarted ourselves-- we tried to avoid renaming
879
 
        these files later by creating them with their final names in their
880
 
        final parents.  But now the previous name or parent is no longer
881
 
        suitable, so we have to rename them.
882
 
 
883
 
        Even for trans_ids that have no new contents, we must remove their
884
 
        entries from _limbo_files, because they are now stale.
885
 
        """
886
 
        for trans_id in trans_ids:
887
 
            old_path = self._limbo_files[trans_id]
888
 
            self._possibly_stale_limbo_files.add(old_path)
889
 
            del self._limbo_files[trans_id]
890
 
            if trans_id not in self._new_contents:
891
 
                continue
892
 
            new_path = self._limbo_name(trans_id)
893
 
            os.rename(old_path, new_path)
894
 
            self._possibly_stale_limbo_files.remove(old_path)
895
 
            for descendant in self._limbo_descendants(trans_id):
896
 
                desc_path = self._limbo_files[descendant]
897
 
                desc_path = new_path + desc_path[len(old_path):]
898
 
                self._limbo_files[descendant] = desc_path
899
 
 
900
 
    def _limbo_descendants(self, trans_id):
901
 
        """Return the set of trans_ids whose limbo paths descend from this."""
902
 
        descendants = set(self._limbo_children.get(trans_id, []))
903
 
        for descendant in list(descendants):
904
 
            descendants.update(self._limbo_descendants(descendant))
905
 
        return descendants
906
 
 
907
 
    def _set_mode(self, trans_id, mode_id, typefunc):
908
 
        raise NotImplementedError(self._set_mode)
909
 
 
910
 
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
911
 
        """Schedule creation of a new file.
912
 
 
913
 
        :seealso: new_file.
914
 
 
915
 
        :param contents: an iterator of strings, all of which will be written
916
 
            to the target destination.
917
 
        :param trans_id: TreeTransform handle
918
 
        :param mode_id: If not None, force the mode of the target file to match
919
 
            the mode of the object referenced by mode_id.
920
 
            Otherwise, we will try to preserve mode bits of an existing file.
921
 
        :param sha1: If the sha1 of this content is already known, pass it in.
922
 
            We can use it to prevent future sha1 computations.
923
 
        """
924
 
        name = self._limbo_name(trans_id)
925
 
        with open(name, 'wb') as f:
926
 
            unique_add(self._new_contents, trans_id, 'file')
927
 
            f.writelines(contents)
928
 
        self._set_mtime(name)
929
 
        self._set_mode(trans_id, mode_id, S_ISREG)
930
 
        # It is unfortunate we have to use lstat instead of fstat, but we just
931
 
        # used utime and chmod on the file, so we need the accurate final
932
 
        # details.
933
 
        if sha1 is not None:
934
 
            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
935
 
 
936
 
    def _read_symlink_target(self, trans_id):
937
 
        return os.readlink(self._limbo_name(trans_id))
938
 
 
939
 
    def _set_mtime(self, path):
940
 
        """All files that are created get the same mtime.
941
 
 
942
 
        This time is set by the first object to be created.
943
 
        """
944
 
        if self._creation_mtime is None:
945
 
            self._creation_mtime = time.time()
946
 
        os.utime(path, (self._creation_mtime, self._creation_mtime))
947
 
 
948
 
    def create_hardlink(self, path, trans_id):
949
 
        """Schedule creation of a hard link"""
950
 
        name = self._limbo_name(trans_id)
951
 
        try:
952
 
            os.link(path, name)
953
 
        except OSError as e:
954
 
            if e.errno != errno.EPERM:
955
 
                raise
956
 
            raise errors.HardLinkNotSupported(path)
957
 
        try:
958
 
            unique_add(self._new_contents, trans_id, 'file')
959
 
        except BaseException:
960
 
            # Clean up the file, it never got registered so
961
 
            # TreeTransform.finalize() won't clean it up.
962
 
            os.unlink(name)
963
 
            raise
964
 
 
965
 
    def create_directory(self, trans_id):
966
 
        """Schedule creation of a new directory.
967
 
 
968
 
        See also new_directory.
969
 
        """
970
 
        os.mkdir(self._limbo_name(trans_id))
971
 
        unique_add(self._new_contents, trans_id, 'directory')
972
 
 
973
 
    def create_symlink(self, target, trans_id):
974
 
        """Schedule creation of a new symbolic link.
975
 
 
976
 
        target is a bytestring.
977
 
        See also new_symlink.
978
 
        """
979
 
        if self._create_symlinks:
980
 
            os.symlink(target, self._limbo_name(trans_id))
981
 
        else:
982
 
            try:
983
 
                path = FinalPaths(self).get_path(trans_id)
984
 
            except KeyError:
985
 
                path = None
986
 
            trace.warning(
987
 
                'Unable to create symlink "%s" on this filesystem.' % (path,))
988
 
            self._symlink_target[trans_id] = target
989
 
        # We add symlink to _new_contents even if they are unsupported
990
 
        # and not created. These entries are subsequently used to avoid
991
 
        # conflicts on platforms that don't support symlink
992
 
        unique_add(self._new_contents, trans_id, 'symlink')
993
 
 
994
 
    def cancel_creation(self, trans_id):
995
 
        """Cancel the creation of new file contents."""
996
 
        del self._new_contents[trans_id]
997
 
        if trans_id in self._observed_sha1s:
998
 
            del self._observed_sha1s[trans_id]
999
 
        children = self._limbo_children.get(trans_id)
1000
 
        # if this is a limbo directory with children, move them before removing
1001
 
        # the directory
1002
 
        if children is not None:
1003
 
            self._rename_in_limbo(children)
1004
 
            del self._limbo_children[trans_id]
1005
 
            del self._limbo_children_names[trans_id]
1006
 
        osutils.delete_any(self._limbo_name(trans_id))
1007
 
 
1008
 
    def new_orphan(self, trans_id, parent_id):
1009
 
        conf = self._tree.get_config_stack()
1010
 
        handle_orphan = conf.get('transform.orphan_policy')
1011
 
        handle_orphan(self, trans_id, parent_id)
1012
 
 
1013
 
    def final_entry(self, trans_id):
1014
 
        is_versioned = self.final_is_versioned(trans_id)
1015
 
        fp = FinalPaths(self)
1016
 
        tree_path = fp.get_path(trans_id)
1017
 
        if trans_id in self._new_contents:
1018
 
            path = self._limbo_name(trans_id)
1019
 
            st = os.lstat(path)
1020
 
            kind = mode_kind(st.st_mode)
1021
 
            name = self.final_name(trans_id)
1022
 
            file_id = self._tree.mapping.generate_file_id(tree_path)
1023
 
            parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1024
 
            if kind == 'directory':
1025
 
                return GitTreeDirectory(
1026
 
                    file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1027
 
            executable = mode_is_executable(st.st_mode)
1028
 
            mode = object_mode(kind, executable)
1029
 
            blob = blob_from_path_and_stat(encode_git_path(path), st)
1030
 
            if kind == 'symlink':
1031
 
                return GitTreeSymlink(
1032
 
                    file_id, name, parent_id,
1033
 
                    decode_git_path(blob.data)), is_versioned
1034
 
            elif kind == 'file':
1035
 
                return GitTreeFile(
1036
 
                    file_id, name, executable=executable, parent_id=parent_id,
1037
 
                    git_sha1=blob.id, text_size=len(blob.data)), is_versioned
1038
 
            else:
1039
 
                raise AssertionError(kind)
1040
 
        elif trans_id in self._removed_contents:
1041
 
            return None, None
1042
 
        else:
1043
 
            orig_path = self.tree_path(trans_id)
1044
 
            if orig_path is None:
1045
 
                return None, None
1046
 
            file_id = self._tree.mapping.generate_file_id(tree_path)
1047
 
            if tree_path == '':
1048
 
                parent_id = None
1049
 
            else:
1050
 
                parent_id = self._tree.mapping.generate_file_id(os.path.dirname(tree_path))
1051
 
            try:
1052
 
                ie = next(self._tree.iter_entries_by_dir(
1053
 
                    specific_files=[orig_path]))[1]
1054
 
                ie.file_id = file_id
1055
 
                ie.parent_id = parent_id
1056
 
                return ie, is_versioned
1057
 
            except StopIteration:
1058
 
                try:
1059
 
                    if self.tree_kind(trans_id) == 'directory':
1060
 
                        return GitTreeDirectory(
1061
 
                            file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
1062
 
                except OSError as e:
1063
 
                    if e.errno != errno.ENOTDIR:
1064
 
                        raise
1065
 
                return None, None
1066
 
 
1067
 
    def final_git_entry(self, trans_id):
1068
 
        if trans_id in self._new_contents:
1069
 
            path = self._limbo_name(trans_id)
1070
 
            st = os.lstat(path)
1071
 
            kind = mode_kind(st.st_mode)
1072
 
            if kind == 'directory':
1073
 
                return None, None
1074
 
            executable = mode_is_executable(st.st_mode)
1075
 
            mode = object_mode(kind, executable)
1076
 
            blob = blob_from_path_and_stat(encode_git_path(path), st)
1077
 
        elif trans_id in self._removed_contents:
1078
 
            return None, None
1079
 
        else:
1080
 
            orig_path = self.tree_path(trans_id)
1081
 
            kind = self._tree.kind(orig_path)
1082
 
            executable = self._tree.is_executable(orig_path)
1083
 
            mode = object_mode(kind, executable)
1084
 
            if kind == 'symlink':
1085
 
                contents = self._tree.get_symlink_target(orig_path)
1086
 
            elif kind == 'file':
1087
 
                contents = self._tree.get_file_text(orig_path)
1088
 
            elif kind == 'directory':
1089
 
                return None, None
1090
 
            else:
1091
 
                raise AssertionError(kind)
1092
 
            blob = Blob.from_string(contents)
1093
 
        return blob, mode
1094
 
 
1095
 
 
1096
 
class GitTreeTransform(DiskTreeTransform):
1097
 
    """Represent a tree transformation.
1098
 
 
1099
 
    This object is designed to support incremental generation of the transform,
1100
 
    in any order.
1101
 
 
1102
 
    However, it gives optimum performance when parent directories are created
1103
 
    before their contents.  The transform is then able to put child files
1104
 
    directly in their parent directory, avoiding later renames.
1105
 
 
1106
 
    It is easy to produce malformed transforms, but they are generally
1107
 
    harmless.  Attempting to apply a malformed transform will cause an
1108
 
    exception to be raised before any modifications are made to the tree.
1109
 
 
1110
 
    Many kinds of malformed transforms can be corrected with the
1111
 
    resolve_conflicts function.  The remaining ones indicate programming error,
1112
 
    such as trying to create a file with no path.
1113
 
 
1114
 
    Two sets of file creation methods are supplied.  Convenience methods are:
1115
 
     * new_file
1116
 
     * new_directory
1117
 
     * new_symlink
1118
 
 
1119
 
    These are composed of the low-level methods:
1120
 
     * create_path
1121
 
     * create_file or create_directory or create_symlink
1122
 
     * version_file
1123
 
     * set_executability
1124
 
 
1125
 
    Transform/Transaction ids
1126
 
    -------------------------
1127
 
    trans_ids are temporary ids assigned to all files involved in a transform.
1128
 
    It's possible, even common, that not all files in the Tree have trans_ids.
1129
 
 
1130
 
    trans_ids are used because filenames and file_ids are not good enough
1131
 
    identifiers; filenames change.
1132
 
 
1133
 
    trans_ids are only valid for the TreeTransform that generated them.
1134
 
 
1135
 
    Limbo
1136
 
    -----
1137
 
    Limbo is a temporary directory use to hold new versions of files.
1138
 
    Files are added to limbo by create_file, create_directory, create_symlink,
1139
 
    and their convenience variants (new_*).  Files may be removed from limbo
1140
 
    using cancel_creation.  Files are renamed from limbo into their final
1141
 
    location as part of TreeTransform.apply
1142
 
 
1143
 
    Limbo must be cleaned up, by either calling TreeTransform.apply or
1144
 
    calling TreeTransform.finalize.
1145
 
 
1146
 
    Files are placed into limbo inside their parent directories, where
1147
 
    possible.  This reduces subsequent renames, and makes operations involving
1148
 
    lots of files faster.  This optimization is only possible if the parent
1149
 
    directory is created *before* creating any of its children, so avoid
1150
 
    creating children before parents, where possible.
1151
 
 
1152
 
    Pending-deletion
1153
 
    ----------------
1154
 
    This temporary directory is used by _FileMover for storing files that are
1155
 
    about to be deleted.  In case of rollback, the files will be restored.
1156
 
    FileMover does not delete files until it is sure that a rollback will not
1157
 
    happen.
1158
 
    """
1159
 
 
1160
 
    def __init__(self, tree, pb=None):
1161
 
        """Note: a tree_write lock is taken on the tree.
1162
 
 
1163
 
        Use TreeTransform.finalize() to release the lock (can be omitted if
1164
 
        TreeTransform.apply() called).
1165
 
        """
1166
 
        tree.lock_tree_write()
1167
 
        try:
1168
 
            limbodir = urlutils.local_path_from_url(
1169
 
                tree._transport.abspath('limbo'))
1170
 
            osutils.ensure_empty_directory_exists(
1171
 
                limbodir,
1172
 
                errors.ExistingLimbo)
1173
 
            deletiondir = urlutils.local_path_from_url(
1174
 
                tree._transport.abspath('pending-deletion'))
1175
 
            osutils.ensure_empty_directory_exists(
1176
 
                deletiondir,
1177
 
                errors.ExistingPendingDeletion)
1178
 
        except BaseException:
1179
 
            tree.unlock()
1180
 
            raise
1181
 
 
1182
 
        # Cache of realpath results, to speed up canonical_path
1183
 
        self._realpaths = {}
1184
 
        # Cache of relpath results, to speed up canonical_path
1185
 
        self._relpaths = {}
1186
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
1187
 
                                   tree.case_sensitive)
1188
 
        self._deletiondir = deletiondir
1189
 
 
1190
 
    def canonical_path(self, path):
1191
 
        """Get the canonical tree-relative path"""
1192
 
        # don't follow final symlinks
1193
 
        abs = self._tree.abspath(path)
1194
 
        if abs in self._relpaths:
1195
 
            return self._relpaths[abs]
1196
 
        dirname, basename = os.path.split(abs)
1197
 
        if dirname not in self._realpaths:
1198
 
            self._realpaths[dirname] = os.path.realpath(dirname)
1199
 
        dirname = self._realpaths[dirname]
1200
 
        abs = osutils.pathjoin(dirname, basename)
1201
 
        if dirname in self._relpaths:
1202
 
            relpath = osutils.pathjoin(self._relpaths[dirname], basename)
1203
 
            relpath = relpath.rstrip('/\\')
1204
 
        else:
1205
 
            relpath = self._tree.relpath(abs)
1206
 
        self._relpaths[abs] = relpath
1207
 
        return relpath
1208
 
 
1209
 
    def tree_kind(self, trans_id):
1210
 
        """Determine the file kind in the working tree.
1211
 
 
1212
 
        :returns: The file kind or None if the file does not exist
1213
 
        """
1214
 
        path = self._tree_id_paths.get(trans_id)
1215
 
        if path is None:
1216
 
            return None
1217
 
        try:
1218
 
            return osutils.file_kind(self._tree.abspath(path))
1219
 
        except errors.NoSuchFile:
1220
 
            return None
1221
 
 
1222
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1223
 
        """Set the mode of new file contents.
1224
 
        The mode_id is the existing file to get the mode from (often the same
1225
 
        as trans_id).  The operation is only performed if there's a mode match
1226
 
        according to typefunc.
1227
 
        """
1228
 
        if mode_id is None:
1229
 
            mode_id = trans_id
1230
 
        try:
1231
 
            old_path = self._tree_id_paths[mode_id]
1232
 
        except KeyError:
1233
 
            return
1234
 
        try:
1235
 
            mode = os.stat(self._tree.abspath(old_path)).st_mode
1236
 
        except OSError as e:
1237
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
1238
 
                # Either old_path doesn't exist, or the parent of the
1239
 
                # target is not a directory (but will be one eventually)
1240
 
                # Either way, we know it doesn't exist *right now*
1241
 
                # See also bug #248448
1242
 
                return
1243
 
            else:
1244
 
                raise
1245
 
        if typefunc(mode):
1246
 
            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1247
 
 
1248
 
    def iter_tree_children(self, parent_id):
1249
 
        """Iterate through the entry's tree children, if any"""
1250
 
        try:
1251
 
            path = self._tree_id_paths[parent_id]
1252
 
        except KeyError:
1253
 
            return
1254
 
        try:
1255
 
            children = os.listdir(self._tree.abspath(path))
1256
 
        except OSError as e:
1257
 
            if not (osutils._is_error_enotdir(e) or
1258
 
                    e.errno in (errno.ENOENT, errno.ESRCH)):
1259
 
                raise
1260
 
            return
1261
 
 
1262
 
        for child in children:
1263
 
            childpath = joinpath(path, child)
1264
 
            if self._tree.is_control_filename(childpath):
1265
 
                continue
1266
 
            yield self.trans_id_tree_path(childpath)
1267
 
 
1268
 
    def _generate_limbo_path(self, trans_id):
1269
 
        """Generate a limbo path using the final path if possible.
1270
 
 
1271
 
        This optimizes the performance of applying the tree transform by
1272
 
        avoiding renames.  These renames can be avoided only when the parent
1273
 
        directory is already scheduled for creation.
1274
 
 
1275
 
        If the final path cannot be used, falls back to using the trans_id as
1276
 
        the relpath.
1277
 
        """
1278
 
        parent = self._new_parent.get(trans_id)
1279
 
        # if the parent directory is already in limbo (e.g. when building a
1280
 
        # tree), choose a limbo name inside the parent, to reduce further
1281
 
        # renames.
1282
 
        use_direct_path = False
1283
 
        if self._new_contents.get(parent) == 'directory':
1284
 
            filename = self._new_name.get(trans_id)
1285
 
            if filename is not None:
1286
 
                if parent not in self._limbo_children:
1287
 
                    self._limbo_children[parent] = set()
1288
 
                    self._limbo_children_names[parent] = {}
1289
 
                    use_direct_path = True
1290
 
                # the direct path can only be used if no other file has
1291
 
                # already taken this pathname, i.e. if the name is unused, or
1292
 
                # if it is already associated with this trans_id.
1293
 
                elif self._case_sensitive_target:
1294
 
                    if (self._limbo_children_names[parent].get(filename)
1295
 
                            in (trans_id, None)):
1296
 
                        use_direct_path = True
1297
 
                else:
1298
 
                    for l_filename, l_trans_id in viewitems(
1299
 
                            self._limbo_children_names[parent]):
1300
 
                        if l_trans_id == trans_id:
1301
 
                            continue
1302
 
                        if l_filename.lower() == filename.lower():
1303
 
                            break
1304
 
                    else:
1305
 
                        use_direct_path = True
1306
 
 
1307
 
        if not use_direct_path:
1308
 
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
1309
 
 
1310
 
        limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
1311
 
        self._limbo_children[parent].add(trans_id)
1312
 
        self._limbo_children_names[parent][filename] = trans_id
1313
 
        return limbo_name
1314
 
 
1315
 
    def cancel_versioning(self, trans_id):
1316
 
        """Undo a previous versioning of a file"""
1317
 
        self._versioned.remove(trans_id)
1318
 
 
1319
 
    def apply(self, no_conflicts=False, _mover=None):
1320
 
        """Apply all changes to the inventory and filesystem.
1321
 
 
1322
 
        If filesystem or inventory conflicts are present, MalformedTransform
1323
 
        will be thrown.
1324
 
 
1325
 
        If apply succeeds, finalize is not necessary.
1326
 
 
1327
 
        :param no_conflicts: if True, the caller guarantees there are no
1328
 
            conflicts, so no check is made.
1329
 
        :param _mover: Supply an alternate FileMover, for testing
1330
 
        """
1331
 
        for hook in MutableTree.hooks['pre_transform']:
1332
 
            hook(self._tree, self)
1333
 
        if not no_conflicts:
1334
 
            self._check_malformed()
1335
 
        self.rename_count = 0
1336
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1337
 
            child_pb.update(gettext('Apply phase'), 0, 2)
1338
 
            index_changes = self._generate_index_changes()
1339
 
            offset = 1
1340
 
            if _mover is None:
1341
 
                mover = _FileMover()
1342
 
            else:
1343
 
                mover = _mover
1344
 
            try:
1345
 
                child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1346
 
                self._apply_removals(mover)
1347
 
                child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1348
 
                modified_paths = self._apply_insertions(mover)
1349
 
            except BaseException:
1350
 
                mover.rollback()
1351
 
                raise
1352
 
            else:
1353
 
                mover.apply_deletions()
1354
 
        self._tree._apply_index_changes(index_changes)
1355
 
        self._done = True
1356
 
        self.finalize()
1357
 
        return _TransformResults(modified_paths, self.rename_count)
1358
 
 
1359
 
    def _apply_removals(self, mover):
1360
 
        """Perform tree operations that remove directory/inventory names.
1361
 
 
1362
 
        That is, delete files that are to be deleted, and put any files that
1363
 
        need renaming into limbo.  This must be done in strict child-to-parent
1364
 
        order.
1365
 
 
1366
 
        If inventory_delta is None, no inventory delta generation is performed.
1367
 
        """
1368
 
        tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1369
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1370
 
            for num, (path, trans_id) in enumerate(tree_paths):
1371
 
                # do not attempt to move root into a subdirectory of itself.
1372
 
                if path == '':
1373
 
                    continue
1374
 
                child_pb.update(gettext('removing file'), num, len(tree_paths))
1375
 
                full_path = self._tree.abspath(path)
1376
 
                if trans_id in self._removed_contents:
1377
 
                    delete_path = os.path.join(self._deletiondir, trans_id)
1378
 
                    mover.pre_delete(full_path, delete_path)
1379
 
                elif (trans_id in self._new_name or
1380
 
                      trans_id in self._new_parent):
1381
 
                    try:
1382
 
                        mover.rename(full_path, self._limbo_name(trans_id))
1383
 
                    except TransformRenameFailed as e:
1384
 
                        if e.errno != errno.ENOENT:
1385
 
                            raise
1386
 
                    else:
1387
 
                        self.rename_count += 1
1388
 
 
1389
 
    def _apply_insertions(self, mover):
1390
 
        """Perform tree operations that insert directory/inventory names.
1391
 
 
1392
 
        That is, create any files that need to be created, and restore from
1393
 
        limbo any files that needed renaming.  This must be done in strict
1394
 
        parent-to-child order.
1395
 
 
1396
 
        If inventory_delta is None, no inventory delta is calculated, and
1397
 
        no list of modified paths is returned.
1398
 
        """
1399
 
        new_paths = self.new_paths(filesystem_only=True)
1400
 
        modified_paths = []
1401
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1402
 
            for num, (path, trans_id) in enumerate(new_paths):
1403
 
                if (num % 10) == 0:
1404
 
                    child_pb.update(gettext('adding file'),
1405
 
                                    num, len(new_paths))
1406
 
                full_path = self._tree.abspath(path)
1407
 
                if trans_id in self._needs_rename:
1408
 
                    try:
1409
 
                        mover.rename(self._limbo_name(trans_id), full_path)
1410
 
                    except TransformRenameFailed as e:
1411
 
                        # We may be renaming a dangling inventory id
1412
 
                        if e.errno != errno.ENOENT:
1413
 
                            raise
1414
 
                    else:
1415
 
                        self.rename_count += 1
1416
 
                    # TODO: if trans_id in self._observed_sha1s, we should
1417
 
                    #       re-stat the final target, since ctime will be
1418
 
                    #       updated by the change.
1419
 
                if (trans_id in self._new_contents
1420
 
                        or self.path_changed(trans_id)):
1421
 
                    if trans_id in self._new_contents:
1422
 
                        modified_paths.append(full_path)
1423
 
                if trans_id in self._new_executability:
1424
 
                    self._set_executability(path, trans_id)
1425
 
                if trans_id in self._observed_sha1s:
1426
 
                    o_sha1, o_st_val = self._observed_sha1s[trans_id]
1427
 
                    st = osutils.lstat(full_path)
1428
 
                    self._observed_sha1s[trans_id] = (o_sha1, st)
1429
 
        for path, trans_id in new_paths:
1430
 
            # new_paths includes stuff like workingtree conflicts. Only the
1431
 
            # stuff in new_contents actually comes from limbo.
1432
 
            if trans_id in self._limbo_files:
1433
 
                del self._limbo_files[trans_id]
1434
 
        self._new_contents.clear()
1435
 
        return modified_paths
1436
 
 
1437
 
    def _generate_index_changes(self):
1438
 
        """Generate an inventory delta for the current transform."""
1439
 
        removed_id = set(self._removed_id)
1440
 
        removed_id.update(self._removed_contents)
1441
 
        changes = {}
1442
 
        changed_ids = set()
1443
 
        for id_set in [self._new_name, self._new_parent,
1444
 
                       self._new_executability]:
1445
 
            changed_ids.update(id_set)
1446
 
        for id_set in [self._new_name, self._new_parent]:
1447
 
            removed_id.update(id_set)
1448
 
        # so does adding
1449
 
        changed_kind = set(self._new_contents)
1450
 
        # Ignore entries that are already known to have changed.
1451
 
        changed_kind.difference_update(changed_ids)
1452
 
        #  to keep only the truly changed ones
1453
 
        changed_kind = (t for t in changed_kind
1454
 
                        if self.tree_kind(t) != self.final_kind(t))
1455
 
        changed_ids.update(changed_kind)
1456
 
        for t in changed_kind:
1457
 
            if self.final_kind(t) == 'directory':
1458
 
                removed_id.add(t)
1459
 
                changed_ids.remove(t)
1460
 
        new_paths = sorted(FinalPaths(self).get_paths(changed_ids))
1461
 
        total_entries = len(new_paths) + len(removed_id)
1462
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1463
 
            for num, trans_id in enumerate(removed_id):
1464
 
                if (num % 10) == 0:
1465
 
                    child_pb.update(gettext('removing file'),
1466
 
                                    num, total_entries)
1467
 
                try:
1468
 
                    path = self._tree_id_paths[trans_id]
1469
 
                except KeyError:
1470
 
                    continue
1471
 
                changes[path] = (None, None, None, None)
1472
 
            for num, (path, trans_id) in enumerate(new_paths):
1473
 
                if (num % 10) == 0:
1474
 
                    child_pb.update(gettext('adding file'),
1475
 
                                    num + len(removed_id), total_entries)
1476
 
 
1477
 
                kind = self.final_kind(trans_id)
1478
 
                if kind is None:
1479
 
                    continue
1480
 
                versioned = self.final_is_versioned(trans_id)
1481
 
                if not versioned:
1482
 
                    continue
1483
 
                executability = self._new_executability.get(trans_id)
1484
 
                reference_revision = self._new_reference_revision.get(trans_id)
1485
 
                symlink_target = self._symlink_target.get(trans_id)
1486
 
                changes[path] = (
1487
 
                    kind, executability, reference_revision, symlink_target)
1488
 
        return [(p, k, e, rr, st) for (p, (k, e, rr, st)) in changes.items()]
1489
 
 
1490
 
 
1491
 
class GitTransformPreview(GitTreeTransform):
1492
 
    """A TreeTransform for generating preview trees.
1493
 
 
1494
 
    Unlike TreeTransform, this version works when the input tree is a
1495
 
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
1496
 
    unversioned files in the input tree.
1497
 
    """
1498
 
 
1499
 
    def __init__(self, tree, pb=None, case_sensitive=True):
1500
 
        tree.lock_read()
1501
 
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
1502
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
1503
 
 
1504
 
    def canonical_path(self, path):
1505
 
        return path
1506
 
 
1507
 
    def tree_kind(self, trans_id):
1508
 
        path = self.tree_path(trans_id)
1509
 
        if path is None:
1510
 
            return None
1511
 
        kind = self._tree.path_content_summary(path)[0]
1512
 
        if kind == 'missing':
1513
 
            kind = None
1514
 
        return kind
1515
 
 
1516
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1517
 
        """Set the mode of new file contents.
1518
 
        The mode_id is the existing file to get the mode from (often the same
1519
 
        as trans_id).  The operation is only performed if there's a mode match
1520
 
        according to typefunc.
1521
 
        """
1522
 
        # is it ok to ignore this?  probably
1523
 
        pass
1524
 
 
1525
 
    def iter_tree_children(self, parent_id):
1526
 
        """Iterate through the entry's tree children, if any"""
1527
 
        try:
1528
 
            path = self._tree_id_paths[parent_id]
1529
 
        except KeyError:
1530
 
            return
1531
 
        try:
1532
 
            for child in self._tree.iter_child_entries(path):
1533
 
                childpath = joinpath(path, child.name)
1534
 
                yield self.trans_id_tree_path(childpath)
1535
 
        except errors.NoSuchFile:
1536
 
            return
1537
 
 
1538
 
    def new_orphan(self, trans_id, parent_id):
1539
 
        raise NotImplementedError(self.new_orphan)
1540
 
 
1541
 
 
1542
 
class GitPreviewTree(PreviewTree, GitTree):
1543
 
    """Partial implementation of Tree to support show_diff_trees"""
1544
 
 
1545
 
    def __init__(self, transform):
1546
 
        PreviewTree.__init__(self, transform)
1547
 
        self.store = transform._tree.store
1548
 
        self.mapping = transform._tree.mapping
1549
 
        self._final_paths = FinalPaths(transform)
1550
 
 
1551
 
    def supports_setting_file_ids(self):
1552
 
        return False
1553
 
 
1554
 
    def _supports_executable(self):
1555
 
        return self._transform._limbo_supports_executable()
1556
 
 
1557
 
    def walkdirs(self, prefix=''):
1558
 
        pending = [self._transform.root]
1559
 
        while len(pending) > 0:
1560
 
            parent_id = pending.pop()
1561
 
            children = []
1562
 
            subdirs = []
1563
 
            prefix = prefix.rstrip('/')
1564
 
            parent_path = self._final_paths.get_path(parent_id)
1565
 
            for child_id in self._all_children(parent_id):
1566
 
                path_from_root = self._final_paths.get_path(child_id)
1567
 
                basename = self._transform.final_name(child_id)
1568
 
                kind = self._transform.final_kind(child_id)
1569
 
                if kind is not None:
1570
 
                    versioned_kind = kind
1571
 
                else:
1572
 
                    kind = 'unknown'
1573
 
                    versioned_kind = self._transform._tree.stored_kind(
1574
 
                        path_from_root)
1575
 
                if versioned_kind == 'directory':
1576
 
                    subdirs.append(child_id)
1577
 
                children.append((path_from_root, basename, kind, None,
1578
 
                                 versioned_kind))
1579
 
            children.sort()
1580
 
            if parent_path.startswith(prefix):
1581
 
                yield parent_path, children
1582
 
            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
1583
 
                                  reverse=True))
1584
 
 
1585
 
    def iter_changes(self, from_tree, include_unchanged=False,
1586
 
                     specific_files=None, pb=None, extra_trees=None,
1587
 
                     require_versioned=True, want_unversioned=False):
1588
 
        """See InterTree.iter_changes.
1589
 
 
1590
 
        This has a fast path that is only used when the from_tree matches
1591
 
        the transform tree, and no fancy options are supplied.
1592
 
        """
1593
 
        return InterTree.get(from_tree, self).iter_changes(
1594
 
            include_unchanged=include_unchanged,
1595
 
            specific_files=specific_files,
1596
 
            pb=pb,
1597
 
            extra_trees=extra_trees,
1598
 
            require_versioned=require_versioned,
1599
 
            want_unversioned=want_unversioned)
1600
 
 
1601
 
    def get_file(self, path):
1602
 
        """See Tree.get_file"""
1603
 
        trans_id = self._path2trans_id(path)
1604
 
        if trans_id is None:
1605
 
            raise errors.NoSuchFile(path)
1606
 
        if trans_id in self._transform._new_contents:
1607
 
            name = self._transform._limbo_name(trans_id)
1608
 
            return open(name, 'rb')
1609
 
        if trans_id in self._transform._removed_contents:
1610
 
            raise errors.NoSuchFile(path)
1611
 
        orig_path = self._transform.tree_path(trans_id)
1612
 
        return self._transform._tree.get_file(orig_path)
1613
 
 
1614
 
    def get_symlink_target(self, path):
1615
 
        """See Tree.get_symlink_target"""
1616
 
        trans_id = self._path2trans_id(path)
1617
 
        if trans_id is None:
1618
 
            raise errors.NoSuchFile(path)
1619
 
        if trans_id not in self._transform._new_contents:
1620
 
            orig_path = self._transform.tree_path(trans_id)
1621
 
            return self._transform._tree.get_symlink_target(orig_path)
1622
 
        name = self._transform._limbo_name(trans_id)
1623
 
        return osutils.readlink(name)
1624
 
 
1625
 
    def annotate_iter(self, path, default_revision=_mod_revision.CURRENT_REVISION):
1626
 
        trans_id = self._path2trans_id(path)
1627
 
        if trans_id is None:
1628
 
            return None
1629
 
        orig_path = self._transform.tree_path(trans_id)
1630
 
        if orig_path is not None:
1631
 
            old_annotation = self._transform._tree.annotate_iter(
1632
 
                orig_path, default_revision=default_revision)
1633
 
        else:
1634
 
            old_annotation = []
1635
 
        try:
1636
 
            lines = self.get_file_lines(path)
1637
 
        except errors.NoSuchFile:
1638
 
            return None
1639
 
        return annotate.reannotate([old_annotation], lines, default_revision)
1640
 
 
1641
 
    def get_file_text(self, path):
1642
 
        """Return the byte content of a file.
1643
 
 
1644
 
        :param path: The path of the file.
1645
 
 
1646
 
        :returns: A single byte string for the whole file.
1647
 
        """
1648
 
        with self.get_file(path) as my_file:
1649
 
            return my_file.read()
1650
 
 
1651
 
    def get_file_lines(self, path):
1652
 
        """Return the content of a file, as lines.
1653
 
 
1654
 
        :param path: The path of the file.
1655
 
        """
1656
 
        return osutils.split_lines(self.get_file_text(path))
1657
 
 
1658
 
    def extras(self):
1659
 
        possible_extras = set(self._transform.trans_id_tree_path(p) for p
1660
 
                              in self._transform._tree.extras())
1661
 
        possible_extras.update(self._transform._new_contents)
1662
 
        possible_extras.update(self._transform._removed_id)
1663
 
        for trans_id in possible_extras:
1664
 
            if not self._transform.final_is_versioned(trans_id):
1665
 
                yield self._final_paths._determine_path(trans_id)
1666
 
 
1667
 
    def path_content_summary(self, path):
1668
 
        trans_id = self._path2trans_id(path)
1669
 
        tt = self._transform
1670
 
        tree_path = tt.tree_path(trans_id)
1671
 
        kind = tt._new_contents.get(trans_id)
1672
 
        if kind is None:
1673
 
            if tree_path is None or trans_id in tt._removed_contents:
1674
 
                return 'missing', None, None, None
1675
 
            summary = tt._tree.path_content_summary(tree_path)
1676
 
            kind, size, executable, link_or_sha1 = summary
1677
 
        else:
1678
 
            link_or_sha1 = None
1679
 
            limbo_name = tt._limbo_name(trans_id)
1680
 
            if trans_id in tt._new_reference_revision:
1681
 
                kind = 'tree-reference'
1682
 
            if kind == 'file':
1683
 
                statval = os.lstat(limbo_name)
1684
 
                size = statval.st_size
1685
 
                if not tt._limbo_supports_executable():
1686
 
                    executable = False
1687
 
                else:
1688
 
                    executable = statval.st_mode & S_IEXEC
1689
 
            else:
1690
 
                size = None
1691
 
                executable = None
1692
 
            if kind == 'symlink':
1693
 
                link_or_sha1 = os.readlink(limbo_name)
1694
 
                if not isinstance(link_or_sha1, text_type):
1695
 
                    link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
1696
 
        executable = tt._new_executability.get(trans_id, executable)
1697
 
        return kind, size, executable, link_or_sha1
1698
 
 
1699
 
    def get_file_mtime(self, path):
1700
 
        """See Tree.get_file_mtime"""
1701
 
        trans_id = self._path2trans_id(path)
1702
 
        if trans_id is None:
1703
 
            raise errors.NoSuchFile(path)
1704
 
        if trans_id not in self._transform._new_contents:
1705
 
            return self._transform._tree.get_file_mtime(
1706
 
                self._transform.tree_path(trans_id))
1707
 
        name = self._transform._limbo_name(trans_id)
1708
 
        statval = os.lstat(name)
1709
 
        return statval.st_mtime
1710
 
 
1711
 
    def is_versioned(self, path):
1712
 
        trans_id = self._path2trans_id(path)
1713
 
        if trans_id is None:
1714
 
            # It doesn't exist, so it's not versioned.
1715
 
            return False
1716
 
        if trans_id in self._transform._versioned:
1717
 
            return True
1718
 
        if trans_id in self._transform._removed_id:
1719
 
            return False
1720
 
        orig_path = self._transform.tree_path(trans_id)
1721
 
        return self._transform._tree.is_versioned(orig_path)
1722
 
 
1723
 
    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
1724
 
        if recurse_nested:
1725
 
            raise NotImplementedError(
1726
 
                'follow tree references not yet supported')
1727
 
 
1728
 
        # This may not be a maximally efficient implementation, but it is
1729
 
        # reasonably straightforward.  An implementation that grafts the
1730
 
        # TreeTransform changes onto the tree's iter_entries_by_dir results
1731
 
        # might be more efficient, but requires tricky inferences about stack
1732
 
        # position.
1733
 
        for trans_id, path in self._list_files_by_dir():
1734
 
            entry, is_versioned = self._transform.final_entry(trans_id)
1735
 
            if entry is None:
1736
 
                continue
1737
 
            if not is_versioned and entry.kind != 'directory':
1738
 
                continue
1739
 
            if specific_files is not None and path not in specific_files:
1740
 
                continue
1741
 
            if entry is not None:
1742
 
                yield path, entry
1743
 
 
1744
 
    def _list_files_by_dir(self):
1745
 
        todo = [ROOT_PARENT]
1746
 
        while len(todo) > 0:
1747
 
            parent = todo.pop()
1748
 
            children = list(self._all_children(parent))
1749
 
            paths = dict(zip(children, self._final_paths.get_paths(children)))
1750
 
            children.sort(key=paths.get)
1751
 
            todo.extend(reversed(children))
1752
 
            for trans_id in children:
1753
 
                yield trans_id, paths[trans_id][0]
1754
 
 
1755
 
    def revision_tree(self, revision_id):
1756
 
        return self._transform._tree.revision_tree(revision_id)
1757
 
 
1758
 
    def _stat_limbo_file(self, trans_id):
1759
 
        name = self._transform._limbo_name(trans_id)
1760
 
        return os.lstat(name)
1761
 
 
1762
 
    def git_snapshot(self, want_unversioned=False):
1763
 
        extra = set()
1764
 
        os = []
1765
 
        for trans_id, path in self._list_files_by_dir():
1766
 
            if not self._transform.final_is_versioned(trans_id):
1767
 
                if not want_unversioned:
1768
 
                    continue
1769
 
                extra.add(path)
1770
 
            o, mode = self._transform.final_git_entry(trans_id)
1771
 
            if o is not None:
1772
 
                self.store.add_object(o)
1773
 
                os.append((encode_git_path(path), o.id, mode))
1774
 
        if not os:
1775
 
            return None, extra
1776
 
        return commit_tree(self.store, os), extra
1777
 
 
1778
 
    def iter_child_entries(self, path):
1779
 
        trans_id = self._path2trans_id(path)
1780
 
        if trans_id is None:
1781
 
            raise errors.NoSuchFile(path)
1782
 
        for child_trans_id in self._all_children(trans_id):
1783
 
            entry, is_versioned = self._transform.final_entry(trans_id)
1784
 
            if not is_versioned:
1785
 
                continue
1786
 
            if entry is not None:
1787
 
                yield entry