~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/workingtree_4.py

  • Committer: Martin Pool
  • Date: 2005-09-30 05:56:05 UTC
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930055605-a2c534529b392a7d
- fix upgrade for transport changes

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