/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/transform.py

  • Committer: Jelmer Vernooij
  • Date: 2020-09-02 16:35:18 UTC
  • mto: (7490.40.109 work)
  • mto: This revision was merged to the branch mainline in revision 7526.
  • Revision ID: jelmer@jelmer.uk-20200902163518-sy9f4unbboljphgu
Handle duplicate directories entries for git.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
 
19
import os
 
20
import errno
 
21
from stat import S_ISREG, S_IEXEC
 
22
import time
 
23
 
 
24
from . import (
 
25
    config as _mod_config,
 
26
    controldir,
 
27
    errors,
 
28
    lazy_import,
 
29
    lock,
 
30
    osutils,
 
31
    registry,
 
32
    trace,
 
33
    )
 
34
lazy_import.lazy_import(globals(), """
 
35
from breezy import (
 
36
    cleanup,
 
37
    multiparent,
 
38
    revision as _mod_revision,
 
39
    ui,
 
40
    urlutils,
 
41
    )
 
42
from breezy.i18n import gettext
 
43
""")
 
44
 
 
45
from .errors import (DuplicateKey,
 
46
                     BzrError, InternalBzrError)
 
47
from .filters import filtered_output_bytes, ContentFilterContext
 
48
from .mutabletree import MutableTree
 
49
from .osutils import (
 
50
    delete_any,
 
51
    file_kind,
 
52
    pathjoin,
 
53
    sha_file,
 
54
    splitpath,
 
55
    supports_symlinks,
 
56
    )
 
57
from .progress import ProgressPhase
 
58
from .sixish import (
 
59
    text_type,
 
60
    viewitems,
 
61
    viewvalues,
 
62
    )
 
63
from .tree import (
 
64
    InterTree,
 
65
    find_previous_path,
 
66
    )
 
67
 
 
68
 
 
69
ROOT_PARENT = "root-parent"
 
70
 
 
71
 
 
72
class NoFinalPath(BzrError):
 
73
 
 
74
    _fmt = ("No final name for trans_id %(trans_id)r\n"
 
75
            "root trans-id: %(root_trans_id)r\n")
 
76
 
 
77
    def __init__(self, trans_id, transform):
 
78
        self.trans_id = trans_id
 
79
        self.root_trans_id = transform.root
 
80
 
 
81
 
 
82
class ReusingTransform(BzrError):
 
83
 
 
84
    _fmt = "Attempt to reuse a transform that has already been applied."
 
85
 
 
86
 
 
87
class MalformedTransform(InternalBzrError):
 
88
 
 
89
    _fmt = "Tree transform is malformed %(conflicts)r"
 
90
 
 
91
 
 
92
class CantMoveRoot(BzrError):
 
93
 
 
94
    _fmt = "Moving the root directory is not supported at this time"
 
95
 
 
96
 
 
97
class ImmortalLimbo(BzrError):
 
98
 
 
99
    _fmt = """Unable to delete transform temporary directory %(limbo_dir)s.
 
100
    Please examine %(limbo_dir)s to see if it contains any files you wish to
 
101
    keep, and delete it when you are done."""
 
102
 
 
103
    def __init__(self, limbo_dir):
 
104
        BzrError.__init__(self)
 
105
        self.limbo_dir = limbo_dir
 
106
 
 
107
 
 
108
class TransformRenameFailed(BzrError):
 
109
 
 
110
    _fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s"
 
111
 
 
112
    def __init__(self, from_path, to_path, why, errno):
 
113
        self.from_path = from_path
 
114
        self.to_path = to_path
 
115
        self.why = why
 
116
        self.errno = errno
 
117
 
 
118
 
 
119
def unique_add(map, key, value):
 
120
    if key in map:
 
121
        raise DuplicateKey(key=key)
 
122
    map[key] = value
 
123
 
 
124
 
 
125
class _TransformResults(object):
 
126
 
 
127
    def __init__(self, modified_paths, rename_count):
 
128
        object.__init__(self)
 
129
        self.modified_paths = modified_paths
 
130
        self.rename_count = rename_count
 
131
 
 
132
 
 
133
class TreeTransform(object):
 
134
    """Represent a tree transformation.
 
135
 
 
136
    This object is designed to support incremental generation of the transform,
 
137
    in any order.
 
138
 
 
139
    However, it gives optimum performance when parent directories are created
 
140
    before their contents.  The transform is then able to put child files
 
141
    directly in their parent directory, avoiding later renames.
 
142
 
 
143
    It is easy to produce malformed transforms, but they are generally
 
144
    harmless.  Attempting to apply a malformed transform will cause an
 
145
    exception to be raised before any modifications are made to the tree.
 
146
 
 
147
    Many kinds of malformed transforms can be corrected with the
 
148
    resolve_conflicts function.  The remaining ones indicate programming error,
 
149
    such as trying to create a file with no path.
 
150
 
 
151
    Two sets of file creation methods are supplied.  Convenience methods are:
 
152
     * new_file
 
153
     * new_directory
 
154
     * new_symlink
 
155
 
 
156
    These are composed of the low-level methods:
 
157
     * create_path
 
158
     * create_file or create_directory or create_symlink
 
159
     * version_file
 
160
     * set_executability
 
161
 
 
162
    Transform/Transaction ids
 
163
    -------------------------
 
164
    trans_ids are temporary ids assigned to all files involved in a transform.
 
165
    It's possible, even common, that not all files in the Tree have trans_ids.
 
166
 
 
167
    trans_ids are only valid for the TreeTransform that generated them.
 
168
    """
 
169
 
 
170
    def __init__(self, tree, pb=None):
 
171
        self._tree = tree
 
172
        # A progress bar
 
173
        self._pb = pb
 
174
        self._id_number = 0
 
175
        # Mapping of path in old tree -> trans_id
 
176
        self._tree_path_ids = {}
 
177
        # Mapping trans_id -> path in old tree
 
178
        self._tree_id_paths = {}
 
179
        # mapping of trans_id -> new basename
 
180
        self._new_name = {}
 
181
        # mapping of trans_id -> new parent trans_id
 
182
        self._new_parent = {}
 
183
        # mapping of trans_id with new contents -> new file_kind
 
184
        self._new_contents = {}
 
185
        # Set of trans_ids whose contents will be removed
 
186
        self._removed_contents = set()
 
187
        # Mapping of trans_id -> new execute-bit value
 
188
        self._new_executability = {}
 
189
        # Mapping of trans_id -> new tree-reference value
 
190
        self._new_reference_revision = {}
 
191
        # Set of trans_ids that will be removed
 
192
        self._removed_id = set()
 
193
        # Indicator of whether the transform has been applied
 
194
        self._done = False
 
195
 
 
196
    def __enter__(self):
 
197
        """Support Context Manager API."""
 
198
        return self
 
199
 
 
200
    def __exit__(self, exc_type, exc_val, exc_tb):
 
201
        """Support Context Manager API."""
 
202
        self.finalize()
 
203
 
 
204
    def iter_tree_children(self, trans_id):
 
205
        """Iterate through the entry's tree children, if any.
 
206
 
 
207
        :param trans_id: trans id to iterate
 
208
        :returns: Iterator over paths
 
209
        """
 
210
        raise NotImplementedError(self.iter_tree_children)
 
211
 
 
212
    def canonical_path(self, path):
 
213
        return path
 
214
 
 
215
    def tree_kind(self, trans_id):
 
216
        raise NotImplementedError(self.tree_kind)
 
217
 
 
218
    def by_parent(self):
 
219
        """Return a map of parent: children for known parents.
 
220
 
 
221
        Only new paths and parents of tree files with assigned ids are used.
 
222
        """
 
223
        by_parent = {}
 
224
        items = list(viewitems(self._new_parent))
 
225
        items.extend((t, self.final_parent(t))
 
226
                     for t in list(self._tree_id_paths))
 
227
        for trans_id, parent_id in items:
 
228
            if parent_id not in by_parent:
 
229
                by_parent[parent_id] = set()
 
230
            by_parent[parent_id].add(trans_id)
 
231
        return by_parent
 
232
 
 
233
    def finalize(self):
 
234
        """Release the working tree lock, if held.
 
235
 
 
236
        This is required if apply has not been invoked, but can be invoked
 
237
        even after apply.
 
238
        """
 
239
        raise NotImplementedError(self.finalize)
 
240
 
 
241
    def create_path(self, name, parent):
 
242
        """Assign a transaction id to a new path"""
 
243
        trans_id = self.assign_id()
 
244
        unique_add(self._new_name, trans_id, name)
 
245
        unique_add(self._new_parent, trans_id, parent)
 
246
        return trans_id
 
247
 
 
248
    def adjust_path(self, name, parent, trans_id):
 
249
        """Change the path that is assigned to a transaction id."""
 
250
        if parent is None:
 
251
            raise ValueError("Parent trans-id may not be None")
 
252
        if trans_id == self.root:
 
253
            raise CantMoveRoot
 
254
        self._new_name[trans_id] = name
 
255
        self._new_parent[trans_id] = parent
 
256
 
 
257
    def adjust_root_path(self, name, parent):
 
258
        """Emulate moving the root by moving all children, instead.
 
259
 
 
260
        We do this by undoing the association of root's transaction id with the
 
261
        current tree.  This allows us to create a new directory with that
 
262
        transaction id.  We unversion the root directory and version the
 
263
        physically new directory, and hope someone versions the tree root
 
264
        later.
 
265
        """
 
266
        raise NotImplementedError(self.adjust_root_path)
 
267
 
 
268
    def fixup_new_roots(self):
 
269
        """Reinterpret requests to change the root directory
 
270
 
 
271
        Instead of creating a root directory, or moving an existing directory,
 
272
        all the attributes and children of the new root are applied to the
 
273
        existing root directory.
 
274
 
 
275
        This means that the old root trans-id becomes obsolete, so it is
 
276
        recommended only to invoke this after the root trans-id has become
 
277
        irrelevant.
 
278
        """
 
279
        raise NotImplementedError(self.fixup_new_roots)
 
280
 
 
281
    def assign_id(self):
 
282
        """Produce a new tranform id"""
 
283
        new_id = "new-%s" % self._id_number
 
284
        self._id_number += 1
 
285
        return new_id
 
286
 
 
287
    def trans_id_tree_path(self, path):
 
288
        """Determine (and maybe set) the transaction ID for a tree path."""
 
289
        path = self.canonical_path(path)
 
290
        if path not in self._tree_path_ids:
 
291
            self._tree_path_ids[path] = self.assign_id()
 
292
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
293
        return self._tree_path_ids[path]
 
294
 
 
295
    def get_tree_parent(self, trans_id):
 
296
        """Determine id of the parent in the tree."""
 
297
        path = self._tree_id_paths[trans_id]
 
298
        if path == "":
 
299
            return ROOT_PARENT
 
300
        return self.trans_id_tree_path(os.path.dirname(path))
 
301
 
 
302
    def delete_contents(self, trans_id):
 
303
        """Schedule the contents of a path entry for deletion"""
 
304
        kind = self.tree_kind(trans_id)
 
305
        if kind is not None:
 
306
            self._removed_contents.add(trans_id)
 
307
 
 
308
    def cancel_deletion(self, trans_id):
 
309
        """Cancel a scheduled deletion"""
 
310
        self._removed_contents.remove(trans_id)
 
311
 
 
312
    def delete_versioned(self, trans_id):
 
313
        """Delete and unversion a versioned file"""
 
314
        self.delete_contents(trans_id)
 
315
        self.unversion_file(trans_id)
 
316
 
 
317
    def set_executability(self, executability, trans_id):
 
318
        """Schedule setting of the 'execute' bit
 
319
        To unschedule, set to None
 
320
        """
 
321
        if executability is None:
 
322
            del self._new_executability[trans_id]
 
323
        else:
 
324
            unique_add(self._new_executability, trans_id, executability)
 
325
 
 
326
    def set_tree_reference(self, revision_id, trans_id):
 
327
        """Set the reference associated with a directory"""
 
328
        unique_add(self._new_reference_revision, trans_id, revision_id)
 
329
 
 
330
    def version_file(self, trans_id, file_id=None):
 
331
        """Schedule a file to become versioned."""
 
332
        raise NotImplementedError(self.version_file)
 
333
 
 
334
    def cancel_versioning(self, trans_id):
 
335
        """Undo a previous versioning of a file"""
 
336
        raise NotImplementedError(self.cancel_versioning)
 
337
 
 
338
    def unversion_file(self, trans_id):
 
339
        """Schedule a path entry to become unversioned"""
 
340
        self._removed_id.add(trans_id)
 
341
 
 
342
    def new_paths(self, filesystem_only=False):
 
343
        """Determine the paths of all new and changed files.
 
344
 
 
345
        :param filesystem_only: if True, only calculate values for files
 
346
            that require renames or execute bit changes.
 
347
        """
 
348
        raise NotImplementedError(self.new_paths)
 
349
 
 
350
    def final_kind(self, trans_id):
 
351
        """Determine the final file kind, after any changes applied.
 
352
 
 
353
        :return: None if the file does not exist/has no contents.  (It is
 
354
            conceivable that a path would be created without the corresponding
 
355
            contents insertion command)
 
356
        """
 
357
        if trans_id in self._new_contents:
 
358
            if trans_id in self._new_reference_revision:
 
359
                return 'tree-reference'
 
360
            return self._new_contents[trans_id]
 
361
        elif trans_id in self._removed_contents:
 
362
            return None
 
363
        else:
 
364
            return self.tree_kind(trans_id)
 
365
 
 
366
    def tree_path(self, trans_id):
 
367
        """Determine the tree path associated with the trans_id."""
 
368
        return self._tree_id_paths.get(trans_id)
 
369
 
 
370
    def final_is_versioned(self, trans_id):
 
371
        raise NotImplementedError(self.final_is_versioned)
 
372
 
 
373
    def final_parent(self, trans_id):
 
374
        """Determine the parent file_id, after any changes are applied.
 
375
 
 
376
        ROOT_PARENT is returned for the tree root.
 
377
        """
 
378
        try:
 
379
            return self._new_parent[trans_id]
 
380
        except KeyError:
 
381
            return self.get_tree_parent(trans_id)
 
382
 
 
383
    def final_name(self, trans_id):
 
384
        """Determine the final filename, after all changes are applied."""
 
385
        try:
 
386
            return self._new_name[trans_id]
 
387
        except KeyError:
 
388
            try:
 
389
                return os.path.basename(self._tree_id_paths[trans_id])
 
390
            except KeyError:
 
391
                raise NoFinalPath(trans_id, self)
 
392
 
 
393
    def path_changed(self, trans_id):
 
394
        """Return True if a trans_id's path has changed."""
 
395
        return (trans_id in self._new_name) or (trans_id in self._new_parent)
 
396
 
 
397
    def new_contents(self, trans_id):
 
398
        return (trans_id in self._new_contents)
 
399
 
 
400
    def find_raw_conflicts(self):
 
401
        """Find any violations of inventory or filesystem invariants"""
 
402
        raise NotImplementedError(self.find_raw_conflicts)
 
403
 
 
404
    def new_file(self, name, parent_id, contents, file_id=None,
 
405
                 executable=None, sha1=None):
 
406
        """Convenience method to create files.
 
407
 
 
408
        name is the name of the file to create.
 
409
        parent_id is the transaction id of the parent directory of the file.
 
410
        contents is an iterator of bytestrings, which will be used to produce
 
411
        the file.
 
412
        :param file_id: The inventory ID of the file, if it is to be versioned.
 
413
        :param executable: Only valid when a file_id has been supplied.
 
414
        """
 
415
        raise NotImplementedError(self.new_file)
 
416
 
 
417
    def new_directory(self, name, parent_id, file_id=None):
 
418
        """Convenience method to create directories.
 
419
 
 
420
        name is the name of the directory to create.
 
421
        parent_id is the transaction id of the parent directory of the
 
422
        directory.
 
423
        file_id is the inventory ID of the directory, if it is to be versioned.
 
424
        """
 
425
        raise NotImplementedError(self.new_directory)
 
426
 
 
427
    def new_symlink(self, name, parent_id, target, file_id=None):
 
428
        """Convenience method to create symbolic link.
 
429
 
 
430
        name is the name of the symlink to create.
 
431
        parent_id is the transaction id of the parent directory of the symlink.
 
432
        target is a bytestring of the target of the symlink.
 
433
        file_id is the inventory ID of the file, if it is to be versioned.
 
434
        """
 
435
        raise NotImplementedError(self.new_symlink)
 
436
 
 
437
    def new_orphan(self, trans_id, parent_id):
 
438
        """Schedule an item to be orphaned.
 
439
 
 
440
        When a directory is about to be removed, its children, if they are not
 
441
        versioned are moved out of the way: they don't have a parent anymore.
 
442
 
 
443
        :param trans_id: The trans_id of the existing item.
 
444
        :param parent_id: The parent trans_id of the item.
 
445
        """
 
446
        raise NotImplementedError(self.new_orphan)
 
447
 
 
448
    def iter_changes(self):
 
449
        """Produce output in the same format as Tree.iter_changes.
 
450
 
 
451
        Will produce nonsensical results if invoked while inventory/filesystem
 
452
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
 
453
 
 
454
        This reads the Transform, but only reproduces changes involving a
 
455
        file_id.  Files that are not versioned in either of the FROM or TO
 
456
        states are not reflected.
 
457
        """
 
458
        raise NotImplementedError(self.iter_changes)
 
459
 
 
460
    def get_preview_tree(self):
 
461
        """Return a tree representing the result of the transform.
 
462
 
 
463
        The tree is a snapshot, and altering the TreeTransform will invalidate
 
464
        it.
 
465
        """
 
466
        raise NotImplementedError(self.get_preview_tree)
 
467
 
 
468
    def commit(self, branch, message, merge_parents=None, strict=False,
 
469
               timestamp=None, timezone=None, committer=None, authors=None,
 
470
               revprops=None, revision_id=None):
 
471
        """Commit the result of this TreeTransform to a branch.
 
472
 
 
473
        :param branch: The branch to commit to.
 
474
        :param message: The message to attach to the commit.
 
475
        :param merge_parents: Additional parent revision-ids specified by
 
476
            pending merges.
 
477
        :param strict: If True, abort the commit if there are unversioned
 
478
            files.
 
479
        :param timestamp: if not None, seconds-since-epoch for the time and
 
480
            date.  (May be a float.)
 
481
        :param timezone: Optional timezone for timestamp, as an offset in
 
482
            seconds.
 
483
        :param committer: Optional committer in email-id format.
 
484
            (e.g. "J Random Hacker <jrandom@example.com>")
 
485
        :param authors: Optional list of authors in email-id format.
 
486
        :param revprops: Optional dictionary of revision properties.
 
487
        :param revision_id: Optional revision id.  (Specifying a revision-id
 
488
            may reduce performance for some non-native formats.)
 
489
        :return: The revision_id of the revision committed.
 
490
        """
 
491
        raise NotImplementedError(self.commit)
 
492
 
 
493
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
 
494
        """Schedule creation of a new file.
 
495
 
 
496
        :seealso: new_file.
 
497
 
 
498
        :param contents: an iterator of strings, all of which will be written
 
499
            to the target destination.
 
500
        :param trans_id: TreeTransform handle
 
501
        :param mode_id: If not None, force the mode of the target file to match
 
502
            the mode of the object referenced by mode_id.
 
503
            Otherwise, we will try to preserve mode bits of an existing file.
 
504
        :param sha1: If the sha1 of this content is already known, pass it in.
 
505
            We can use it to prevent future sha1 computations.
 
506
        """
 
507
        raise NotImplementedError(self.create_file)
 
508
 
 
509
    def create_directory(self, trans_id):
 
510
        """Schedule creation of a new directory.
 
511
 
 
512
        See also new_directory.
 
513
        """
 
514
        raise NotImplementedError(self.create_directory)
 
515
 
 
516
    def create_symlink(self, target, trans_id):
 
517
        """Schedule creation of a new symbolic link.
 
518
 
 
519
        target is a bytestring.
 
520
        See also new_symlink.
 
521
        """
 
522
        raise NotImplementedError(self.create_symlink)
 
523
 
 
524
    def create_hardlink(self, path, trans_id):
 
525
        """Schedule creation of a hard link"""
 
526
        raise NotImplementedError(self.create_hardlink)
 
527
 
 
528
    def cancel_creation(self, trans_id):
 
529
        """Cancel the creation of new file contents."""
 
530
        raise NotImplementedError(self.cancel_creation)
 
531
 
 
532
    def cook_conflicts(self, raw_conflicts):
 
533
        """Cook conflicts.
 
534
        """
 
535
        raise NotImplementedError(self.cook_conflicts)
 
536
 
 
537
 
 
538
class OrphaningError(errors.BzrError):
 
539
 
 
540
    # Only bugs could lead to such exception being seen by the user
 
541
    internal_error = True
 
542
    _fmt = "Error while orphaning %s in %s directory"
 
543
 
 
544
    def __init__(self, orphan, parent):
 
545
        errors.BzrError.__init__(self)
 
546
        self.orphan = orphan
 
547
        self.parent = parent
 
548
 
 
549
 
 
550
class OrphaningForbidden(OrphaningError):
 
551
 
 
552
    _fmt = "Policy: %s doesn't allow creating orphans."
 
553
 
 
554
    def __init__(self, policy):
 
555
        errors.BzrError.__init__(self)
 
556
        self.policy = policy
 
557
 
 
558
 
 
559
def move_orphan(tt, orphan_id, parent_id):
 
560
    """See TreeTransformBase.new_orphan.
 
561
 
 
562
    This creates a new orphan in the `brz-orphans` dir at the root of the
 
563
    `TreeTransform`.
 
564
 
 
565
    :param tt: The TreeTransform orphaning `trans_id`.
 
566
 
 
567
    :param orphan_id: The trans id that should be orphaned.
 
568
 
 
569
    :param parent_id: The orphan parent trans id.
 
570
    """
 
571
    # Add the orphan dir if it doesn't exist
 
572
    orphan_dir_basename = 'brz-orphans'
 
573
    od_id = tt.trans_id_tree_path(orphan_dir_basename)
 
574
    if tt.final_kind(od_id) is None:
 
575
        tt.create_directory(od_id)
 
576
    parent_path = tt._tree_id_paths[parent_id]
 
577
    # Find a name that doesn't exist yet in the orphan dir
 
578
    actual_name = tt.final_name(orphan_id)
 
579
    new_name = tt._available_backup_name(actual_name, od_id)
 
580
    tt.adjust_path(new_name, od_id, orphan_id)
 
581
    trace.warning('%s has been orphaned in %s'
 
582
                  % (joinpath(parent_path, actual_name), orphan_dir_basename))
 
583
 
 
584
 
 
585
def refuse_orphan(tt, orphan_id, parent_id):
 
586
    """See TreeTransformBase.new_orphan.
 
587
 
 
588
    This refuses to create orphan, letting the caller handle the conflict.
 
589
    """
 
590
    raise OrphaningForbidden('never')
 
591
 
 
592
 
 
593
orphaning_registry = registry.Registry()
 
594
orphaning_registry.register(
 
595
    u'conflict', refuse_orphan,
 
596
    'Leave orphans in place and create a conflict on the directory.')
 
597
orphaning_registry.register(
 
598
    u'move', move_orphan,
 
599
    'Move orphans into the brz-orphans directory.')
 
600
orphaning_registry._set_default_key(u'conflict')
 
601
 
 
602
 
 
603
opt_transform_orphan = _mod_config.RegistryOption(
 
604
    'transform.orphan_policy', orphaning_registry,
 
605
    help='Policy for orphaned files during transform operations.',
 
606
    invalid='warning')
 
607
 
 
608
 
 
609
def joinpath(parent, child):
 
610
    """Join tree-relative paths, handling the tree root specially"""
 
611
    if parent is None or parent == "":
 
612
        return child
 
613
    else:
 
614
        return pathjoin(parent, child)
 
615
 
 
616
 
 
617
class FinalPaths(object):
 
618
    """Make path calculation cheap by memoizing paths.
 
619
 
 
620
    The underlying tree must not be manipulated between calls, or else
 
621
    the results will likely be incorrect.
 
622
    """
 
623
 
 
624
    def __init__(self, transform):
 
625
        object.__init__(self)
 
626
        self._known_paths = {}
 
627
        self.transform = transform
 
628
 
 
629
    def _determine_path(self, trans_id):
 
630
        if trans_id == self.transform.root or trans_id == ROOT_PARENT:
 
631
            return u""
 
632
        name = self.transform.final_name(trans_id)
 
633
        parent_id = self.transform.final_parent(trans_id)
 
634
        if parent_id == self.transform.root:
 
635
            return name
 
636
        else:
 
637
            return pathjoin(self.get_path(parent_id), name)
 
638
 
 
639
    def get_path(self, trans_id):
 
640
        """Find the final path associated with a trans_id"""
 
641
        if trans_id not in self._known_paths:
 
642
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
643
        return self._known_paths[trans_id]
 
644
 
 
645
    def get_paths(self, trans_ids):
 
646
        return [(self.get_path(t), t) for t in trans_ids]
 
647
 
 
648
 
 
649
def _reparent_children(tt, old_parent, new_parent):
 
650
    for child in tt.iter_tree_children(old_parent):
 
651
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
652
 
 
653
 
 
654
def _reparent_transform_children(tt, old_parent, new_parent):
 
655
    by_parent = tt.by_parent()
 
656
    for child in by_parent[old_parent]:
 
657
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
658
    return by_parent[old_parent]
 
659
 
 
660
 
 
661
def _content_match(tree, entry, tree_path, kind, target_path):
 
662
    if entry.kind != kind:
 
663
        return False
 
664
    if entry.kind == "directory":
 
665
        return True
 
666
    if entry.kind == "file":
 
667
        with open(target_path, 'rb') as f1, \
 
668
                tree.get_file(tree_path) as f2:
 
669
            if osutils.compare_files(f1, f2):
 
670
                return True
 
671
    elif entry.kind == "symlink":
 
672
        if tree.get_symlink_target(tree_path) == os.readlink(target_path):
 
673
            return True
 
674
    return False
 
675
 
 
676
 
 
677
def new_by_entry(path, tt, entry, parent_id, tree):
 
678
    """Create a new file according to its inventory entry"""
 
679
    name = entry.name
 
680
    kind = entry.kind
 
681
    if kind == 'file':
 
682
        with tree.get_file(path) as f:
 
683
            executable = tree.is_executable(path)
 
684
            return tt.new_file(
 
685
                name, parent_id, osutils.file_iterator(f), entry.file_id,
 
686
                executable)
 
687
    elif kind in ('directory', 'tree-reference'):
 
688
        trans_id = tt.new_directory(name, parent_id, entry.file_id)
 
689
        if kind == 'tree-reference':
 
690
            tt.set_tree_reference(entry.reference_revision, trans_id)
 
691
        return trans_id
 
692
    elif kind == 'symlink':
 
693
        target = tree.get_symlink_target(path)
 
694
        return tt.new_symlink(name, parent_id, target, entry.file_id)
 
695
    else:
 
696
        raise errors.BadFileKindError(name, kind)
 
697
 
 
698
 
 
699
def create_from_tree(tt, trans_id, tree, path, chunks=None,
 
700
                     filter_tree_path=None):
 
701
    """Create new file contents according to tree contents.
 
702
 
 
703
    :param filter_tree_path: the tree path to use to lookup
 
704
      content filters to apply to the bytes output in the working tree.
 
705
      This only applies if the working tree supports content filtering.
 
706
    """
 
707
    kind = tree.kind(path)
 
708
    if kind == 'directory':
 
709
        tt.create_directory(trans_id)
 
710
    elif kind == "file":
 
711
        if chunks is None:
 
712
            f = tree.get_file(path)
 
713
            chunks = osutils.file_iterator(f)
 
714
        else:
 
715
            f = None
 
716
        try:
 
717
            wt = tt._tree
 
718
            if wt.supports_content_filtering() and filter_tree_path is not None:
 
719
                filters = wt._content_filter_stack(filter_tree_path)
 
720
                chunks = filtered_output_bytes(
 
721
                    chunks, filters,
 
722
                    ContentFilterContext(filter_tree_path, tree))
 
723
            tt.create_file(chunks, trans_id)
 
724
        finally:
 
725
            if f is not None:
 
726
                f.close()
 
727
    elif kind == "symlink":
 
728
        tt.create_symlink(tree.get_symlink_target(path), trans_id)
 
729
    else:
 
730
        raise AssertionError('Unknown kind %r' % kind)
 
731
 
 
732
 
 
733
def create_entry_executability(tt, entry, trans_id):
 
734
    """Set the executability of a trans_id according to an inventory entry"""
 
735
    if entry.kind == "file":
 
736
        tt.set_executability(entry.executable, trans_id)
 
737
 
 
738
 
 
739
def _prepare_revert_transform(es, working_tree, target_tree, tt, filenames,
 
740
                              backups, pp, basis_tree=None,
 
741
                              merge_modified=None):
 
742
    with ui.ui_factory.nested_progress_bar() as child_pb:
 
743
        if merge_modified is None:
 
744
            merge_modified = working_tree.merge_modified()
 
745
        merge_modified = _alter_files(es, working_tree, target_tree, tt,
 
746
                                      child_pb, filenames, backups,
 
747
                                      merge_modified, basis_tree)
 
748
    with ui.ui_factory.nested_progress_bar() as child_pb:
 
749
        raw_conflicts = resolve_conflicts(
 
750
            tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
 
751
    conflicts = tt.cook_conflicts(raw_conflicts)
 
752
    return conflicts, merge_modified
 
753
 
 
754
 
 
755
def revert(working_tree, target_tree, filenames, backups=False,
 
756
           pb=None, change_reporter=None, merge_modified=None, basis_tree=None):
 
757
    """Revert a working tree's contents to those of a target tree."""
 
758
    with cleanup.ExitStack() as es:
 
759
        pb = es.enter_context(ui.ui_factory.nested_progress_bar())
 
760
        es.enter_context(target_tree.lock_read())
 
761
        tt = es.enter_context(working_tree.transform(pb))
 
762
        pp = ProgressPhase("Revert phase", 3, pb)
 
763
        conflicts, merge_modified = _prepare_revert_transform(
 
764
            es, working_tree, target_tree, tt, filenames, backups, pp)
 
765
        if change_reporter:
 
766
            from . import delta
 
767
            change_reporter = delta._ChangeReporter(
 
768
                unversioned_filter=working_tree.is_ignored)
 
769
            delta.report_changes(tt.iter_changes(), change_reporter)
 
770
        for conflict in conflicts:
 
771
            trace.warning(text_type(conflict))
 
772
        pp.next_phase()
 
773
        tt.apply()
 
774
        if working_tree.supports_merge_modified():
 
775
            working_tree.set_merge_modified(merge_modified)
 
776
    return conflicts
 
777
 
 
778
 
 
779
def _alter_files(es, working_tree, target_tree, tt, pb, specific_files,
 
780
                 backups, merge_modified, basis_tree=None):
 
781
    if basis_tree is not None:
 
782
        es.enter_context(basis_tree.lock_read())
 
783
    # We ask the working_tree for its changes relative to the target, rather
 
784
    # than the target changes relative to the working tree. Because WT4 has an
 
785
    # optimizer to compare itself to a target, but no optimizer for the
 
786
    # reverse.
 
787
    change_list = working_tree.iter_changes(
 
788
        target_tree, specific_files=specific_files, pb=pb)
 
789
    if not target_tree.is_versioned(u''):
 
790
        skip_root = True
 
791
    else:
 
792
        skip_root = False
 
793
    deferred_files = []
 
794
    for id_num, change in enumerate(change_list):
 
795
        target_path, wt_path = change.path
 
796
        target_versioned, wt_versioned = change.versioned
 
797
        target_parent = change.parent_id[0]
 
798
        target_name, wt_name = change.name
 
799
        target_kind, wt_kind = change.kind
 
800
        target_executable, wt_executable = change.executable
 
801
        if skip_root and wt_path == '':
 
802
            continue
 
803
        mode_id = None
 
804
        if wt_path is not None:
 
805
            trans_id = tt.trans_id_tree_path(wt_path)
 
806
        else:
 
807
            trans_id = tt.assign_id()
 
808
        if change.changed_content:
 
809
            keep_content = False
 
810
            if wt_kind == 'file' and (backups or target_kind is None):
 
811
                wt_sha1 = working_tree.get_file_sha1(wt_path)
 
812
                if merge_modified.get(wt_path) != wt_sha1:
 
813
                    # acquire the basis tree lazily to prevent the
 
814
                    # expense of accessing it when it's not needed ?
 
815
                    # (Guessing, RBC, 200702)
 
816
                    if basis_tree is None:
 
817
                        basis_tree = working_tree.basis_tree()
 
818
                        es.enter_context(basis_tree.lock_read())
 
819
                    basis_inter = InterTree.get(basis_tree, working_tree)
 
820
                    basis_path = basis_inter.find_source_path(wt_path)
 
821
                    if basis_path is None:
 
822
                        if target_kind is None and not target_versioned:
 
823
                            keep_content = True
 
824
                    else:
 
825
                        if wt_sha1 != basis_tree.get_file_sha1(basis_path):
 
826
                            keep_content = True
 
827
            if wt_kind is not None:
 
828
                if not keep_content:
 
829
                    tt.delete_contents(trans_id)
 
830
                elif target_kind is not None:
 
831
                    parent_trans_id = tt.trans_id_tree_path(osutils.dirname(wt_path))
 
832
                    backup_name = tt._available_backup_name(
 
833
                        wt_name, parent_trans_id)
 
834
                    tt.adjust_path(backup_name, parent_trans_id, trans_id)
 
835
                    new_trans_id = tt.create_path(wt_name, parent_trans_id)
 
836
                    if wt_versioned and target_versioned:
 
837
                        tt.unversion_file(trans_id)
 
838
                        tt.version_file(
 
839
                            new_trans_id, file_id=getattr(change, 'file_id', None))
 
840
                    # New contents should have the same unix perms as old
 
841
                    # contents
 
842
                    mode_id = trans_id
 
843
                    trans_id = new_trans_id
 
844
            if target_kind in ('directory', 'tree-reference'):
 
845
                tt.create_directory(trans_id)
 
846
                if target_kind == 'tree-reference':
 
847
                    revision = target_tree.get_reference_revision(
 
848
                        target_path)
 
849
                    tt.set_tree_reference(revision, trans_id)
 
850
            elif target_kind == 'symlink':
 
851
                tt.create_symlink(target_tree.get_symlink_target(
 
852
                    target_path), trans_id)
 
853
            elif target_kind == 'file':
 
854
                deferred_files.append(
 
855
                    (target_path, (trans_id, mode_id, target_path)))
 
856
                if basis_tree is None:
 
857
                    basis_tree = working_tree.basis_tree()
 
858
                    es.enter_context(basis_tree.lock_read())
 
859
                new_sha1 = target_tree.get_file_sha1(target_path)
 
860
                basis_inter = InterTree.get(basis_tree, target_tree)
 
861
                basis_path = basis_inter.find_source_path(target_path)
 
862
                if (basis_path is not None and
 
863
                        new_sha1 == basis_tree.get_file_sha1(basis_path)):
 
864
                    # If the new contents of the file match what is in basis,
 
865
                    # then there is no need to store in merge_modified.
 
866
                    if basis_path in merge_modified:
 
867
                        del merge_modified[basis_path]
 
868
                else:
 
869
                    merge_modified[target_path] = new_sha1
 
870
 
 
871
                # preserve the execute bit when backing up
 
872
                if keep_content and wt_executable == target_executable:
 
873
                    tt.set_executability(target_executable, trans_id)
 
874
            elif target_kind is not None:
 
875
                raise AssertionError(target_kind)
 
876
        if not wt_versioned and target_versioned:
 
877
            tt.version_file(
 
878
                trans_id, file_id=getattr(change, 'file_id', None))
 
879
        if wt_versioned and not target_versioned:
 
880
            tt.unversion_file(trans_id)
 
881
        if (target_name is not None
 
882
                and (wt_name != target_name or change.is_reparented())):
 
883
            if target_path == '':
 
884
                parent_trans = ROOT_PARENT
 
885
            else:
 
886
                parent_trans = tt.trans_id_file_id(target_parent)
 
887
            if wt_path == '' and wt_versioned:
 
888
                tt.adjust_root_path(target_name, parent_trans)
 
889
            else:
 
890
                tt.adjust_path(target_name, parent_trans, trans_id)
 
891
        if wt_executable != target_executable and target_kind == "file":
 
892
            tt.set_executability(target_executable, trans_id)
 
893
    if working_tree.supports_content_filtering():
 
894
        for (trans_id, mode_id, target_path), bytes in (
 
895
                target_tree.iter_files_bytes(deferred_files)):
 
896
            # We're reverting a tree to the target tree so using the
 
897
            # target tree to find the file path seems the best choice
 
898
            # here IMO - Ian C 27/Oct/2009
 
899
            filters = working_tree._content_filter_stack(target_path)
 
900
            bytes = filtered_output_bytes(
 
901
                bytes, filters,
 
902
                ContentFilterContext(target_path, working_tree))
 
903
            tt.create_file(bytes, trans_id, mode_id)
 
904
    else:
 
905
        for (trans_id, mode_id, target_path), bytes in target_tree.iter_files_bytes(
 
906
                deferred_files):
 
907
            tt.create_file(bytes, trans_id, mode_id)
 
908
    tt.fixup_new_roots()
 
909
    return merge_modified
 
910
 
 
911
 
 
912
def resolve_conflicts(tt, pb=None, pass_func=None):
 
913
    """Make many conflict-resolution attempts, but die if they fail"""
 
914
    if pass_func is None:
 
915
        pass_func = conflict_pass
 
916
    new_conflicts = set()
 
917
    with ui.ui_factory.nested_progress_bar() as pb:
 
918
        for n in range(10):
 
919
            pb.update(gettext('Resolution pass'), n + 1, 10)
 
920
            conflicts = tt.find_raw_conflicts()
 
921
            if len(conflicts) == 0:
 
922
                return new_conflicts
 
923
            new_conflicts.update(pass_func(tt, conflicts))
 
924
        raise MalformedTransform(conflicts=conflicts)
 
925
 
 
926
 
 
927
def resolve_duplicate_id(tt, path_tree, c_type, old_trans_id, trans_id):
 
928
    tt.unversion_file(old_trans_id)
 
929
    yield (c_type, 'Unversioned existing file', old_trans_id, trans_id)
 
930
 
 
931
 
 
932
def resolve_duplicate(tt, path_tree, c_type, last_trans_id, trans_id, name):
 
933
    # files that were renamed take precedence
 
934
    final_parent = tt.final_parent(last_trans_id)
 
935
    if tt.path_changed(last_trans_id):
 
936
        existing_file, new_file = trans_id, last_trans_id
 
937
    else:
 
938
        existing_file, new_file = last_trans_id, trans_id
 
939
    if (not tt._tree.has_versioned_directories() and
 
940
            tt.final_kind(trans_id) == 'directory' and
 
941
            tt.final_kind(last_trans_id) == 'directory'):
 
942
        _reparent_transform_children(tt, existing_file, new_file)
 
943
        tt.delete_contents(existing_file)
 
944
        tt.unversion_file(existing_file)
 
945
        tt.cancel_creation(existing_file)
 
946
    else:
 
947
        new_name = tt.final_name(existing_file) + '.moved'
 
948
        tt.adjust_path(new_name, final_parent, existing_file)
 
949
        yield (c_type, 'Moved existing file to', existing_file, new_file)
 
950
 
 
951
 
 
952
def resolve_parent_loop(tt, path_tree, c_type, cur):
 
953
    # break the loop by undoing one of the ops that caused the loop
 
954
    while not tt.path_changed(cur):
 
955
        cur = tt.final_parent(cur)
 
956
    yield (c_type, 'Cancelled move', cur, tt.final_parent(cur),)
 
957
    tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
958
 
 
959
 
 
960
def resolve_missing_parent(tt, path_tree, c_type, trans_id):
 
961
    if trans_id in tt._removed_contents:
 
962
        cancel_deletion = True
 
963
        orphans = tt._get_potential_orphans(trans_id)
 
964
        if orphans:
 
965
            cancel_deletion = False
 
966
            # All children are orphans
 
967
            for o in orphans:
 
968
                try:
 
969
                    tt.new_orphan(o, trans_id)
 
970
                except OrphaningError:
 
971
                    # Something bad happened so we cancel the directory
 
972
                    # deletion which will leave it in place with a
 
973
                    # conflict. The user can deal with it from there.
 
974
                    # Note that this also catch the case where we don't
 
975
                    # want to create orphans and leave the directory in
 
976
                    # place.
 
977
                    cancel_deletion = True
 
978
                    break
 
979
        if cancel_deletion:
 
980
            # Cancel the directory deletion
 
981
            tt.cancel_deletion(trans_id)
 
982
            yield ('deleting parent', 'Not deleting', trans_id)
 
983
    else:
 
984
        create = True
 
985
        try:
 
986
            tt.final_name(trans_id)
 
987
        except NoFinalPath:
 
988
            if path_tree is not None:
 
989
                file_id = tt.final_file_id(trans_id)
 
990
                if file_id is None:
 
991
                    file_id = tt.inactive_file_id(trans_id)
 
992
                _, entry = next(path_tree.iter_entries_by_dir(
 
993
                    specific_files=[path_tree.id2path(file_id)]))
 
994
                # special-case the other tree root (move its
 
995
                # children to current root)
 
996
                if entry.parent_id is None:
 
997
                    create = False
 
998
                    moved = _reparent_transform_children(
 
999
                        tt, trans_id, tt.root)
 
1000
                    for child in moved:
 
1001
                        yield (c_type, 'Moved to root', child)
 
1002
                else:
 
1003
                    parent_trans_id = tt.trans_id_file_id(
 
1004
                        entry.parent_id)
 
1005
                    tt.adjust_path(entry.name, parent_trans_id,
 
1006
                                   trans_id)
 
1007
        if create:
 
1008
            tt.create_directory(trans_id)
 
1009
            yield (c_type, 'Created directory', trans_id)
 
1010
 
 
1011
 
 
1012
def resolve_unversioned_parent(tt, path_tree, c_type, trans_id):
 
1013
    file_id = tt.inactive_file_id(trans_id)
 
1014
    # special-case the other tree root (move its children instead)
 
1015
    if path_tree and path_tree.path2id('') == file_id:
 
1016
        # This is the root entry, skip it
 
1017
        return
 
1018
    tt.version_file(trans_id, file_id=file_id)
 
1019
    yield (c_type, 'Versioned directory', trans_id)
 
1020
 
 
1021
 
 
1022
def resolve_non_directory_parent(tt, path_tree, c_type, parent_id):
 
1023
    parent_parent = tt.final_parent(parent_id)
 
1024
    parent_name = tt.final_name(parent_id)
 
1025
    # TODO(jelmer): Make this code transform-specific
 
1026
    if tt._tree.supports_setting_file_ids():
 
1027
        parent_file_id = tt.final_file_id(parent_id)
 
1028
    else:
 
1029
        parent_file_id = b'DUMMY'
 
1030
    new_parent_id = tt.new_directory(parent_name + '.new',
 
1031
                                     parent_parent, parent_file_id)
 
1032
    _reparent_transform_children(tt, parent_id, new_parent_id)
 
1033
    if parent_file_id is not None:
 
1034
        tt.unversion_file(parent_id)
 
1035
    yield (c_type, 'Created directory', new_parent_id)
 
1036
 
 
1037
 
 
1038
def resolve_versioning_no_contents(tt, path_tree, c_type, trans_id):
 
1039
    tt.cancel_versioning(trans_id)
 
1040
    return []
 
1041
 
 
1042
 
 
1043
CONFLICT_RESOLVERS = {
 
1044
    'duplicate id': resolve_duplicate_id,
 
1045
    'duplicate': resolve_duplicate,
 
1046
    'parent loop': resolve_parent_loop,
 
1047
    'missing parent': resolve_missing_parent,
 
1048
    'unversioned parent': resolve_unversioned_parent,
 
1049
    'non-directory parent': resolve_non_directory_parent,
 
1050
    'versioning no contents': resolve_versioning_no_contents,
 
1051
}
 
1052
 
 
1053
 
 
1054
def conflict_pass(tt, conflicts, path_tree=None):
 
1055
    """Resolve some classes of conflicts.
 
1056
 
 
1057
    :param tt: The transform to resolve conflicts in
 
1058
    :param conflicts: The conflicts to resolve
 
1059
    :param path_tree: A Tree to get supplemental paths from
 
1060
    """
 
1061
    new_conflicts = set()
 
1062
    for conflict in conflicts:
 
1063
        resolver = CONFLICT_RESOLVERS.get(conflict[0])
 
1064
        if resolver is None:
 
1065
            continue
 
1066
        new_conflicts.update(resolver(tt, path_tree, *conflict))
 
1067
    return new_conflicts
 
1068
 
 
1069
 
 
1070
class _FileMover(object):
 
1071
    """Moves and deletes files for TreeTransform, tracking operations"""
 
1072
 
 
1073
    def __init__(self):
 
1074
        self.past_renames = []
 
1075
        self.pending_deletions = []
 
1076
 
 
1077
    def rename(self, from_, to):
 
1078
        """Rename a file from one path to another."""
 
1079
        try:
 
1080
            os.rename(from_, to)
 
1081
        except OSError as e:
 
1082
            if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
 
1083
                raise errors.FileExists(to, str(e))
 
1084
            # normal OSError doesn't include filenames so it's hard to see where
 
1085
            # the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
 
1086
            raise TransformRenameFailed(from_, to, str(e), e.errno)
 
1087
        self.past_renames.append((from_, to))
 
1088
 
 
1089
    def pre_delete(self, from_, to):
 
1090
        """Rename a file out of the way and mark it for deletion.
 
1091
 
 
1092
        Unlike os.unlink, this works equally well for files and directories.
 
1093
        :param from_: The current file path
 
1094
        :param to: A temporary path for the file
 
1095
        """
 
1096
        self.rename(from_, to)
 
1097
        self.pending_deletions.append(to)
 
1098
 
 
1099
    def rollback(self):
 
1100
        """Reverse all renames that have been performed"""
 
1101
        for from_, to in reversed(self.past_renames):
 
1102
            try:
 
1103
                os.rename(to, from_)
 
1104
            except OSError as e:
 
1105
                raise TransformRenameFailed(to, from_, str(e), e.errno)
 
1106
        # after rollback, don't reuse _FileMover
 
1107
        self.past_renames = None
 
1108
        self.pending_deletions = None
 
1109
 
 
1110
    def apply_deletions(self):
 
1111
        """Apply all marked deletions"""
 
1112
        for path in self.pending_deletions:
 
1113
            delete_any(path)
 
1114
        # after apply_deletions, don't reuse _FileMover
 
1115
        self.past_renames = None
 
1116
        self.pending_deletions = None
 
1117
 
 
1118
 
 
1119
def link_tree(target_tree, source_tree):
 
1120
    """Where possible, hard-link files in a tree to those in another tree.
 
1121
 
 
1122
    :param target_tree: Tree to change
 
1123
    :param source_tree: Tree to hard-link from
 
1124
    """
 
1125
    with target_tree.transform() as tt:
 
1126
        for change in target_tree.iter_changes(source_tree, include_unchanged=True):
 
1127
            if change.changed_content:
 
1128
                continue
 
1129
            if change.kind != ('file', 'file'):
 
1130
                continue
 
1131
            if change.executable[0] != change.executable[1]:
 
1132
                continue
 
1133
            trans_id = tt.trans_id_tree_path(change.path[1])
 
1134
            tt.delete_contents(trans_id)
 
1135
            tt.create_hardlink(source_tree.abspath(change.path[0]), trans_id)
 
1136
        tt.apply()
 
1137
 
 
1138
 
 
1139
class PreviewTree(object):
 
1140
    """Preview tree."""
 
1141
 
 
1142
    def __init__(self, transform):
 
1143
        self._transform = transform
 
1144
        self._parent_ids = []
 
1145
        self.__by_parent = None
 
1146
        self._path2trans_id_cache = {}
 
1147
        self._all_children_cache = {}
 
1148
        self._final_name_cache = {}
 
1149
 
 
1150
    def supports_setting_file_ids(self):
 
1151
        raise NotImplementedError(self.supports_setting_file_ids)
 
1152
 
 
1153
    @property
 
1154
    def _by_parent(self):
 
1155
        if self.__by_parent is None:
 
1156
            self.__by_parent = self._transform.by_parent()
 
1157
        return self.__by_parent
 
1158
 
 
1159
    def get_parent_ids(self):
 
1160
        return self._parent_ids
 
1161
 
 
1162
    def set_parent_ids(self, parent_ids):
 
1163
        self._parent_ids = parent_ids
 
1164
 
 
1165
    def get_revision_tree(self, revision_id):
 
1166
        return self._transform._tree.get_revision_tree(revision_id)
 
1167
 
 
1168
    def is_locked(self):
 
1169
        return False
 
1170
 
 
1171
    def lock_read(self):
 
1172
        # Perhaps in theory, this should lock the TreeTransform?
 
1173
        return lock.LogicalLockResult(self.unlock)
 
1174
 
 
1175
    def unlock(self):
 
1176
        pass
 
1177
 
 
1178
    def _path2trans_id(self, path):
 
1179
        """Look up the trans id associated with a path.
 
1180
 
 
1181
        :param path: path to look up, None when the path does not exist
 
1182
        :return: trans_id
 
1183
        """
 
1184
        # We must not use None here, because that is a valid value to store.
 
1185
        trans_id = self._path2trans_id_cache.get(path, object)
 
1186
        if trans_id is not object:
 
1187
            return trans_id
 
1188
        segments = osutils.splitpath(path)
 
1189
        cur_parent = self._transform.root
 
1190
        for cur_segment in segments:
 
1191
            for child in self._all_children(cur_parent):
 
1192
                final_name = self._final_name_cache.get(child)
 
1193
                if final_name is None:
 
1194
                    final_name = self._transform.final_name(child)
 
1195
                    self._final_name_cache[child] = final_name
 
1196
                if final_name == cur_segment:
 
1197
                    cur_parent = child
 
1198
                    break
 
1199
            else:
 
1200
                self._path2trans_id_cache[path] = None
 
1201
                return None
 
1202
        self._path2trans_id_cache[path] = cur_parent
 
1203
        return cur_parent
 
1204
 
 
1205
    def _all_children(self, trans_id):
 
1206
        children = self._all_children_cache.get(trans_id)
 
1207
        if children is not None:
 
1208
            return children
 
1209
        children = set(self._transform.iter_tree_children(trans_id))
 
1210
        # children in the _new_parent set are provided by _by_parent.
 
1211
        children.difference_update(self._transform._new_parent)
 
1212
        children.update(self._by_parent.get(trans_id, []))
 
1213
        self._all_children_cache[trans_id] = children
 
1214
        return children
 
1215
 
 
1216
    def get_file_with_stat(self, path):
 
1217
        return self.get_file(path), None
 
1218
 
 
1219
    def is_executable(self, path):
 
1220
        trans_id = self._path2trans_id(path)
 
1221
        if trans_id is None:
 
1222
            return False
 
1223
        try:
 
1224
            return self._transform._new_executability[trans_id]
 
1225
        except KeyError:
 
1226
            try:
 
1227
                return self._transform._tree.is_executable(path)
 
1228
            except OSError as e:
 
1229
                if e.errno == errno.ENOENT:
 
1230
                    return False
 
1231
                raise
 
1232
            except errors.NoSuchFile:
 
1233
                return False
 
1234
 
 
1235
    def has_filename(self, path):
 
1236
        trans_id = self._path2trans_id(path)
 
1237
        if trans_id in self._transform._new_contents:
 
1238
            return True
 
1239
        elif trans_id in self._transform._removed_contents:
 
1240
            return False
 
1241
        else:
 
1242
            return self._transform._tree.has_filename(path)
 
1243
 
 
1244
    def get_file_sha1(self, path, stat_value=None):
 
1245
        trans_id = self._path2trans_id(path)
 
1246
        if trans_id is None:
 
1247
            raise errors.NoSuchFile(path)
 
1248
        kind = self._transform._new_contents.get(trans_id)
 
1249
        if kind is None:
 
1250
            return self._transform._tree.get_file_sha1(path)
 
1251
        if kind == 'file':
 
1252
            with self.get_file(path) as fileobj:
 
1253
                return osutils.sha_file(fileobj)
 
1254
 
 
1255
    def get_file_verifier(self, path, stat_value=None):
 
1256
        trans_id = self._path2trans_id(path)
 
1257
        if trans_id is None:
 
1258
            raise errors.NoSuchFile(path)
 
1259
        kind = self._transform._new_contents.get(trans_id)
 
1260
        if kind is None:
 
1261
            return self._transform._tree.get_file_verifier(path)
 
1262
        if kind == 'file':
 
1263
            with self.get_file(path) as fileobj:
 
1264
                return ("SHA1", osutils.sha_file(fileobj))
 
1265
 
 
1266
    def kind(self, path):
 
1267
        trans_id = self._path2trans_id(path)
 
1268
        if trans_id is None:
 
1269
            raise errors.NoSuchFile(path)
 
1270
        return self._transform.final_kind(trans_id)
 
1271
 
 
1272
    def stored_kind(self, path):
 
1273
        trans_id = self._path2trans_id(path)
 
1274
        if trans_id is None:
 
1275
            raise errors.NoSuchFile(path)
 
1276
        try:
 
1277
            return self._transform._new_contents[trans_id]
 
1278
        except KeyError:
 
1279
            return self._transform._tree.stored_kind(path)
 
1280
 
 
1281
    def _get_repository(self):
 
1282
        repo = getattr(self._transform._tree, '_repository', None)
 
1283
        if repo is None:
 
1284
            repo = self._transform._tree.branch.repository
 
1285
        return repo
 
1286
 
 
1287
    def _iter_parent_trees(self):
 
1288
        for revision_id in self.get_parent_ids():
 
1289
            try:
 
1290
                yield self.revision_tree(revision_id)
 
1291
            except errors.NoSuchRevisionInTree:
 
1292
                yield self._get_repository().revision_tree(revision_id)
 
1293
 
 
1294
    def get_file_size(self, path):
 
1295
        """See Tree.get_file_size"""
 
1296
        trans_id = self._path2trans_id(path)
 
1297
        if trans_id is None:
 
1298
            raise errors.NoSuchFile(path)
 
1299
        kind = self._transform.final_kind(trans_id)
 
1300
        if kind != 'file':
 
1301
            return None
 
1302
        if trans_id in self._transform._new_contents:
 
1303
            return self._stat_limbo_file(trans_id).st_size
 
1304
        if self.kind(path) == 'file':
 
1305
            return self._transform._tree.get_file_size(path)
 
1306
        else:
 
1307
            return None
 
1308
 
 
1309
    def get_reference_revision(self, path):
 
1310
        trans_id = self._path2trans_id(path)
 
1311
        if trans_id is None:
 
1312
            raise errors.NoSuchFile(path)
 
1313
        reference_revision = self._transform._new_reference_revision.get(trans_id)
 
1314
        if reference_revision is None:
 
1315
            return self._transform._tree.get_reference_revision(path)
 
1316
        return reference_revision
 
1317
 
 
1318
    def tree_kind(self, trans_id):
 
1319
        path = self._tree_id_paths.get(trans_id)
 
1320
        if path is None:
 
1321
            return None
 
1322
        kind = self._tree.path_content_summary(path)[0]
 
1323
        if kind == 'missing':
 
1324
            kind = None
 
1325
        return kind