/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to breezy/git/transform.py

  • Committer: Jelmer Vernooij
  • Date: 2020-09-02 11:51:19 UTC
  • mto: (7490.40.109 work)
  • mto: This revision was merged to the branch mainline in revision 7526.
  • Revision ID: jelmer@jelmer.uk-20200902115119-otuspc349t9rmhua
add test for git file merge.

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