/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-11-16 18:15:40 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181116181540-7y2wbhqzjk067mqy
Fix repo acquisition.

Show diffs side-by-side

added added

removed removed

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