/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: Jelmer Vernooij
  • Date: 2018-08-26 02:01:46 UTC
  • mto: This revision was merged to the branch mainline in revision 7087.
  • Revision ID: jelmer@jelmer.uk-20180826020146-owq7fxsr6ermorlh
Fix remaining warnings on Python 3.

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