/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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