~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/workingtree_4.py

track down a couple other places where we are using list_files.

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, make_entry
 
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 flush(self):
 
197
        """Write all cached data to disk."""
 
198
        if self._control_files._lock_mode != 'w':
 
199
            raise errors.NotWriteLocked(self)
 
200
        self.current_dirstate().save()
 
201
        self._inventory = None
 
202
        self._dirty = False
 
203
 
 
204
    def _generate_inventory(self):
 
205
        """Create and set self.inventory from the dirstate object.
 
206
        
 
207
        This is relatively expensive: we have to walk the entire dirstate.
 
208
        Ideally we would not, and can deprecate this function.
 
209
        """
 
210
        dirstate = self.current_dirstate()
 
211
        rows = self._dirstate._iter_rows()
 
212
        root_row = rows.next()
 
213
        inv = Inventory(root_id=root_row[0][3].decode('utf8'))
 
214
        for line in rows:
 
215
            dirname, name, kind, fileid_utf8, size, stat, link_or_sha1 = line[0]
 
216
            if dirname == '/':
 
217
                # not in this revision tree.
 
218
                continue
 
219
            parent_id = inv[inv.path2id(dirname.decode('utf8'))].file_id
 
220
            file_id = fileid_utf8.decode('utf8')
 
221
            entry = make_entry(kind, name.decode('utf8'), parent_id, file_id)
 
222
            if kind == 'file':
 
223
                #entry.executable = executable
 
224
                #entry.text_size = size
 
225
                #entry.text_sha1 = sha1
 
226
                pass
 
227
            inv.add(entry)
 
228
        self._inventory = inv
 
229
 
 
230
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
231
        #if not path:
 
232
        #    path = self.inventory.id2path(file_id)
 
233
        #    # now lookup row by path
 
234
        row, parents = self._get_row(file_id=file_id)
 
235
        assert row is not None, 'what error should this raise'
 
236
        # TODO:
 
237
        # if row stat is valid, use cached sha1, else, get a new sha1.
 
238
        path = (row[0] + '/' + row[1]).strip('/').decode('utf8')
 
239
        return self._hashcache.get_sha1(path, stat_value)
 
240
 
 
241
    def _get_inventory(self):
 
242
        """Get the inventory for the tree. This is only valid within a lock."""
 
243
        if self._inventory is not None:
 
244
            return self._inventory
 
245
        self._generate_inventory()
 
246
        return self._inventory
 
247
 
 
248
    inventory = property(_get_inventory,
 
249
                         doc="Inventory of this Tree")
 
250
 
 
251
    @needs_read_lock
 
252
    def get_parent_ids(self):
 
253
        """See Tree.get_parent_ids.
 
254
        
 
255
        This implementation requests the ids list from the dirstate file.
 
256
        """
 
257
        return self.current_dirstate().get_parent_ids()
 
258
 
 
259
    @needs_read_lock
 
260
    def get_root_id(self):
 
261
        """Return the id of this trees root"""
 
262
        return self.current_dirstate()._iter_rows().next()[0][3].decode('utf8')
 
263
 
 
264
    def _get_block_row_index(self, dirname, basename):
 
265
        """Get the coordinates for a path in the state structure.
 
266
 
 
267
        :param dirname: The dirname to lookup.
 
268
        :param basename: The basename to lookup.
 
269
        :return: A tuple describing where the path is located, or should be
 
270
            inserted. The tuple contains four fields: the block index, the row
 
271
            index, anda two booleans are True when the directory is present, and
 
272
            when the entire path is present.  There is no guarantee that either
 
273
            coordinate is currently reachable unless the found field for it is
 
274
            True. For instance, a directory not present in the state may be
 
275
            returned with a value one greater than the current highest block
 
276
            offset. The directory present field will always be True when the
 
277
            path present field is True.
 
278
        """
 
279
        assert not (dirname == '' and basename == ''), 'blackhole lookup error'
 
280
        state = self.current_dirstate()
 
281
        state._read_dirblocks_if_needed()
 
282
        block_index = bisect_left(state._dirblocks, (dirname, []))
 
283
        if (block_index == len(state._dirblocks) or
 
284
            state._dirblocks[block_index][0] != dirname):
 
285
            # no such directory - return the dir index and 0 for the row.
 
286
            return block_index, 0, False, False
 
287
        block = state._dirblocks[block_index][1] # access the rows only
 
288
        search = ((dirname, basename), [])
 
289
        row_index = bisect_left(block, search)
 
290
        if row_index == len(block) or block[row_index][0][1] != basename:
 
291
            return block_index, row_index, True, False
 
292
        return block_index, row_index, True, True
 
293
 
 
294
    def _get_row(self, file_id=None, path=None):
 
295
        """Get the dirstate row for file_id or path.
 
296
 
 
297
        If either file_id or path is supplied, it is used as the key to lookup.
 
298
        If both are supplied, the fastest lookup is used, and an error is
 
299
        raised if they do not both point at the same row.
 
300
        
 
301
        :param file_id: An optional unicode file_id to be looked up.
 
302
        :param path: An optional unicode path to be looked up.
 
303
        :return: The dirstate row tuple for path/file_id, or (None, None)
 
304
        """
 
305
        if file_id is None and path is None:
 
306
            raise errors.BzrError('must supply file_id or path')
 
307
        state = self.current_dirstate()
 
308
        state._read_dirblocks_if_needed()
 
309
        if file_id is not None:
 
310
            fileid_utf8 = file_id.encode('utf8')
 
311
        if path is not None:
 
312
            # path lookups are faster
 
313
            if path == '':
 
314
                return state._root_row
 
315
            dirname, basename = os.path.split(path.encode('utf8'))
 
316
            block_index, row_index, dir_present, file_present = \
 
317
                self._get_block_row_index(dirname, basename)
 
318
            if not file_present:
 
319
                return None, None
 
320
            row = state._dirblocks[block_index][1][row_index]
 
321
            if file_id:
 
322
                if row[0][3] != fileid_utf8:
 
323
                    raise BzrError('integrity error ? : mismatching file_id and path')
 
324
            assert row[0][3], 'unversioned row?!?!'
 
325
            return row
 
326
        else:
 
327
            for row in state._iter_rows():
 
328
                if row[0][3] == fileid_utf8:
 
329
                    return row
 
330
            return None, None
 
331
 
 
332
    def has_id(self, file_id):
 
333
        state = self.current_dirstate()
 
334
        fileid_utf8 = file_id.encode('utf8')
 
335
        row, parents = self._get_row(file_id)
 
336
        if row is None:
 
337
            return False
 
338
        return osutils.lexists(pathjoin(
 
339
                    self.basedir, row[0].decode('utf8'), row[1].decode('utf8')))
 
340
 
 
341
    @needs_read_lock
 
342
    def id2path(self, fileid):
 
343
        state = self.current_dirstate()
 
344
        fileid_utf8 = fileid.encode('utf8')
 
345
        for row, parents in state._iter_rows():
 
346
            if row[3] == fileid_utf8:
 
347
                return (row[0] + '/' + row[1]).decode('utf8').strip('/')
 
348
 
 
349
    @needs_read_lock
 
350
    def __iter__(self):
 
351
        """Iterate through file_ids for this tree.
 
352
 
 
353
        file_ids are in a WorkingTree if they are in the working inventory
 
354
        and the working file exists.
 
355
        """
 
356
        result = []
 
357
        for row, parents in self.current_dirstate()._iter_rows():
 
358
            if row[0] == '/':
 
359
                continue
 
360
            path = pathjoin(self.basedir, row[0].decode('utf8'), row[1].decode('utf8'))
 
361
            if osutils.lexists(path):
 
362
                result.append(row[3].decode('utf8'))
 
363
        return iter(result)
 
364
 
 
365
    @needs_read_lock
 
366
    def _last_revision(self):
 
367
        """See Mutable.last_revision."""
 
368
        parent_ids = self.current_dirstate().get_parent_ids()
 
369
        if parent_ids:
 
370
            return parent_ids[0].decode('utf8')
 
371
        else:
 
372
            return None
 
373
 
 
374
    @needs_tree_write_lock
 
375
    def move(self, from_paths, to_dir=None, after=False, **kwargs):
 
376
        """See WorkingTree.move()."""
 
377
        if not from_paths:
 
378
            return ()
 
379
 
 
380
        state = self.current_dirstate()
 
381
 
 
382
        # check for deprecated use of signature
 
383
        if to_dir is None:
 
384
            to_dir = kwargs.get('to_name', None)
 
385
            if to_dir is None:
 
386
                raise TypeError('You must supply a target directory')
 
387
            else:
 
388
                symbol_versioning.warn('The parameter to_name was deprecated'
 
389
                                       ' in version 0.13. Use to_dir instead',
 
390
                                       DeprecationWarning)
 
391
 
 
392
        assert not isinstance(from_paths, basestring)
 
393
        to_dir_utf8 = to_dir.encode('utf8')
 
394
        to_row_dirname, to_basename = os.path.split(to_dir_utf8)
 
395
        # check destination directory
 
396
        # get the details for it
 
397
        to_row_block_index, to_row_row_index, dir_present, row_present = \
 
398
            self._get_block_row_index(to_row_dirname, to_basename)
 
399
        if not row_present:
 
400
            raise errors.BzrMoveFailedError('', to_dir,
 
401
                errors.NotInWorkingDirectory(to_dir))
 
402
        to_row = state._dirblocks[to_row_block_index][1][to_row_row_index][0]
 
403
        # get a handle on the block itself.
 
404
        to_block_index = state._ensure_block(
 
405
            to_row_block_index, to_row_row_index, to_dir_utf8)
 
406
        to_block = state._dirblocks[to_block_index]
 
407
        to_abs = self.abspath(to_dir)
 
408
        if not isdir(to_abs):
 
409
            raise errors.BzrMoveFailedError('',to_dir,
 
410
                errors.NotADirectory(to_abs))
 
411
 
 
412
        if to_row[2] != 'directory':
 
413
            raise errors.BzrMoveFailedError('',to_dir,
 
414
                errors.NotADirectory(to_abs))
 
415
 
 
416
        if self._inventory is not None:
 
417
            update_inventory = True
 
418
            inv = self.inventory
 
419
            to_dir_ie = inv[to_dir_id]
 
420
            to_dir_id = to_row[3].decode('utf8')
 
421
        else:
 
422
            update_inventory = False
 
423
 
 
424
        # create rename entries and tuples
 
425
        for from_rel in from_paths:
 
426
            # from_rel is 'pathinroot/foo/bar'
 
427
            from_dirname, from_tail = os.path.split(from_rel)
 
428
            from_dirname = from_dirname.encode('utf8')
 
429
            from_row = self._get_row(path=from_rel)
 
430
            if from_row == (None, None):
 
431
                raise errors.BzrMoveFailedError(from_rel,to_dir,
 
432
                    errors.NotVersionedError(path=str(from_rel)))
 
433
 
 
434
            from_id = from_row[0][3].decode('utf8')
 
435
            to_rel = pathjoin(to_dir, from_tail)
 
436
            item_to_row = self._get_row(path=to_rel)
 
437
            if item_to_row != (None, None):
 
438
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
439
                    "Target is already versioned.")
 
440
 
 
441
            if from_rel == to_rel:
 
442
                raise errors.BzrMoveFailedError(from_rel, to_rel,
 
443
                    "Source and target are identical.")
 
444
 
 
445
            from_missing = not self.has_filename(from_rel)
 
446
            to_missing = not self.has_filename(to_rel)
 
447
            if after:
 
448
                move_file = False
 
449
            else:
 
450
                move_file = True
 
451
            if to_missing:
 
452
                if not move_file:
 
453
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
454
                        errors.NoSuchFile(path=to_rel,
 
455
                        extra="New file has not been created yet"))
 
456
                elif from_missing:
 
457
                    # neither path exists
 
458
                    raise errors.BzrRenameFailedError(from_rel, to_rel,
 
459
                        errors.PathsDoNotExist(paths=(from_rel, to_rel)))
 
460
            else:
 
461
                if from_missing: # implicitly just update our path mapping
 
462
                    move_file = False
 
463
                else:
 
464
                    raise errors.RenameFailedFilesExist(from_rel, to_rel,
 
465
                        extra="(Use --after to update the Bazaar id)")
 
466
 
 
467
            rollbacks = []
 
468
            def rollback_rename():
 
469
                """A single rename has failed, roll it back."""
 
470
                error = None
 
471
                for rollback in reversed(rollbacks):
 
472
                    try:
 
473
                        rollback()
 
474
                    except e:
 
475
                        error = e
 
476
                if error:
 
477
                    raise error
 
478
 
 
479
            # perform the disk move first - its the most likely failure point.
 
480
            from_rel_abs = self.abspath(from_rel)
 
481
            to_rel_abs = self.abspath(to_rel)
 
482
            try:
 
483
                osutils.rename(from_rel_abs, to_rel_abs)
 
484
            except OSError, e:
 
485
                raise errors.BzrMoveFailedError(from_rel, to_rel, e[1])
 
486
            rollbacks.append(lambda: osutils.rename(to_rel_abs, from_rel_abs))
 
487
            try:
 
488
                # perform the rename in the inventory next if needed: its easy
 
489
                # to rollback
 
490
                if update_inventory:
 
491
                    # rename the entry
 
492
                    from_entry = inv[from_id]
 
493
                    current_parent = from_entry.parent_id
 
494
                    inv.rename(from_id, to_dir_id, from_tail)
 
495
                    rollbacks.append(
 
496
                        lambda: inv.rename(from_id, current_parent, from_tail))
 
497
                # finally do the rename in the dirstate, which is a little
 
498
                # tricky to rollback, but least likely to need it.
 
499
                basename = from_tail.encode('utf8')
 
500
                old_block_index, old_row_index, dir_present, file_present = \
 
501
                    self._get_block_row_index(from_dirname, basename)
 
502
                old_block = state._dirblocks[old_block_index][1]
 
503
                # remove the old row
 
504
                old_row = old_block.pop(old_row_index)
 
505
                rollbacks.append(lambda:old_block.insert(old_row_index, old_row))
 
506
                # create new row in current block
 
507
                new_row = ((to_block[0],) + old_row[0][1:], old_row[1])
 
508
                insert_position = bisect_left(to_block[1], new_row)
 
509
                to_block[1].insert(insert_position, new_row)
 
510
                rollbacks.append(lambda:to_block[1].pop(insert_position))
 
511
                if new_row[0][2] == 'directory':
 
512
                    import pdb;pdb.set_trace()
 
513
                    # if a directory, rename all the contents of child blocks
 
514
                    # adding rollbacks as each is inserted to remove them and
 
515
                    # restore the original
 
516
                    # TODO: large scale slice assignment.
 
517
                    # setup new list
 
518
                    # save old list region
 
519
                    # move up or down the old region
 
520
                    # add rollback to move the region back
 
521
                    # assign new list to new region
 
522
                    # done
 
523
            except:
 
524
                rollback_rename()
 
525
                raise
 
526
            state._dirblock_state = dirstate.DirState.IN_MEMORY_MODIFIED
 
527
            self._dirty = True
 
528
 
 
529
        return #rename_tuples
 
530
 
 
531
    def _new_tree(self):
 
532
        """Initialize the state in this tree to be a new tree."""
 
533
        self._dirty = True
 
534
 
 
535
    @needs_read_lock
 
536
    def path2id(self, path):
 
537
        """Return the id for path in this tree."""
 
538
        state = self.current_dirstate()
 
539
        row = self._get_row(path=path)
 
540
        if row == (None, None):
 
541
            return None
 
542
        return row[0][3].decode('utf8')
 
543
 
 
544
    def read_working_inventory(self):
 
545
        """Read the working inventory.
 
546
        
 
547
        This is a meaningless operation for dirstate, but we obey it anyhow.
 
548
        """
 
549
        return self.inventory
 
550
 
 
551
    @needs_read_lock
 
552
    def revision_tree(self, revision_id):
 
553
        """See Tree.revision_tree.
 
554
 
 
555
        WorkingTree4 supplies revision_trees for any basis tree.
 
556
        """
 
557
        dirstate = self.current_dirstate()
 
558
        parent_ids = dirstate.get_parent_ids()
 
559
        if revision_id not in parent_ids:
 
560
            raise errors.NoSuchRevisionInTree(self, revision_id)
 
561
        if revision_id in dirstate.get_ghosts():
 
562
            raise errors.NoSuchRevisionInTree(self, revision_id)
 
563
        return DirStateRevisionTree(dirstate, revision_id,
 
564
            self.branch.repository)
 
565
 
 
566
    @needs_tree_write_lock
 
567
    def set_last_revision(self, new_revision):
 
568
        """Change the last revision in the working tree."""
 
569
        parents = self.get_parent_ids()
 
570
        if new_revision in (NULL_REVISION, None):
 
571
            assert len(parents) < 2, (
 
572
                "setting the last parent to none with a pending merge is "
 
573
                "unsupported.")
 
574
            self.set_parent_ids([])
 
575
        else:
 
576
            self.set_parent_ids([new_revision] + parents[1:],
 
577
                allow_leftmost_as_ghost=True)
 
578
 
 
579
    @needs_tree_write_lock
 
580
    def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
 
581
        """Set the parent ids to revision_ids.
 
582
        
 
583
        See also set_parent_trees. This api will try to retrieve the tree data
 
584
        for each element of revision_ids from the trees repository. If you have
 
585
        tree data already available, it is more efficient to use
 
586
        set_parent_trees rather than set_parent_ids. set_parent_ids is however
 
587
        an easier API to use.
 
588
 
 
589
        :param revision_ids: The revision_ids to set as the parent ids of this
 
590
            working tree. Any of these may be ghosts.
 
591
        """
 
592
        trees = []
 
593
        for revision_id in revision_ids:
 
594
            try:
 
595
                revtree = self.branch.repository.revision_tree(revision_id)
 
596
                # TODO: jam 20070213 KnitVersionedFile raises
 
597
                #       RevisionNotPresent rather than NoSuchRevision if a
 
598
                #       given revision_id is not present. Should Repository be
 
599
                #       catching it and re-raising NoSuchRevision?
 
600
            except (errors.NoSuchRevision, errors.RevisionNotPresent):
 
601
                revtree = None
 
602
            trees.append((revision_id, revtree))
 
603
        self.set_parent_trees(trees,
 
604
            allow_leftmost_as_ghost=allow_leftmost_as_ghost)
 
605
 
 
606
    @needs_tree_write_lock
 
607
    def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
 
608
        """Set the parents of the working tree.
 
609
 
 
610
        :param parents_list: A list of (revision_id, tree) tuples. 
 
611
            If tree is None, then that element is treated as an unreachable
 
612
            parent tree - i.e. a ghost.
 
613
        """
 
614
        dirstate = self.current_dirstate()
 
615
        if len(parents_list) > 0:
 
616
            if not allow_leftmost_as_ghost and parents_list[0][1] is None:
 
617
                raise errors.GhostRevisionUnusableHere(parents_list[0][0])
 
618
        real_trees = []
 
619
        ghosts = []
 
620
        # convert absent trees to the null tree, which we convert back to 
 
621
        # missing on access.
 
622
        for rev_id, tree in parents_list:
 
623
            if tree is not None:
 
624
                real_trees.append((rev_id, tree))
 
625
            else:
 
626
                real_trees.append((rev_id,
 
627
                    self.branch.repository.revision_tree(None)))
 
628
                ghosts.append(rev_id)
 
629
        dirstate.set_parent_trees(real_trees, ghosts=ghosts)
 
630
        self._dirty = True
 
631
 
 
632
    def _set_root_id(self, file_id):
 
633
        """See WorkingTree.set_root_id."""
 
634
        state = self.current_dirstate()
 
635
        state.set_path_id('', file_id)
 
636
        self._dirty = state._dirblock_state == dirstate.DirState.IN_MEMORY_MODIFIED
 
637
 
 
638
    def unlock(self):
 
639
        """Unlock in format 4 trees needs to write the entire dirstate."""
 
640
        if self._control_files._lock_count == 1:
 
641
            self._write_hashcache_if_dirty()
 
642
            # eventually we should do signature checking during read locks for
 
643
            # dirstate updates.
 
644
            if self._control_files._lock_mode == 'w':
 
645
                if self._dirty:
 
646
                    self.flush()
 
647
            self._dirstate = None
 
648
            self._inventory = None
 
649
        # reverse order of locking.
 
650
        try:
 
651
            return self._control_files.unlock()
 
652
        finally:
 
653
            self.branch.unlock()
 
654
 
 
655
    @needs_tree_write_lock
 
656
    def unversion(self, file_ids):
 
657
        """Remove the file ids in file_ids from the current versioned set.
 
658
 
 
659
        When a file_id is unversioned, all of its children are automatically
 
660
        unversioned.
 
661
 
 
662
        :param file_ids: The file ids to stop versioning.
 
663
        :raises: NoSuchId if any fileid is not currently versioned.
 
664
        """
 
665
        if not file_ids:
 
666
            return
 
667
        state = self.current_dirstate()
 
668
        state._read_dirblocks_if_needed()
 
669
        ids_to_unversion = set()
 
670
        for fileid in file_ids:
 
671
            ids_to_unversion.add(fileid.encode('utf8'))
 
672
        paths_to_unversion = set()
 
673
        # sketch:
 
674
        # check if the root is to be unversioned, if so, assert for now.
 
675
        # make a copy of the _dirblocks data 
 
676
        # during the copy,
 
677
        #  skip paths in paths_to_unversion
 
678
        #  skip ids in ids_to_unversion, and add their paths to
 
679
        #  paths_to_unversion if they are a directory
 
680
        # if there are any un-unversioned ids at the end, raise
 
681
        if state._root_row[0][3] in ids_to_unversion:
 
682
            # I haven't written the code to unversion / yet - it should be 
 
683
            # supported.
 
684
            raise errors.BzrError('Unversioning the / is not currently supported')
 
685
        new_blocks = []
 
686
        deleted_rows = []
 
687
        for block in state._dirblocks:
 
688
            # first check: is the path one to remove - it or its children
 
689
            delete_block = False
 
690
            for path in paths_to_unversion:
 
691
                if (block[0].startswith(path) and
 
692
                    (len(block[0]) == len(path) or
 
693
                     block[0][len(path)] == '/')):
 
694
                    # this path should be deleted
 
695
                    delete_block = True
 
696
                    break
 
697
            # TODO: trim paths_to_unversion as we pass by paths
 
698
            if delete_block:
 
699
                # this block is to be deleted. skip it.
 
700
                continue
 
701
            # copy undeleted rows from within the the block
 
702
            new_blocks.append((block[0], []))
 
703
            new_row = new_blocks[-1][1]
 
704
            for row, row_parents in block[1]:
 
705
                if row[3] not in ids_to_unversion:
 
706
                    new_row.append((row, row_parents))
 
707
                else:
 
708
                    # skip the row, and if its a dir mark its path to be removed
 
709
                    if row[2] == 'directory':
 
710
                        paths_to_unversion.add((row[0] + '/' + row[1]).strip('/'))
 
711
                    if row_parents:
 
712
                        deleted_rows.append((row[3], row_parents))
 
713
                    ids_to_unversion.remove(row[3])
 
714
        if ids_to_unversion:
 
715
            raise errors.NoSuchId(self, iter(ids_to_unversion).next())
 
716
        state._dirblocks = new_blocks
 
717
        for fileid_utf8, parents in deleted_rows:
 
718
            state.add_deleted(fileid_utf8, parents)
 
719
        state._dirblock_state = dirstate.DirState.IN_MEMORY_MODIFIED
 
720
        self._dirty = True
 
721
        # have to change the legacy inventory too.
 
722
        if self._inventory is not None:
 
723
            for file_id in file_ids:
 
724
                self._inventory.remove_recursive_id(file_id)
 
725
 
 
726
    @needs_tree_write_lock
 
727
    def _write_inventory(self, inv):
 
728
        """Write inventory as the current inventory."""
 
729
        assert not self._dirty, "attempting to write an inventory when the dirstate is dirty will cause data loss"
 
730
        self.current_dirstate().set_state_from_inventory(inv)
 
731
        self._dirty = True
 
732
        self.flush()
 
733
 
 
734
 
 
735
class WorkingTreeFormat4(WorkingTreeFormat3):
 
736
    """The first consolidated dirstate working tree format.
 
737
 
 
738
    This format:
 
739
        - exists within a metadir controlling .bzr
 
740
        - includes an explicit version marker for the workingtree control
 
741
          files, separate from the BzrDir format
 
742
        - modifies the hash cache format
 
743
        - is new in bzr TODO FIXME SETBEFOREMERGE
 
744
        - uses a LockDir to guard access to it.
 
745
    """
 
746
 
 
747
    def get_format_string(self):
 
748
        """See WorkingTreeFormat.get_format_string()."""
 
749
        return "Bazaar Working Tree format 4\n"
 
750
 
 
751
    def get_format_description(self):
 
752
        """See WorkingTreeFormat.get_format_description()."""
 
753
        return "Working tree format 4"
 
754
 
 
755
    def initialize(self, a_bzrdir, revision_id=None):
 
756
        """See WorkingTreeFormat.initialize().
 
757
        
 
758
        revision_id allows creating a working tree at a different
 
759
        revision than the branch is at.
 
760
        """
 
761
        if not isinstance(a_bzrdir.transport, LocalTransport):
 
762
            raise errors.NotLocalUrl(a_bzrdir.transport.base)
 
763
        transport = a_bzrdir.get_workingtree_transport(self)
 
764
        control_files = self._open_control_files(a_bzrdir)
 
765
        control_files.create_lock()
 
766
        control_files.lock_write()
 
767
        control_files.put_utf8('format', self.get_format_string())
 
768
        branch = a_bzrdir.open_branch()
 
769
        if revision_id is None:
 
770
            revision_id = branch.last_revision()
 
771
        local_path = transport.local_abspath('dirstate')
 
772
        dirstate.DirState.initialize(local_path)
 
773
        wt = WorkingTree4(a_bzrdir.root_transport.local_abspath('.'),
 
774
                         branch,
 
775
                         _format=self,
 
776
                         _bzrdir=a_bzrdir,
 
777
                         _control_files=control_files)
 
778
        wt._new_tree()
 
779
        wt.lock_write()
 
780
        try:
 
781
            #wt.current_dirstate().set_path_id('', NEWROOT)
 
782
            wt.set_last_revision(revision_id)
 
783
            wt.flush()
 
784
            basis = wt.basis_tree()
 
785
            basis.lock_read()
 
786
            transform.build_tree(basis, wt)
 
787
            basis.unlock()
 
788
        finally:
 
789
            control_files.unlock()
 
790
            wt.unlock()
 
791
        return wt
 
792
 
 
793
 
 
794
    def _open(self, a_bzrdir, control_files):
 
795
        """Open the tree itself.
 
796
        
 
797
        :param a_bzrdir: the dir for the tree.
 
798
        :param control_files: the control files for the tree.
 
799
        """
 
800
        return WorkingTree4(a_bzrdir.root_transport.local_abspath('.'),
 
801
                           branch=a_bzrdir.open_branch(),
 
802
                           _format=self,
 
803
                           _bzrdir=a_bzrdir,
 
804
                           _control_files=control_files)
 
805
 
 
806
 
 
807
class DirStateRevisionTree(Tree):
 
808
    """A revision tree pulling the inventory from a dirstate."""
 
809
 
 
810
    def __init__(self, dirstate, revision_id, repository):
 
811
        self._dirstate = dirstate
 
812
        self._revision_id = revision_id
 
813
        self._repository = repository
 
814
        self._inventory = None
 
815
        self._locked = 0
 
816
 
 
817
    def _comparison_data(self, entry, path):
 
818
        """See Tree._comparison_data."""
 
819
        if entry is None:
 
820
            return None, False, None
 
821
        # trust the entry as RevisionTree does, but this may not be
 
822
        # sensible: the entry might not have come from us?
 
823
        return entry.kind, entry.executable, None
 
824
 
 
825
    def _file_size(self, entry, stat_value):
 
826
        return entry.text_size
 
827
 
 
828
    def _generate_inventory(self):
 
829
        """Create and set self.inventory from the dirstate object.
 
830
        
 
831
        This is relatively expensive: we have to walk the entire dirstate.
 
832
        Ideally we would not, and instead would """
 
833
        assert self._locked, 'cannot generate inventory of an unlocked '\
 
834
            'dirstate revision tree'
 
835
        assert self._revision_id in self._dirstate.get_parent_ids(), \
 
836
            'parent %s has disappeared from %s' % (
 
837
            self._revision_id, self._dirstate.get_parent_ids())
 
838
        parent_index = self._dirstate.get_parent_ids().index(self._revision_id)
 
839
        rows = self._dirstate._iter_rows()
 
840
        root_row = rows.next()
 
841
        inv = Inventory(root_id=root_row[0][3].decode('utf8'),
 
842
            revision_id=self._revision_id)
 
843
        for line in rows:
 
844
            revid, kind, dirname, name, size, executable, sha1 = line[1][parent_index]
 
845
            if not revid:
 
846
                # not in this revision tree.
 
847
                continue
 
848
            parent_id = inv[inv.path2id(dirname.decode('utf8'))].file_id
 
849
            file_id = line[0][3].decode('utf8')
 
850
            entry = make_entry(kind, name.decode('utf8'), parent_id, file_id)
 
851
            entry.revision = revid.decode('utf8')
 
852
            if kind == 'file':
 
853
                entry.executable = executable
 
854
                entry.text_size = size
 
855
                entry.text_sha1 = sha1
 
856
            inv.add(entry)
 
857
        self._inventory = inv
 
858
 
 
859
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
860
        # TODO: if path is present, fast-path on that, as inventory
 
861
        # might not be present
 
862
        ie = self.inventory[file_id]
 
863
        if ie.kind == "file":
 
864
            return ie.text_sha1
 
865
        return None
 
866
 
 
867
    def get_file(self, file_id):
 
868
        return StringIO(self.get_file_text(file_id))
 
869
 
 
870
    def get_file_lines(self, file_id):
 
871
        ie = self.inventory[file_id]
 
872
        return self._repository.weave_store.get_weave(file_id,
 
873
                self._repository.get_transaction()).get_lines(ie.revision)
 
874
 
 
875
    def get_file_size(self, file_id):
 
876
        return self.inventory[file_id].text_size
 
877
 
 
878
    def get_file_text(self, file_id):
 
879
        return ''.join(self.get_file_lines(file_id))
 
880
 
 
881
    def _get_inventory(self):
 
882
        if self._inventory is not None:
 
883
            return self._inventory
 
884
        self._generate_inventory()
 
885
        return self._inventory
 
886
 
 
887
    inventory = property(_get_inventory,
 
888
                         doc="Inventory of this Tree")
 
889
 
 
890
    def get_parent_ids(self):
 
891
        """The parents of a tree in the dirstate are not cached."""
 
892
        return self._repository.get_revision(self._revision_id).parent_ids
 
893
 
 
894
    def has_filename(self, filename):
 
895
        return bool(self.inventory.path2id(filename))
 
896
 
 
897
    def kind(self, file_id):
 
898
        return self.inventory[file_id].kind
 
899
 
 
900
    def is_executable(self, file_id, path=None):
 
901
        ie = self.inventory[file_id]
 
902
        if ie.kind != "file":
 
903
            return None 
 
904
        return ie.executable
 
905
 
 
906
    def lock_read(self):
 
907
        """Lock the tree for a set of operations."""
 
908
        self._locked += 1
 
909
 
 
910
    def unlock(self):
 
911
        """Unlock, freeing any cache memory used during the lock."""
 
912
        # outside of a lock, the inventory is suspect: release it.
 
913
        self._locked -=1
 
914
        if not self._locked:
 
915
            self._inventory = None
 
916
            self._locked = False