/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: 2019-03-05 07:32:38 UTC
  • mto: (7290.1.21 work)
  • mto: This revision was merged to the branch mainline in revision 7311.
  • Revision ID: jelmer@jelmer.uk-20190305073238-zlqn981opwnqsmzi
Add appveyor configuration.

Show diffs side-by-side

added added

removed removed

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