1
# (C) 2005 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
18
# TODO: Maybe also keep the full path of the entry, and the children?
19
# But those depend on its position within a particular inventory, and
20
# it would be nice not to need to hold the backpointer here.
22
# This should really be an id randomly assigned when the tree is
23
# created, but it's not for now.
34
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
35
appendpath, sha_strings)
36
from bzrlib.trace import mutter
37
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
38
BzrError, BzrCheckError)
41
class InventoryEntry(object):
42
"""Description of a versioned file.
44
An InventoryEntry has the following fields, which are also
45
present in the XML inventory-entry element:
50
(within the parent directory)
53
file_id of the parent directory, or ROOT_ID
56
the revision_id in which this variation of this file was
60
Indicates that this file should be executable on systems
64
sha-1 of the text of the file
67
size in bytes of the text of the file
69
(reading a version 4 tree created a text_id field.)
74
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
75
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
76
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
77
InventoryFile('2323', 'hello.c', parent_id='123')
78
>>> for j in i.iter_entries():
81
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
82
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
83
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
84
Traceback (most recent call last):
86
BzrError: inventory already contains entry with id {2323}
87
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
88
InventoryFile('2324', 'bye.c', parent_id='123')
89
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
90
InventoryDirectory('2325', 'wibble', parent_id='123')
91
>>> i.path2id('src/wibble')
95
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
96
InventoryFile('2326', 'wibble.c', parent_id='2325')
98
InventoryFile('2326', 'wibble.c', parent_id='2325')
99
>>> for path, entry in i.iter_entries():
100
... print path.replace('\\\\', '/') # for win32 os.sep
101
... assert i.path2id(path)
108
>>> i.id2path('2326').replace('\\\\', '/')
109
'src/wibble/wibble.c'
112
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
113
'text_id', 'parent_id', 'children', 'executable',
116
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
117
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
120
def detect_changes(self, old_entry):
121
"""Return a (text_modified, meta_modified) from this to old_entry.
123
_read_tree_state must have been called on self and old_entry prior to
124
calling detect_changes.
128
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
129
output_to, reverse=False):
130
"""Perform a diff from this to to_entry.
132
text_diff will be used for textual difference calculation.
133
This is a template method, override _diff in child classes.
135
self._read_tree_state(tree.id2path(self.file_id), tree)
137
# cannot diff from one kind to another - you must do a removal
138
# and an addif they do not match.
139
assert self.kind == to_entry.kind
140
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
142
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
145
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
146
output_to, reverse=False):
147
"""Perform a diff between two entries of the same kind."""
149
def find_previous_heads(self, previous_inventories, entry_weave):
150
"""Return the revisions and entries that directly preceed this.
152
Returned as a map from revision to inventory entry.
154
This is a map containing the file revisions in all parents
155
for which the file exists, and its revision is not a parent of
156
any other. If the file is new, the set will be empty.
158
def get_ancestors(weave, entry):
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
163
for inv in previous_inventories:
164
if self.file_id in inv:
165
ie = inv[self.file_id]
166
assert ie.file_id == self.file_id
167
if ie.revision in heads:
168
# fixup logic, there was a bug in revision updates.
169
# with x bit support.
171
if heads[ie.revision].executable != ie.executable:
172
heads[ie.revision].executable = False
173
ie.executable = False
174
except AttributeError:
176
assert heads[ie.revision] == ie
178
# may want to add it.
179
# may already be covered:
180
already_present = 0 != len(
181
[head for head in heads
182
if ie.revision in head_ancestors[head]])
184
# an ancestor of a known head.
187
ancestors = get_ancestors(entry_weave, ie)
188
# may knock something else out:
189
check_heads = list(heads.keys())
190
for head in check_heads:
191
if head in ancestors:
192
# this head is not really a head
194
head_ancestors[ie.revision] = ancestors
195
heads[ie.revision] = ie
198
def get_tar_item(self, root, dp, now, tree):
199
"""Get a tarfile item and a file stream for its content."""
200
item = tarfile.TarInfo(os.path.join(root, dp))
201
# TODO: would be cool to actually set it to the timestamp of the
202
# revision it was last changed
204
fileobj = self._put_in_tar(item, tree)
208
"""Return true if the object this entry represents has textual data.
210
Note that textual data includes binary content.
212
Also note that all entries get weave files created for them.
213
This attribute is primarily used when upgrading from old trees that
214
did not have the weave index for all inventory entries.
218
def __init__(self, file_id, name, parent_id, text_id=None):
219
"""Create an InventoryEntry
221
The filename must be a single component, relative to the
222
parent directory; it cannot be a whole path or relative name.
224
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
229
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
230
Traceback (most recent call last):
231
InvalidEntryName: Invalid entry name: src/hello.c
233
assert isinstance(name, basestring), name
234
if '/' in name or '\\' in name:
235
raise InvalidEntryName(name=name)
236
self.executable = False
238
self.text_sha1 = None
239
self.text_size = None
240
self.file_id = file_id
242
self.text_id = text_id
243
self.parent_id = parent_id
244
self.symlink_target = None
246
def kind_character(self):
247
"""Return a short kind indicator useful for appending to names."""
248
raise BzrError('unknown kind %r' % self.kind)
250
known_kinds = ('file', 'directory', 'symlink', 'root_directory')
252
def _put_in_tar(self, item, tree):
253
"""populate item for stashing in a tar, and return the content stream.
255
If no content is available, return None.
257
raise BzrError("don't know how to export {%s} of kind %r" %
258
(self.file_id, self.kind))
260
def put_on_disk(self, dest, dp, tree):
261
"""Create a representation of self on disk in the prefix dest.
263
This is a template method - implement _put_on_disk in subclasses.
265
fullpath = appendpath(dest, dp)
266
self._put_on_disk(fullpath, tree)
267
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
269
def _put_on_disk(self, fullpath, tree):
270
"""Put this entry onto disk at fullpath, from tree tree."""
271
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
273
def sorted_children(self):
274
l = self.children.items()
279
def versionable_kind(kind):
280
return kind in ('file', 'directory', 'symlink')
282
def check(self, checker, rev_id, inv, tree):
283
"""Check this inventory entry is intact.
285
This is a template method, override _check for kind specific
288
if self.parent_id != None:
289
if not inv.has_id(self.parent_id):
290
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
291
% (self.parent_id, rev_id))
292
self._check(checker, rev_id, tree)
294
def _check(self, checker, rev_id, tree):
295
"""Check this inventory entry for kind specific errors."""
296
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
301
"""Clone this inventory entry."""
302
raise NotImplementedError
304
def _get_snapshot_change(self, previous_entries):
305
if len(previous_entries) > 1:
307
elif len(previous_entries) == 0:
310
return 'modified/renamed/reparented'
313
return ("%s(%r, %r, parent_id=%r)"
314
% (self.__class__.__name__,
319
def snapshot(self, revision, path, previous_entries,
320
work_tree, weave_store, transaction):
321
"""Make a snapshot of this entry which may or may not have changed.
323
This means that all its fields are populated, that it has its
324
text stored in the text store or weave.
326
mutter('new parents of %s are %r', path, previous_entries)
327
self._read_tree_state(path, work_tree)
328
if len(previous_entries) == 1:
329
# cannot be unchanged unless there is only one parent file rev.
330
parent_ie = previous_entries.values()[0]
331
if self._unchanged(parent_ie):
332
mutter("found unchanged entry")
333
self.revision = parent_ie.revision
335
return self.snapshot_revision(revision, previous_entries,
336
work_tree, weave_store, transaction)
338
def snapshot_revision(self, revision, previous_entries, work_tree,
339
weave_store, transaction):
340
"""Record this revision unconditionally."""
341
mutter('new revision for {%s}', self.file_id)
342
self.revision = revision
343
change = self._get_snapshot_change(previous_entries)
344
self._snapshot_text(previous_entries, work_tree, weave_store,
348
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
349
"""Record the 'text' of this entry, whatever form that takes.
351
This default implementation simply adds an empty text.
353
mutter('storing file {%s} in revision {%s}',
354
self.file_id, self.revision)
355
self._add_text_to_weave([], file_parents, weave_store, transaction)
357
def __eq__(self, other):
358
if not isinstance(other, InventoryEntry):
359
return NotImplemented
361
return ((self.file_id == other.file_id)
362
and (self.name == other.name)
363
and (other.symlink_target == self.symlink_target)
364
and (self.text_sha1 == other.text_sha1)
365
and (self.text_size == other.text_size)
366
and (self.text_id == other.text_id)
367
and (self.parent_id == other.parent_id)
368
and (self.kind == other.kind)
369
and (self.revision == other.revision)
370
and (self.executable == other.executable)
373
def __ne__(self, other):
374
return not (self == other)
377
raise ValueError('not hashable')
379
def _unchanged(self, previous_ie):
380
"""Has this entry changed relative to previous_ie.
382
This method should be overriden in child classes.
385
# different inv parent
386
if previous_ie.parent_id != self.parent_id:
389
elif previous_ie.name != self.name:
393
def _read_tree_state(self, path, work_tree):
394
"""Populate fields in the inventory entry from the given tree.
396
Note that this should be modified to be a noop on virtual trees
397
as all entries created there are prepopulated.
399
# TODO: Rather than running this manually, we should check the
400
# working sha1 and other expensive properties when they're
401
# first requested, or preload them if they're already known
402
pass # nothing to do by default
405
class RootEntry(InventoryEntry):
407
def _check(self, checker, rev_id, tree):
408
"""See InventoryEntry._check"""
410
def __init__(self, file_id):
411
self.file_id = file_id
413
self.kind = 'root_directory'
414
self.parent_id = None
417
def __eq__(self, other):
418
if not isinstance(other, RootEntry):
419
return NotImplemented
421
return (self.file_id == other.file_id) \
422
and (self.children == other.children)
425
class InventoryDirectory(InventoryEntry):
426
"""A directory in an inventory."""
428
def _check(self, checker, rev_id, tree):
429
"""See InventoryEntry._check"""
430
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
431
raise BzrCheckError('directory {%s} has text in revision {%s}'
432
% (self.file_id, rev_id))
435
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
436
other.revision = self.revision
437
# note that children are *not* copied; they're pulled across when
441
def __init__(self, file_id, name, parent_id):
442
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
444
self.kind = 'directory'
446
def kind_character(self):
447
"""See InventoryEntry.kind_character."""
450
def _put_in_tar(self, item, tree):
451
"""See InventoryEntry._put_in_tar."""
452
item.type = tarfile.DIRTYPE
459
def _put_on_disk(self, fullpath, tree):
460
"""See InventoryEntry._put_on_disk."""
464
class InventoryFile(InventoryEntry):
465
"""A file in an inventory."""
467
def _check(self, checker, rev_id, tree):
468
"""See InventoryEntry._check"""
469
revision = self.revision
470
t = (self.file_id, revision)
471
if t in checker.checked_texts:
472
prev_sha = checker.checked_texts[t]
473
if prev_sha != self.text_sha1:
474
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
475
(self.file_id, rev_id))
477
checker.repeated_text_cnt += 1
479
mutter('check version {%s} of {%s}', rev_id, self.file_id)
480
file_lines = tree.get_file_lines(self.file_id)
481
checker.checked_text_cnt += 1
482
if self.text_size != sum(map(len, file_lines)):
483
raise BzrCheckError('text {%s} wrong size' % self.text_id)
484
if self.text_sha1 != sha_strings(file_lines):
485
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
486
checker.checked_texts[t] = self.text_sha1
489
other = InventoryFile(self.file_id, self.name, self.parent_id)
490
other.executable = self.executable
491
other.text_id = self.text_id
492
other.text_sha1 = self.text_sha1
493
other.text_size = self.text_size
494
other.revision = self.revision
497
def detect_changes(self, old_entry):
498
"""See InventoryEntry.detect_changes."""
499
assert self.text_sha1 != None
500
assert old_entry.text_sha1 != None
501
text_modified = (self.text_sha1 != old_entry.text_sha1)
502
meta_modified = (self.executable != old_entry.executable)
503
return text_modified, meta_modified
505
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
506
output_to, reverse=False):
507
"""See InventoryEntry._diff."""
508
from_text = tree.get_file(self.file_id).readlines()
510
to_text = to_tree.get_file(to_entry.file_id).readlines()
514
text_diff(from_label, from_text,
515
to_label, to_text, output_to)
517
text_diff(to_label, to_text,
518
from_label, from_text, output_to)
521
"""See InventoryEntry.has_text."""
524
def __init__(self, file_id, name, parent_id):
525
super(InventoryFile, self).__init__(file_id, name, parent_id)
528
def kind_character(self):
529
"""See InventoryEntry.kind_character."""
532
def _put_in_tar(self, item, tree):
533
"""See InventoryEntry._put_in_tar."""
534
item.type = tarfile.REGTYPE
535
fileobj = tree.get_file(self.file_id)
536
item.size = self.text_size
537
if tree.is_executable(self.file_id):
543
def _put_on_disk(self, fullpath, tree):
544
"""See InventoryEntry._put_on_disk."""
545
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
546
if tree.is_executable(self.file_id):
547
os.chmod(fullpath, 0755)
549
def _read_tree_state(self, path, work_tree):
550
"""See InventoryEntry._read_tree_state."""
551
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
552
self.executable = work_tree.is_executable(self.file_id)
554
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
555
"""See InventoryEntry._snapshot_text."""
556
mutter('storing file {%s} in revision {%s}',
557
self.file_id, self.revision)
558
# special case to avoid diffing on renames or
560
if (len(file_parents) == 1
561
and self.text_sha1 == file_parents.values()[0].text_sha1
562
and self.text_size == file_parents.values()[0].text_size):
563
previous_ie = file_parents.values()[0]
564
weave_store.add_identical_text(
565
self.file_id, previous_ie.revision,
566
self.revision, file_parents, transaction)
568
new_lines = work_tree.get_file(self.file_id).readlines()
569
self._add_text_to_weave(new_lines, file_parents, weave_store,
571
self.text_sha1 = sha_strings(new_lines)
572
self.text_size = sum(map(len, new_lines))
575
def _unchanged(self, previous_ie):
576
"""See InventoryEntry._unchanged."""
577
compatible = super(InventoryFile, self)._unchanged(previous_ie)
578
if self.text_sha1 != previous_ie.text_sha1:
581
# FIXME: 20050930 probe for the text size when getting sha1
582
# in _read_tree_state
583
self.text_size = previous_ie.text_size
584
if self.executable != previous_ie.executable:
589
class InventoryLink(InventoryEntry):
590
"""A file in an inventory."""
592
__slots__ = ['symlink_target']
594
def _check(self, checker, rev_id, tree):
595
"""See InventoryEntry._check"""
596
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
597
raise BzrCheckError('symlink {%s} has text in revision {%s}'
598
% (self.file_id, rev_id))
599
if self.symlink_target == None:
600
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
601
% (self.file_id, rev_id))
604
other = InventoryLink(self.file_id, self.name, self.parent_id)
605
other.symlink_target = self.symlink_target
606
other.revision = self.revision
609
def detect_changes(self, old_entry):
610
"""See InventoryEntry.detect_changes."""
611
# FIXME: which _modified field should we use ? RBC 20051003
612
text_modified = (self.symlink_target != old_entry.symlink_target)
614
mutter(" symlink target changed")
615
meta_modified = False
616
return text_modified, meta_modified
618
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
619
output_to, reverse=False):
620
"""See InventoryEntry._diff."""
621
from_text = self.symlink_target
622
if to_entry is not None:
623
to_text = to_entry.symlink_target
628
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
631
print >>output_to, '=== target was %r' % self.symlink_target
633
print >>output_to, '=== target is %r' % self.symlink_target
635
def __init__(self, file_id, name, parent_id):
636
super(InventoryLink, self).__init__(file_id, name, parent_id)
637
self.kind = 'symlink'
639
def kind_character(self):
640
"""See InventoryEntry.kind_character."""
643
def _put_in_tar(self, item, tree):
644
"""See InventoryEntry._put_in_tar."""
645
iterm.type = tarfile.SYMTYPE
649
item.linkname = self.symlink_target
652
def _put_on_disk(self, fullpath, tree):
653
"""See InventoryEntry._put_on_disk."""
655
os.symlink(self.symlink_target, fullpath)
657
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
659
def _read_tree_state(self, path, work_tree):
660
"""See InventoryEntry._read_tree_state."""
661
self.symlink_target = work_tree.get_symlink_target(self.file_id)
663
def _unchanged(self, previous_ie):
664
"""See InventoryEntry._unchanged."""
665
compatible = super(InventoryLink, self)._unchanged(previous_ie)
666
if self.symlink_target != previous_ie.symlink_target:
671
class Inventory(object):
672
"""Inventory of versioned files in a tree.
674
This describes which file_id is present at each point in the tree,
675
and possibly the SHA-1 or other information about the file.
676
Entries can be looked up either by path or by file_id.
678
The inventory represents a typical unix file tree, with
679
directories containing files and subdirectories. We never store
680
the full path to a file, because renaming a directory implicitly
681
moves all of its contents. This class internally maintains a
682
lookup tree that allows the children under a directory to be
685
InventoryEntry objects must not be modified after they are
686
inserted, other than through the Inventory API.
688
>>> inv = Inventory()
689
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
690
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
691
>>> inv['123-123'].name
694
May be treated as an iterator or set to look up file ids:
696
>>> bool(inv.path2id('hello.c'))
701
May also look up by name:
703
>>> [x[0] for x in inv.iter_entries()]
705
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
706
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
707
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
709
def __init__(self, root_id=ROOT_ID):
710
"""Create or read an inventory.
712
If a working directory is specified, the inventory is read
713
from there. If the file is specified, read from that. If not,
714
the inventory is created empty.
716
The inventory is created with a default root directory, with
719
# We are letting Branch.initialize() create a unique inventory
720
# root id. Rather than generating a random one here.
722
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
723
self.root = RootEntry(root_id)
724
self._byid = {self.root.file_id: self.root}
728
other = Inventory(self.root.file_id)
729
# copy recursively so we know directories will be added before
730
# their children. There are more efficient ways than this...
731
for path, entry in self.iter_entries():
732
if entry == self.root:
734
other.add(entry.copy())
739
return iter(self._byid)
743
"""Returns number of entries."""
744
return len(self._byid)
747
def iter_entries(self, from_dir=None):
748
"""Return (path, entry) pairs, in order by name."""
752
elif isinstance(from_dir, basestring):
753
from_dir = self._byid[from_dir]
755
kids = from_dir.children.items()
757
for name, ie in kids:
759
if ie.kind == 'directory':
760
for cn, cie in self.iter_entries(from_dir=ie.file_id):
761
yield os.path.join(name, cn), cie
765
"""Return list of (path, ie) for all entries except the root.
767
This may be faster than iter_entries.
770
def descend(dir_ie, dir_path):
771
kids = dir_ie.children.items()
773
for name, ie in kids:
774
child_path = os.path.join(dir_path, name)
775
accum.append((child_path, ie))
776
if ie.kind == 'directory':
777
descend(ie, child_path)
779
descend(self.root, '')
783
def directories(self):
784
"""Return (path, entry) pairs for all directories, including the root.
787
def descend(parent_ie, parent_path):
788
accum.append((parent_path, parent_ie))
790
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
793
for name, child_ie in kids:
794
child_path = os.path.join(parent_path, name)
795
descend(child_ie, child_path)
796
descend(self.root, '')
801
def __contains__(self, file_id):
802
"""True if this entry contains a file with given id.
804
>>> inv = Inventory()
805
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
806
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
812
return file_id in self._byid
815
def __getitem__(self, file_id):
816
"""Return the entry for given file_id.
818
>>> inv = Inventory()
819
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
820
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
821
>>> inv['123123'].name
825
return self._byid[file_id]
828
raise BzrError("can't look up file_id None")
830
raise BzrError("file_id {%s} not in inventory" % file_id)
833
def get_file_kind(self, file_id):
834
return self._byid[file_id].kind
836
def get_child(self, parent_id, filename):
837
return self[parent_id].children.get(filename)
840
def add(self, entry):
841
"""Add entry to inventory.
843
To add a file to a branch ready to be committed, use Branch.add,
846
Returns the new entry object.
848
if entry.file_id in self._byid:
849
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
851
if entry.parent_id == ROOT_ID or entry.parent_id is None:
852
entry.parent_id = self.root.file_id
855
parent = self._byid[entry.parent_id]
857
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
859
if parent.children.has_key(entry.name):
860
raise BzrError("%s is already versioned" %
861
appendpath(self.id2path(parent.file_id), entry.name))
863
self._byid[entry.file_id] = entry
864
parent.children[entry.name] = entry
868
def add_path(self, relpath, kind, file_id=None):
869
"""Add entry from a path.
871
The immediate parent must already be versioned.
873
Returns the new entry object."""
874
from bzrlib.branch import gen_file_id
876
parts = bzrlib.osutils.splitpath(relpath)
878
raise BzrError("cannot re-add root of inventory")
881
file_id = gen_file_id(relpath)
883
parent_path = parts[:-1]
884
parent_id = self.path2id(parent_path)
885
if parent_id == None:
886
raise NotVersionedError(parent_path)
888
if kind == 'directory':
889
ie = InventoryDirectory(file_id, parts[-1], parent_id)
891
ie = InventoryFile(file_id, parts[-1], parent_id)
892
elif kind == 'symlink':
893
ie = InventoryLink(file_id, parts[-1], parent_id)
895
raise BzrError("unknown kind %r" % kind)
899
def __delitem__(self, file_id):
900
"""Remove entry by id.
902
>>> inv = Inventory()
903
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
904
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
913
assert self[ie.parent_id].children[ie.name] == ie
915
# TODO: Test deleting all children; maybe hoist to a separate
917
if ie.kind == 'directory':
918
for cie in ie.children.values():
919
del self[cie.file_id]
922
del self._byid[file_id]
923
del self[ie.parent_id].children[ie.name]
926
def __eq__(self, other):
927
"""Compare two sets by comparing their contents.
933
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
934
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
937
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
938
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
942
if not isinstance(other, Inventory):
943
return NotImplemented
945
if len(self._byid) != len(other._byid):
946
# shortcut: obviously not the same
949
return self._byid == other._byid
952
def __ne__(self, other):
953
return not self.__eq__(other)
957
raise ValueError('not hashable')
960
def get_idpath(self, file_id):
961
"""Return a list of file_ids for the path to an entry.
963
The list contains one element for each directory followed by
964
the id of the file itself. So the length of the returned list
965
is equal to the depth of the file in the tree, counting the
966
root directory as depth 1.
969
while file_id != None:
971
ie = self._byid[file_id]
973
raise BzrError("file_id {%s} not found in inventory" % file_id)
974
p.insert(0, ie.file_id)
975
file_id = ie.parent_id
979
def id2path(self, file_id):
980
"""Return as a list the path to file_id.
983
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
984
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
985
>>> print i.id2path('foo-id').replace(os.sep, '/')
988
# get all names, skipping root
989
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
990
return os.sep.join(p)
994
def path2id(self, name):
995
"""Walk down through directories to return entry of last component.
997
names may be either a list of path components, or a single
998
string, in which case it is automatically split.
1000
This returns the entry of the last component in the path,
1001
which may be either a file or a directory.
1003
Returns None iff the path is not found.
1005
if isinstance(name, types.StringTypes):
1006
name = splitpath(name)
1008
mutter("lookup path %r" % name)
1013
cie = parent.children[f]
1014
assert cie.name == f
1015
assert cie.parent_id == parent.file_id
1018
# or raise an error?
1021
return parent.file_id
1024
def has_filename(self, names):
1025
return bool(self.path2id(names))
1028
def has_id(self, file_id):
1029
return self._byid.has_key(file_id)
1032
def rename(self, file_id, new_parent_id, new_name):
1033
"""Move a file within the inventory.
1035
This can change either the name, or the parent, or both.
1037
This does not move the working file."""
1038
if not is_valid_name(new_name):
1039
raise BzrError("not an acceptable filename: %r" % new_name)
1041
new_parent = self._byid[new_parent_id]
1042
if new_name in new_parent.children:
1043
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1045
new_parent_idpath = self.get_idpath(new_parent_id)
1046
if file_id in new_parent_idpath:
1047
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1048
% (self.id2path(file_id), self.id2path(new_parent_id)))
1050
file_ie = self._byid[file_id]
1051
old_parent = self._byid[file_ie.parent_id]
1053
# TODO: Don't leave things messed up if this fails
1055
del old_parent.children[file_ie.name]
1056
new_parent.children[new_name] = file_ie
1058
file_ie.name = new_name
1059
file_ie.parent_id = new_parent_id
1066
def is_valid_name(name):
1068
if _NAME_RE == None:
1069
_NAME_RE = re.compile(r'^[^/\\]+$')
1071
return bool(_NAME_RE.match(name))