/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 bzrlib/workingtree_4.py

(working), fix dirstate to use utf8 file ids.
Also fix a bug in _generate_inventory for non-ascii paths. It was
combining the decoded path with the utf8 prefix and assuming the
whole thing was utf8.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""WorkingTree4 format and implementation.
 
18
 
 
19
WorkingTree4 provides the dirstate based working tree logic.
 
20
 
 
21
To get a WorkingTree, call bzrdir.open_workingtree() or
 
22
WorkingTree.open(dir).
 
23
"""
 
24
 
 
25
from cStringIO import StringIO
 
26
import os
 
27
 
 
28
from bzrlib.lazy_import import lazy_import
 
29
lazy_import(globals(), """
 
30
from bisect import bisect_left
 
31
import collections
 
32
from copy import deepcopy
 
33
import errno
 
34
import itertools
 
35
import operator
 
36
import stat
 
37
from time import time
 
38
import warnings
 
39
 
 
40
import bzrlib
 
41
from bzrlib import (
 
42
    bzrdir,
 
43
    conflicts as _mod_conflicts,
 
44
    dirstate,
 
45
    errors,
 
46
    generate_ids,
 
47
    globbing,
 
48
    hashcache,
 
49
    ignores,
 
50
    merge,
 
51
    osutils,
 
52
    textui,
 
53
    transform,
 
54
    urlutils,
 
55
    xml5,
 
56
    xml6,
 
57
    )
 
58
import bzrlib.branch
 
59
from bzrlib.transport import get_transport
 
60
import bzrlib.ui
 
61
""")
 
62
 
 
63
from bzrlib import symbol_versioning
 
64
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
65
from bzrlib.inventory import InventoryEntry, Inventory, ROOT_ID, entry_factory
 
66
from bzrlib.lockable_files import LockableFiles, TransportLock
 
67
from bzrlib.lockdir import LockDir
 
68
import bzrlib.mutabletree
 
69
from bzrlib.mutabletree import needs_tree_write_lock
 
70
from bzrlib.osutils import (
 
71
    compact_date,
 
72
    file_kind,
 
73
    isdir,
 
74
    normpath,
 
75
    pathjoin,
 
76
    rand_chars,
 
77
    realpath,
 
78
    safe_unicode,
 
79
    splitpath,
 
80
    supports_executable,
 
81
    )
 
82
from bzrlib.trace import mutter, note
 
83
from bzrlib.transport.local import LocalTransport
 
84
from bzrlib.progress import DummyProgress, ProgressPhase
 
85
from bzrlib.revision import NULL_REVISION, CURRENT_REVISION
 
86
from bzrlib.rio import RioReader, rio_file, Stanza
 
87
from bzrlib.symbol_versioning import (deprecated_passed,
 
88
        deprecated_method,
 
89
        deprecated_function,
 
90
        DEPRECATED_PARAMETER,
 
91
        zero_eight,
 
92
        zero_eleven,
 
93
        zero_thirteen,
 
94
        )
 
95
from bzrlib.tree import Tree
 
96
from bzrlib.workingtree import WorkingTree, WorkingTree3, WorkingTreeFormat3
 
97
 
 
98
 
 
99
class WorkingTree4(WorkingTree3):
 
100
    """This is the Format 4 working tree.
 
101
 
 
102
    This differs from WorkingTree3 by:
 
103
     - having a consolidated internal dirstate.
 
104
     - not having a regular inventory attribute.
 
105
 
 
106
    This is new in bzr TODO FIXME SETMEBEFORE MERGE.
 
107
    """
 
108
 
 
109
    def __init__(self, basedir,
 
110
                 branch,
 
111
                 _control_files=None,
 
112
                 _format=None,
 
113
                 _bzrdir=None):
 
114
        """Construct a WorkingTree for basedir.
 
115
 
 
116
        If the branch is not supplied, it is opened automatically.
 
117
        If the branch is supplied, it must be the branch for this basedir.
 
118
        (branch.base is not cross checked, because for remote branches that
 
119
        would be meaningless).
 
120
        """
 
121
        self._format = _format
 
122
        self.bzrdir = _bzrdir
 
123
        from bzrlib.hashcache import HashCache
 
124
        from bzrlib.trace import note, mutter
 
125
        assert isinstance(basedir, basestring), \
 
126
            "base directory %r is not a string" % basedir
 
127
        basedir = safe_unicode(basedir)
 
128
        mutter("opening working tree %r", basedir)
 
129
        self._branch = branch
 
130
        assert isinstance(self.branch, bzrlib.branch.Branch), \
 
131
            "branch %r is not a Branch" % self.branch
 
132
        self.basedir = realpath(basedir)
 
133
        # if branch is at our basedir and is a format 6 or less
 
134
        # assume all other formats have their own control files.
 
135
        assert isinstance(_control_files, LockableFiles), \
 
136
            "_control_files must be a LockableFiles, not %r" % _control_files
 
137
        self._control_files = _control_files
 
138
        # update the whole cache up front and write to disk if anything changed;
 
139
        # in the future we might want to do this more selectively
 
140
        # two possible ways offer themselves : in self._unlock, write the cache
 
141
        # if needed, or, when the cache sees a change, append it to the hash
 
142
        # cache file, and have the parser take the most recent entry for a
 
143
        # given path only.
 
144
        cache_filename = self.bzrdir.get_workingtree_transport(None).local_abspath('stat-cache')
 
145
        hc = self._hashcache = HashCache(basedir, cache_filename, self._control_files._file_mode)
 
146
        hc.read()
 
147
        # is this scan needed ? it makes things kinda slow.
 
148
        #hc.scan()
 
149
 
 
150
        if hc.needs_write:
 
151
            mutter("write hc")
 
152
            hc.write()
 
153
 
 
154
        self._dirty = None
 
155
        #-------------
 
156
        # during a read or write lock these objects are set, and are
 
157
        # None the rest of the time.
 
158
        self._dirstate = None
 
159
        self._inventory = None
 
160
        #-------------
 
161
 
 
162
    @needs_tree_write_lock
 
163
    def _add(self, files, ids, kinds):
 
164
        """See MutableTree._add."""
 
165
        state = self.current_dirstate()
 
166
        for f, file_id, kind in zip(files, ids, kinds):
 
167
            f = f.strip('/')
 
168
            assert '//' not in f
 
169
            assert '..' not in f
 
170
            if file_id is None:
 
171
                file_id = generate_ids.gen_file_id(f)
 
172
            # deliberately add the file with no cached stat or sha1
 
173
            # - on the first access it will be gathered, and we can
 
174
            # always change this once tests are all passing.
 
175
            state.add(f, file_id, kind, None, '')
 
176
        self._dirty = True
 
177
 
 
178
    def current_dirstate(self):
 
179
        """Return the current dirstate object. 
 
180
 
 
181
        This is not part of the tree interface and only exposed for ease of
 
182
        testing.
 
183
 
 
184
        :raises errors.NotWriteLocked: when not in a lock. 
 
185
            XXX: This should probably be errors.NotLocked.
 
186
        """
 
187
        if not self._control_files._lock_count:
 
188
            raise errors.ObjectNotLocked(self)
 
189
        if self._dirstate is not None:
 
190
            return self._dirstate
 
191
        local_path = self.bzrdir.get_workingtree_transport(None
 
192
            ).local_abspath('dirstate')
 
193
        self._dirstate = dirstate.DirState.on_file(local_path)
 
194
        return self._dirstate
 
195
 
 
196
    def filter_unversioned_files(self, paths):
 
197
        """Filter out paths that are not versioned.
 
198
 
 
199
        :return: set of paths.
 
200
        """
 
201
        # TODO: make a generic multi-bisect routine roughly that should list
 
202
        # the paths, then process one half at a time recursively, and feed the
 
203
        # results of each bisect in further still
 
204
        paths = sorted(paths)
 
205
        result = set()
 
206
        state = self.current_dirstate()
 
207
        # TODO we want a paths_to_dirblocks helper I think
 
208
        for path in paths:
 
209
            dirname, basename = os.path.split(path.encode('utf8'))
 
210
            _, _, _, path_is_versioned = state._get_block_entry_index(
 
211
                dirname, basename, 0)
 
212
            if path_is_versioned:
 
213
                result.add(path)
 
214
        return result
 
215
 
 
216
    def flush(self):
 
217
        """Write all cached data to disk."""
 
218
        if self._control_files._lock_mode != 'w':
 
219
            raise errors.NotWriteLocked(self)
 
220
        self.current_dirstate().save()
 
221
        self._inventory = None
 
222
        self._dirty = False
 
223
 
 
224
    def _generate_inventory(self):
 
225
        """Create and set self.inventory from the dirstate object.
 
226
        
 
227
        This is relatively expensive: we have to walk the entire dirstate.
 
228
        Ideally we would not, and can deprecate this function.
 
229
        """
 
230
        #: uncomment to trap on inventory requests.
 
231
        # import pdb;pdb.set_trace()
 
232
        state = self.current_dirstate()
 
233
        state._read_dirblocks_if_needed()
 
234
        root_key, current_entry = self._get_entry(path='')
 
235
        current_id = root_key[2]
 
236
        assert current_entry[0][0] == 'directory'
 
237
        inv = Inventory(root_id=current_id)
 
238
        # we could do this straight out of the dirstate; it might be fast
 
239
        # and should be profiled - RBC 20070216
 
240
        parent_ids = {'' : inv.root.file_id}
 
241
        for block in state._dirblocks[1:]: # skip the root
 
242
            dirname = block[0]
 
243
            try:
 
244
                parent_id = parent_ids[block[0]]
 
245
            except KeyError:
 
246
                # all the paths in this block are not versioned in this tree
 
247
                continue
 
248
            for key, entry in block[1]:
 
249
                if entry[0][0] in ('absent', 'relocated'):
 
250
                    # a parent tree only entry
 
251
                    continue
 
252
                name = key[1]
 
253
                name_unicode = name.decode('utf8')
 
254
                file_id = key[2]
 
255
                kind, link_or_sha1, size, executable, stat = entry[0]
 
256
                inv_entry = entry_factory[kind](file_id, name_unicode, parent_id)
 
257
                if kind == 'file':
 
258
                    # not strictly needed: working tree
 
259
                    #entry.executable = executable
 
260
                    #entry.text_size = size
 
261
                    #entry.text_sha1 = sha1
 
262
                    pass
 
263
                elif kind == 'directory':
 
264
                    # add this entry to the parent map.
 
265
                    parent_ids[(dirname + '/' + name).strip('/')] = file_id
 
266
                inv.add(inv_entry)
 
267
        self._inventory = inv
 
268
 
 
269
    def _get_entry(self, file_id=None, path=None):
 
270
        """Get the dirstate row for file_id or path.
 
271
 
 
272
        If either file_id or path is supplied, it is used as the key to lookup.
 
273
        If both are supplied, the fastest lookup is used, and an error is
 
274
        raised if they do not both point at the same row.
 
275
        
 
276
        :param file_id: An optional unicode file_id to be looked up.
 
277
        :param path: An optional unicode path to be looked up.
 
278
        :return: The dirstate row tuple for path/file_id, or (None, None)
 
279
        """
 
280
        if file_id is None and path is None:
 
281
            raise errors.BzrError('must supply file_id or path')
 
282
        state = self.current_dirstate()
 
283
        if path is not None:
 
284
            path = path.encode('utf8')
 
285
        return state._get_entry(0, fileid_utf8=file_id, path_utf8=path)
 
286
 
 
287
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
288
        # check file id is valid unconditionally.
 
289
        key, details = self._get_entry(file_id=file_id, path=path)
 
290
        assert key is not None, 'what error should this raise'
 
291
        # TODO:
 
292
        # if row stat is valid, use cached sha1, else, get a new sha1.
 
293
        if path is None:
 
294
            path = os.path.join(*key[0:2]).decode('utf8')
 
295
        return self._hashcache.get_sha1(path, stat_value)
 
296
 
 
297
    def _get_inventory(self):
 
298
        """Get the inventory for the tree. This is only valid within a lock."""
 
299
        if self._inventory is not None:
 
300
            return self._inventory
 
301
        self._generate_inventory()
 
302
        return self._inventory
 
303
 
 
304
    inventory = property(_get_inventory,
 
305
                         doc="Inventory of this Tree")
 
306
 
 
307
    @needs_read_lock
 
308
    def get_parent_ids(self):
 
309
        """See Tree.get_parent_ids.
 
310
        
 
311
        This implementation requests the ids list from the dirstate file.
 
312
        """
 
313
        return self.current_dirstate().get_parent_ids()
 
314
 
 
315
    @needs_read_lock
 
316
    def get_root_id(self):
 
317
        """Return the id of this trees root"""
 
318
        return self._get_entry(path='')[0][2]
 
319
 
 
320
    def has_id(self, file_id):
 
321
        state = self.current_dirstate()
 
322
        file_id = osutils.safe_file_id(file_id)
 
323
        row, parents = self._get_entry(file_id=file_id)
 
324
        if row is None:
 
325
            return False
 
326
        return osutils.lexists(pathjoin(
 
327
                    self.basedir, row[0].decode('utf8'), row[1].decode('utf8')))
 
328
 
 
329
    @needs_read_lock
 
330
    def id2path(self, fileid):
 
331
        state = self.current_dirstate()
 
332
        fileid = osutils.safe_file_id(fileid)
 
333
        key, tree_details = state._get_entry(0, fileid_utf8=fileid)
 
334
        return os.path.join(*key[0:2]).decode('utf8')
 
335
 
 
336
    @needs_read_lock
 
337
    def __iter__(self):
 
338
        """Iterate through file_ids for this tree.
 
339
 
 
340
        file_ids are in a WorkingTree if they are in the working inventory
 
341
        and the working file exists.
 
342
        """
 
343
        result = []
 
344
        for key, tree_details in self.current_dirstate()._iter_entries():
 
345
            if tree_details[0][0] in ('absent', 'relocated'):
 
346
                # not relevant to the working tree
 
347
                continue
 
348
            path = pathjoin(self.basedir, key[0].decode('utf8'), key[1].decode('utf8'))
 
349
            if osutils.lexists(path):
 
350
                result.append(key[2])
 
351
        return iter(result)
 
352
 
 
353
    @needs_read_lock
 
354
    def _last_revision(self):
 
355
        """See Mutable.last_revision."""
 
356
        parent_ids = self.current_dirstate().get_parent_ids()
 
357
        if parent_ids:
 
358
            return parent_ids[0]
 
359
        else:
 
360
            return None
 
361
 
 
362
    @needs_tree_write_lock
 
363
    def move(self, from_paths, to_dir=None, after=False, **kwargs):
 
364
        """See WorkingTree.move()."""
 
365
        if not from_paths:
 
366
            return ()
 
367
 
 
368
        state = self.current_dirstate()
 
369
 
 
370
        # check for deprecated use of signature
 
371
        if to_dir is None:
 
372
            to_dir = kwargs.get('to_name', None)
 
373
            if to_dir is None:
 
374
                raise TypeError('You must supply a target directory')
 
375
            else:
 
376
                symbol_versioning.warn('The parameter to_name was deprecated'
 
377
                                       ' in version 0.13. Use to_dir instead',
 
378
                                       DeprecationWarning)
 
379
 
 
380
        assert not isinstance(from_paths, basestring)
 
381
        to_dir_utf8 = to_dir.encode('utf8')
 
382
        to_entry_dirname, to_basename = os.path.split(to_dir_utf8)
 
383
        # check destination directory
 
384
        # get the details for it
 
385
        to_entry_block_index, to_entry_entry_index, dir_present, entry_present = \
 
386
            state._get_block_entry_index(to_entry_dirname, to_basename, 0)
 
387
        if not entry_present:
 
388
            raise errors.BzrMoveFailedError('', to_dir,
 
389
                errors.NotInWorkingDirectory(to_dir))
 
390
        to_entry = state._dirblocks[to_entry_block_index][1][to_entry_entry_index]
 
391
        # get a handle on the block itself.
 
392
        to_block_index = state._ensure_block(
 
393
            to_entry_block_index, to_entry_entry_index, to_dir_utf8)
 
394
        to_block = state._dirblocks[to_block_index]
 
395
        to_abs = self.abspath(to_dir)
 
396
        if not isdir(to_abs):
 
397
            raise errors.BzrMoveFailedError('',to_dir,
 
398
                errors.NotADirectory(to_abs))
 
399
 
 
400
        if to_entry[1][0][0] != 'directory':
 
401
            raise errors.BzrMoveFailedError('',to_dir,
 
402
                errors.NotADirectory(to_abs))
 
403
 
 
404
        if self._inventory is not None:
 
405
            update_inventory = True
 
406
            inv = self.inventory
 
407
            to_dir_ie = inv[to_dir_id]
 
408
            to_dir_id = to_entry[0][2]
 
409
        else:
 
410
            update_inventory = False
 
411
 
 
412
        # create rename entries and tuples
 
413
        for from_rel in from_paths:
 
414
            # from_rel is 'pathinroot/foo/bar'
 
415
            from_dirname, from_tail = os.path.split(from_rel)
 
416
            from_dirname = from_dirname.encode('utf8')
 
417
            from_entry = self._get_entry(path=from_rel)
 
418
            if from_entry == (None, None):
 
419
                raise errors.BzrMoveFailedError(from_rel,to_dir,
 
420
                    errors.NotVersionedError(path=str(from_rel)))
 
421
 
 
422
            from_id = from_entry[0][2]
 
423
            to_rel = pathjoin(to_dir, from_tail)
 
424
            item_to_entry = self._get_entry(path=to_rel)
 
425
            if item_to_entry != (None, None):
 
426
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
427
                    "Target is already versioned.")
 
428
 
 
429
            if from_rel == to_rel:
 
430
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
431
                    "Source and target are identical.")
 
432
 
 
433
            from_missing = not self.has_filename(from_rel)
 
434
            to_missing = not self.has_filename(to_rel)
 
435
            if after:
 
436
                move_file = False
 
437
            else:
 
438
                move_file = True
 
439
            if to_missing:
 
440
                if not move_file:
 
441
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
442
                        errors.NoSuchFile(path=to_rel,
 
443
                        extra="New file has not been created yet"))
 
444
                elif from_missing:
 
445
                    # neither path exists
 
446
                    raise errors.BzrRenameFailedError(from_rel, to_rel,
 
447
                        errors.PathsDoNotExist(paths=(from_rel, to_rel)))
 
448
            else:
 
449
                if from_missing: # implicitly just update our path mapping
 
450
                    move_file = False
 
451
                else:
 
452
                    raise errors.RenameFailedFilesExist(from_rel, to_rel,
 
453
                        extra="(Use --after to update the Bazaar id)")
 
454
 
 
455
            rollbacks = []
 
456
            def rollback_rename():
 
457
                """A single rename has failed, roll it back."""
 
458
                error = None
 
459
                for rollback in reversed(rollbacks):
 
460
                    try:
 
461
                        rollback()
 
462
                    except Exception, e:
 
463
                        import pdb;pdb.set_trace()
 
464
                        error = e
 
465
                if error:
 
466
                    raise error
 
467
 
 
468
            # perform the disk move first - its the most likely failure point.
 
469
            from_rel_abs = self.abspath(from_rel)
 
470
            to_rel_abs = self.abspath(to_rel)
 
471
            try:
 
472
                osutils.rename(from_rel_abs, to_rel_abs)
 
473
            except OSError, e:
 
474
                raise errors.BzrMoveFailedError(from_rel, to_rel, e[1])
 
475
            rollbacks.append(lambda: osutils.rename(to_rel_abs, from_rel_abs))
 
476
            try:
 
477
                # perform the rename in the inventory next if needed: its easy
 
478
                # to rollback
 
479
                if update_inventory:
 
480
                    # rename the entry
 
481
                    from_entry = inv[from_id]
 
482
                    current_parent = from_entry.parent_id
 
483
                    inv.rename(from_id, to_dir_id, from_tail)
 
484
                    rollbacks.append(
 
485
                        lambda: inv.rename(from_id, current_parent, from_tail))
 
486
                # finally do the rename in the dirstate, which is a little
 
487
                # tricky to rollback, but least likely to need it.
 
488
                basename = from_tail.encode('utf8')
 
489
                old_block_index, old_entry_index, dir_present, file_present = \
 
490
                    state._get_block_entry_index(from_dirname, basename, 0)
 
491
                old_block = state._dirblocks[old_block_index][1]
 
492
                old_entry_details = old_block[old_entry_index][1]
 
493
                # remove the old row
 
494
                from_key = old_block[old_entry_index][0]
 
495
                to_key = ((to_block[0],) + from_key[1:3])
 
496
                state._make_absent(old_block[old_entry_index])
 
497
                rollbacks.append(
 
498
                    lambda:state.update_minimal(from_key,
 
499
                        old_entry_details[0][0],
 
500
                        num_present_parents=len(old_entry_details) - 1,
 
501
                        executable=old_entry_details[0][3],
 
502
                        fingerprint=old_entry_details[0][1],
 
503
                        packed_stat=old_entry_details[0][4],
 
504
                        size=old_entry_details[0][2],
 
505
                        id_index=state._get_id_index(),
 
506
                        path_utf8=from_rel.encode('utf8')))
 
507
                # create new row in current block
 
508
                state.update_minimal(to_key,
 
509
                        old_entry_details[0][0],
 
510
                        num_present_parents=len(old_entry_details) - 1,
 
511
                        executable=old_entry_details[0][3],
 
512
                        fingerprint=old_entry_details[0][1],
 
513
                        packed_stat=old_entry_details[0][4],
 
514
                        size=old_entry_details[0][2],
 
515
                        id_index=state._get_id_index(),
 
516
                        path_utf8=to_rel.encode('utf8'))
 
517
                added_entry_index, _ = state._find_entry_index(to_key, to_block[1])
 
518
                new_entry = to_block[added_entry_index]
 
519
                rollbacks.append(lambda:state._make_absent(new_entry))
 
520
                if new_entry[1][0][0] == 'directory':
 
521
                    import pdb;pdb.set_trace()
 
522
                    # if a directory, rename all the contents of child blocks
 
523
                    # adding rollbacks as each is inserted to remove them and
 
524
                    # restore the original
 
525
                    # TODO: large scale slice assignment.
 
526
                    # setup new list
 
527
                    # save old list region
 
528
                    # move up or down the old region
 
529
                    # add rollback to move the region back
 
530
                    # assign new list to new region
 
531
                    # done
 
532
            except:
 
533
                rollback_rename()
 
534
                raise
 
535
            state._dirblock_state = dirstate.DirState.IN_MEMORY_MODIFIED
 
536
            self._dirty = True
 
537
 
 
538
        return #rename_tuples
 
539
 
 
540
    def _new_tree(self):
 
541
        """Initialize the state in this tree to be a new tree."""
 
542
        self._dirty = True
 
543
 
 
544
    @needs_read_lock
 
545
    def path2id(self, path):
 
546
        """Return the id for path in this tree."""
 
547
        entry = self._get_entry(path=path)
 
548
        if entry == (None, None):
 
549
            return None
 
550
        return entry[0][2]
 
551
 
 
552
    def paths2ids(self, paths, trees=[], require_versioned=True):
 
553
        """See Tree.paths2ids().
 
554
        
 
555
        This specialisation fast-paths the case where all the trees are in the
 
556
        dirstate.
 
557
        """
 
558
        if paths is None:
 
559
            return None
 
560
        parents = self.get_parent_ids()
 
561
        for tree in trees:
 
562
            if not (isinstance(tree, DirStateRevisionTree) and tree._revision_id in
 
563
                parents):
 
564
                return super(WorkingTree4, self).paths2ids(paths, trees, require_versioned)
 
565
        search_indexes = [0] + [1 + parents.index(tree._revision_id) for tree in trees]
 
566
        # -- make all paths utf8 --
 
567
        paths_utf8 = set()
 
568
        for path in paths:
 
569
            paths_utf8.add(path.encode('utf8'))
 
570
        paths = paths_utf8
 
571
        # -- paths is now a utf8 path set --
 
572
        # -- get the state object and prepare it.
 
573
        state = self.current_dirstate()
 
574
        state._read_dirblocks_if_needed()
 
575
        def _entries_for_path(path):
 
576
            """Return a list with all the entries that match path for all ids.
 
577
            """
 
578
            dirname, basename = os.path.split(path)
 
579
            key = (dirname, basename, '')
 
580
            block_index, present = state._find_block_index_from_key(key)
 
581
            if not present:
 
582
                # the block which should contain path is absent.
 
583
                return []
 
584
            result = []
 
585
            block = state._dirblocks[block_index][1]
 
586
            entry_index, _ = state._find_entry_index(key, block)
 
587
            # we may need to look at multiple entries at this path: walk while the paths match.
 
588
            while (entry_index < len(block) and
 
589
                block[entry_index][0][0:2] == key[0:2]):
 
590
                result.append(block[entry_index])
 
591
                entry_index += 1
 
592
            return result
 
593
        if require_versioned:
 
594
            # -- check all supplied paths are versioned in all search trees. --
 
595
            all_versioned = True
 
596
            for path in paths:
 
597
                path_entries = _entries_for_path(path)
 
598
                if not path_entries:
 
599
                    # this specified path is not present at all: error
 
600
                    all_versioned = False
 
601
                    break
 
602
                found_versioned = False
 
603
                # for each id at this path
 
604
                for entry in path_entries:
 
605
                    # for each tree.
 
606
                    for index in search_indexes:
 
607
                        if entry[1][index][0] != 'absent':
 
608
                            found_versioned = True
 
609
                            # all good: found a versioned cell
 
610
                            break
 
611
                if not found_versioned:
 
612
                    # non of the indexes was not 'absent' at all ids for this
 
613
                    # path.
 
614
                    all_versioned = False
 
615
                    break
 
616
            if not all_versioned:
 
617
                raise errors.PathsNotVersionedError(paths)
 
618
        # -- remove redundancy in supplied paths to prevent over-scanning --
 
619
        search_paths = set()
 
620
        for path in paths:
 
621
            other_paths = paths.difference(set([path]))
 
622
            if not osutils.is_inside_any(other_paths, path):
 
623
                # this is a top level path, we must check it.
 
624
                search_paths.add(path)
 
625
        # sketch: 
 
626
        # for all search_indexs in each path at or under each element of
 
627
        # search_paths, if the detail is relocated: add the id, and add the
 
628
        # relocated path as one to search if its not searched already. If the
 
629
        # detail is not relocated, add the id.
 
630
        searched_paths = set()
 
631
        found_ids = set()
 
632
        def _process_entry(entry):
 
633
            """Look at search_indexes within entry.
 
634
 
 
635
            If a specific tree's details are relocated, add the relocation
 
636
            target to search_paths if not searched already. If it is absent, do
 
637
            nothing. Otherwise add the id to found_ids.
 
638
            """
 
639
            for index in search_indexes:
 
640
                if entry[1][index][0] == 'relocated':
 
641
                    if not osutils.is_inside_any(searched_paths, entry[1][index][1]):
 
642
                        search_paths.add(entry[1][index][1])
 
643
                elif entry[1][index][0] != 'absent':
 
644
                    found_ids.add(entry[0][2])
 
645
        while search_paths:
 
646
            current_root = search_paths.pop()
 
647
            searched_paths.add(current_root)
 
648
            # process the entries for this containing directory: the rest will be
 
649
            # found by their parents recursively.
 
650
            root_entries = _entries_for_path(current_root)
 
651
            if not root_entries:
 
652
                # this specified path is not present at all, skip it.
 
653
                continue
 
654
            for entry in root_entries:
 
655
                _process_entry(entry)
 
656
            initial_key = (current_root, '', '')
 
657
            block_index, _ = state._find_block_index_from_key(initial_key)
 
658
            while (block_index < len(state._dirblocks) and
 
659
                osutils.is_inside(current_root, state._dirblocks[block_index][0])):
 
660
                for entry in state._dirblocks[block_index][1]:
 
661
                    _process_entry(entry)
 
662
                block_index += 1
 
663
        return found_ids
 
664
 
 
665
    def read_working_inventory(self):
 
666
        """Read the working inventory.
 
667
        
 
668
        This is a meaningless operation for dirstate, but we obey it anyhow.
 
669
        """
 
670
        return self.inventory
 
671
 
 
672
    @needs_read_lock
 
673
    def revision_tree(self, revision_id):
 
674
        """See Tree.revision_tree.
 
675
 
 
676
        WorkingTree4 supplies revision_trees for any basis tree.
 
677
        """
 
678
        revision_id = osutils.safe_revision_id(revision_id)
 
679
        dirstate = self.current_dirstate()
 
680
        parent_ids = dirstate.get_parent_ids()
 
681
        if revision_id not in parent_ids:
 
682
            raise errors.NoSuchRevisionInTree(self, revision_id)
 
683
        if revision_id in dirstate.get_ghosts():
 
684
            raise errors.NoSuchRevisionInTree(self, revision_id)
 
685
        return DirStateRevisionTree(dirstate, revision_id,
 
686
            self.branch.repository)
 
687
 
 
688
    @needs_tree_write_lock
 
689
    def set_last_revision(self, new_revision):
 
690
        """Change the last revision in the working tree."""
 
691
        new_revision = osutils.safe_revision_id(new_revision)
 
692
        parents = self.get_parent_ids()
 
693
        if new_revision in (NULL_REVISION, None):
 
694
            assert len(parents) < 2, (
 
695
                "setting the last parent to none with a pending merge is "
 
696
                "unsupported.")
 
697
            self.set_parent_ids([])
 
698
        else:
 
699
            self.set_parent_ids([new_revision] + parents[1:],
 
700
                allow_leftmost_as_ghost=True)
 
701
 
 
702
    @needs_tree_write_lock
 
703
    def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
 
704
        """Set the parent ids to revision_ids.
 
705
        
 
706
        See also set_parent_trees. This api will try to retrieve the tree data
 
707
        for each element of revision_ids from the trees repository. If you have
 
708
        tree data already available, it is more efficient to use
 
709
        set_parent_trees rather than set_parent_ids. set_parent_ids is however
 
710
        an easier API to use.
 
711
 
 
712
        :param revision_ids: The revision_ids to set as the parent ids of this
 
713
            working tree. Any of these may be ghosts.
 
714
        """
 
715
        revision_ids = [osutils.safe_revision_id(r) for r in revision_ids]
 
716
        trees = []
 
717
        for revision_id in revision_ids:
 
718
            try:
 
719
                revtree = self.branch.repository.revision_tree(revision_id)
 
720
                # TODO: jam 20070213 KnitVersionedFile raises
 
721
                #       RevisionNotPresent rather than NoSuchRevision if a
 
722
                #       given revision_id is not present. Should Repository be
 
723
                #       catching it and re-raising NoSuchRevision?
 
724
            except (errors.NoSuchRevision, errors.RevisionNotPresent):
 
725
                revtree = None
 
726
            trees.append((revision_id, revtree))
 
727
        self.set_parent_trees(trees,
 
728
            allow_leftmost_as_ghost=allow_leftmost_as_ghost)
 
729
 
 
730
    @needs_tree_write_lock
 
731
    def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
 
732
        """Set the parents of the working tree.
 
733
 
 
734
        :param parents_list: A list of (revision_id, tree) tuples.
 
735
            If tree is None, then that element is treated as an unreachable
 
736
            parent tree - i.e. a ghost.
 
737
        """
 
738
        dirstate = self.current_dirstate()
 
739
        if len(parents_list) > 0:
 
740
            if not allow_leftmost_as_ghost and parents_list[0][1] is None:
 
741
                raise errors.GhostRevisionUnusableHere(parents_list[0][0])
 
742
        real_trees = []
 
743
        ghosts = []
 
744
        # convert absent trees to the null tree, which we convert back to
 
745
        # missing on access.
 
746
        for rev_id, tree in parents_list:
 
747
            rev_id = osutils.safe_revision_id(rev_id)
 
748
            if tree is not None:
 
749
                real_trees.append((rev_id, tree))
 
750
            else:
 
751
                real_trees.append((rev_id,
 
752
                    self.branch.repository.revision_tree(None)))
 
753
                ghosts.append(rev_id)
 
754
        dirstate.set_parent_trees(real_trees, ghosts=ghosts)
 
755
        self._dirty = True
 
756
 
 
757
    def _set_root_id(self, file_id):
 
758
        """See WorkingTree.set_root_id."""
 
759
        state = self.current_dirstate()
 
760
        state.set_path_id('', file_id)
 
761
        self._dirty = state._dirblock_state == dirstate.DirState.IN_MEMORY_MODIFIED
 
762
 
 
763
    def unlock(self):
 
764
        """Unlock in format 4 trees needs to write the entire dirstate."""
 
765
        if self._control_files._lock_count == 1:
 
766
            self._write_hashcache_if_dirty()
 
767
            # eventually we should do signature checking during read locks for
 
768
            # dirstate updates.
 
769
            if self._control_files._lock_mode == 'w':
 
770
                if self._dirty:
 
771
                    self.flush()
 
772
            self._dirstate = None
 
773
            self._inventory = None
 
774
        # reverse order of locking.
 
775
        try:
 
776
            return self._control_files.unlock()
 
777
        finally:
 
778
            self.branch.unlock()
 
779
 
 
780
    @needs_tree_write_lock
 
781
    def unversion(self, file_ids):
 
782
        """Remove the file ids in file_ids from the current versioned set.
 
783
 
 
784
        When a file_id is unversioned, all of its children are automatically
 
785
        unversioned.
 
786
 
 
787
        :param file_ids: The file ids to stop versioning.
 
788
        :raises: NoSuchId if any fileid is not currently versioned.
 
789
        """
 
790
        if not file_ids:
 
791
            return
 
792
        state = self.current_dirstate()
 
793
        state._read_dirblocks_if_needed()
 
794
        ids_to_unversion = set()
 
795
        for file_id in file_ids:
 
796
            ids_to_unversion.add(osutils.safe_file_id(file_id))
 
797
        paths_to_unversion = set()
 
798
        # sketch:
 
799
        # check if the root is to be unversioned, if so, assert for now.
 
800
        # walk the state marking unversioned things as absent.
 
801
        # if there are any un-unversioned ids at the end, raise
 
802
        for key, details in state._dirblocks[0][1]:
 
803
            if (details[0][0] not in ('absent', 'relocated') and
 
804
                key[2] in ids_to_unversion):
 
805
                # I haven't written the code to unversion / yet - it should be
 
806
                # supported.
 
807
                raise errors.BzrError('Unversioning the / is not currently supported')
 
808
        details_length = len(state._dirblocks[0][1][0][1])
 
809
        block_index = 0
 
810
        while block_index < len(state._dirblocks):
 
811
            # process one directory at a time.
 
812
            block = state._dirblocks[block_index]
 
813
            # first check: is the path one to remove - it or its children
 
814
            delete_block = False
 
815
            for path in paths_to_unversion:
 
816
                if (block[0].startswith(path) and
 
817
                    (len(block[0]) == len(path) or
 
818
                     block[0][len(path)] == '/')):
 
819
                    # this entire block should be deleted - its the block for a
 
820
                    # path to unversion; or the child of one
 
821
                    delete_block = True
 
822
                    break
 
823
            # TODO: trim paths_to_unversion as we pass by paths
 
824
            if delete_block:
 
825
                # this block is to be deleted: process it.
 
826
                # TODO: we can special case the no-parents case and
 
827
                # just forget the whole block.
 
828
                entry_index = 0
 
829
                while entry_index < len(block[1]):
 
830
                    if not state._make_absent(block[1][entry_index]):
 
831
                        entry_index += 1
 
832
                # go to the next block. (At the moment we dont delete empty
 
833
                # dirblocks)
 
834
                block_index += 1
 
835
                continue
 
836
            entry_index = 0
 
837
            while entry_index < len(block[1]):
 
838
                entry = block[1][entry_index]
 
839
                if (entry[1][0][0] in ('absent', 'relocated') or
 
840
                    # ^ some parent row.
 
841
                    entry[0][2] not in ids_to_unversion):
 
842
                    # ^ not an id to unversion
 
843
                    entry_index += 1
 
844
                    continue
 
845
                if entry[1][0][0] == 'directory':
 
846
                    paths_to_unversion.add(os.path.join(*entry[0][0:2]))
 
847
                if not state._make_absent(entry):
 
848
                    entry_index += 1
 
849
                # we have unversioned this id
 
850
                ids_to_unversion.remove(entry[0][2])
 
851
            block_index += 1
 
852
        if ids_to_unversion:
 
853
            raise errors.NoSuchId(self, iter(ids_to_unversion).next())
 
854
        self._dirty = True
 
855
        # have to change the legacy inventory too.
 
856
        if self._inventory is not None:
 
857
            for file_id in file_ids:
 
858
                self._inventory.remove_recursive_id(file_id)
 
859
 
 
860
    @needs_tree_write_lock
 
861
    def _write_inventory(self, inv):
 
862
        """Write inventory as the current inventory."""
 
863
        assert not self._dirty, "attempting to write an inventory when the dirstate is dirty will cause data loss"
 
864
        self.current_dirstate().set_state_from_inventory(inv)
 
865
        self._dirty = True
 
866
        self.flush()
 
867
 
 
868
 
 
869
class WorkingTreeFormat4(WorkingTreeFormat3):
 
870
    """The first consolidated dirstate working tree format.
 
871
 
 
872
    This format:
 
873
        - exists within a metadir controlling .bzr
 
874
        - includes an explicit version marker for the workingtree control
 
875
          files, separate from the BzrDir format
 
876
        - modifies the hash cache format
 
877
        - is new in bzr TODO FIXME SETBEFOREMERGE
 
878
        - uses a LockDir to guard access to it.
 
879
    """
 
880
 
 
881
    def get_format_string(self):
 
882
        """See WorkingTreeFormat.get_format_string()."""
 
883
        return "Bazaar Working Tree format 4\n"
 
884
 
 
885
    def get_format_description(self):
 
886
        """See WorkingTreeFormat.get_format_description()."""
 
887
        return "Working tree format 4"
 
888
 
 
889
    def initialize(self, a_bzrdir, revision_id=None):
 
890
        """See WorkingTreeFormat.initialize().
 
891
 
 
892
        revision_id allows creating a working tree at a different
 
893
        revision than the branch is at.
 
894
        """
 
895
        revision_id = osutils.safe_revision_id(revision_id)
 
896
        if not isinstance(a_bzrdir.transport, LocalTransport):
 
897
            raise errors.NotLocalUrl(a_bzrdir.transport.base)
 
898
        transport = a_bzrdir.get_workingtree_transport(self)
 
899
        control_files = self._open_control_files(a_bzrdir)
 
900
        control_files.create_lock()
 
901
        control_files.lock_write()
 
902
        control_files.put_utf8('format', self.get_format_string())
 
903
        branch = a_bzrdir.open_branch()
 
904
        if revision_id is None:
 
905
            revision_id = branch.last_revision()
 
906
        local_path = transport.local_abspath('dirstate')
 
907
        dirstate.DirState.initialize(local_path)
 
908
        wt = WorkingTree4(a_bzrdir.root_transport.local_abspath('.'),
 
909
                         branch,
 
910
                         _format=self,
 
911
                         _bzrdir=a_bzrdir,
 
912
                         _control_files=control_files)
 
913
        wt._new_tree()
 
914
        wt.lock_write()
 
915
        try:
 
916
            #wt.current_dirstate().set_path_id('', NEWROOT)
 
917
            wt.set_last_revision(revision_id)
 
918
            wt.flush()
 
919
            basis = wt.basis_tree()
 
920
            basis.lock_read()
 
921
            transform.build_tree(basis, wt)
 
922
            basis.unlock()
 
923
        finally:
 
924
            control_files.unlock()
 
925
            wt.unlock()
 
926
        return wt
 
927
 
 
928
 
 
929
    def _open(self, a_bzrdir, control_files):
 
930
        """Open the tree itself.
 
931
 
 
932
        :param a_bzrdir: the dir for the tree.
 
933
        :param control_files: the control files for the tree.
 
934
        """
 
935
        return WorkingTree4(a_bzrdir.root_transport.local_abspath('.'),
 
936
                           branch=a_bzrdir.open_branch(),
 
937
                           _format=self,
 
938
                           _bzrdir=a_bzrdir,
 
939
                           _control_files=control_files)
 
940
 
 
941
 
 
942
class DirStateRevisionTree(Tree):
 
943
    """A revision tree pulling the inventory from a dirstate."""
 
944
 
 
945
    def __init__(self, dirstate, revision_id, repository):
 
946
        self._dirstate = dirstate
 
947
        self._revision_id = osutils.safe_revision_id(revision_id)
 
948
        self._repository = repository
 
949
        self._inventory = None
 
950
        self._locked = 0
 
951
 
 
952
    def annotate_iter(self, file_id):
 
953
        """See Tree.annotate_iter"""
 
954
        w = self._repository.weave_store.get_weave(file_id,
 
955
                           self._repository.get_transaction())
 
956
        return w.annotate_iter(self.inventory[file_id].revision)
 
957
 
 
958
    def _comparison_data(self, entry, path):
 
959
        """See Tree._comparison_data."""
 
960
        if entry is None:
 
961
            return None, False, None
 
962
        # trust the entry as RevisionTree does, but this may not be
 
963
        # sensible: the entry might not have come from us?
 
964
        return entry.kind, entry.executable, None
 
965
 
 
966
    def _file_size(self, entry, stat_value):
 
967
        return entry.text_size
 
968
 
 
969
    def filter_unversioned_files(self, paths):
 
970
        """Filter out paths that are not versioned.
 
971
 
 
972
        :return: set of paths.
 
973
        """
 
974
        pred = self.has_filename
 
975
        return set((p for p in paths if not pred(p)))
 
976
 
 
977
    def _get_entry(self, file_id=None, path=None):
 
978
        """Get the dirstate row for file_id or path.
 
979
 
 
980
        If either file_id or path is supplied, it is used as the key to lookup.
 
981
        If both are supplied, the fastest lookup is used, and an error is
 
982
        raised if they do not both point at the same row.
 
983
        
 
984
        :param file_id: An optional unicode file_id to be looked up.
 
985
        :param path: An optional unicode path to be looked up.
 
986
        :return: The dirstate row tuple for path/file_id, or (None, None)
 
987
        """
 
988
        if file_id is None and path is None:
 
989
            raise errors.BzrError('must supply file_id or path')
 
990
        file_id = osutils.safe_file_id(file_id)
 
991
        if path is not None:
 
992
            path = path.encode('utf8')
 
993
        parent_index = self._dirstate.get_parent_ids().index(self._revision_id) + 1
 
994
        return self._dirstate._get_entry(parent_index, fileid_utf8=file_id, path_utf8=path)
 
995
 
 
996
    def _generate_inventory(self):
 
997
        """Create and set self.inventory from the dirstate object.
 
998
 
 
999
        This is relatively expensive: we have to walk the entire dirstate.
 
1000
        Ideally we would not, and instead would """
 
1001
        assert self._locked, 'cannot generate inventory of an unlocked '\
 
1002
            'dirstate revision tree'
 
1003
        # separate call for profiling - makes it clear where the costs are.
 
1004
        self._dirstate._read_dirblocks_if_needed()
 
1005
        assert self._revision_id in self._dirstate.get_parent_ids(), \
 
1006
            'parent %s has disappeared from %s' % (
 
1007
            self._revision_id, self._dirstate.get_parent_ids())
 
1008
        parent_index = self._dirstate.get_parent_ids().index(self._revision_id) + 1
 
1009
        # This is identical now to the WorkingTree _generate_inventory except
 
1010
        # for the tree index use.
 
1011
        root_key, current_entry = self._dirstate._get_entry(parent_index, path_utf8='')
 
1012
        current_id = root_key[2]
 
1013
        assert current_entry[parent_index][0] == 'directory'
 
1014
        inv = Inventory(root_id=current_id, revision_id=self._revision_id)
 
1015
        inv.root.revision = current_entry[parent_index][4]
 
1016
        # we could do this straight out of the dirstate; it might be fast
 
1017
        # and should be profiled - RBC 20070216
 
1018
        parent_ids = {'' : inv.root.file_id}
 
1019
        for block in self._dirstate._dirblocks[1:]: #skip root
 
1020
            dirname = block[0]
 
1021
            try:
 
1022
                parent_id = parent_ids[dirname]
 
1023
            except KeyError:
 
1024
                # all the paths in this block are not versioned in this tree
 
1025
                continue
 
1026
            for key, entry in block[1]:
 
1027
                if entry[parent_index][0] in ('absent', 'relocated'):
 
1028
                    # not this tree
 
1029
                    continue
 
1030
                name = key[1]
 
1031
                name_unicode = name.decode('utf8')
 
1032
                file_id = key[2]
 
1033
                kind, link_or_sha1, size, executable, revid = entry[parent_index]
 
1034
                inv_entry = entry_factory[kind](file_id, name_unicode, parent_id)
 
1035
                inv_entry.revision = revid
 
1036
                if kind == 'file':
 
1037
                    inv_entry.executable = executable
 
1038
                    inv_entry.text_size = size
 
1039
                    inv_entry.text_sha1 = link_or_sha1
 
1040
                elif kind == 'directory':
 
1041
                    parent_ids[(dirname + '/' + name).strip('/')] = file_id
 
1042
                elif kind == 'symlink':
 
1043
                    inv_entry.executable = False
 
1044
                    inv_entry.text_size = size
 
1045
                    inv_entry.symlink_target = link_or_sha1.decode('utf8')
 
1046
                else:
 
1047
                    raise Exception, kind
 
1048
                inv.add(inv_entry)
 
1049
        self._inventory = inv
 
1050
 
 
1051
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
1052
        # TODO: if path is present, fast-path on that, as inventory
 
1053
        # might not be present
 
1054
        ie = self.inventory[file_id]
 
1055
        if ie.kind == "file":
 
1056
            return ie.text_sha1
 
1057
        return None
 
1058
 
 
1059
    def get_file(self, file_id):
 
1060
        return StringIO(self.get_file_text(file_id))
 
1061
 
 
1062
    def get_file_lines(self, file_id):
 
1063
        ie = self.inventory[file_id]
 
1064
        return self._repository.weave_store.get_weave(file_id,
 
1065
                self._repository.get_transaction()).get_lines(ie.revision)
 
1066
 
 
1067
    def get_file_size(self, file_id):
 
1068
        return self.inventory[file_id].text_size
 
1069
 
 
1070
    def get_file_text(self, file_id):
 
1071
        return ''.join(self.get_file_lines(file_id))
 
1072
 
 
1073
    def get_revision_id(self):
 
1074
        """Return the revision id for this tree."""
 
1075
        return self._revision_id
 
1076
 
 
1077
    def _get_inventory(self):
 
1078
        if self._inventory is not None:
 
1079
            return self._inventory
 
1080
        self._generate_inventory()
 
1081
        return self._inventory
 
1082
 
 
1083
    inventory = property(_get_inventory,
 
1084
                         doc="Inventory of this Tree")
 
1085
 
 
1086
    def get_parent_ids(self):
 
1087
        """The parents of a tree in the dirstate are not cached."""
 
1088
        return self._repository.get_revision(self._revision_id).parent_ids
 
1089
 
 
1090
    def has_filename(self, filename):
 
1091
        return bool(self.path2id(filename))
 
1092
 
 
1093
    def kind(self, file_id):
 
1094
        return self.inventory[file_id].kind
 
1095
 
 
1096
    def is_executable(self, file_id, path=None):
 
1097
        ie = self.inventory[file_id]
 
1098
        if ie.kind != "file":
 
1099
            return None
 
1100
        return ie.executable
 
1101
 
 
1102
    def list_files(self, include_root=False):
 
1103
        # We use a standard implementation, because DirStateRevisionTree is
 
1104
        # dealing with one of the parents of the current state
 
1105
        inv = self._get_inventory()
 
1106
        entries = inv.iter_entries()
 
1107
        if self.inventory.root is not None and not include_root:
 
1108
            entries.next()
 
1109
        for path, entry in entries:
 
1110
            yield path, 'V', entry.kind, entry.file_id, entry
 
1111
 
 
1112
    def lock_read(self):
 
1113
        """Lock the tree for a set of operations."""
 
1114
        if not self._locked:
 
1115
            self._repository.lock_read()
 
1116
        self._locked += 1
 
1117
 
 
1118
    @needs_read_lock
 
1119
    def path2id(self, path):
 
1120
        """Return the id for path in this tree."""
 
1121
        # lookup by path: faster than splitting and walking the ivnentory.
 
1122
        entry = self._get_entry(path=path)
 
1123
        if entry == (None, None):
 
1124
            return None
 
1125
        return entry[0][2]
 
1126
 
 
1127
    def unlock(self):
 
1128
        """Unlock, freeing any cache memory used during the lock."""
 
1129
        # outside of a lock, the inventory is suspect: release it.
 
1130
        self._locked -=1
 
1131
        if not self._locked:
 
1132
            self._inventory = None
 
1133
            self._locked = False
 
1134
            self._repository.unlock()
 
1135
 
 
1136
    def walkdirs(self, prefix=""):
 
1137
        # TODO: jam 20070215 This is the cheap way by cheating and using the
 
1138
        #       RevisionTree implementation.
 
1139
        #       This should be cleaned up to use the much faster Dirstate code
 
1140
        #       This is a little tricky, though, because the dirstate is
 
1141
        #       indexed by current path, not by parent path.
 
1142
        #       So for now, we just build up the parent inventory, and extract
 
1143
        #       it the same way RevisionTree does.
 
1144
        _directory = 'directory'
 
1145
        inv = self._get_inventory()
 
1146
        top_id = inv.path2id(prefix)
 
1147
        if top_id is None:
 
1148
            pending = []
 
1149
        else:
 
1150
            pending = [(prefix, top_id)]
 
1151
        while pending:
 
1152
            dirblock = []
 
1153
            relpath, file_id = pending.pop()
 
1154
            # 0 - relpath, 1- file-id
 
1155
            if relpath:
 
1156
                relroot = relpath + '/'
 
1157
            else:
 
1158
                relroot = ""
 
1159
            # FIXME: stash the node in pending
 
1160
            entry = inv[file_id]
 
1161
            for name, child in entry.sorted_children():
 
1162
                toppath = relroot + name
 
1163
                dirblock.append((toppath, name, child.kind, None,
 
1164
                    child.file_id, child.kind
 
1165
                    ))
 
1166
            yield (relpath, entry.file_id), dirblock
 
1167
            # push the user specified dirs from dirblock
 
1168
            for dir in reversed(dirblock):
 
1169
                if dir[2] == _directory:
 
1170
                    pending.append((dir[0], dir[4]))