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.errors import BzrError, BzrCheckError
36
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
37
appendpath, sha_strings)
38
from bzrlib.trace import mutter
39
from bzrlib.errors import NotVersionedError
42
class InventoryEntry(object):
43
"""Description of a versioned file.
45
An InventoryEntry has the following fields, which are also
46
present in the XML inventory-entry element:
51
(within the parent directory)
54
file_id of the parent directory, or ROOT_ID
57
the revision_id in which this variation of this file was
61
Indicates that this file should be executable on systems
65
sha-1 of the text of the file
68
size in bytes of the text of the file
70
(reading a version 4 tree created a text_id field.)
75
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
77
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
84
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
Traceback (most recent call last):
87
BzrError: inventory already contains entry with id {2323}
88
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
89
InventoryFile('2324', 'bye.c', parent_id='123')
90
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
91
InventoryDirectory('2325', 'wibble', parent_id='123')
92
>>> i.path2id('src/wibble')
96
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
100
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
102
... assert i.path2id(path)
109
>>> i.id2path('2326').replace('\\\\', '/')
110
'src/wibble/wibble.c'
113
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
'text_id', 'parent_id', 'children', 'executable',
117
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
118
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
121
def detect_changes(self, old_entry):
122
"""Return a (text_modified, meta_modified) from this to old_entry.
124
_read_tree_state must have been called on self and old_entry prior to
125
calling detect_changes.
129
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
130
output_to, reverse=False):
131
"""Perform a diff from this to to_entry.
133
text_diff will be used for textual difference calculation.
134
This is a template method, override _diff in child classes.
136
self._read_tree_state(tree.id2path(self.file_id), tree)
138
# cannot diff from one kind to another - you must do a removal
139
# and an addif they do not match.
140
assert self.kind == to_entry.kind
141
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
143
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
146
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
147
output_to, reverse=False):
148
"""Perform a diff between two entries of the same kind."""
150
def find_previous_heads(self, previous_inventories, entry_weave):
151
"""Return the revisions and entries that directly preceed this.
153
Returned as a map from revision to inventory entry.
155
This is a map containing the file revisions in all parents
156
for which the file exists, and its revision is not a parent of
157
any other. If the file is new, the set will be empty.
159
def get_ancestors(weave, entry):
160
return set(map(weave.idx_to_name,
161
weave.inclusions([weave.lookup(entry.revision)])))
164
for inv in previous_inventories:
165
if self.file_id in inv:
166
ie = inv[self.file_id]
167
assert ie.file_id == self.file_id
168
if ie.revision in heads:
169
assert heads[ie.revision] == ie
171
# may want to add it.
172
# may already be covered:
173
already_present = 0 != len(
174
[head for head in heads
175
if ie.revision in head_ancestors[head]])
177
# an ancestor of a known head.
180
ancestors = get_ancestors(entry_weave, ie)
181
# may knock something else out:
182
check_heads = list(heads.keys())
183
for head in check_heads:
184
if head in ancestors:
185
# this head is not really a head
187
head_ancestors[ie.revision] = ancestors
188
heads[ie.revision] = ie
191
def get_tar_item(self, root, dp, now, tree):
192
"""Get a tarfile item and a file stream for its content."""
193
item = tarfile.TarInfo(os.path.join(root, dp))
194
# TODO: would be cool to actually set it to the timestamp of the
195
# revision it was last changed
197
fileobj = self._put_in_tar(item, tree)
201
"""Return true if the object this entry represents has textual data.
203
Note that textual data includes binary content.
205
Also note that all entries get weave files created for them.
206
This attribute is primarily used when upgrading from old trees that
207
did not have the weave index for all inventory entries.
211
def __init__(self, file_id, name, parent_id, text_id=None):
212
"""Create an InventoryEntry
214
The filename must be a single component, relative to the
215
parent directory; it cannot be a whole path or relative name.
217
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
222
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
223
Traceback (most recent call last):
224
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
226
assert isinstance(name, basestring), name
227
if '/' in name or '\\' in name:
228
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
230
self.executable = False
232
self.text_sha1 = None
233
self.text_size = None
234
self.file_id = file_id
236
self.text_id = text_id
237
self.parent_id = parent_id
238
self.symlink_target = None
240
def kind_character(self):
241
"""Return a short kind indicator useful for appending to names."""
242
raise BzrError('unknown kind %r' % self.kind)
244
known_kinds = ('file', 'directory', 'symlink', 'root_directory')
246
def _put_in_tar(self, item, tree):
247
"""populate item for stashing in a tar, and return the content stream.
249
If no content is available, return None.
251
raise BzrError("don't know how to export {%s} of kind %r" %
252
(self.file_id, self.kind))
254
def put_on_disk(self, dest, dp, tree):
255
"""Create a representation of self on disk in the prefix dest.
257
This is a template method - implement _put_on_disk in subclasses.
259
fullpath = appendpath(dest, dp)
260
self._put_on_disk(fullpath, tree)
261
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
263
def _put_on_disk(self, fullpath, tree):
264
"""Put this entry onto disk at fullpath, from tree tree."""
265
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
267
def sorted_children(self):
268
l = self.children.items()
273
def versionable_kind(kind):
274
return kind in ('file', 'directory', 'symlink')
276
def check(self, checker, rev_id, inv, tree):
277
"""Check this inventory entry is intact.
279
This is a template method, override _check for kind specific
282
if self.parent_id != None:
283
if not inv.has_id(self.parent_id):
284
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
285
% (self.parent_id, rev_id))
286
self._check(checker, rev_id, tree)
288
def _check(self, checker, rev_id, tree):
289
"""Check this inventory entry for kind specific errors."""
290
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
295
"""Clone this inventory entry."""
296
raise NotImplementedError
298
def _get_snapshot_change(self, previous_entries):
299
if len(previous_entries) > 1:
301
elif len(previous_entries) == 0:
304
return 'modified/renamed/reparented'
307
return ("%s(%r, %r, parent_id=%r)"
308
% (self.__class__.__name__,
313
def snapshot(self, revision, path, previous_entries,
314
work_tree, weave_store, transaction):
315
"""Make a snapshot of this entry which may or may not have changed.
317
This means that all its fields are populated, that it has its
318
text stored in the text store or weave.
320
mutter('new parents of %s are %r', path, previous_entries)
321
self._read_tree_state(path, work_tree)
322
if len(previous_entries) == 1:
323
# cannot be unchanged unless there is only one parent file rev.
324
parent_ie = previous_entries.values()[0]
325
if self._unchanged(parent_ie):
326
mutter("found unchanged entry")
327
self.revision = parent_ie.revision
329
return self.snapshot_revision(revision, previous_entries,
330
work_tree, weave_store, transaction)
332
def snapshot_revision(self, revision, previous_entries, work_tree,
333
weave_store, transaction):
334
"""Record this revision unconditionally."""
335
mutter('new revision for {%s}', self.file_id)
336
self.revision = revision
337
change = self._get_snapshot_change(previous_entries)
338
self._snapshot_text(previous_entries, work_tree, weave_store,
342
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
343
"""Record the 'text' of this entry, whatever form that takes.
345
This default implementation simply adds an empty text.
347
mutter('storing file {%s} in revision {%s}',
348
self.file_id, self.revision)
349
self._add_text_to_weave([], file_parents, weave_store, transaction)
351
def __eq__(self, other):
352
if not isinstance(other, InventoryEntry):
353
return NotImplemented
355
return ((self.file_id == other.file_id)
356
and (self.name == other.name)
357
and (other.symlink_target == self.symlink_target)
358
and (self.text_sha1 == other.text_sha1)
359
and (self.text_size == other.text_size)
360
and (self.text_id == other.text_id)
361
and (self.parent_id == other.parent_id)
362
and (self.kind == other.kind)
363
and (self.revision == other.revision)
364
and (self.executable == other.executable)
367
def __ne__(self, other):
368
return not (self == other)
371
raise ValueError('not hashable')
373
def _unchanged(self, previous_ie):
374
"""Has this entry changed relative to previous_ie.
376
This method should be overriden in child classes.
379
# different inv parent
380
if previous_ie.parent_id != self.parent_id:
383
elif previous_ie.name != self.name:
387
def _read_tree_state(self, path, work_tree):
388
"""Populate fields in the inventory entry from the given tree.
390
Note that this should be modified to be a noop on virtual trees
391
as all entries created there are prepopulated.
395
class RootEntry(InventoryEntry):
397
def _check(self, checker, rev_id, tree):
398
"""See InventoryEntry._check"""
400
def __init__(self, file_id):
401
self.file_id = file_id
403
self.kind = 'root_directory'
404
self.parent_id = None
407
def __eq__(self, other):
408
if not isinstance(other, RootEntry):
409
return NotImplemented
411
return (self.file_id == other.file_id) \
412
and (self.children == other.children)
415
class InventoryDirectory(InventoryEntry):
416
"""A directory in an inventory."""
418
def _check(self, checker, rev_id, tree):
419
"""See InventoryEntry._check"""
420
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
421
raise BzrCheckError('directory {%s} has text in revision {%s}'
422
% (self.file_id, rev_id))
425
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
426
other.revision = self.revision
427
# note that children are *not* copied; they're pulled across when
431
def __init__(self, file_id, name, parent_id):
432
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
434
self.kind = 'directory'
436
def kind_character(self):
437
"""See InventoryEntry.kind_character."""
440
def _put_in_tar(self, item, tree):
441
"""See InventoryEntry._put_in_tar."""
442
item.type = tarfile.DIRTYPE
449
def _put_on_disk(self, fullpath, tree):
450
"""See InventoryEntry._put_on_disk."""
454
class InventoryFile(InventoryEntry):
455
"""A file in an inventory."""
457
def _check(self, checker, rev_id, tree):
458
"""See InventoryEntry._check"""
459
revision = self.revision
460
t = (self.file_id, revision)
461
if t in checker.checked_texts:
462
prev_sha = checker.checked_texts[t]
463
if prev_sha != self.text_sha1:
464
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
465
(self.file_id, rev_id))
467
checker.repeated_text_cnt += 1
469
mutter('check version {%s} of {%s}', rev_id, self.file_id)
470
file_lines = tree.get_file_lines(self.file_id)
471
checker.checked_text_cnt += 1
472
if self.text_size != sum(map(len, file_lines)):
473
raise BzrCheckError('text {%s} wrong size' % self.text_id)
474
if self.text_sha1 != sha_strings(file_lines):
475
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
476
checker.checked_texts[t] = self.text_sha1
479
other = InventoryFile(self.file_id, self.name, self.parent_id)
480
other.executable = self.executable
481
other.text_id = self.text_id
482
other.text_sha1 = self.text_sha1
483
other.text_size = self.text_size
484
other.revision = self.revision
487
def detect_changes(self, old_entry):
488
"""See InventoryEntry.detect_changes."""
489
assert self.text_sha1 != None
490
assert old_entry.text_sha1 != None
491
text_modified = (self.text_sha1 != old_entry.text_sha1)
492
meta_modified = (self.executable != old_entry.executable)
493
return text_modified, meta_modified
495
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
496
output_to, reverse=False):
497
"""See InventoryEntry._diff."""
498
from_text = tree.get_file(self.file_id).readlines()
500
to_text = to_tree.get_file(to_entry.file_id).readlines()
504
text_diff(from_label, from_text,
505
to_label, to_text, output_to)
507
text_diff(to_label, to_text,
508
from_label, from_text, output_to)
511
"""See InventoryEntry.has_text."""
514
def __init__(self, file_id, name, parent_id):
515
super(InventoryFile, self).__init__(file_id, name, parent_id)
518
def kind_character(self):
519
"""See InventoryEntry.kind_character."""
522
def _put_in_tar(self, item, tree):
523
"""See InventoryEntry._put_in_tar."""
524
item.type = tarfile.REGTYPE
525
fileobj = tree.get_file(self.file_id)
526
item.size = self.text_size
527
if tree.is_executable(self.file_id):
533
def _put_on_disk(self, fullpath, tree):
534
"""See InventoryEntry._put_on_disk."""
535
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
536
if tree.is_executable(self.file_id):
537
os.chmod(fullpath, 0755)
539
def _read_tree_state(self, path, work_tree):
540
"""See InventoryEntry._read_tree_state."""
541
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
542
self.executable = work_tree.is_executable(self.file_id)
544
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
545
"""See InventoryEntry._snapshot_text."""
546
mutter('storing file {%s} in revision {%s}',
547
self.file_id, self.revision)
548
# special case to avoid diffing on renames or
550
if (len(file_parents) == 1
551
and self.text_sha1 == file_parents.values()[0].text_sha1
552
and self.text_size == file_parents.values()[0].text_size):
553
previous_ie = file_parents.values()[0]
554
weave_store.add_identical_text(
555
self.file_id, previous_ie.revision,
556
self.revision, file_parents, transaction)
558
new_lines = work_tree.get_file(self.file_id).readlines()
559
self._add_text_to_weave(new_lines, file_parents, weave_store,
561
self.text_sha1 = sha_strings(new_lines)
562
self.text_size = sum(map(len, new_lines))
565
def _unchanged(self, previous_ie):
566
"""See InventoryEntry._unchanged."""
567
compatible = super(InventoryFile, self)._unchanged(previous_ie)
568
if self.text_sha1 != previous_ie.text_sha1:
571
# FIXME: 20050930 probe for the text size when getting sha1
572
# in _read_tree_state
573
self.text_size = previous_ie.text_size
577
class InventoryLink(InventoryEntry):
578
"""A file in an inventory."""
580
__slots__ = ['symlink_target']
582
def _check(self, checker, rev_id, tree):
583
"""See InventoryEntry._check"""
584
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
585
raise BzrCheckError('symlink {%s} has text in revision {%s}'
586
% (self.file_id, rev_id))
587
if self.symlink_target == None:
588
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
589
% (self.file_id, rev_id))
592
other = InventoryLink(self.file_id, self.name, self.parent_id)
593
other.symlink_target = self.symlink_target
594
other.revision = self.revision
597
def detect_changes(self, old_entry):
598
"""See InventoryEntry.detect_changes."""
599
# FIXME: which _modified field should we use ? RBC 20051003
600
text_modified = (self.symlink_target != old_entry.symlink_target)
602
mutter(" symlink target changed")
603
meta_modified = False
604
return text_modified, meta_modified
606
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
607
output_to, reverse=False):
608
"""See InventoryEntry._diff."""
609
from_text = self.symlink_target
610
if to_entry is not None:
611
to_text = to_entry.symlink_target
616
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
619
print >>output_to, '=== target was %r' % self.symlink_target
621
print >>output_to, '=== target is %r' % self.symlink_target
623
def __init__(self, file_id, name, parent_id):
624
super(InventoryLink, self).__init__(file_id, name, parent_id)
625
self.kind = 'symlink'
627
def kind_character(self):
628
"""See InventoryEntry.kind_character."""
631
def _put_in_tar(self, item, tree):
632
"""See InventoryEntry._put_in_tar."""
633
iterm.type = tarfile.SYMTYPE
637
item.linkname = self.symlink_target
640
def _put_on_disk(self, fullpath, tree):
641
"""See InventoryEntry._put_on_disk."""
643
os.symlink(self.symlink_target, fullpath)
645
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
647
def _read_tree_state(self, path, work_tree):
648
"""See InventoryEntry._read_tree_state."""
649
self.symlink_target = work_tree.get_symlink_target(self.file_id)
651
def _unchanged(self, previous_ie):
652
"""See InventoryEntry._unchanged."""
653
compatible = super(InventoryLink, self)._unchanged(previous_ie)
654
if self.symlink_target != previous_ie.symlink_target:
659
class Inventory(object):
660
"""Inventory of versioned files in a tree.
662
This describes which file_id is present at each point in the tree,
663
and possibly the SHA-1 or other information about the file.
664
Entries can be looked up either by path or by file_id.
666
The inventory represents a typical unix file tree, with
667
directories containing files and subdirectories. We never store
668
the full path to a file, because renaming a directory implicitly
669
moves all of its contents. This class internally maintains a
670
lookup tree that allows the children under a directory to be
673
InventoryEntry objects must not be modified after they are
674
inserted, other than through the Inventory API.
676
>>> inv = Inventory()
677
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
678
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
679
>>> inv['123-123'].name
682
May be treated as an iterator or set to look up file ids:
684
>>> bool(inv.path2id('hello.c'))
689
May also look up by name:
691
>>> [x[0] for x in inv.iter_entries()]
693
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
694
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
695
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
697
def __init__(self, root_id=ROOT_ID):
698
"""Create or read an inventory.
700
If a working directory is specified, the inventory is read
701
from there. If the file is specified, read from that. If not,
702
the inventory is created empty.
704
The inventory is created with a default root directory, with
707
# We are letting Branch.initialize() create a unique inventory
708
# root id. Rather than generating a random one here.
710
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
711
self.root = RootEntry(root_id)
712
self._byid = {self.root.file_id: self.root}
716
other = Inventory(self.root.file_id)
717
# copy recursively so we know directories will be added before
718
# their children. There are more efficient ways than this...
719
for path, entry in self.iter_entries():
720
if entry == self.root:
722
other.add(entry.copy())
727
return iter(self._byid)
731
"""Returns number of entries."""
732
return len(self._byid)
735
def iter_entries(self, from_dir=None):
736
"""Return (path, entry) pairs, in order by name."""
740
elif isinstance(from_dir, basestring):
741
from_dir = self._byid[from_dir]
743
kids = from_dir.children.items()
745
for name, ie in kids:
747
if ie.kind == 'directory':
748
for cn, cie in self.iter_entries(from_dir=ie.file_id):
749
yield os.path.join(name, cn), cie
753
"""Return list of (path, ie) for all entries except the root.
755
This may be faster than iter_entries.
758
def descend(dir_ie, dir_path):
759
kids = dir_ie.children.items()
761
for name, ie in kids:
762
child_path = os.path.join(dir_path, name)
763
accum.append((child_path, ie))
764
if ie.kind == 'directory':
765
descend(ie, child_path)
767
descend(self.root, '')
771
def directories(self):
772
"""Return (path, entry) pairs for all directories, including the root.
775
def descend(parent_ie, parent_path):
776
accum.append((parent_path, parent_ie))
778
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
781
for name, child_ie in kids:
782
child_path = os.path.join(parent_path, name)
783
descend(child_ie, child_path)
784
descend(self.root, '')
789
def __contains__(self, file_id):
790
"""True if this entry contains a file with given id.
792
>>> inv = Inventory()
793
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
794
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
800
return file_id in self._byid
803
def __getitem__(self, file_id):
804
"""Return the entry for given file_id.
806
>>> inv = Inventory()
807
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
808
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
809
>>> inv['123123'].name
813
return self._byid[file_id]
816
raise BzrError("can't look up file_id None")
818
raise BzrError("file_id {%s} not in inventory" % file_id)
821
def get_file_kind(self, file_id):
822
return self._byid[file_id].kind
824
def get_child(self, parent_id, filename):
825
return self[parent_id].children.get(filename)
828
def add(self, entry):
829
"""Add entry to inventory.
831
To add a file to a branch ready to be committed, use Branch.add,
834
Returns the new entry object.
836
if entry.file_id in self._byid:
837
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
839
if entry.parent_id == ROOT_ID or entry.parent_id is None:
840
entry.parent_id = self.root.file_id
843
parent = self._byid[entry.parent_id]
845
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
847
if parent.children.has_key(entry.name):
848
raise BzrError("%s is already versioned" %
849
appendpath(self.id2path(parent.file_id), entry.name))
851
self._byid[entry.file_id] = entry
852
parent.children[entry.name] = entry
856
def add_path(self, relpath, kind, file_id=None):
857
"""Add entry from a path.
859
The immediate parent must already be versioned.
861
Returns the new entry object."""
862
from bzrlib.branch import gen_file_id
864
parts = bzrlib.osutils.splitpath(relpath)
866
raise BzrError("cannot re-add root of inventory")
869
file_id = gen_file_id(relpath)
871
parent_path = parts[:-1]
872
parent_id = self.path2id(parent_path)
873
if parent_id == None:
874
raise NotVersionedError(parent_path)
876
if kind == 'directory':
877
ie = InventoryDirectory(file_id, parts[-1], parent_id)
879
ie = InventoryFile(file_id, parts[-1], parent_id)
880
elif kind == 'symlink':
881
ie = InventoryLink(file_id, parts[-1], parent_id)
883
raise BzrError("unknown kind %r" % kind)
887
def __delitem__(self, file_id):
888
"""Remove entry by id.
890
>>> inv = Inventory()
891
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
892
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
901
assert self[ie.parent_id].children[ie.name] == ie
903
# TODO: Test deleting all children; maybe hoist to a separate
905
if ie.kind == 'directory':
906
for cie in ie.children.values():
907
del self[cie.file_id]
910
del self._byid[file_id]
911
del self[ie.parent_id].children[ie.name]
914
def __eq__(self, other):
915
"""Compare two sets by comparing their contents.
921
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
922
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
925
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
926
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
930
if not isinstance(other, Inventory):
931
return NotImplemented
933
if len(self._byid) != len(other._byid):
934
# shortcut: obviously not the same
937
return self._byid == other._byid
940
def __ne__(self, other):
941
return not self.__eq__(other)
945
raise ValueError('not hashable')
948
def get_idpath(self, file_id):
949
"""Return a list of file_ids for the path to an entry.
951
The list contains one element for each directory followed by
952
the id of the file itself. So the length of the returned list
953
is equal to the depth of the file in the tree, counting the
954
root directory as depth 1.
957
while file_id != None:
959
ie = self._byid[file_id]
961
raise BzrError("file_id {%s} not found in inventory" % file_id)
962
p.insert(0, ie.file_id)
963
file_id = ie.parent_id
967
def id2path(self, file_id):
968
"""Return as a list the path to file_id.
971
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
972
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
973
>>> print i.id2path('foo-id').replace(os.sep, '/')
976
# get all names, skipping root
977
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
978
return os.sep.join(p)
982
def path2id(self, name):
983
"""Walk down through directories to return entry of last component.
985
names may be either a list of path components, or a single
986
string, in which case it is automatically split.
988
This returns the entry of the last component in the path,
989
which may be either a file or a directory.
991
Returns None iff the path is not found.
993
if isinstance(name, types.StringTypes):
994
name = splitpath(name)
996
mutter("lookup path %r" % name)
1001
cie = parent.children[f]
1002
assert cie.name == f
1003
assert cie.parent_id == parent.file_id
1006
# or raise an error?
1009
return parent.file_id
1012
def has_filename(self, names):
1013
return bool(self.path2id(names))
1016
def has_id(self, file_id):
1017
return self._byid.has_key(file_id)
1020
def rename(self, file_id, new_parent_id, new_name):
1021
"""Move a file within the inventory.
1023
This can change either the name, or the parent, or both.
1025
This does not move the working file."""
1026
if not is_valid_name(new_name):
1027
raise BzrError("not an acceptable filename: %r" % new_name)
1029
new_parent = self._byid[new_parent_id]
1030
if new_name in new_parent.children:
1031
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
1033
new_parent_idpath = self.get_idpath(new_parent_id)
1034
if file_id in new_parent_idpath:
1035
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
1036
% (self.id2path(file_id), self.id2path(new_parent_id)))
1038
file_ie = self._byid[file_id]
1039
old_parent = self._byid[file_ie.parent_id]
1041
# TODO: Don't leave things messed up if this fails
1043
del old_parent.children[file_ie.name]
1044
new_parent.children[new_name] = file_ie
1046
file_ie.name = new_name
1047
file_ie.parent_id = new_parent_id
1054
def is_valid_name(name):
1056
if _NAME_RE == None:
1057
_NAME_RE = re.compile(r'^[^/\\]+$')
1059
return bool(_NAME_RE.match(name))