1
# Copyright (C) 2005, 2006 Canonical Ltd
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.
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.
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
17
"""WorkingTree4 format and implementation.
19
WorkingTree4 provides the dirstate based working tree logic.
21
To get a WorkingTree, call bzrdir.open_workingtree() or
22
WorkingTree.open(dir).
25
from cStringIO import StringIO
28
from bzrlib.lazy_import import lazy_import
29
lazy_import(globals(), """
30
from bisect import bisect_left
32
from copy import deepcopy
43
conflicts as _mod_conflicts,
59
from bzrlib.transport import get_transport
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 (
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,
95
from bzrlib.tree import Tree
96
from bzrlib.workingtree import WorkingTree, WorkingTree3, WorkingTreeFormat3
99
class WorkingTree4(WorkingTree3):
100
"""This is the Format 4 working tree.
102
This differs from WorkingTree3 by:
103
- having a consolidated internal dirstate.
104
- not having a regular inventory attribute.
106
This is new in bzr TODO FIXME SETMEBEFORE MERGE.
109
def __init__(self, basedir,
114
"""Construct a WorkingTree for basedir.
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).
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
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)
147
# is this scan needed ? it makes things kinda slow.
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
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):
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, '')
178
def current_dirstate(self):
179
"""Return the current dirstate object.
181
This is not part of the tree interface and only exposed for ease of
184
:raises errors.NotWriteLocked: when not in a lock.
185
XXX: This should probably be errors.NotLocked.
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
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
204
def _generate_inventory(self):
205
"""Create and set self.inventory from the dirstate object.
207
This is relatively expensive: we have to walk the entire dirstate.
208
Ideally we would not, and can deprecate this function.
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'))
215
dirname, name, kind, fileid_utf8, size, stat, link_or_sha1 = line[0]
217
# not in this revision tree.
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)
223
#entry.executable = executable
224
#entry.text_size = size
225
#entry.text_sha1 = sha1
228
self._inventory = inv
230
def get_file_sha1(self, file_id, path=None, stat_value=None):
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'
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)
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
248
inventory = property(_get_inventory,
249
doc="Inventory of this Tree")
252
def get_parent_ids(self):
253
"""See Tree.get_parent_ids.
255
This implementation requests the ids list from the dirstate file.
257
return self.current_dirstate().get_parent_ids()
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')
264
def _get_block_row_index(self, dirname, basename):
265
"""Get the coordinates for a path in the state structure.
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.
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
294
def _get_row(self, file_id=None, path=None):
295
"""Get the dirstate row for file_id or path.
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.
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)
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')
312
# path lookups are faster
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)
320
row = state._dirblocks[block_index][1][row_index]
322
if row[0][3] != fileid_utf8:
323
raise BzrError('integrity error ? : mismatching file_id and path')
324
assert row[0][3], 'unversioned row?!?!'
327
for row in state._iter_rows():
328
if row[0][3] == fileid_utf8:
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)
338
return osutils.lexists(pathjoin(
339
self.basedir, row[0].decode('utf8'), row[1].decode('utf8')))
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('/')
351
"""Iterate through file_ids for this tree.
353
file_ids are in a WorkingTree if they are in the working inventory
354
and the working file exists.
357
for row, parents in self.current_dirstate()._iter_rows():
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'))
366
def _last_revision(self):
367
"""See Mutable.last_revision."""
368
parent_ids = self.current_dirstate().get_parent_ids()
370
return parent_ids[0].decode('utf8')
374
@needs_tree_write_lock
375
def move(self, from_paths, to_dir=None, after=False, **kwargs):
376
"""See WorkingTree.move()."""
380
state = self.current_dirstate()
382
# check for deprecated use of signature
384
to_dir = kwargs.get('to_name', None)
386
raise TypeError('You must supply a target directory')
388
symbol_versioning.warn('The parameter to_name was deprecated'
389
' in version 0.13. Use to_dir instead',
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)
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))
412
if to_row[2] != 'directory':
413
raise errors.BzrMoveFailedError('',to_dir,
414
errors.NotADirectory(to_abs))
416
if self._inventory is not None:
417
update_inventory = True
419
to_dir_ie = inv[to_dir_id]
420
to_dir_id = to_row[3].decode('utf8')
422
update_inventory = False
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)))
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.")
441
if from_rel == to_rel:
442
raise errors.BzrMoveFailedError(from_rel, to_rel,
443
"Source and target are identical.")
445
from_missing = not self.has_filename(from_rel)
446
to_missing = not self.has_filename(to_rel)
453
raise errors.BzrMoveFailedError(from_rel, to_rel,
454
errors.NoSuchFile(path=to_rel,
455
extra="New file has not been created yet"))
457
# neither path exists
458
raise errors.BzrRenameFailedError(from_rel, to_rel,
459
errors.PathsDoNotExist(paths=(from_rel, to_rel)))
461
if from_missing: # implicitly just update our path mapping
464
raise errors.RenameFailedFilesExist(from_rel, to_rel,
465
extra="(Use --after to update the Bazaar id)")
468
def rollback_rename():
469
"""A single rename has failed, roll it back."""
471
for rollback in reversed(rollbacks):
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)
483
osutils.rename(from_rel_abs, to_rel_abs)
485
raise errors.BzrMoveFailedError(from_rel, to_rel, e[1])
486
rollbacks.append(lambda: osutils.rename(to_rel_abs, from_rel_abs))
488
# perform the rename in the inventory next if needed: its easy
492
from_entry = inv[from_id]
493
current_parent = from_entry.parent_id
494
inv.rename(from_id, to_dir_id, from_tail)
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]
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.
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
526
state._dirblock_state = dirstate.DirState.IN_MEMORY_MODIFIED
529
return #rename_tuples
532
"""Initialize the state in this tree to be a new tree."""
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):
542
return row[0][3].decode('utf8')
544
def read_working_inventory(self):
545
"""Read the working inventory.
547
This is a meaningless operation for dirstate, but we obey it anyhow.
549
return self.inventory
552
def revision_tree(self, revision_id):
553
"""See Tree.revision_tree.
555
WorkingTree4 supplies revision_trees for any basis tree.
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)
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 "
574
self.set_parent_ids([])
576
self.set_parent_ids([new_revision] + parents[1:],
577
allow_leftmost_as_ghost=True)
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.
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.
589
:param revision_ids: The revision_ids to set as the parent ids of this
590
working tree. Any of these may be ghosts.
593
for revision_id in revision_ids:
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):
602
trees.append((revision_id, revtree))
603
self.set_parent_trees(trees,
604
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
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.
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.
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])
620
# convert absent trees to the null tree, which we convert back to
622
for rev_id, tree in parents_list:
624
real_trees.append((rev_id, tree))
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)
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
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
644
if self._control_files._lock_mode == 'w':
647
self._dirstate = None
648
self._inventory = None
649
# reverse order of locking.
651
return self._control_files.unlock()
655
@needs_tree_write_lock
656
def unversion(self, file_ids):
657
"""Remove the file ids in file_ids from the current versioned set.
659
When a file_id is unversioned, all of its children are automatically
662
:param file_ids: The file ids to stop versioning.
663
:raises: NoSuchId if any fileid is not currently versioned.
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()
674
# check if the root is to be unversioned, if so, assert for now.
675
# make a copy of the _dirblocks data
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
684
raise errors.BzrError('Unversioning the / is not currently supported')
687
for block in state._dirblocks:
688
# first check: is the path one to remove - it or its children
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
697
# TODO: trim paths_to_unversion as we pass by paths
699
# this block is to be deleted. skip it.
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))
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('/'))
712
deleted_rows.append((row[3], row_parents))
713
ids_to_unversion.remove(row[3])
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
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)
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)
735
class WorkingTreeFormat4(WorkingTreeFormat3):
736
"""The first consolidated dirstate working tree 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.
747
def get_format_string(self):
748
"""See WorkingTreeFormat.get_format_string()."""
749
return "Bazaar Working Tree format 4\n"
751
def get_format_description(self):
752
"""See WorkingTreeFormat.get_format_description()."""
753
return "Working tree format 4"
755
def initialize(self, a_bzrdir, revision_id=None):
756
"""See WorkingTreeFormat.initialize().
758
revision_id allows creating a working tree at a different
759
revision than the branch is at.
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('.'),
777
_control_files=control_files)
781
#wt.current_dirstate().set_path_id('', NEWROOT)
782
wt.set_last_revision(revision_id)
784
basis = wt.basis_tree()
786
transform.build_tree(basis, wt)
789
control_files.unlock()
794
def _open(self, a_bzrdir, control_files):
795
"""Open the tree itself.
797
:param a_bzrdir: the dir for the tree.
798
:param control_files: the control files for the tree.
800
return WorkingTree4(a_bzrdir.root_transport.local_abspath('.'),
801
branch=a_bzrdir.open_branch(),
804
_control_files=control_files)
807
class DirStateRevisionTree(Tree):
808
"""A revision tree pulling the inventory from a dirstate."""
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
817
def _comparison_data(self, entry, path):
818
"""See Tree._comparison_data."""
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
825
def _file_size(self, entry, stat_value):
826
return entry.text_size
828
def _generate_inventory(self):
829
"""Create and set self.inventory from the dirstate object.
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)
844
revid, kind, dirname, name, size, executable, sha1 = line[1][parent_index]
846
# not in this revision tree.
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')
853
entry.executable = executable
854
entry.text_size = size
855
entry.text_sha1 = sha1
857
self._inventory = inv
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":
867
def get_file(self, file_id):
868
return StringIO(self.get_file_text(file_id))
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)
875
def get_file_size(self, file_id):
876
return self.inventory[file_id].text_size
878
def get_file_text(self, file_id):
879
return ''.join(self.get_file_lines(file_id))
881
def _get_inventory(self):
882
if self._inventory is not None:
883
return self._inventory
884
self._generate_inventory()
885
return self._inventory
887
inventory = property(_get_inventory,
888
doc="Inventory of this Tree")
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
894
def has_filename(self, filename):
895
return bool(self.inventory.path2id(filename))
897
def kind(self, file_id):
898
return self.inventory[file_id].kind
900
def is_executable(self, file_id, path=None):
901
ie = self.inventory[file_id]
902
if ie.kind != "file":
907
"""Lock the tree for a set of operations."""
911
"""Unlock, freeing any cache memory used during the lock."""
912
# outside of a lock, the inventory is suspect: release it.
915
self._inventory = None