/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/bzr/workingtree.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2018-11-16 18:59:44 UTC
  • mfrom: (7143.15.15 more-cleanups)
  • Revision ID: breezy.the.bot@gmail.com-20181116185944-biefv1sub37qfybm
Sprinkle some PEP8iness.

Merged from https://code.launchpad.net/~jelmer/brz/more-cleanups/+merge/358611

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-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
"""InventoryWorkingTree object and friends.
 
18
 
 
19
A WorkingTree represents the editable working copy of a branch.
 
20
Operations which represent the WorkingTree are also done here,
 
21
such as renaming or adding files.  The WorkingTree has an inventory
 
22
which is updated by these operations.  A commit produces a
 
23
new revision based on the workingtree and its inventory.
 
24
 
 
25
At the moment every WorkingTree has its own branch.  Remote
 
26
WorkingTrees aren't supported.
 
27
 
 
28
To get a WorkingTree, call bzrdir.open_workingtree() or
 
29
WorkingTree.open(dir).
 
30
"""
 
31
 
 
32
 
 
33
from __future__ import absolute_import
 
34
 
 
35
from bisect import bisect_left
 
36
import breezy
 
37
import collections
 
38
import errno
 
39
import itertools
 
40
import operator
 
41
import os
 
42
import stat
 
43
import sys
 
44
 
 
45
# Explicitly import breezy.bzrdir so that the BzrProber
 
46
# is guaranteed to be registered.
 
47
from . import bzrdir
 
48
 
 
49
from .. import lazy_import
 
50
lazy_import.lazy_import(globals(), """
 
51
from breezy import (
 
52
    cache_utf8,
 
53
    conflicts as _mod_conflicts,
 
54
    globbing,
 
55
    ignores,
 
56
    revision as _mod_revision,
 
57
    rio as _mod_rio,
 
58
    )
 
59
from breezy.bzr import (
 
60
    inventory,
 
61
    xml5,
 
62
    xml7,
 
63
    )
 
64
""")
 
65
 
 
66
from .. import (
 
67
    errors,
 
68
    osutils,
 
69
    )
 
70
from ..lock import LogicalLockResult
 
71
from .inventorytree import InventoryRevisionTree, MutableInventoryTree
 
72
from ..sixish import (
 
73
    BytesIO,
 
74
    text_type,
 
75
    )
 
76
from ..trace import mutter, note
 
77
from ..tree import (
 
78
    get_canonical_path,
 
79
    TreeDirectory,
 
80
    TreeEntry,
 
81
    TreeFile,
 
82
    TreeLink,
 
83
    )
 
84
from ..workingtree import (
 
85
    WorkingTree,
 
86
    WorkingTreeFormat,
 
87
    format_registry,
 
88
    )
 
89
 
 
90
 
 
91
MERGE_MODIFIED_HEADER_1 = b"BZR merge-modified list format 1"
 
92
# TODO: Modifying the conflict objects or their type is currently nearly
 
93
# impossible as there is no clear relationship between the working tree format
 
94
# and the conflict list file format.
 
95
CONFLICT_HEADER_1 = b"BZR conflict list format 1"
 
96
 
 
97
 
 
98
class InventoryWorkingTree(WorkingTree, MutableInventoryTree):
 
99
    """Base class for working trees that are inventory-oriented.
 
100
 
 
101
    The inventory is held in the `Branch` working-inventory, and the
 
102
    files are in a directory on disk.
 
103
 
 
104
    It is possible for a `WorkingTree` to have a filename which is
 
105
    not listed in the Inventory and vice versa.
 
106
    """
 
107
 
 
108
    def __init__(self, basedir='.',
 
109
                 branch=None,
 
110
                 _inventory=None,
 
111
                 _control_files=None,
 
112
                 _internal=False,
 
113
                 _format=None,
 
114
                 _controldir=None):
 
115
        """Construct a InventoryWorkingTree instance. This is not a public API.
 
116
 
 
117
        :param branch: A branch to override probing for the branch.
 
118
        """
 
119
        super(InventoryWorkingTree, self).__init__(
 
120
            basedir=basedir, branch=branch,
 
121
            _transport=_control_files._transport, _internal=_internal,
 
122
            _format=_format, _controldir=_controldir)
 
123
 
 
124
        self._control_files = _control_files
 
125
        self._detect_case_handling()
 
126
 
 
127
        if _inventory is None:
 
128
            # This will be acquired on lock_read() or lock_write()
 
129
            self._inventory_is_modified = False
 
130
            self._inventory = None
 
131
        else:
 
132
            # the caller of __init__ has provided an inventory,
 
133
            # we assume they know what they are doing - as its only
 
134
            # the Format factory and creation methods that are
 
135
            # permitted to do this.
 
136
            self._set_inventory(_inventory, dirty=False)
 
137
 
 
138
    def _set_inventory(self, inv, dirty):
 
139
        """Set the internal cached inventory.
 
140
 
 
141
        :param inv: The inventory to set.
 
142
        :param dirty: A boolean indicating whether the inventory is the same
 
143
            logical inventory as whats on disk. If True the inventory is not
 
144
            the same and should be written to disk or data will be lost, if
 
145
            False then the inventory is the same as that on disk and any
 
146
            serialisation would be unneeded overhead.
 
147
        """
 
148
        self._inventory = inv
 
149
        self._inventory_is_modified = dirty
 
150
 
 
151
    def _detect_case_handling(self):
 
152
        wt_trans = self.controldir.get_workingtree_transport(None)
 
153
        try:
 
154
            wt_trans.stat(self._format.case_sensitive_filename)
 
155
        except errors.NoSuchFile:
 
156
            self.case_sensitive = True
 
157
        else:
 
158
            self.case_sensitive = False
 
159
 
 
160
        self._setup_directory_is_tree_reference()
 
161
 
 
162
    def _serialize(self, inventory, out_file):
 
163
        xml5.serializer_v5.write_inventory(
 
164
            self._inventory, out_file, working=True)
 
165
 
 
166
    def _deserialize(selt, in_file):
 
167
        return xml5.serializer_v5.read_inventory(in_file)
 
168
 
 
169
    def break_lock(self):
 
170
        """Break a lock if one is present from another instance.
 
171
 
 
172
        Uses the ui factory to ask for confirmation if the lock may be from
 
173
        an active process.
 
174
 
 
175
        This will probe the repository for its lock as well.
 
176
        """
 
177
        self._control_files.break_lock()
 
178
        self.branch.break_lock()
 
179
 
 
180
    def is_locked(self):
 
181
        return self._control_files.is_locked()
 
182
 
 
183
    def _must_be_locked(self):
 
184
        if not self.is_locked():
 
185
            raise errors.ObjectNotLocked(self)
 
186
 
 
187
    def lock_read(self):
 
188
        """Lock the tree for reading.
 
189
 
 
190
        This also locks the branch, and can be unlocked via self.unlock().
 
191
 
 
192
        :return: A breezy.lock.LogicalLockResult.
 
193
        """
 
194
        if not self.is_locked():
 
195
            self._reset_data()
 
196
        self.branch.lock_read()
 
197
        try:
 
198
            self._control_files.lock_read()
 
199
            return LogicalLockResult(self.unlock)
 
200
        except BaseException:
 
201
            self.branch.unlock()
 
202
            raise
 
203
 
 
204
    def lock_tree_write(self):
 
205
        """See MutableTree.lock_tree_write, and WorkingTree.unlock.
 
206
 
 
207
        :return: A breezy.lock.LogicalLockResult.
 
208
        """
 
209
        if not self.is_locked():
 
210
            self._reset_data()
 
211
        self.branch.lock_read()
 
212
        try:
 
213
            self._control_files.lock_write()
 
214
            return LogicalLockResult(self.unlock)
 
215
        except BaseException:
 
216
            self.branch.unlock()
 
217
            raise
 
218
 
 
219
    def lock_write(self):
 
220
        """See MutableTree.lock_write, and WorkingTree.unlock.
 
221
 
 
222
        :return: A breezy.lock.LogicalLockResult.
 
223
        """
 
224
        if not self.is_locked():
 
225
            self._reset_data()
 
226
        self.branch.lock_write()
 
227
        try:
 
228
            self._control_files.lock_write()
 
229
            return LogicalLockResult(self.unlock)
 
230
        except BaseException:
 
231
            self.branch.unlock()
 
232
            raise
 
233
 
 
234
    def get_physical_lock_status(self):
 
235
        return self._control_files.get_physical_lock_status()
 
236
 
 
237
    def _write_inventory(self, inv):
 
238
        """Write inventory as the current inventory."""
 
239
        with self.lock_tree_write():
 
240
            self._set_inventory(inv, dirty=True)
 
241
            self.flush()
 
242
 
 
243
    # XXX: This method should be deprecated in favour of taking in a proper
 
244
    # new Inventory object.
 
245
    def set_inventory(self, new_inventory_list):
 
246
        from .inventory import (
 
247
            Inventory,
 
248
            InventoryDirectory,
 
249
            InventoryFile,
 
250
            InventoryLink)
 
251
        with self.lock_tree_write():
 
252
            inv = Inventory(self.get_root_id())
 
253
            for path, file_id, parent, kind in new_inventory_list:
 
254
                name = os.path.basename(path)
 
255
                if name == "":
 
256
                    continue
 
257
                # fixme, there should be a factory function inv,add_??
 
258
                if kind == 'directory':
 
259
                    inv.add(InventoryDirectory(file_id, name, parent))
 
260
                elif kind == 'file':
 
261
                    inv.add(InventoryFile(file_id, name, parent))
 
262
                elif kind == 'symlink':
 
263
                    inv.add(InventoryLink(file_id, name, parent))
 
264
                else:
 
265
                    raise errors.BzrError("unknown kind %r" % kind)
 
266
            self._write_inventory(inv)
 
267
 
 
268
    def _write_basis_inventory(self, xml):
 
269
        """Write the basis inventory XML to the basis-inventory file"""
 
270
        path = self._basis_inventory_name()
 
271
        sio = BytesIO(xml)
 
272
        self._transport.put_file(path, sio,
 
273
                                 mode=self.controldir._get_file_mode())
 
274
 
 
275
    def _reset_data(self):
 
276
        """Reset transient data that cannot be revalidated."""
 
277
        self._inventory_is_modified = False
 
278
        with self._transport.get('inventory') as f:
 
279
            result = self._deserialize(f)
 
280
        self._set_inventory(result, dirty=False)
 
281
 
 
282
    def store_uncommitted(self):
 
283
        """Store uncommitted changes from the tree in the branch."""
 
284
        with self.lock_write():
 
285
            target_tree = self.basis_tree()
 
286
            from ..shelf import ShelfCreator
 
287
            shelf_creator = ShelfCreator(self, target_tree)
 
288
            try:
 
289
                if not shelf_creator.shelve_all():
 
290
                    return
 
291
                self.branch.store_uncommitted(shelf_creator)
 
292
                shelf_creator.transform()
 
293
            finally:
 
294
                shelf_creator.finalize()
 
295
            note('Uncommitted changes stored in branch "%s".',
 
296
                 self.branch.nick)
 
297
 
 
298
    def restore_uncommitted(self):
 
299
        """Restore uncommitted changes from the branch into the tree."""
 
300
        with self.lock_write():
 
301
            unshelver = self.branch.get_unshelver(self)
 
302
            if unshelver is None:
 
303
                return
 
304
            try:
 
305
                merger = unshelver.make_merger()
 
306
                merger.ignore_zero = True
 
307
                merger.do_merge()
 
308
                self.branch.store_uncommitted(None)
 
309
            finally:
 
310
                unshelver.finalize()
 
311
 
 
312
    def get_shelf_manager(self):
 
313
        """Return the ShelfManager for this WorkingTree."""
 
314
        from ..shelf import ShelfManager
 
315
        return ShelfManager(self, self._transport)
 
316
 
 
317
    def _set_root_id(self, file_id):
 
318
        """Set the root id for this tree, in a format specific manner.
 
319
 
 
320
        :param file_id: The file id to assign to the root. It must not be
 
321
            present in the current inventory or an error will occur. It must
 
322
            not be None, but rather a valid file id.
 
323
        """
 
324
        inv = self._inventory
 
325
        orig_root_id = inv.root.file_id
 
326
        # TODO: it might be nice to exit early if there was nothing
 
327
        # to do, saving us from trigger a sync on unlock.
 
328
        self._inventory_is_modified = True
 
329
        # we preserve the root inventory entry object, but
 
330
        # unlinkit from the byid index
 
331
        inv.delete(inv.root.file_id)
 
332
        inv.root.file_id = file_id
 
333
        # and link it into the index with the new changed id.
 
334
        inv._byid[inv.root.file_id] = inv.root
 
335
        # and finally update all children to reference the new id.
 
336
        # XXX: this should be safe to just look at the root.children
 
337
        # list, not the WHOLE INVENTORY.
 
338
        for fid in inv.iter_all_ids():
 
339
            entry = inv.get_entry(fid)
 
340
            if entry.parent_id == orig_root_id:
 
341
                entry.parent_id = inv.root.file_id
 
342
 
 
343
    def remove(self, files, verbose=False, to_file=None, keep_files=True,
 
344
               force=False):
 
345
        """Remove nominated files from the working tree metadata.
 
346
 
 
347
        :files: File paths relative to the basedir.
 
348
        :keep_files: If true, the files will also be kept.
 
349
        :force: Delete files and directories, even if they are changed and
 
350
            even if the directories are not empty.
 
351
        """
 
352
        if isinstance(files, (str, text_type)):
 
353
            files = [files]
 
354
 
 
355
        inv_delta = []
 
356
 
 
357
        all_files = set()  # specified and nested files
 
358
        if to_file is None:
 
359
            to_file = sys.stdout
 
360
 
 
361
        files_to_backup = []
 
362
 
 
363
        def recurse_directory_to_add_files(directory):
 
364
            # Recurse directory and add all files
 
365
            # so we can check if they have changed.
 
366
            for parent_info, file_infos in self.walkdirs(directory):
 
367
                for relpath, basename, kind, lstat, fileid, kind in file_infos:
 
368
                    # Is it versioned or ignored?
 
369
                    if self.is_versioned(relpath):
 
370
                        # Add nested content for deletion.
 
371
                        all_files.add(relpath)
 
372
                    else:
 
373
                        # Files which are not versioned
 
374
                        # should be treated as unknown.
 
375
                        files_to_backup.append(relpath)
 
376
 
 
377
        with self.lock_tree_write():
 
378
 
 
379
            for filename in files:
 
380
                # Get file name into canonical form.
 
381
                abspath = self.abspath(filename)
 
382
                filename = self.relpath(abspath)
 
383
                if len(filename) > 0:
 
384
                    all_files.add(filename)
 
385
                    recurse_directory_to_add_files(filename)
 
386
 
 
387
            files = list(all_files)
 
388
 
 
389
            if len(files) == 0:
 
390
                return  # nothing to do
 
391
 
 
392
            # Sort needed to first handle directory content before the
 
393
            # directory
 
394
            files.sort(reverse=True)
 
395
 
 
396
            # Bail out if we are going to delete files we shouldn't
 
397
            if not keep_files and not force:
 
398
                for (file_id, path, content_change, versioned, parent_id, name,
 
399
                     kind, executable) in self.iter_changes(
 
400
                         self.basis_tree(), include_unchanged=True,
 
401
                         require_versioned=False, want_unversioned=True,
 
402
                         specific_files=files):
 
403
                    if versioned[0] is False:
 
404
                        # The record is unknown or newly added
 
405
                        files_to_backup.append(path[1])
 
406
                    elif (content_change and (kind[1] is not None)
 
407
                            and osutils.is_inside_any(files, path[1])):
 
408
                        # Versioned and changed, but not deleted, and still
 
409
                        # in one of the dirs to be deleted.
 
410
                        files_to_backup.append(path[1])
 
411
 
 
412
            def backup(file_to_backup):
 
413
                backup_name = self.controldir._available_backup_name(
 
414
                    file_to_backup)
 
415
                osutils.rename(abs_path, self.abspath(backup_name))
 
416
                return "removed %s (but kept a copy: %s)" % (file_to_backup,
 
417
                                                             backup_name)
 
418
 
 
419
            # Build inv_delta and delete files where applicable,
 
420
            # do this before any modifications to meta data.
 
421
            for f in files:
 
422
                fid = self.path2id(f)
 
423
                message = None
 
424
                if not fid:
 
425
                    message = "%s is not versioned." % (f,)
 
426
                else:
 
427
                    if verbose:
 
428
                        # having removed it, it must be either ignored or
 
429
                        # unknown
 
430
                        if self.is_ignored(f):
 
431
                            new_status = 'I'
 
432
                        else:
 
433
                            new_status = '?'
 
434
                        # XXX: Really should be a more abstract reporter
 
435
                        # interface
 
436
                        kind_ch = osutils.kind_marker(self.kind(f))
 
437
                        to_file.write(
 
438
                            new_status + '       ' + f + kind_ch + '\n')
 
439
                    # Unversion file
 
440
                    inv_delta.append((f, None, fid, None))
 
441
                    message = "removed %s" % (f,)
 
442
 
 
443
                if not keep_files:
 
444
                    abs_path = self.abspath(f)
 
445
                    if osutils.lexists(abs_path):
 
446
                        if (osutils.isdir(abs_path)
 
447
                                and len(os.listdir(abs_path)) > 0):
 
448
                            if force:
 
449
                                osutils.rmtree(abs_path)
 
450
                                message = "deleted %s" % (f,)
 
451
                            else:
 
452
                                message = backup(f)
 
453
                        else:
 
454
                            if f in files_to_backup:
 
455
                                message = backup(f)
 
456
                            else:
 
457
                                osutils.delete_any(abs_path)
 
458
                                message = "deleted %s" % (f,)
 
459
                    elif message is not None:
 
460
                        # Only care if we haven't done anything yet.
 
461
                        message = "%s does not exist." % (f,)
 
462
 
 
463
                # Print only one message (if any) per file.
 
464
                if message is not None:
 
465
                    note(message)
 
466
            self.apply_inventory_delta(inv_delta)
 
467
 
 
468
    def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
 
469
        """See MutableTree.set_parent_trees."""
 
470
        parent_ids = [rev for (rev, tree) in parents_list]
 
471
        for revision_id in parent_ids:
 
472
            _mod_revision.check_not_reserved_id(revision_id)
 
473
 
 
474
        with self.lock_tree_write():
 
475
            self._check_parents_for_ghosts(parent_ids,
 
476
                                           allow_leftmost_as_ghost=allow_leftmost_as_ghost)
 
477
 
 
478
            parent_ids = self._filter_parent_ids_by_ancestry(parent_ids)
 
479
 
 
480
            if len(parent_ids) == 0:
 
481
                leftmost_parent_id = _mod_revision.NULL_REVISION
 
482
                leftmost_parent_tree = None
 
483
            else:
 
484
                leftmost_parent_id, leftmost_parent_tree = parents_list[0]
 
485
 
 
486
            if self._change_last_revision(leftmost_parent_id):
 
487
                if leftmost_parent_tree is None:
 
488
                    # If we don't have a tree, fall back to reading the
 
489
                    # parent tree from the repository.
 
490
                    self._cache_basis_inventory(leftmost_parent_id)
 
491
                else:
 
492
                    inv = leftmost_parent_tree.root_inventory
 
493
                    xml = self._create_basis_xml_from_inventory(
 
494
                        leftmost_parent_id, inv)
 
495
                    self._write_basis_inventory(xml)
 
496
            self._set_merges_from_parent_ids(parent_ids)
 
497
 
 
498
    def _cache_basis_inventory(self, new_revision):
 
499
        """Cache new_revision as the basis inventory."""
 
500
        # TODO: this should allow the ready-to-use inventory to be passed in,
 
501
        # as commit already has that ready-to-use [while the format is the
 
502
        # same, that is].
 
503
        try:
 
504
            # this double handles the inventory - unpack and repack -
 
505
            # but is easier to understand. We can/should put a conditional
 
506
            # in here based on whether the inventory is in the latest format
 
507
            # - perhaps we should repack all inventories on a repository
 
508
            # upgrade ?
 
509
            # the fast path is to copy the raw xml from the repository. If the
 
510
            # xml contains 'revision_id="', then we assume the right
 
511
            # revision_id is set. We must check for this full string, because a
 
512
            # root node id can legitimately look like 'revision_id' but cannot
 
513
            # contain a '"'.
 
514
            xml = self.branch.repository._get_inventory_xml(new_revision)
 
515
            firstline = xml.split(b'\n', 1)[0]
 
516
            if (b'revision_id="' not in firstline
 
517
                    or b'format="7"' not in firstline):
 
518
                inv = self.branch.repository._serializer.read_inventory_from_string(
 
519
                    xml, new_revision)
 
520
                xml = self._create_basis_xml_from_inventory(new_revision, inv)
 
521
            self._write_basis_inventory(xml)
 
522
        except (errors.NoSuchRevision, errors.RevisionNotPresent):
 
523
            pass
 
524
 
 
525
    def _basis_inventory_name(self):
 
526
        return 'basis-inventory-cache'
 
527
 
 
528
    def _create_basis_xml_from_inventory(self, revision_id, inventory):
 
529
        """Create the text that will be saved in basis-inventory"""
 
530
        inventory.revision_id = revision_id
 
531
        return xml7.serializer_v7.write_inventory_to_string(inventory)
 
532
 
 
533
    def set_conflicts(self, conflicts):
 
534
        with self.lock_tree_write():
 
535
            self._put_rio('conflicts', conflicts.to_stanzas(),
 
536
                          CONFLICT_HEADER_1)
 
537
 
 
538
    def add_conflicts(self, new_conflicts):
 
539
        with self.lock_tree_write():
 
540
            conflict_set = set(self.conflicts())
 
541
            conflict_set.update(set(list(new_conflicts)))
 
542
            self.set_conflicts(_mod_conflicts.ConflictList(
 
543
                sorted(conflict_set, key=_mod_conflicts.Conflict.sort_key)))
 
544
 
 
545
    def conflicts(self):
 
546
        with self.lock_read():
 
547
            try:
 
548
                confile = self._transport.get('conflicts')
 
549
            except errors.NoSuchFile:
 
550
                return _mod_conflicts.ConflictList()
 
551
            try:
 
552
                try:
 
553
                    if next(confile) != CONFLICT_HEADER_1 + b'\n':
 
554
                        raise errors.ConflictFormatError()
 
555
                except StopIteration:
 
556
                    raise errors.ConflictFormatError()
 
557
                reader = _mod_rio.RioReader(confile)
 
558
                return _mod_conflicts.ConflictList.from_stanzas(reader)
 
559
            finally:
 
560
                confile.close()
 
561
 
 
562
    def get_ignore_list(self):
 
563
        """Return list of ignore patterns.
 
564
 
 
565
        Cached in the Tree object after the first call.
 
566
        """
 
567
        ignoreset = getattr(self, '_ignoreset', None)
 
568
        if ignoreset is not None:
 
569
            return ignoreset
 
570
 
 
571
        ignore_globs = set()
 
572
        ignore_globs.update(ignores.get_runtime_ignores())
 
573
        ignore_globs.update(ignores.get_user_ignores())
 
574
        if self.has_filename(self._format.ignore_filename):
 
575
            with self.get_file(self._format.ignore_filename) as f:
 
576
                ignore_globs.update(ignores.parse_ignore_file(f))
 
577
        self._ignoreset = ignore_globs
 
578
        return ignore_globs
 
579
 
 
580
    def _cleanup(self):
 
581
        self._flush_ignore_list_cache()
 
582
 
 
583
    def _flush_ignore_list_cache(self):
 
584
        """Resets the cached ignore list to force a cache rebuild."""
 
585
        self._ignoreset = None
 
586
        self._ignoreglobster = None
 
587
 
 
588
    def is_ignored(self, filename):
 
589
        r"""Check whether the filename matches an ignore pattern.
 
590
 
 
591
        Patterns containing '/' or '\' need to match the whole path;
 
592
        others match against only the last component.  Patterns starting
 
593
        with '!' are ignore exceptions.  Exceptions take precedence
 
594
        over regular patterns and cause the filename to not be ignored.
 
595
 
 
596
        If the file is ignored, returns the pattern which caused it to
 
597
        be ignored, otherwise None.  So this can simply be used as a
 
598
        boolean if desired."""
 
599
        if getattr(self, '_ignoreglobster', None) is None:
 
600
            self._ignoreglobster = globbing.ExceptionGlobster(
 
601
                self.get_ignore_list())
 
602
        return self._ignoreglobster.match(filename)
 
603
 
 
604
    def read_basis_inventory(self):
 
605
        """Read the cached basis inventory."""
 
606
        path = self._basis_inventory_name()
 
607
        return self._transport.get_bytes(path)
 
608
 
 
609
    def read_working_inventory(self):
 
610
        """Read the working inventory.
 
611
 
 
612
        :raises errors.InventoryModified: read_working_inventory will fail
 
613
            when the current in memory inventory has been modified.
 
614
        """
 
615
        # conceptually this should be an implementation detail of the tree.
 
616
        # XXX: Deprecate this.
 
617
        # ElementTree does its own conversion from UTF-8, so open in
 
618
        # binary.
 
619
        with self.lock_read():
 
620
            if self._inventory_is_modified:
 
621
                raise errors.InventoryModified(self)
 
622
            with self._transport.get('inventory') as f:
 
623
                result = self._deserialize(f)
 
624
            self._set_inventory(result, dirty=False)
 
625
            return result
 
626
 
 
627
    def get_root_id(self):
 
628
        """Return the id of this trees root"""
 
629
        with self.lock_read():
 
630
            return self._inventory.root.file_id
 
631
 
 
632
    def has_id(self, file_id):
 
633
        # files that have been deleted are excluded
 
634
        inv, inv_file_id = self._unpack_file_id(file_id)
 
635
        if not inv.has_id(inv_file_id):
 
636
            return False
 
637
        path = inv.id2path(inv_file_id)
 
638
        return osutils.lexists(self.abspath(path))
 
639
 
 
640
    def has_or_had_id(self, file_id):
 
641
        if file_id == self.get_root_id():
 
642
            return True
 
643
        inv, inv_file_id = self._unpack_file_id(file_id)
 
644
        return inv.has_id(inv_file_id)
 
645
 
 
646
    def all_file_ids(self):
 
647
        """Iterate through file_ids for this tree.
 
648
 
 
649
        file_ids are in a WorkingTree if they are in the working inventory
 
650
        and the working file exists.
 
651
        """
 
652
        return {ie.file_id for path, ie in self.iter_entries_by_dir()}
 
653
 
 
654
    def all_versioned_paths(self):
 
655
        return {path for path, ie in self.iter_entries_by_dir()}
 
656
 
 
657
    def set_last_revision(self, new_revision):
 
658
        """Change the last revision in the working tree."""
 
659
        with self.lock_tree_write():
 
660
            if self._change_last_revision(new_revision):
 
661
                self._cache_basis_inventory(new_revision)
 
662
 
 
663
    def _get_check_refs(self):
 
664
        """Return the references needed to perform a check of this tree.
 
665
 
 
666
        The default implementation returns no refs, and is only suitable for
 
667
        trees that have no local caching and can commit on ghosts at any time.
 
668
 
 
669
        :seealso: breezy.check for details about check_refs.
 
670
        """
 
671
        return []
 
672
 
 
673
    def _check(self, references):
 
674
        """Check the tree for consistency.
 
675
 
 
676
        :param references: A dict with keys matching the items returned by
 
677
            self._get_check_refs(), and values from looking those keys up in
 
678
            the repository.
 
679
        """
 
680
        with self.lock_read():
 
681
            tree_basis = self.basis_tree()
 
682
            with tree_basis.lock_read():
 
683
                repo_basis = references[('trees', self.last_revision())]
 
684
                if len(list(repo_basis.iter_changes(tree_basis))) > 0:
 
685
                    raise errors.BzrCheckError(
 
686
                        "Mismatched basis inventory content.")
 
687
                self._validate()
 
688
 
 
689
    def check_state(self):
 
690
        """Check that the working state is/isn't valid."""
 
691
        with self.lock_read():
 
692
            check_refs = self._get_check_refs()
 
693
            refs = {}
 
694
            for ref in check_refs:
 
695
                kind, value = ref
 
696
                if kind == 'trees':
 
697
                    refs[ref] = self.branch.repository.revision_tree(value)
 
698
            self._check(refs)
 
699
 
 
700
    def reset_state(self, revision_ids=None):
 
701
        """Reset the state of the working tree.
 
702
 
 
703
        This does a hard-reset to a last-known-good state. This is a way to
 
704
        fix if something got corrupted (like the .bzr/checkout/dirstate file)
 
705
        """
 
706
        with self.lock_tree_write():
 
707
            if revision_ids is None:
 
708
                revision_ids = self.get_parent_ids()
 
709
            if not revision_ids:
 
710
                rt = self.branch.repository.revision_tree(
 
711
                    _mod_revision.NULL_REVISION)
 
712
            else:
 
713
                rt = self.branch.repository.revision_tree(revision_ids[0])
 
714
            self._write_inventory(rt.root_inventory)
 
715
            self.set_parent_ids(revision_ids)
 
716
 
 
717
    def flush(self):
 
718
        """Write the in memory inventory to disk."""
 
719
        # TODO: Maybe this should only write on dirty ?
 
720
        if self._control_files._lock_mode != 'w':
 
721
            raise errors.NotWriteLocked(self)
 
722
        sio = BytesIO()
 
723
        self._serialize(self._inventory, sio)
 
724
        sio.seek(0)
 
725
        self._transport.put_file('inventory', sio,
 
726
                                 mode=self.controldir._get_file_mode())
 
727
        self._inventory_is_modified = False
 
728
 
 
729
    def get_file_mtime(self, path):
 
730
        """See Tree.get_file_mtime."""
 
731
        try:
 
732
            return os.lstat(self.abspath(path)).st_mtime
 
733
        except OSError as e:
 
734
            if e.errno == errno.ENOENT:
 
735
                raise errors.NoSuchFile(path)
 
736
            raise
 
737
 
 
738
    def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
 
739
        try:
 
740
            return self._path2ie(path).executable
 
741
        except errors.NoSuchFile:
 
742
            # For unversioned files on win32, we just assume they are not
 
743
            # executable
 
744
            return False
 
745
 
 
746
    def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
 
747
        mode = stat_result.st_mode
 
748
        return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
 
749
 
 
750
    def is_executable(self, path):
 
751
        if not self._supports_executable():
 
752
            ie = self._path2ie(path)
 
753
            return ie.executable
 
754
        else:
 
755
            mode = os.lstat(self.abspath(path)).st_mode
 
756
            return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
 
757
 
 
758
    def _is_executable_from_path_and_stat(self, path, stat_result):
 
759
        if not self._supports_executable():
 
760
            return self._is_executable_from_path_and_stat_from_basis(
 
761
                path, stat_result)
 
762
        else:
 
763
            return self._is_executable_from_path_and_stat_from_stat(
 
764
                path, stat_result)
 
765
 
 
766
    def _add(self, files, ids, kinds):
 
767
        """See MutableTree._add."""
 
768
        with self.lock_tree_write():
 
769
            # TODO: Re-adding a file that is removed in the working copy
 
770
            # should probably put it back with the previous ID.
 
771
            # the read and write working inventory should not occur in this
 
772
            # function - they should be part of lock_write and unlock.
 
773
            # FIXME: nested trees
 
774
            inv = self.root_inventory
 
775
            for f, file_id, kind in zip(files, ids, kinds):
 
776
                if file_id is None:
 
777
                    inv.add_path(f, kind=kind)
 
778
                else:
 
779
                    inv.add_path(f, kind=kind, file_id=file_id)
 
780
                self._inventory_is_modified = True
 
781
 
 
782
    def revision_tree(self, revision_id):
 
783
        """See WorkingTree.revision_id."""
 
784
        if revision_id == self.last_revision():
 
785
            try:
 
786
                xml = self.read_basis_inventory()
 
787
            except errors.NoSuchFile:
 
788
                pass
 
789
            else:
 
790
                try:
 
791
                    inv = xml7.serializer_v7.read_inventory_from_string(xml)
 
792
                    # dont use the repository revision_tree api because we want
 
793
                    # to supply the inventory.
 
794
                    if inv.revision_id == revision_id:
 
795
                        return InventoryRevisionTree(
 
796
                            self.branch.repository, inv, revision_id)
 
797
                except errors.BadInventoryFormat:
 
798
                    pass
 
799
        # raise if there was no inventory, or if we read the wrong inventory.
 
800
        raise errors.NoSuchRevisionInTree(self, revision_id)
 
801
 
 
802
    def annotate_iter(self, path,
 
803
                      default_revision=_mod_revision.CURRENT_REVISION):
 
804
        """See Tree.annotate_iter
 
805
 
 
806
        This implementation will use the basis tree implementation if possible.
 
807
        Lines not in the basis are attributed to CURRENT_REVISION
 
808
 
 
809
        If there are pending merges, lines added by those merges will be
 
810
        incorrectly attributed to CURRENT_REVISION (but after committing, the
 
811
        attribution will be correct).
 
812
        """
 
813
        with self.lock_read():
 
814
            file_id = self.path2id(path)
 
815
            if file_id is None:
 
816
                raise errors.NoSuchFile(path)
 
817
            maybe_file_parent_keys = []
 
818
            for parent_id in self.get_parent_ids():
 
819
                try:
 
820
                    parent_tree = self.revision_tree(parent_id)
 
821
                except errors.NoSuchRevisionInTree:
 
822
                    parent_tree = self.branch.repository.revision_tree(
 
823
                        parent_id)
 
824
                with parent_tree.lock_read():
 
825
 
 
826
                    try:
 
827
                        kind = parent_tree.kind(path)
 
828
                    except errors.NoSuchFile:
 
829
                        continue
 
830
                    if kind != 'file':
 
831
                        # Note: this is slightly unnecessary, because symlinks
 
832
                        # and directories have a "text" which is the empty
 
833
                        # text, and we know that won't mess up annotations. But
 
834
                        # it seems cleaner
 
835
                        continue
 
836
                    parent_path = parent_tree.id2path(file_id)
 
837
                    parent_text_key = (
 
838
                        file_id,
 
839
                        parent_tree.get_file_revision(parent_path))
 
840
                    if parent_text_key not in maybe_file_parent_keys:
 
841
                        maybe_file_parent_keys.append(parent_text_key)
 
842
            graph = self.branch.repository.get_file_graph()
 
843
            heads = graph.heads(maybe_file_parent_keys)
 
844
            file_parent_keys = []
 
845
            for key in maybe_file_parent_keys:
 
846
                if key in heads:
 
847
                    file_parent_keys.append(key)
 
848
 
 
849
            # Now we have the parents of this content
 
850
            annotator = self.branch.repository.texts.get_annotator()
 
851
            text = self.get_file_text(path)
 
852
            this_key = (file_id, default_revision)
 
853
            annotator.add_special_text(this_key, file_parent_keys, text)
 
854
            annotations = [(key[-1], line)
 
855
                           for key, line in annotator.annotate_flat(this_key)]
 
856
            return annotations
 
857
 
 
858
    def _put_rio(self, filename, stanzas, header):
 
859
        self._must_be_locked()
 
860
        my_file = _mod_rio.rio_file(stanzas, header)
 
861
        self._transport.put_file(filename, my_file,
 
862
                                 mode=self.controldir._get_file_mode())
 
863
 
 
864
    def set_merge_modified(self, modified_hashes):
 
865
        def iter_stanzas():
 
866
            for file_id in modified_hashes:
 
867
                yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
 
868
                                      hash=modified_hashes[file_id])
 
869
        with self.lock_tree_write():
 
870
            self._put_rio('merge-hashes', iter_stanzas(),
 
871
                          MERGE_MODIFIED_HEADER_1)
 
872
 
 
873
    def merge_modified(self):
 
874
        """Return a dictionary of files modified by a merge.
 
875
 
 
876
        The list is initialized by WorkingTree.set_merge_modified, which is
 
877
        typically called after we make some automatic updates to the tree
 
878
        because of a merge.
 
879
 
 
880
        This returns a map of file_id->sha1, containing only files which are
 
881
        still in the working inventory and have that text hash.
 
882
        """
 
883
        with self.lock_read():
 
884
            try:
 
885
                hashfile = self._transport.get('merge-hashes')
 
886
            except errors.NoSuchFile:
 
887
                return {}
 
888
            try:
 
889
                merge_hashes = {}
 
890
                try:
 
891
                    if next(hashfile) != MERGE_MODIFIED_HEADER_1 + b'\n':
 
892
                        raise errors.MergeModifiedFormatError()
 
893
                except StopIteration:
 
894
                    raise errors.MergeModifiedFormatError()
 
895
                for s in _mod_rio.RioReader(hashfile):
 
896
                    # RioReader reads in Unicode, so convert file_ids back to
 
897
                    # utf8
 
898
                    file_id = cache_utf8.encode(s.get("file_id"))
 
899
                    if not self.has_id(file_id):
 
900
                        continue
 
901
                    text_hash = s.get("hash").encode('ascii')
 
902
                    path = self.id2path(file_id)
 
903
                    if text_hash == self.get_file_sha1(path):
 
904
                        merge_hashes[file_id] = text_hash
 
905
                return merge_hashes
 
906
            finally:
 
907
                hashfile.close()
 
908
 
 
909
    def subsume(self, other_tree):
 
910
        def add_children(inventory, entry):
 
911
            for child_entry in entry.children.values():
 
912
                inventory._byid[child_entry.file_id] = child_entry
 
913
                if child_entry.kind == 'directory':
 
914
                    add_children(inventory, child_entry)
 
915
        with self.lock_write():
 
916
            if other_tree.get_root_id() == self.get_root_id():
 
917
                raise errors.BadSubsumeSource(self, other_tree,
 
918
                                              'Trees have the same root')
 
919
            try:
 
920
                other_tree_path = self.relpath(other_tree.basedir)
 
921
            except errors.PathNotChild:
 
922
                raise errors.BadSubsumeSource(
 
923
                    self, other_tree, 'Tree is not contained by the other')
 
924
            new_root_parent = self.path2id(osutils.dirname(other_tree_path))
 
925
            if new_root_parent is None:
 
926
                raise errors.BadSubsumeSource(
 
927
                    self, other_tree, 'Parent directory is not versioned.')
 
928
            # We need to ensure that the result of a fetch will have a
 
929
            # versionedfile for the other_tree root, and only fetching into
 
930
            # RepositoryKnit2 guarantees that.
 
931
            if not self.branch.repository.supports_rich_root():
 
932
                raise errors.SubsumeTargetNeedsUpgrade(other_tree)
 
933
            with other_tree.lock_tree_write():
 
934
                other_root = other_tree.root_inventory.root
 
935
                other_root.parent_id = new_root_parent
 
936
                other_root.name = osutils.basename(other_tree_path)
 
937
                self.root_inventory.add(other_root)
 
938
                add_children(self.root_inventory, other_root)
 
939
                self._write_inventory(self.root_inventory)
 
940
                # normally we don't want to fetch whole repositories, but i
 
941
                # think here we really do want to consolidate the whole thing.
 
942
                for parent_id in other_tree.get_parent_ids():
 
943
                    self.branch.fetch(other_tree.branch, parent_id)
 
944
                    self.add_parent_tree_id(parent_id)
 
945
            other_tree.controldir.retire_bzrdir()
 
946
 
 
947
    def extract(self, sub_path, format=None):
 
948
        """Extract a subtree from this tree.
 
949
 
 
950
        A new branch will be created, relative to the path for this tree.
 
951
        """
 
952
        def mkdirs(path):
 
953
            segments = osutils.splitpath(path)
 
954
            transport = self.branch.controldir.root_transport
 
955
            for name in segments:
 
956
                transport = transport.clone(name)
 
957
                transport.ensure_base()
 
958
            return transport
 
959
 
 
960
        with self.lock_tree_write():
 
961
            self.flush()
 
962
            branch_transport = mkdirs(sub_path)
 
963
            if format is None:
 
964
                format = self.controldir.cloning_metadir()
 
965
            branch_transport.ensure_base()
 
966
            branch_bzrdir = format.initialize_on_transport(branch_transport)
 
967
            try:
 
968
                repo = branch_bzrdir.find_repository()
 
969
            except errors.NoRepositoryPresent:
 
970
                repo = branch_bzrdir.create_repository()
 
971
            if not repo.supports_rich_root():
 
972
                raise errors.RootNotRich()
 
973
            new_branch = branch_bzrdir.create_branch()
 
974
            new_branch.pull(self.branch)
 
975
            for parent_id in self.get_parent_ids():
 
976
                new_branch.fetch(self.branch, parent_id)
 
977
            tree_transport = self.controldir.root_transport.clone(sub_path)
 
978
            if tree_transport.base != branch_transport.base:
 
979
                tree_bzrdir = format.initialize_on_transport(tree_transport)
 
980
                tree_bzrdir.set_branch_reference(new_branch)
 
981
            else:
 
982
                tree_bzrdir = branch_bzrdir
 
983
            wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
 
984
            wt.set_parent_ids(self.get_parent_ids())
 
985
            # FIXME: Support nested trees
 
986
            my_inv = self.root_inventory
 
987
            child_inv = inventory.Inventory(root_id=None)
 
988
            file_id = self.path2id(sub_path)
 
989
            new_root = my_inv.get_entry(file_id)
 
990
            my_inv.remove_recursive_id(file_id)
 
991
            new_root.parent_id = None
 
992
            child_inv.add(new_root)
 
993
            self._write_inventory(my_inv)
 
994
            wt._write_inventory(child_inv)
 
995
            return wt
 
996
 
 
997
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
998
        """List all files as (path, class, kind, id, entry).
 
999
 
 
1000
        Lists, but does not descend into unversioned directories.
 
1001
        This does not include files that have been deleted in this
 
1002
        tree. Skips the control directory.
 
1003
 
 
1004
        :param include_root: if True, return an entry for the root
 
1005
        :param from_dir: start from this directory or None for the root
 
1006
        :param recursive: whether to recurse into subdirectories or not
 
1007
        """
 
1008
        with self.lock_read():
 
1009
            if from_dir is None and include_root is True:
 
1010
                yield ('', 'V', 'directory', self.get_root_id(),
 
1011
                       self.root_inventory.root)
 
1012
            # Convert these into local objects to save lookup times
 
1013
            pathjoin = osutils.pathjoin
 
1014
 
 
1015
            # transport.base ends in a slash, we want the piece
 
1016
            # between the last two slashes
 
1017
            transport_base_dir = self.controldir.transport.base.rsplit(
 
1018
                '/', 2)[1]
 
1019
 
 
1020
            fk_entries = {
 
1021
                'directory': TreeDirectory,
 
1022
                'file': TreeFile,
 
1023
                'symlink': TreeLink
 
1024
                }
 
1025
 
 
1026
            # directory file_id, relative path, absolute path, reverse sorted
 
1027
            # children
 
1028
            if from_dir is not None:
 
1029
                inv, from_dir_id = self._path2inv_file_id(from_dir)
 
1030
                if from_dir_id is None:
 
1031
                    # Directory not versioned
 
1032
                    return
 
1033
                from_dir_abspath = pathjoin(self.basedir, from_dir)
 
1034
            else:
 
1035
                inv = self.root_inventory
 
1036
                from_dir_id = inv.root.file_id
 
1037
                from_dir_abspath = self.basedir
 
1038
            children = sorted(os.listdir(from_dir_abspath))
 
1039
            # jam 20060527 The kernel sized tree seems equivalent whether we
 
1040
            # use a deque and popleft to keep them sorted, or if we use a plain
 
1041
            # list and just reverse() them.
 
1042
            children = collections.deque(children)
 
1043
            stack = [(from_dir_id, u'', from_dir_abspath, children)]
 
1044
            while stack:
 
1045
                (from_dir_id, from_dir_relpath, from_dir_abspath,
 
1046
                 children) = stack[-1]
 
1047
 
 
1048
                while children:
 
1049
                    f = children.popleft()
 
1050
                    # TODO: If we find a subdirectory with its own .bzr
 
1051
                    # directory, then that is a separate tree and we
 
1052
                    # should exclude it.
 
1053
 
 
1054
                    # the bzrdir for this tree
 
1055
                    if transport_base_dir == f:
 
1056
                        continue
 
1057
 
 
1058
                    # we know that from_dir_relpath and from_dir_abspath never
 
1059
                    # end in a slash and 'f' doesn't begin with one, we can do
 
1060
                    # a string op, rather than the checks of pathjoin(), all
 
1061
                    # relative paths will have an extra slash at the beginning
 
1062
                    fp = from_dir_relpath + '/' + f
 
1063
 
 
1064
                    # absolute path
 
1065
                    fap = from_dir_abspath + '/' + f
 
1066
 
 
1067
                    dir_ie = inv.get_entry(from_dir_id)
 
1068
                    if dir_ie.kind == 'directory':
 
1069
                        f_ie = dir_ie.children.get(f)
 
1070
                    else:
 
1071
                        f_ie = None
 
1072
                    if f_ie:
 
1073
                        c = 'V'
 
1074
                    elif self.is_ignored(fp[1:]):
 
1075
                        c = 'I'
 
1076
                    else:
 
1077
                        # we may not have found this file, because of a unicode
 
1078
                        # issue, or because the directory was actually a
 
1079
                        # symlink.
 
1080
                        f_norm, can_access = osutils.normalized_filename(f)
 
1081
                        if f == f_norm or not can_access:
 
1082
                            # No change, so treat this file normally
 
1083
                            c = '?'
 
1084
                        else:
 
1085
                            # this file can be accessed by a normalized path
 
1086
                            # check again if it is versioned
 
1087
                            # these lines are repeated here for performance
 
1088
                            f = f_norm
 
1089
                            fp = from_dir_relpath + '/' + f
 
1090
                            fap = from_dir_abspath + '/' + f
 
1091
                            f_ie = inv.get_child(from_dir_id, f)
 
1092
                            if f_ie:
 
1093
                                c = 'V'
 
1094
                            elif self.is_ignored(fp[1:]):
 
1095
                                c = 'I'
 
1096
                            else:
 
1097
                                c = '?'
 
1098
 
 
1099
                    fk = osutils.file_kind(fap)
 
1100
 
 
1101
                    # make a last minute entry
 
1102
                    if f_ie:
 
1103
                        yield fp[1:], c, fk, f_ie.file_id, f_ie
 
1104
                    else:
 
1105
                        try:
 
1106
                            yield fp[1:], c, fk, None, fk_entries[fk]()
 
1107
                        except KeyError:
 
1108
                            yield fp[1:], c, fk, None, TreeEntry()
 
1109
                        continue
 
1110
 
 
1111
                    if fk != 'directory':
 
1112
                        continue
 
1113
 
 
1114
                    # But do this child first if recursing down
 
1115
                    if recursive:
 
1116
                        new_children = sorted(os.listdir(fap))
 
1117
                        new_children = collections.deque(new_children)
 
1118
                        stack.append((f_ie.file_id, fp, fap, new_children))
 
1119
                        # Break out of inner loop,
 
1120
                        # so that we start outer loop with child
 
1121
                        break
 
1122
                else:
 
1123
                    # if we finished all children, pop it off the stack
 
1124
                    stack.pop()
 
1125
 
 
1126
    def move(self, from_paths, to_dir=None, after=False):
 
1127
        """Rename files.
 
1128
 
 
1129
        to_dir must exist in the inventory.
 
1130
 
 
1131
        If to_dir exists and is a directory, the files are moved into
 
1132
        it, keeping their old names.
 
1133
 
 
1134
        Note that to_dir is only the last component of the new name;
 
1135
        this doesn't change the directory.
 
1136
 
 
1137
        For each entry in from_paths the move mode will be determined
 
1138
        independently.
 
1139
 
 
1140
        The first mode moves the file in the filesystem and updates the
 
1141
        inventory. The second mode only updates the inventory without
 
1142
        touching the file on the filesystem.
 
1143
 
 
1144
        move uses the second mode if 'after == True' and the target is
 
1145
        either not versioned or newly added, and present in the working tree.
 
1146
 
 
1147
        move uses the second mode if 'after == False' and the source is
 
1148
        versioned but no longer in the working tree, and the target is not
 
1149
        versioned but present in the working tree.
 
1150
 
 
1151
        move uses the first mode if 'after == False' and the source is
 
1152
        versioned and present in the working tree, and the target is not
 
1153
        versioned and not present in the working tree.
 
1154
 
 
1155
        Everything else results in an error.
 
1156
 
 
1157
        This returns a list of (from_path, to_path) pairs for each
 
1158
        entry that is moved.
 
1159
        """
 
1160
        rename_entries = []
 
1161
        rename_tuples = []
 
1162
 
 
1163
        # check for deprecated use of signature
 
1164
        if to_dir is None:
 
1165
            raise TypeError('You must supply a target directory')
 
1166
        # check destination directory
 
1167
        if isinstance(from_paths, (str, text_type)):
 
1168
            raise ValueError()
 
1169
        with self.lock_tree_write():
 
1170
            to_abs = self.abspath(to_dir)
 
1171
            if not osutils.isdir(to_abs):
 
1172
                raise errors.BzrMoveFailedError(
 
1173
                    '', to_dir, errors.NotADirectory(to_abs))
 
1174
            if not self.has_filename(to_dir):
 
1175
                raise errors.BzrMoveFailedError(
 
1176
                    '', to_dir, errors.NotInWorkingDirectory(to_dir))
 
1177
            to_inv, to_dir_id = self._path2inv_file_id(to_dir)
 
1178
            if to_dir_id is None:
 
1179
                raise errors.BzrMoveFailedError(
 
1180
                    '', to_dir, errors.NotVersionedError(path=to_dir))
 
1181
 
 
1182
            to_dir_ie = to_inv.get_entry(to_dir_id)
 
1183
            if to_dir_ie.kind != 'directory':
 
1184
                raise errors.BzrMoveFailedError(
 
1185
                    '', to_dir, errors.NotADirectory(to_abs))
 
1186
 
 
1187
            # create rename entries and tuples
 
1188
            for from_rel in from_paths:
 
1189
                from_tail = osutils.splitpath(from_rel)[-1]
 
1190
                from_inv, from_id = self._path2inv_file_id(from_rel)
 
1191
                if from_id is None:
 
1192
                    raise errors.BzrMoveFailedError(from_rel, to_dir,
 
1193
                                                    errors.NotVersionedError(path=from_rel))
 
1194
 
 
1195
                from_entry = from_inv.get_entry(from_id)
 
1196
                from_parent_id = from_entry.parent_id
 
1197
                to_rel = osutils.pathjoin(to_dir, from_tail)
 
1198
                rename_entry = InventoryWorkingTree._RenameEntry(
 
1199
                    from_rel=from_rel,
 
1200
                    from_id=from_id,
 
1201
                    from_tail=from_tail,
 
1202
                    from_parent_id=from_parent_id,
 
1203
                    to_rel=to_rel, to_tail=from_tail,
 
1204
                    to_parent_id=to_dir_id)
 
1205
                rename_entries.append(rename_entry)
 
1206
                rename_tuples.append((from_rel, to_rel))
 
1207
 
 
1208
            # determine which move mode to use. checks also for movability
 
1209
            rename_entries = self._determine_mv_mode(rename_entries, after)
 
1210
 
 
1211
            original_modified = self._inventory_is_modified
 
1212
            try:
 
1213
                if len(from_paths):
 
1214
                    self._inventory_is_modified = True
 
1215
                self._move(rename_entries)
 
1216
            except BaseException:
 
1217
                # restore the inventory on error
 
1218
                self._inventory_is_modified = original_modified
 
1219
                raise
 
1220
            # FIXME: Should potentially also write the from_invs
 
1221
            self._write_inventory(to_inv)
 
1222
            return rename_tuples
 
1223
 
 
1224
    def rename_one(self, from_rel, to_rel, after=False):
 
1225
        """Rename one file.
 
1226
 
 
1227
        This can change the directory or the filename or both.
 
1228
 
 
1229
        rename_one has several 'modes' to work. First, it can rename a physical
 
1230
        file and change the file_id. That is the normal mode. Second, it can
 
1231
        only change the file_id without touching any physical file.
 
1232
 
 
1233
        rename_one uses the second mode if 'after == True' and 'to_rel' is not
 
1234
        versioned but present in the working tree.
 
1235
 
 
1236
        rename_one uses the second mode if 'after == False' and 'from_rel' is
 
1237
        versioned but no longer in the working tree, and 'to_rel' is not
 
1238
        versioned but present in the working tree.
 
1239
 
 
1240
        rename_one uses the first mode if 'after == False' and 'from_rel' is
 
1241
        versioned and present in the working tree, and 'to_rel' is not
 
1242
        versioned and not present in the working tree.
 
1243
 
 
1244
        Everything else results in an error.
 
1245
        """
 
1246
        with self.lock_tree_write():
 
1247
            rename_entries = []
 
1248
 
 
1249
            # create rename entries and tuples
 
1250
            from_tail = osutils.splitpath(from_rel)[-1]
 
1251
            from_inv, from_id = self._path2inv_file_id(from_rel)
 
1252
            if from_id is None:
 
1253
                # if file is missing in the inventory maybe it's in the
 
1254
                # basis_tree
 
1255
                basis_tree = self.branch.basis_tree()
 
1256
                from_id = basis_tree.path2id(from_rel)
 
1257
                if from_id is None:
 
1258
                    raise errors.BzrRenameFailedError(
 
1259
                        from_rel, to_rel,
 
1260
                        errors.NotVersionedError(path=from_rel))
 
1261
                # put entry back in the inventory so we can rename it
 
1262
                from_entry = basis_tree.root_inventory.get_entry(
 
1263
                    from_id).copy()
 
1264
                from_inv.add(from_entry)
 
1265
            else:
 
1266
                from_inv, from_inv_id = self._unpack_file_id(from_id)
 
1267
                from_entry = from_inv.get_entry(from_inv_id)
 
1268
            from_parent_id = from_entry.parent_id
 
1269
            to_dir, to_tail = os.path.split(to_rel)
 
1270
            to_inv, to_dir_id = self._path2inv_file_id(to_dir)
 
1271
            rename_entry = InventoryWorkingTree._RenameEntry(
 
1272
                from_rel=from_rel,
 
1273
                from_id=from_id,
 
1274
                from_tail=from_tail,
 
1275
                from_parent_id=from_parent_id,
 
1276
                to_rel=to_rel, to_tail=to_tail,
 
1277
                to_parent_id=to_dir_id)
 
1278
            rename_entries.append(rename_entry)
 
1279
 
 
1280
            # determine which move mode to use. checks also for movability
 
1281
            rename_entries = self._determine_mv_mode(rename_entries, after)
 
1282
 
 
1283
            # check if the target changed directory and if the target directory
 
1284
            # is versioned
 
1285
            if to_dir_id is None:
 
1286
                raise errors.BzrMoveFailedError(
 
1287
                    from_rel, to_rel, errors.NotVersionedError(path=to_dir))
 
1288
 
 
1289
            # all checks done. now we can continue with our actual work
 
1290
            mutter('rename_one:\n'
 
1291
                   '  from_id   {%s}\n'
 
1292
                   '  from_rel: %r\n'
 
1293
                   '  to_rel:   %r\n'
 
1294
                   '  to_dir    %r\n'
 
1295
                   '  to_dir_id {%s}\n',
 
1296
                   from_id, from_rel, to_rel, to_dir, to_dir_id)
 
1297
 
 
1298
            self._move(rename_entries)
 
1299
            self._write_inventory(to_inv)
 
1300
 
 
1301
    class _RenameEntry(object):
 
1302
        def __init__(self, from_rel, from_id, from_tail, from_parent_id,
 
1303
                     to_rel, to_tail, to_parent_id, only_change_inv=False,
 
1304
                     change_id=False):
 
1305
            self.from_rel = from_rel
 
1306
            self.from_id = from_id
 
1307
            self.from_tail = from_tail
 
1308
            self.from_parent_id = from_parent_id
 
1309
            self.to_rel = to_rel
 
1310
            self.to_tail = to_tail
 
1311
            self.to_parent_id = to_parent_id
 
1312
            self.change_id = change_id
 
1313
            self.only_change_inv = only_change_inv
 
1314
 
 
1315
    def _determine_mv_mode(self, rename_entries, after=False):
 
1316
        """Determines for each from-to pair if both inventory and working tree
 
1317
        or only the inventory has to be changed.
 
1318
 
 
1319
        Also does basic plausability tests.
 
1320
        """
 
1321
        # FIXME: Handling of nested trees
 
1322
        inv = self.root_inventory
 
1323
 
 
1324
        for rename_entry in rename_entries:
 
1325
            # store to local variables for easier reference
 
1326
            from_rel = rename_entry.from_rel
 
1327
            from_id = rename_entry.from_id
 
1328
            to_rel = rename_entry.to_rel
 
1329
            to_id = inv.path2id(to_rel)
 
1330
            only_change_inv = False
 
1331
 
 
1332
            # check the inventory for source and destination
 
1333
            if from_id is None:
 
1334
                raise errors.BzrMoveFailedError(
 
1335
                    from_rel, to_rel, errors.NotVersionedError(path=from_rel))
 
1336
            if to_id is not None:
 
1337
                allowed = False
 
1338
                # allow it with --after but only if dest is newly added
 
1339
                if after:
 
1340
                    basis = self.basis_tree()
 
1341
                    with basis.lock_read():
 
1342
                        if not basis.has_id(to_id):
 
1343
                            rename_entry.change_id = True
 
1344
                            allowed = True
 
1345
                if not allowed:
 
1346
                    raise errors.BzrMoveFailedError(
 
1347
                        from_rel, to_rel,
 
1348
                        errors.AlreadyVersionedError(path=to_rel))
 
1349
 
 
1350
            # try to determine the mode for rename (only change inv or change
 
1351
            # inv and file system)
 
1352
            if after:
 
1353
                if not self.has_filename(to_rel):
 
1354
                    raise errors.BzrMoveFailedError(
 
1355
                        from_rel, to_rel,
 
1356
                        errors.NoSuchFile(
 
1357
                            path=to_rel,
 
1358
                            extra="New file has not been created yet"))
 
1359
                only_change_inv = True
 
1360
            elif not self.has_filename(from_rel) and self.has_filename(to_rel):
 
1361
                only_change_inv = True
 
1362
            elif self.has_filename(from_rel) and not self.has_filename(to_rel):
 
1363
                only_change_inv = False
 
1364
            elif (not self.case_sensitive and
 
1365
                  from_rel.lower() == to_rel.lower() and
 
1366
                  self.has_filename(from_rel)):
 
1367
                only_change_inv = False
 
1368
            else:
 
1369
                # something is wrong, so lets determine what exactly
 
1370
                if not self.has_filename(from_rel) and \
 
1371
                   not self.has_filename(to_rel):
 
1372
                    raise errors.BzrRenameFailedError(
 
1373
                        from_rel, to_rel,
 
1374
                        errors.PathsDoNotExist(paths=(from_rel, to_rel)))
 
1375
                else:
 
1376
                    raise errors.RenameFailedFilesExist(from_rel, to_rel)
 
1377
            rename_entry.only_change_inv = only_change_inv
 
1378
        return rename_entries
 
1379
 
 
1380
    def _move(self, rename_entries):
 
1381
        """Moves a list of files.
 
1382
 
 
1383
        Depending on the value of the flag 'only_change_inv', the
 
1384
        file will be moved on the file system or not.
 
1385
        """
 
1386
        moved = []
 
1387
 
 
1388
        for entry in rename_entries:
 
1389
            try:
 
1390
                self._move_entry(entry)
 
1391
            except BaseException:
 
1392
                self._rollback_move(moved)
 
1393
                raise
 
1394
            moved.append(entry)
 
1395
 
 
1396
    def _rollback_move(self, moved):
 
1397
        """Try to rollback a previous move in case of an filesystem error."""
 
1398
        for entry in moved:
 
1399
            try:
 
1400
                self._move_entry(WorkingTree._RenameEntry(
 
1401
                    entry.to_rel, entry.from_id,
 
1402
                    entry.to_tail, entry.to_parent_id, entry.from_rel,
 
1403
                    entry.from_tail, entry.from_parent_id,
 
1404
                    entry.only_change_inv))
 
1405
            except errors.BzrMoveFailedError as e:
 
1406
                raise errors.BzrMoveFailedError(
 
1407
                    '', '', "Rollback failed."
 
1408
                    " The working tree is in an inconsistent state."
 
1409
                    " Please consider doing a 'bzr revert'."
 
1410
                    " Error message is: %s" % e)
 
1411
 
 
1412
    def _move_entry(self, entry):
 
1413
        inv = self.root_inventory
 
1414
        from_rel_abs = self.abspath(entry.from_rel)
 
1415
        to_rel_abs = self.abspath(entry.to_rel)
 
1416
        if from_rel_abs == to_rel_abs:
 
1417
            raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
 
1418
                                            "Source and target are identical.")
 
1419
 
 
1420
        if not entry.only_change_inv:
 
1421
            try:
 
1422
                osutils.rename(from_rel_abs, to_rel_abs)
 
1423
            except OSError as e:
 
1424
                raise errors.BzrMoveFailedError(
 
1425
                    entry.from_rel, entry.to_rel, e[1])
 
1426
        if entry.change_id:
 
1427
            to_id = inv.path2id(entry.to_rel)
 
1428
            inv.remove_recursive_id(to_id)
 
1429
        inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
 
1430
 
 
1431
    def unversion(self, paths):
 
1432
        """Remove the paths in paths from the current versioned set.
 
1433
 
 
1434
        When a path is unversioned, all of its children are automatically
 
1435
        unversioned.
 
1436
 
 
1437
        :param paths: The paths to stop versioning.
 
1438
        :raises NoSuchFile: if any path is not currently versioned.
 
1439
        """
 
1440
        with self.lock_tree_write():
 
1441
            file_ids = set()
 
1442
            for path in paths:
 
1443
                file_id = self._inventory.path2id(path)
 
1444
                if file_id is None:
 
1445
                    raise errors.NoSuchFile(path, self)
 
1446
                file_ids.add(file_id)
 
1447
            for file_id in file_ids:
 
1448
                if self._inventory.has_id(file_id):
 
1449
                    self._inventory.remove_recursive_id(file_id)
 
1450
            if len(file_ids):
 
1451
                # in the future this should just set a dirty bit to wait for
 
1452
                # the final unlock. However, until all methods of workingtree
 
1453
                # start with the current in -memory inventory rather than
 
1454
                # triggering a read, it is more complex - we need to teach
 
1455
                # read_inventory to know when to read, and when to not read
 
1456
                # first... and possibly to save first when the in memory one
 
1457
                # may be corrupted.  so for now, we just only write it if it is
 
1458
                # indeed dirty.  - RBC 20060907
 
1459
                self._write_inventory(self._inventory)
 
1460
 
 
1461
    def stored_kind(self, path, file_id=None):
 
1462
        """See Tree.stored_kind"""
 
1463
        return self._path2ie(path).kind
 
1464
 
 
1465
    def extras(self):
 
1466
        """Yield all unversioned files in this WorkingTree.
 
1467
 
 
1468
        If there are any unversioned directories then only the directory is
 
1469
        returned, not all its children.  But if there are unversioned files
 
1470
        under a versioned subdirectory, they are returned.
 
1471
 
 
1472
        Currently returned depth-first, sorted by name within directories.
 
1473
        This is the same order used by 'osutils.walkdirs'.
 
1474
        """
 
1475
        # TODO: Work from given directory downwards
 
1476
        for path, dir_entry in self.iter_entries_by_dir():
 
1477
            if dir_entry.kind != 'directory':
 
1478
                continue
 
1479
            # mutter("search for unknowns in %r", path)
 
1480
            dirabs = self.abspath(path)
 
1481
            if not osutils.isdir(dirabs):
 
1482
                # e.g. directory deleted
 
1483
                continue
 
1484
 
 
1485
            fl = []
 
1486
            for subf in os.listdir(dirabs.encode(osutils._fs_enc)):
 
1487
                try:
 
1488
                    subf = subf.decode(osutils._fs_enc)
 
1489
                except UnicodeDecodeError:
 
1490
                    path_os_enc = path.encode(osutils._fs_enc)
 
1491
                    relpath = path_os_enc + b'/' + subf
 
1492
                    raise errors.BadFilenameEncoding(relpath,
 
1493
                                                     osutils._fs_enc)
 
1494
 
 
1495
                if self.controldir.is_control_filename(subf):
 
1496
                    continue
 
1497
                if subf not in dir_entry.children:
 
1498
                    try:
 
1499
                        (subf_norm,
 
1500
                         can_access) = osutils.normalized_filename(subf)
 
1501
                    except UnicodeDecodeError:
 
1502
                        path_os_enc = path.encode(osutils._fs_enc)
 
1503
                        relpath = path_os_enc + '/' + subf
 
1504
                        raise errors.BadFilenameEncoding(relpath,
 
1505
                                                         osutils._fs_enc)
 
1506
                    if subf_norm != subf and can_access:
 
1507
                        if subf_norm not in dir_entry.children:
 
1508
                            fl.append(subf_norm)
 
1509
                    else:
 
1510
                        fl.append(subf)
 
1511
 
 
1512
            fl.sort()
 
1513
            for subf in fl:
 
1514
                subp = osutils.pathjoin(path, subf)
 
1515
                yield subp
 
1516
 
 
1517
    def walkdirs(self, prefix=""):
 
1518
        """Walk the directories of this tree.
 
1519
 
 
1520
        returns a generator which yields items in the form:
 
1521
                ((curren_directory_path, fileid),
 
1522
                 [(file1_path, file1_name, file1_kind, (lstat), file1_id,
 
1523
                   file1_kind), ... ])
 
1524
 
 
1525
        This API returns a generator, which is only valid during the current
 
1526
        tree transaction - within a single lock_read or lock_write duration.
 
1527
 
 
1528
        If the tree is not locked, it may cause an error to be raised,
 
1529
        depending on the tree implementation.
 
1530
        """
 
1531
        disk_top = self.abspath(prefix)
 
1532
        if disk_top.endswith('/'):
 
1533
            disk_top = disk_top[:-1]
 
1534
        top_strip_len = len(disk_top) + 1
 
1535
        inventory_iterator = self._walkdirs(prefix)
 
1536
        disk_iterator = osutils.walkdirs(disk_top, prefix)
 
1537
        try:
 
1538
            current_disk = next(disk_iterator)
 
1539
            disk_finished = False
 
1540
        except OSError as e:
 
1541
            if not (e.errno == errno.ENOENT
 
1542
                    or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
 
1543
                raise
 
1544
            current_disk = None
 
1545
            disk_finished = True
 
1546
        try:
 
1547
            current_inv = next(inventory_iterator)
 
1548
            inv_finished = False
 
1549
        except StopIteration:
 
1550
            current_inv = None
 
1551
            inv_finished = True
 
1552
        while not inv_finished or not disk_finished:
 
1553
            if current_disk:
 
1554
                ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
 
1555
                    cur_disk_dir_content) = current_disk
 
1556
            else:
 
1557
                ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
 
1558
                    cur_disk_dir_content) = ((None, None), None)
 
1559
            if not disk_finished:
 
1560
                # strip out .bzr dirs
 
1561
                if (cur_disk_dir_path_from_top[top_strip_len:] == ''
 
1562
                        and len(cur_disk_dir_content) > 0):
 
1563
                    # osutils.walkdirs can be made nicer -
 
1564
                    # yield the path-from-prefix rather than the pathjoined
 
1565
                    # value.
 
1566
                    bzrdir_loc = bisect_left(cur_disk_dir_content,
 
1567
                                             ('.bzr', '.bzr'))
 
1568
                    if (bzrdir_loc < len(cur_disk_dir_content) and
 
1569
                        self.controldir.is_control_filename(
 
1570
                            cur_disk_dir_content[bzrdir_loc][0])):
 
1571
                        # we dont yield the contents of, or, .bzr itself.
 
1572
                        del cur_disk_dir_content[bzrdir_loc]
 
1573
            if inv_finished:
 
1574
                # everything is unknown
 
1575
                direction = 1
 
1576
            elif disk_finished:
 
1577
                # everything is missing
 
1578
                direction = -1
 
1579
            else:
 
1580
                direction = ((current_inv[0][0] > cur_disk_dir_relpath)
 
1581
                             - (current_inv[0][0] < cur_disk_dir_relpath))
 
1582
 
 
1583
            if direction > 0:
 
1584
                # disk is before inventory - unknown
 
1585
                dirblock = [(relpath, basename, kind, stat, None, None) for
 
1586
                            relpath, basename, kind, stat, top_path in
 
1587
                            cur_disk_dir_content]
 
1588
                yield (cur_disk_dir_relpath, None), dirblock
 
1589
                try:
 
1590
                    current_disk = next(disk_iterator)
 
1591
                except StopIteration:
 
1592
                    disk_finished = True
 
1593
            elif direction < 0:
 
1594
                # inventory is before disk - missing.
 
1595
                dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
 
1596
                            for relpath, basename, dkind, stat, fileid, kind in
 
1597
                            current_inv[1]]
 
1598
                yield (current_inv[0][0], current_inv[0][1]), dirblock
 
1599
                try:
 
1600
                    current_inv = next(inventory_iterator)
 
1601
                except StopIteration:
 
1602
                    inv_finished = True
 
1603
            else:
 
1604
                # versioned present directory
 
1605
                # merge the inventory and disk data together
 
1606
                dirblock = []
 
1607
                for relpath, subiterator in itertools.groupby(sorted(
 
1608
                        current_inv[1] + cur_disk_dir_content,
 
1609
                        key=operator.itemgetter(0)), operator.itemgetter(1)):
 
1610
                    path_elements = list(subiterator)
 
1611
                    if len(path_elements) == 2:
 
1612
                        inv_row, disk_row = path_elements
 
1613
                        # versioned, present file
 
1614
                        dirblock.append((inv_row[0],
 
1615
                                         inv_row[1], disk_row[2],
 
1616
                                         disk_row[3], inv_row[4],
 
1617
                                         inv_row[5]))
 
1618
                    elif len(path_elements[0]) == 5:
 
1619
                        # unknown disk file
 
1620
                        dirblock.append(
 
1621
                            (path_elements[0][0], path_elements[0][1],
 
1622
                             path_elements[0][2], path_elements[0][3], None,
 
1623
                             None))
 
1624
                    elif len(path_elements[0]) == 6:
 
1625
                        # versioned, absent file.
 
1626
                        dirblock.append(
 
1627
                            (path_elements[0][0], path_elements[0][1],
 
1628
                             'unknown', None, path_elements[0][4],
 
1629
                             path_elements[0][5]))
 
1630
                    else:
 
1631
                        raise NotImplementedError('unreachable code')
 
1632
                yield current_inv[0], dirblock
 
1633
                try:
 
1634
                    current_inv = next(inventory_iterator)
 
1635
                except StopIteration:
 
1636
                    inv_finished = True
 
1637
                try:
 
1638
                    current_disk = next(disk_iterator)
 
1639
                except StopIteration:
 
1640
                    disk_finished = True
 
1641
 
 
1642
    def _walkdirs(self, prefix=""):
 
1643
        """Walk the directories of this tree.
 
1644
 
 
1645
        :param prefix: is used as the directrory to start with.
 
1646
        :returns: a generator which yields items in the form::
 
1647
 
 
1648
            ((curren_directory_path, fileid),
 
1649
             [(file1_path, file1_name, file1_kind, None, file1_id,
 
1650
               file1_kind), ... ])
 
1651
        """
 
1652
        _directory = 'directory'
 
1653
        # get the root in the inventory
 
1654
        inv, top_id = self._path2inv_file_id(prefix)
 
1655
        if top_id is None:
 
1656
            pending = []
 
1657
        else:
 
1658
            pending = [(prefix, '', _directory, None, top_id, None)]
 
1659
        while pending:
 
1660
            dirblock = []
 
1661
            currentdir = pending.pop()
 
1662
            # 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
 
1663
            top_id = currentdir[4]
 
1664
            if currentdir[0]:
 
1665
                relroot = currentdir[0] + '/'
 
1666
            else:
 
1667
                relroot = ""
 
1668
            # FIXME: stash the node in pending
 
1669
            entry = inv.get_entry(top_id)
 
1670
            if entry.kind == 'directory':
 
1671
                for name, child in entry.sorted_children():
 
1672
                    dirblock.append((relroot + name, name, child.kind, None,
 
1673
                                     child.file_id, child.kind
 
1674
                                     ))
 
1675
            yield (currentdir[0], entry.file_id), dirblock
 
1676
            # push the user specified dirs from dirblock
 
1677
            for dir in reversed(dirblock):
 
1678
                if dir[2] == _directory:
 
1679
                    pending.append(dir)
 
1680
 
 
1681
    def update_feature_flags(self, updated_flags):
 
1682
        """Update the feature flags for this branch.
 
1683
 
 
1684
        :param updated_flags: Dictionary mapping feature names to necessities
 
1685
            A necessity can be None to indicate the feature should be removed
 
1686
        """
 
1687
        with self.lock_write():
 
1688
            self._format._update_feature_flags(updated_flags)
 
1689
            self.control_transport.put_bytes(
 
1690
                'format', self._format.as_string())
 
1691
 
 
1692
    def _check_for_tree_references(self, iterator):
 
1693
        """See if directories have become tree-references."""
 
1694
        blocked_parent_ids = set()
 
1695
        for path, ie in iterator:
 
1696
            if ie.parent_id in blocked_parent_ids:
 
1697
                # This entry was pruned because one of its parents became a
 
1698
                # TreeReference. If this is a directory, mark it as blocked.
 
1699
                if ie.kind == 'directory':
 
1700
                    blocked_parent_ids.add(ie.file_id)
 
1701
                continue
 
1702
            if (ie.kind == 'directory' and
 
1703
                    self._directory_is_tree_reference(path)):
 
1704
                # This InventoryDirectory needs to be a TreeReference
 
1705
                ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
 
1706
                blocked_parent_ids.add(ie.file_id)
 
1707
            yield path, ie
 
1708
 
 
1709
    def iter_entries_by_dir(self, specific_files=None):
 
1710
        """See Tree.iter_entries_by_dir()"""
 
1711
        # The only trick here is that if we supports_tree_reference then we
 
1712
        # need to detect if a directory becomes a tree-reference.
 
1713
        iterator = super(WorkingTree, self).iter_entries_by_dir(
 
1714
            specific_files=specific_files)
 
1715
        if not self.supports_tree_reference():
 
1716
            return iterator
 
1717
        else:
 
1718
            return self._check_for_tree_references(iterator)
 
1719
 
 
1720
    def get_canonical_paths(self, paths):
 
1721
        """Look up canonical paths for multiple items.
 
1722
 
 
1723
        :param paths: A sequence of paths relative to the root of the tree.
 
1724
        :return: A iterator over paths, with each item the corresponding input
 
1725
            path adjusted to account for existing elements that match case
 
1726
            insensitively.
 
1727
        """
 
1728
        with self.lock_read():
 
1729
            if not self.case_sensitive:
 
1730
                def normalize(x):
 
1731
                    return x.lower()
 
1732
            elif sys.platform == 'darwin':
 
1733
                import unicodedata
 
1734
 
 
1735
                def normalize(x):
 
1736
                    return unicodedata.normalize('NFC', x)
 
1737
            else:
 
1738
                normalize = None
 
1739
            for path in paths:
 
1740
                if normalize is None or self.is_versioned(path):
 
1741
                    yield path
 
1742
                else:
 
1743
                    yield get_canonical_path(self, path, normalize)
 
1744
 
 
1745
 
 
1746
class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
 
1747
    """Base class for working trees that live in bzr meta directories."""
 
1748
 
 
1749
    ignore_filename = '.bzrignore'
 
1750
 
 
1751
    def __init__(self):
 
1752
        WorkingTreeFormat.__init__(self)
 
1753
        bzrdir.BzrFormat.__init__(self)
 
1754
 
 
1755
    @classmethod
 
1756
    def find_format_string(klass, controldir):
 
1757
        """Return format name for the working tree object in controldir."""
 
1758
        try:
 
1759
            transport = controldir.get_workingtree_transport(None)
 
1760
            return transport.get_bytes("format")
 
1761
        except errors.NoSuchFile:
 
1762
            raise errors.NoWorkingTree(base=transport.base)
 
1763
 
 
1764
    @classmethod
 
1765
    def find_format(klass, controldir):
 
1766
        """Return the format for the working tree object in controldir."""
 
1767
        format_string = klass.find_format_string(controldir)
 
1768
        return klass._find_format(format_registry, 'working tree',
 
1769
                                  format_string)
 
1770
 
 
1771
    def check_support_status(self, allow_unsupported, recommend_upgrade=True,
 
1772
                             basedir=None):
 
1773
        WorkingTreeFormat.check_support_status(
 
1774
            self, allow_unsupported=allow_unsupported,
 
1775
            recommend_upgrade=recommend_upgrade, basedir=basedir)
 
1776
        bzrdir.BzrFormat.check_support_status(
 
1777
            self, allow_unsupported=allow_unsupported,
 
1778
            recommend_upgrade=recommend_upgrade, basedir=basedir)