/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-05-06 11:47:47 UTC
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@jelmer.uk-20180506114747-ao1dqex1ei2hj7kq
Remove brz-git specific setup.py.

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('\n', 1)[0]
 
511
            if (not 'revision_id="' in firstline or
 
512
                '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
            maybe_file_parent_keys = []
 
809
            for parent_id in self.get_parent_ids():
 
810
                try:
 
811
                    parent_tree = self.revision_tree(parent_id)
 
812
                except errors.NoSuchRevisionInTree:
 
813
                    parent_tree = self.branch.repository.revision_tree(
 
814
                            parent_id)
 
815
                with parent_tree.lock_read():
 
816
 
 
817
                    try:
 
818
                        kind = parent_tree.kind(path, file_id)
 
819
                    except errors.NoSuchFile:
 
820
                        continue
 
821
                    if kind != 'file':
 
822
                        # Note: this is slightly unnecessary, because symlinks and
 
823
                        # directories have a "text" which is the empty text, and we
 
824
                        # know that won't mess up annotations. But it seems cleaner
 
825
                        continue
 
826
                    parent_path = parent_tree.id2path(file_id)
 
827
                    parent_text_key = (
 
828
                        file_id,
 
829
                        parent_tree.get_file_revision(parent_path, file_id))
 
830
                    if parent_text_key not in maybe_file_parent_keys:
 
831
                        maybe_file_parent_keys.append(parent_text_key)
 
832
            graph = self.branch.repository.get_file_graph()
 
833
            heads = graph.heads(maybe_file_parent_keys)
 
834
            file_parent_keys = []
 
835
            for key in maybe_file_parent_keys:
 
836
                if key in heads:
 
837
                    file_parent_keys.append(key)
 
838
 
 
839
            # Now we have the parents of this content
 
840
            annotator = self.branch.repository.texts.get_annotator()
 
841
            text = self.get_file_text(path, file_id)
 
842
            this_key = (file_id, default_revision)
 
843
            annotator.add_special_text(this_key, file_parent_keys, text)
 
844
            annotations = [(key[-1], line)
 
845
                           for key, line in annotator.annotate_flat(this_key)]
 
846
            return annotations
 
847
 
 
848
    def _put_rio(self, filename, stanzas, header):
 
849
        self._must_be_locked()
 
850
        my_file = _mod_rio.rio_file(stanzas, header)
 
851
        self._transport.put_file(filename, my_file,
 
852
            mode=self.controldir._get_file_mode())
 
853
 
 
854
    def set_merge_modified(self, modified_hashes):
 
855
        def iter_stanzas():
 
856
            for file_id in modified_hashes:
 
857
                yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
 
858
                    hash=modified_hashes[file_id])
 
859
        with self.lock_tree_write():
 
860
            self._put_rio('merge-hashes', iter_stanzas(), MERGE_MODIFIED_HEADER_1)
 
861
 
 
862
    def merge_modified(self):
 
863
        """Return a dictionary of files modified by a merge.
 
864
 
 
865
        The list is initialized by WorkingTree.set_merge_modified, which is
 
866
        typically called after we make some automatic updates to the tree
 
867
        because of a merge.
 
868
 
 
869
        This returns a map of file_id->sha1, containing only files which are
 
870
        still in the working inventory and have that text hash.
 
871
        """
 
872
        with self.lock_read():
 
873
            try:
 
874
                hashfile = self._transport.get('merge-hashes')
 
875
            except errors.NoSuchFile:
 
876
                return {}
 
877
            try:
 
878
                merge_hashes = {}
 
879
                try:
 
880
                    if next(hashfile) != MERGE_MODIFIED_HEADER_1 + b'\n':
 
881
                        raise errors.MergeModifiedFormatError()
 
882
                except StopIteration:
 
883
                    raise errors.MergeModifiedFormatError()
 
884
                for s in _mod_rio.RioReader(hashfile):
 
885
                    # RioReader reads in Unicode, so convert file_ids back to utf8
 
886
                    file_id = cache_utf8.encode(s.get("file_id"))
 
887
                    if not self.has_id(file_id):
 
888
                        continue
 
889
                    text_hash = s.get("hash")
 
890
                    path = self.id2path(file_id)
 
891
                    if text_hash == self.get_file_sha1(path, file_id):
 
892
                        merge_hashes[file_id] = text_hash
 
893
                return merge_hashes
 
894
            finally:
 
895
                hashfile.close()
 
896
 
 
897
    def subsume(self, other_tree):
 
898
        def add_children(inventory, entry):
 
899
            for child_entry in entry.children.values():
 
900
                inventory._byid[child_entry.file_id] = child_entry
 
901
                if child_entry.kind == 'directory':
 
902
                    add_children(inventory, child_entry)
 
903
        with self.lock_write():
 
904
            if other_tree.get_root_id() == self.get_root_id():
 
905
                raise errors.BadSubsumeSource(self, other_tree,
 
906
                                              'Trees have the same root')
 
907
            try:
 
908
                other_tree_path = self.relpath(other_tree.basedir)
 
909
            except errors.PathNotChild:
 
910
                raise errors.BadSubsumeSource(self, other_tree,
 
911
                    'Tree is not contained by the other')
 
912
            new_root_parent = self.path2id(osutils.dirname(other_tree_path))
 
913
            if new_root_parent is None:
 
914
                raise errors.BadSubsumeSource(self, other_tree,
 
915
                    'Parent directory is not versioned.')
 
916
            # We need to ensure that the result of a fetch will have a
 
917
            # versionedfile for the other_tree root, and only fetching into
 
918
            # RepositoryKnit2 guarantees that.
 
919
            if not self.branch.repository.supports_rich_root():
 
920
                raise errors.SubsumeTargetNeedsUpgrade(other_tree)
 
921
            with other_tree.lock_tree_write():
 
922
                new_parents = other_tree.get_parent_ids()
 
923
                other_root = other_tree.root_inventory.root
 
924
                other_root.parent_id = new_root_parent
 
925
                other_root.name = osutils.basename(other_tree_path)
 
926
                self.root_inventory.add(other_root)
 
927
                add_children(self.root_inventory, other_root)
 
928
                self._write_inventory(self.root_inventory)
 
929
                # normally we don't want to fetch whole repositories, but i think
 
930
                # here we really do want to consolidate the whole thing.
 
931
                for parent_id in other_tree.get_parent_ids():
 
932
                    self.branch.fetch(other_tree.branch, parent_id)
 
933
                    self.add_parent_tree_id(parent_id)
 
934
            other_tree.controldir.retire_bzrdir()
 
935
 
 
936
    def extract(self, sub_path, file_id=None, format=None):
 
937
        """Extract a subtree from this tree.
 
938
 
 
939
        A new branch will be created, relative to the path for this tree.
 
940
        """
 
941
        def mkdirs(path):
 
942
            segments = osutils.splitpath(path)
 
943
            transport = self.branch.controldir.root_transport
 
944
            for name in segments:
 
945
                transport = transport.clone(name)
 
946
                transport.ensure_base()
 
947
            return transport
 
948
 
 
949
        with self.lock_tree_write():
 
950
            self.flush()
 
951
            branch_transport = mkdirs(sub_path)
 
952
            if format is None:
 
953
                format = self.controldir.cloning_metadir()
 
954
            branch_transport.ensure_base()
 
955
            branch_bzrdir = format.initialize_on_transport(branch_transport)
 
956
            try:
 
957
                repo = branch_bzrdir.find_repository()
 
958
            except errors.NoRepositoryPresent:
 
959
                repo = branch_bzrdir.create_repository()
 
960
            if not repo.supports_rich_root():
 
961
                raise errors.RootNotRich()
 
962
            new_branch = branch_bzrdir.create_branch()
 
963
            new_branch.pull(self.branch)
 
964
            for parent_id in self.get_parent_ids():
 
965
                new_branch.fetch(self.branch, parent_id)
 
966
            tree_transport = self.controldir.root_transport.clone(sub_path)
 
967
            if tree_transport.base != branch_transport.base:
 
968
                tree_bzrdir = format.initialize_on_transport(tree_transport)
 
969
                tree_bzrdir.set_branch_reference(new_branch)
 
970
            else:
 
971
                tree_bzrdir = branch_bzrdir
 
972
            wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
 
973
            wt.set_parent_ids(self.get_parent_ids())
 
974
            # FIXME: Support nested trees
 
975
            my_inv = self.root_inventory
 
976
            child_inv = inventory.Inventory(root_id=None)
 
977
            if file_id is None:
 
978
                file_id = self.path2id(sub_path)
 
979
            new_root = my_inv.get_entry(file_id)
 
980
            my_inv.remove_recursive_id(file_id)
 
981
            new_root.parent_id = None
 
982
            child_inv.add(new_root)
 
983
            self._write_inventory(my_inv)
 
984
            wt._write_inventory(child_inv)
 
985
            return wt
 
986
 
 
987
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
988
        """List all files as (path, class, kind, id, entry).
 
989
 
 
990
        Lists, but does not descend into unversioned directories.
 
991
        This does not include files that have been deleted in this
 
992
        tree. Skips the control directory.
 
993
 
 
994
        :param include_root: if True, return an entry for the root
 
995
        :param from_dir: start from this directory or None for the root
 
996
        :param recursive: whether to recurse into subdirectories or not
 
997
        """
 
998
        with self.lock_read():
 
999
            if from_dir is None and include_root is True:
 
1000
                yield ('', 'V', 'directory', self.get_root_id(),
 
1001
                       self.root_inventory.root)
 
1002
            # Convert these into local objects to save lookup times
 
1003
            pathjoin = osutils.pathjoin
 
1004
 
 
1005
            # transport.base ends in a slash, we want the piece
 
1006
            # between the last two slashes
 
1007
            transport_base_dir = self.controldir.transport.base.rsplit('/', 2)[1]
 
1008
 
 
1009
            fk_entries = {
 
1010
                    'directory': TreeDirectory,
 
1011
                    'file': TreeFile,
 
1012
                    'symlink': TreeLink
 
1013
                    }
 
1014
 
 
1015
            # directory file_id, relative path, absolute path, reverse sorted
 
1016
            # children
 
1017
            if from_dir is not None:
 
1018
                inv, from_dir_id = self._path2inv_file_id(from_dir)
 
1019
                if from_dir_id is None:
 
1020
                    # Directory not versioned
 
1021
                    return
 
1022
                from_dir_abspath = pathjoin(self.basedir, from_dir)
 
1023
            else:
 
1024
                inv = self.root_inventory
 
1025
                from_dir_id = inv.root.file_id
 
1026
                from_dir_abspath = self.basedir
 
1027
            children = sorted(os.listdir(from_dir_abspath))
 
1028
            # jam 20060527 The kernel sized tree seems equivalent whether we
 
1029
            # use a deque and popleft to keep them sorted, or if we use a plain
 
1030
            # list and just reverse() them.
 
1031
            children = collections.deque(children)
 
1032
            stack = [(from_dir_id, u'', from_dir_abspath, children)]
 
1033
            while stack:
 
1034
                from_dir_id, from_dir_relpath, from_dir_abspath, children = stack[-1]
 
1035
 
 
1036
                while children:
 
1037
                    f = children.popleft()
 
1038
                    # TODO: If we find a subdirectory with its own .bzr
 
1039
                    # directory, then that is a separate tree and we
 
1040
                    # should exclude it.
 
1041
 
 
1042
                    # the bzrdir for this tree
 
1043
                    if transport_base_dir == f:
 
1044
                        continue
 
1045
 
 
1046
                    # we know that from_dir_relpath and from_dir_abspath never
 
1047
                    # end in a slash and 'f' doesn't begin with one, we can do
 
1048
                    # a string op, rather than the checks of pathjoin(), all
 
1049
                    # relative paths will have an extra slash at the beginning
 
1050
                    fp = from_dir_relpath + '/' + f
 
1051
 
 
1052
                    # absolute path
 
1053
                    fap = from_dir_abspath + '/' + f
 
1054
 
 
1055
                    dir_ie = inv.get_entry(from_dir_id)
 
1056
                    if dir_ie.kind == 'directory':
 
1057
                        f_ie = dir_ie.children.get(f)
 
1058
                    else:
 
1059
                        f_ie = None
 
1060
                    if f_ie:
 
1061
                        c = 'V'
 
1062
                    elif self.is_ignored(fp[1:]):
 
1063
                        c = 'I'
 
1064
                    else:
 
1065
                        # we may not have found this file, because of a unicode
 
1066
                        # issue, or because the directory was actually a
 
1067
                        # symlink.
 
1068
                        f_norm, can_access = osutils.normalized_filename(f)
 
1069
                        if f == f_norm or not can_access:
 
1070
                            # No change, so treat this file normally
 
1071
                            c = '?'
 
1072
                        else:
 
1073
                            # this file can be accessed by a normalized path
 
1074
                            # check again if it is versioned
 
1075
                            # these lines are repeated here for performance
 
1076
                            f = f_norm
 
1077
                            fp = from_dir_relpath + '/' + f
 
1078
                            fap = from_dir_abspath + '/' + f
 
1079
                            f_ie = inv.get_child(from_dir_id, f)
 
1080
                            if f_ie:
 
1081
                                c = 'V'
 
1082
                            elif self.is_ignored(fp[1:]):
 
1083
                                c = 'I'
 
1084
                            else:
 
1085
                                c = '?'
 
1086
 
 
1087
                    fk = osutils.file_kind(fap)
 
1088
 
 
1089
                    # make a last minute entry
 
1090
                    if f_ie:
 
1091
                        yield fp[1:], c, fk, f_ie.file_id, f_ie
 
1092
                    else:
 
1093
                        try:
 
1094
                            yield fp[1:], c, fk, None, fk_entries[fk]()
 
1095
                        except KeyError:
 
1096
                            yield fp[1:], c, fk, None, TreeEntry()
 
1097
                        continue
 
1098
 
 
1099
                    if fk != 'directory':
 
1100
                        continue
 
1101
 
 
1102
                    # But do this child first if recursing down
 
1103
                    if recursive:
 
1104
                        new_children = sorted(os.listdir(fap))
 
1105
                        new_children = collections.deque(new_children)
 
1106
                        stack.append((f_ie.file_id, fp, fap, new_children))
 
1107
                        # Break out of inner loop,
 
1108
                        # so that we start outer loop with child
 
1109
                        break
 
1110
                else:
 
1111
                    # if we finished all children, pop it off the stack
 
1112
                    stack.pop()
 
1113
 
 
1114
    def move(self, from_paths, to_dir=None, after=False):
 
1115
        """Rename files.
 
1116
 
 
1117
        to_dir must exist in the inventory.
 
1118
 
 
1119
        If to_dir exists and is a directory, the files are moved into
 
1120
        it, keeping their old names.
 
1121
 
 
1122
        Note that to_dir is only the last component of the new name;
 
1123
        this doesn't change the directory.
 
1124
 
 
1125
        For each entry in from_paths the move mode will be determined
 
1126
        independently.
 
1127
 
 
1128
        The first mode moves the file in the filesystem and updates the
 
1129
        inventory. The second mode only updates the inventory without
 
1130
        touching the file on the filesystem.
 
1131
 
 
1132
        move uses the second mode if 'after == True' and the target is
 
1133
        either not versioned or newly added, and present in the working tree.
 
1134
 
 
1135
        move uses the second mode if 'after == False' and the source is
 
1136
        versioned but no longer in the working tree, and the target is not
 
1137
        versioned but present in the working tree.
 
1138
 
 
1139
        move uses the first mode if 'after == False' and the source is
 
1140
        versioned and present in the working tree, and the target is not
 
1141
        versioned and not present in the working tree.
 
1142
 
 
1143
        Everything else results in an error.
 
1144
 
 
1145
        This returns a list of (from_path, to_path) pairs for each
 
1146
        entry that is moved.
 
1147
        """
 
1148
        rename_entries = []
 
1149
        rename_tuples = []
 
1150
 
 
1151
        invs_to_write = set()
 
1152
 
 
1153
        # check for deprecated use of signature
 
1154
        if to_dir is None:
 
1155
            raise TypeError('You must supply a target directory')
 
1156
        # check destination directory
 
1157
        if isinstance(from_paths, (str, text_type)):
 
1158
            raise ValueError()
 
1159
        with self.lock_tree_write():
 
1160
            to_abs = self.abspath(to_dir)
 
1161
            if not osutils.isdir(to_abs):
 
1162
                raise errors.BzrMoveFailedError(
 
1163
                        '', to_dir, errors.NotADirectory(to_abs))
 
1164
            if not self.has_filename(to_dir):
 
1165
                raise errors.BzrMoveFailedError(
 
1166
                        '', to_dir, errors.NotInWorkingDirectory(to_dir))
 
1167
            to_inv, to_dir_id = self._path2inv_file_id(to_dir)
 
1168
            if to_dir_id is None:
 
1169
                raise errors.BzrMoveFailedError(
 
1170
                        '', to_dir, errors.NotVersionedError(path=to_dir))
 
1171
 
 
1172
            to_dir_ie = to_inv.get_entry(to_dir_id)
 
1173
            if to_dir_ie.kind != 'directory':
 
1174
                raise errors.BzrMoveFailedError(
 
1175
                        '', to_dir, errors.NotADirectory(to_abs))
 
1176
 
 
1177
            # create rename entries and tuples
 
1178
            for from_rel in from_paths:
 
1179
                from_tail = osutils.splitpath(from_rel)[-1]
 
1180
                from_inv, from_id = self._path2inv_file_id(from_rel)
 
1181
                if from_id is None:
 
1182
                    raise errors.BzrMoveFailedError(from_rel, to_dir,
 
1183
                        errors.NotVersionedError(path=from_rel))
 
1184
 
 
1185
                from_entry = from_inv.get_entry(from_id)
 
1186
                from_parent_id = from_entry.parent_id
 
1187
                to_rel = osutils.pathjoin(to_dir, from_tail)
 
1188
                rename_entry = InventoryWorkingTree._RenameEntry(
 
1189
                    from_rel=from_rel,
 
1190
                    from_id=from_id,
 
1191
                    from_tail=from_tail,
 
1192
                    from_parent_id=from_parent_id,
 
1193
                    to_rel=to_rel, to_tail=from_tail,
 
1194
                    to_parent_id=to_dir_id)
 
1195
                rename_entries.append(rename_entry)
 
1196
                rename_tuples.append((from_rel, to_rel))
 
1197
 
 
1198
            # determine which move mode to use. checks also for movability
 
1199
            rename_entries = self._determine_mv_mode(rename_entries, after)
 
1200
 
 
1201
            original_modified = self._inventory_is_modified
 
1202
            try:
 
1203
                if len(from_paths):
 
1204
                    self._inventory_is_modified = True
 
1205
                self._move(rename_entries)
 
1206
            except:
 
1207
                # restore the inventory on error
 
1208
                self._inventory_is_modified = original_modified
 
1209
                raise
 
1210
            #FIXME: Should potentially also write the from_invs
 
1211
            self._write_inventory(to_inv)
 
1212
            return rename_tuples
 
1213
 
 
1214
    def rename_one(self, from_rel, to_rel, after=False):
 
1215
        """Rename one file.
 
1216
 
 
1217
        This can change the directory or the filename or both.
 
1218
 
 
1219
        rename_one has several 'modes' to work. First, it can rename a physical
 
1220
        file and change the file_id. That is the normal mode. Second, it can
 
1221
        only change the file_id without touching any physical file.
 
1222
 
 
1223
        rename_one uses the second mode if 'after == True' and 'to_rel' is not
 
1224
        versioned but present in the working tree.
 
1225
 
 
1226
        rename_one uses the second mode if 'after == False' and 'from_rel' is
 
1227
        versioned but no longer in the working tree, and 'to_rel' is not
 
1228
        versioned but present in the working tree.
 
1229
 
 
1230
        rename_one uses the first mode if 'after == False' and 'from_rel' is
 
1231
        versioned and present in the working tree, and 'to_rel' is not
 
1232
        versioned and not present in the working tree.
 
1233
 
 
1234
        Everything else results in an error.
 
1235
        """
 
1236
        with self.lock_tree_write():
 
1237
            rename_entries = []
 
1238
 
 
1239
            # create rename entries and tuples
 
1240
            from_tail = osutils.splitpath(from_rel)[-1]
 
1241
            from_inv, from_id = self._path2inv_file_id(from_rel)
 
1242
            if from_id is None:
 
1243
                # if file is missing in the inventory maybe it's in the
 
1244
                # basis_tree
 
1245
                basis_tree = self.branch.basis_tree()
 
1246
                from_id = basis_tree.path2id(from_rel)
 
1247
                if from_id is None:
 
1248
                    raise errors.BzrRenameFailedError(
 
1249
                        from_rel, to_rel,
 
1250
                        errors.NotVersionedError(path=from_rel))
 
1251
                # put entry back in the inventory so we can rename it
 
1252
                from_entry = basis_tree.root_inventory.get_entry(from_id).copy()
 
1253
                from_inv.add(from_entry)
 
1254
            else:
 
1255
                from_inv, from_inv_id = self._unpack_file_id(from_id)
 
1256
                from_entry = from_inv.get_entry(from_inv_id)
 
1257
            from_parent_id = from_entry.parent_id
 
1258
            to_dir, to_tail = os.path.split(to_rel)
 
1259
            to_inv, to_dir_id = self._path2inv_file_id(to_dir)
 
1260
            rename_entry = InventoryWorkingTree._RenameEntry(
 
1261
                    from_rel=from_rel,
 
1262
                    from_id=from_id,
 
1263
                    from_tail=from_tail,
 
1264
                    from_parent_id=from_parent_id,
 
1265
                    to_rel=to_rel, to_tail=to_tail,
 
1266
                    to_parent_id=to_dir_id)
 
1267
            rename_entries.append(rename_entry)
 
1268
 
 
1269
            # determine which move mode to use. checks also for movability
 
1270
            rename_entries = self._determine_mv_mode(rename_entries, after)
 
1271
 
 
1272
            # check if the target changed directory and if the target directory is
 
1273
            # versioned
 
1274
            if to_dir_id is None:
 
1275
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1276
                    errors.NotVersionedError(path=to_dir))
 
1277
 
 
1278
            # all checks done. now we can continue with our actual work
 
1279
            mutter('rename_one:\n'
 
1280
                   '  from_id   {%s}\n'
 
1281
                   '  from_rel: %r\n'
 
1282
                   '  to_rel:   %r\n'
 
1283
                   '  to_dir    %r\n'
 
1284
                   '  to_dir_id {%s}\n',
 
1285
                   from_id, from_rel, to_rel, to_dir, to_dir_id)
 
1286
 
 
1287
            self._move(rename_entries)
 
1288
            self._write_inventory(to_inv)
 
1289
 
 
1290
    class _RenameEntry(object):
 
1291
        def __init__(self, from_rel, from_id, from_tail, from_parent_id,
 
1292
                     to_rel, to_tail, to_parent_id, only_change_inv=False,
 
1293
                     change_id=False):
 
1294
            self.from_rel = from_rel
 
1295
            self.from_id = from_id
 
1296
            self.from_tail = from_tail
 
1297
            self.from_parent_id = from_parent_id
 
1298
            self.to_rel = to_rel
 
1299
            self.to_tail = to_tail
 
1300
            self.to_parent_id = to_parent_id
 
1301
            self.change_id = change_id
 
1302
            self.only_change_inv = only_change_inv
 
1303
 
 
1304
    def _determine_mv_mode(self, rename_entries, after=False):
 
1305
        """Determines for each from-to pair if both inventory and working tree
 
1306
        or only the inventory has to be changed.
 
1307
 
 
1308
        Also does basic plausability tests.
 
1309
        """
 
1310
        # FIXME: Handling of nested trees
 
1311
        inv = self.root_inventory
 
1312
 
 
1313
        for rename_entry in rename_entries:
 
1314
            # store to local variables for easier reference
 
1315
            from_rel = rename_entry.from_rel
 
1316
            from_id = rename_entry.from_id
 
1317
            to_rel = rename_entry.to_rel
 
1318
            to_id = inv.path2id(to_rel)
 
1319
            only_change_inv = False
 
1320
            change_id = False
 
1321
 
 
1322
            # check the inventory for source and destination
 
1323
            if from_id is None:
 
1324
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1325
                    errors.NotVersionedError(path=from_rel))
 
1326
            if to_id is not None:
 
1327
                allowed = False
 
1328
                # allow it with --after but only if dest is newly added
 
1329
                if after:
 
1330
                    basis = self.basis_tree()
 
1331
                    with basis.lock_read():
 
1332
                        if not basis.has_id(to_id):
 
1333
                            rename_entry.change_id = True
 
1334
                            allowed = True
 
1335
                if not allowed:
 
1336
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1337
                        errors.AlreadyVersionedError(path=to_rel))
 
1338
 
 
1339
            # try to determine the mode for rename (only change inv or change
 
1340
            # inv and file system)
 
1341
            if after:
 
1342
                if not self.has_filename(to_rel):
 
1343
                    raise errors.BzrMoveFailedError(from_id, to_rel,
 
1344
                        errors.NoSuchFile(path=to_rel,
 
1345
                        extra="New file has not been created yet"))
 
1346
                only_change_inv = True
 
1347
            elif not self.has_filename(from_rel) and self.has_filename(to_rel):
 
1348
                only_change_inv = True
 
1349
            elif self.has_filename(from_rel) and not self.has_filename(to_rel):
 
1350
                only_change_inv = False
 
1351
            elif (not self.case_sensitive
 
1352
                  and from_rel.lower() == to_rel.lower()
 
1353
                  and self.has_filename(from_rel)):
 
1354
                only_change_inv = False
 
1355
            else:
 
1356
                # something is wrong, so lets determine what exactly
 
1357
                if not self.has_filename(from_rel) and \
 
1358
                   not self.has_filename(to_rel):
 
1359
                    raise errors.BzrRenameFailedError(from_rel, to_rel,
 
1360
                        errors.PathsDoNotExist(paths=(from_rel, to_rel)))
 
1361
                else:
 
1362
                    raise errors.RenameFailedFilesExist(from_rel, to_rel)
 
1363
            rename_entry.only_change_inv = only_change_inv
 
1364
        return rename_entries
 
1365
 
 
1366
    def _move(self, rename_entries):
 
1367
        """Moves a list of files.
 
1368
 
 
1369
        Depending on the value of the flag 'only_change_inv', the
 
1370
        file will be moved on the file system or not.
 
1371
        """
 
1372
        moved = []
 
1373
 
 
1374
        for entry in rename_entries:
 
1375
            try:
 
1376
                self._move_entry(entry)
 
1377
            except:
 
1378
                self._rollback_move(moved)
 
1379
                raise
 
1380
            moved.append(entry)
 
1381
 
 
1382
    def _rollback_move(self, moved):
 
1383
        """Try to rollback a previous move in case of an filesystem error."""
 
1384
        for entry in moved:
 
1385
            try:
 
1386
                self._move_entry(WorkingTree._RenameEntry(
 
1387
                    entry.to_rel, entry.from_id,
 
1388
                    entry.to_tail, entry.to_parent_id, entry.from_rel,
 
1389
                    entry.from_tail, entry.from_parent_id,
 
1390
                    entry.only_change_inv))
 
1391
            except errors.BzrMoveFailedError as e:
 
1392
                raise errors.BzrMoveFailedError( '', '', "Rollback failed."
 
1393
                        " The working tree is in an inconsistent state."
 
1394
                        " Please consider doing a 'bzr revert'."
 
1395
                        " Error message is: %s" % e)
 
1396
 
 
1397
    def _move_entry(self, entry):
 
1398
        inv = self.root_inventory
 
1399
        from_rel_abs = self.abspath(entry.from_rel)
 
1400
        to_rel_abs = self.abspath(entry.to_rel)
 
1401
        if from_rel_abs == to_rel_abs:
 
1402
            raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
 
1403
                "Source and target are identical.")
 
1404
 
 
1405
        if not entry.only_change_inv:
 
1406
            try:
 
1407
                osutils.rename(from_rel_abs, to_rel_abs)
 
1408
            except OSError as e:
 
1409
                raise errors.BzrMoveFailedError(
 
1410
                    entry.from_rel, entry.to_rel, e[1])
 
1411
        if entry.change_id:
 
1412
            to_id = inv.path2id(entry.to_rel)
 
1413
            inv.remove_recursive_id(to_id)
 
1414
        inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
 
1415
 
 
1416
    def unversion(self, paths, file_ids=None):
 
1417
        """Remove the paths in paths from the current versioned set.
 
1418
 
 
1419
        When a path is unversioned, all of its children are automatically
 
1420
        unversioned.
 
1421
 
 
1422
        :param paths: The paths to stop versioning.
 
1423
        :param file_ids: Optional file_ids for the paths
 
1424
        :raises NoSuchFile: if any path is not currently versioned.
 
1425
        :raises NoSuchId: if any fileid is not currently versioned.
 
1426
        """
 
1427
        with self.lock_tree_write():
 
1428
            if file_ids is not None:
 
1429
                for file_id in file_ids:
 
1430
                    if not self._inventory.has_id(file_id):
 
1431
                        raise errors.NoSuchId(self, file_id)
 
1432
            else:
 
1433
                file_ids = set()
 
1434
                for path in paths:
 
1435
                    file_id = self._inventory.path2id(path)
 
1436
                    if file_id is None:
 
1437
                        raise errors.NoSuchFile(self, path)
 
1438
                    file_ids.add(file_id)
 
1439
            for file_id in file_ids:
 
1440
                if self._inventory.has_id(file_id):
 
1441
                    self._inventory.remove_recursive_id(file_id)
 
1442
            if len(file_ids):
 
1443
                # in the future this should just set a dirty bit to wait for the
 
1444
                # final unlock. However, until all methods of workingtree start
 
1445
                # with the current in -memory inventory rather than triggering
 
1446
                # a read, it is more complex - we need to teach read_inventory
 
1447
                # to know when to read, and when to not read first... and possibly
 
1448
                # to save first when the in memory one may be corrupted.
 
1449
                # so for now, we just only write it if it is indeed dirty.
 
1450
                # - RBC 20060907
 
1451
                self._write_inventory(self._inventory)
 
1452
 
 
1453
    def stored_kind(self, path, file_id=None):
 
1454
        """See Tree.stored_kind"""
 
1455
        inv, inv_file_id = self._path2inv_file_id(path)
 
1456
        if inv_file_id is None:
 
1457
            raise errors.NoSuchFile(self, path)
 
1458
        return inv.get_entry(inv_file_id).kind
 
1459
 
 
1460
    def extras(self):
 
1461
        """Yield all unversioned files in this WorkingTree.
 
1462
 
 
1463
        If there are any unversioned directories then only the directory is
 
1464
        returned, not all its children.  But if there are unversioned files
 
1465
        under a versioned subdirectory, they are returned.
 
1466
 
 
1467
        Currently returned depth-first, sorted by name within directories.
 
1468
        This is the same order used by 'osutils.walkdirs'.
 
1469
        """
 
1470
        ## TODO: Work from given directory downwards
 
1471
        for path, dir_entry in self.iter_entries_by_dir():
 
1472
            if dir_entry.kind != 'directory':
 
1473
                continue
 
1474
            # mutter("search for unknowns in %r", path)
 
1475
            dirabs = self.abspath(path)
 
1476
            if not osutils.isdir(dirabs):
 
1477
                # e.g. directory deleted
 
1478
                continue
 
1479
 
 
1480
            fl = []
 
1481
            for subf in os.listdir(dirabs):
 
1482
                if self.controldir.is_control_filename(subf):
 
1483
                    continue
 
1484
                if subf not in dir_entry.children:
 
1485
                    try:
 
1486
                        (subf_norm,
 
1487
                         can_access) = osutils.normalized_filename(subf)
 
1488
                    except UnicodeDecodeError:
 
1489
                        path_os_enc = path.encode(osutils._fs_enc)
 
1490
                        relpath = path_os_enc + '/' + subf
 
1491
                        raise errors.BadFilenameEncoding(relpath,
 
1492
                                                         osutils._fs_enc)
 
1493
                    if subf_norm != subf and can_access:
 
1494
                        if subf_norm not in dir_entry.children:
 
1495
                            fl.append(subf_norm)
 
1496
                    else:
 
1497
                        fl.append(subf)
 
1498
 
 
1499
            fl.sort()
 
1500
            for subf in fl:
 
1501
                subp = osutils.pathjoin(path, subf)
 
1502
                yield subp
 
1503
 
 
1504
    def walkdirs(self, prefix=""):
 
1505
        """Walk the directories of this tree.
 
1506
 
 
1507
        returns a generator which yields items in the form:
 
1508
                ((curren_directory_path, fileid),
 
1509
                 [(file1_path, file1_name, file1_kind, (lstat), file1_id,
 
1510
                   file1_kind), ... ])
 
1511
 
 
1512
        This API returns a generator, which is only valid during the current
 
1513
        tree transaction - within a single lock_read or lock_write duration.
 
1514
 
 
1515
        If the tree is not locked, it may cause an error to be raised,
 
1516
        depending on the tree implementation.
 
1517
        """
 
1518
        disk_top = self.abspath(prefix)
 
1519
        if disk_top.endswith('/'):
 
1520
            disk_top = disk_top[:-1]
 
1521
        top_strip_len = len(disk_top) + 1
 
1522
        inventory_iterator = self._walkdirs(prefix)
 
1523
        disk_iterator = osutils.walkdirs(disk_top, prefix)
 
1524
        try:
 
1525
            current_disk = next(disk_iterator)
 
1526
            disk_finished = False
 
1527
        except OSError as e:
 
1528
            if not (e.errno == errno.ENOENT or
 
1529
                (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
 
1530
                raise
 
1531
            current_disk = None
 
1532
            disk_finished = True
 
1533
        try:
 
1534
            current_inv = next(inventory_iterator)
 
1535
            inv_finished = False
 
1536
        except StopIteration:
 
1537
            current_inv = None
 
1538
            inv_finished = True
 
1539
        while not inv_finished or not disk_finished:
 
1540
            if current_disk:
 
1541
                ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
 
1542
                    cur_disk_dir_content) = current_disk
 
1543
            else:
 
1544
                ((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
 
1545
                    cur_disk_dir_content) = ((None, None), None)
 
1546
            if not disk_finished:
 
1547
                # strip out .bzr dirs
 
1548
                if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
 
1549
                    len(cur_disk_dir_content) > 0):
 
1550
                    # osutils.walkdirs can be made nicer -
 
1551
                    # yield the path-from-prefix rather than the pathjoined
 
1552
                    # value.
 
1553
                    bzrdir_loc = bisect_left(cur_disk_dir_content,
 
1554
                        ('.bzr', '.bzr'))
 
1555
                    if (bzrdir_loc < len(cur_disk_dir_content)
 
1556
                        and self.controldir.is_control_filename(
 
1557
                            cur_disk_dir_content[bzrdir_loc][0])):
 
1558
                        # we dont yield the contents of, or, .bzr itself.
 
1559
                        del cur_disk_dir_content[bzrdir_loc]
 
1560
            if inv_finished:
 
1561
                # everything is unknown
 
1562
                direction = 1
 
1563
            elif disk_finished:
 
1564
                # everything is missing
 
1565
                direction = -1
 
1566
            else:
 
1567
                direction = cmp(current_inv[0][0], cur_disk_dir_relpath)
 
1568
            if direction > 0:
 
1569
                # disk is before inventory - unknown
 
1570
                dirblock = [(relpath, basename, kind, stat, None, None) for
 
1571
                    relpath, basename, kind, stat, top_path in
 
1572
                    cur_disk_dir_content]
 
1573
                yield (cur_disk_dir_relpath, None), dirblock
 
1574
                try:
 
1575
                    current_disk = next(disk_iterator)
 
1576
                except StopIteration:
 
1577
                    disk_finished = True
 
1578
            elif direction < 0:
 
1579
                # inventory is before disk - missing.
 
1580
                dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
 
1581
                    for relpath, basename, dkind, stat, fileid, kind in
 
1582
                    current_inv[1]]
 
1583
                yield (current_inv[0][0], current_inv[0][1]), dirblock
 
1584
                try:
 
1585
                    current_inv = next(inventory_iterator)
 
1586
                except StopIteration:
 
1587
                    inv_finished = True
 
1588
            else:
 
1589
                # versioned present directory
 
1590
                # merge the inventory and disk data together
 
1591
                dirblock = []
 
1592
                for relpath, subiterator in itertools.groupby(sorted(
 
1593
                    current_inv[1] + cur_disk_dir_content,
 
1594
                    key=operator.itemgetter(0)), operator.itemgetter(1)):
 
1595
                    path_elements = list(subiterator)
 
1596
                    if len(path_elements) == 2:
 
1597
                        inv_row, disk_row = path_elements
 
1598
                        # versioned, present file
 
1599
                        dirblock.append((inv_row[0],
 
1600
                            inv_row[1], disk_row[2],
 
1601
                            disk_row[3], inv_row[4],
 
1602
                            inv_row[5]))
 
1603
                    elif len(path_elements[0]) == 5:
 
1604
                        # unknown disk file
 
1605
                        dirblock.append((path_elements[0][0],
 
1606
                            path_elements[0][1], path_elements[0][2],
 
1607
                            path_elements[0][3], None, None))
 
1608
                    elif len(path_elements[0]) == 6:
 
1609
                        # versioned, absent file.
 
1610
                        dirblock.append((path_elements[0][0],
 
1611
                            path_elements[0][1], 'unknown', None,
 
1612
                            path_elements[0][4], path_elements[0][5]))
 
1613
                    else:
 
1614
                        raise NotImplementedError('unreachable code')
 
1615
                yield current_inv[0], dirblock
 
1616
                try:
 
1617
                    current_inv = next(inventory_iterator)
 
1618
                except StopIteration:
 
1619
                    inv_finished = True
 
1620
                try:
 
1621
                    current_disk = next(disk_iterator)
 
1622
                except StopIteration:
 
1623
                    disk_finished = True
 
1624
 
 
1625
    def _walkdirs(self, prefix=""):
 
1626
        """Walk the directories of this tree.
 
1627
 
 
1628
        :param prefix: is used as the directrory to start with.
 
1629
        :returns: a generator which yields items in the form::
 
1630
 
 
1631
            ((curren_directory_path, fileid),
 
1632
             [(file1_path, file1_name, file1_kind, None, file1_id,
 
1633
               file1_kind), ... ])
 
1634
        """
 
1635
        _directory = 'directory'
 
1636
        # get the root in the inventory
 
1637
        inv, top_id = self._path2inv_file_id(prefix)
 
1638
        if top_id is None:
 
1639
            pending = []
 
1640
        else:
 
1641
            pending = [(prefix, '', _directory, None, top_id, None)]
 
1642
        while pending:
 
1643
            dirblock = []
 
1644
            currentdir = pending.pop()
 
1645
            # 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
 
1646
            top_id = currentdir[4]
 
1647
            if currentdir[0]:
 
1648
                relroot = currentdir[0] + '/'
 
1649
            else:
 
1650
                relroot = ""
 
1651
            # FIXME: stash the node in pending
 
1652
            entry = inv.get_entry(top_id)
 
1653
            if entry.kind == 'directory':
 
1654
                for name, child in entry.sorted_children():
 
1655
                    dirblock.append((relroot + name, name, child.kind, None,
 
1656
                        child.file_id, child.kind
 
1657
                        ))
 
1658
            yield (currentdir[0], entry.file_id), dirblock
 
1659
            # push the user specified dirs from dirblock
 
1660
            for dir in reversed(dirblock):
 
1661
                if dir[2] == _directory:
 
1662
                    pending.append(dir)
 
1663
 
 
1664
    def update_feature_flags(self, updated_flags):
 
1665
        """Update the feature flags for this branch.
 
1666
 
 
1667
        :param updated_flags: Dictionary mapping feature names to necessities
 
1668
            A necessity can be None to indicate the feature should be removed
 
1669
        """
 
1670
        with self.lock_write():
 
1671
            self._format._update_feature_flags(updated_flags)
 
1672
            self.control_transport.put_bytes('format', self._format.as_string())
 
1673
 
 
1674
    def _check_for_tree_references(self, iterator):
 
1675
        """See if directories have become tree-references."""
 
1676
        blocked_parent_ids = set()
 
1677
        for path, ie in iterator:
 
1678
            if ie.parent_id in blocked_parent_ids:
 
1679
                # This entry was pruned because one of its parents became a
 
1680
                # TreeReference. If this is a directory, mark it as blocked.
 
1681
                if ie.kind == 'directory':
 
1682
                    blocked_parent_ids.add(ie.file_id)
 
1683
                continue
 
1684
            if ie.kind == 'directory' and self._directory_is_tree_reference(path):
 
1685
                # This InventoryDirectory needs to be a TreeReference
 
1686
                ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
 
1687
                blocked_parent_ids.add(ie.file_id)
 
1688
            yield path, ie
 
1689
 
 
1690
    def iter_entries_by_dir(self, specific_files=None):
 
1691
        """See Tree.iter_entries_by_dir()"""
 
1692
        # The only trick here is that if we supports_tree_reference then we
 
1693
        # need to detect if a directory becomes a tree-reference.
 
1694
        iterator = super(WorkingTree, self).iter_entries_by_dir(
 
1695
                specific_files=specific_files)
 
1696
        if not self.supports_tree_reference():
 
1697
            return iterator
 
1698
        else:
 
1699
            return self._check_for_tree_references(iterator)
 
1700
 
 
1701
 
 
1702
class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
 
1703
    """Base class for working trees that live in bzr meta directories."""
 
1704
 
 
1705
    ignore_filename = '.bzrignore'
 
1706
 
 
1707
    def __init__(self):
 
1708
        WorkingTreeFormat.__init__(self)
 
1709
        bzrdir.BzrFormat.__init__(self)
 
1710
 
 
1711
    @classmethod
 
1712
    def find_format_string(klass, controldir):
 
1713
        """Return format name for the working tree object in controldir."""
 
1714
        try:
 
1715
            transport = controldir.get_workingtree_transport(None)
 
1716
            return transport.get_bytes("format")
 
1717
        except errors.NoSuchFile:
 
1718
            raise errors.NoWorkingTree(base=transport.base)
 
1719
 
 
1720
    @classmethod
 
1721
    def find_format(klass, controldir):
 
1722
        """Return the format for the working tree object in controldir."""
 
1723
        format_string = klass.find_format_string(controldir)
 
1724
        return klass._find_format(format_registry, 'working tree',
 
1725
                format_string)
 
1726
 
 
1727
    def check_support_status(self, allow_unsupported, recommend_upgrade=True,
 
1728
            basedir=None):
 
1729
        WorkingTreeFormat.check_support_status(self,
 
1730
            allow_unsupported=allow_unsupported, recommend_upgrade=recommend_upgrade,
 
1731
            basedir=basedir)
 
1732
        bzrdir.BzrFormat.check_support_status(self, allow_unsupported=allow_unsupported,
 
1733
            recommend_upgrade=recommend_upgrade, basedir=basedir)