78
80
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
81
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
82
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> for j in i.iter_entries():
83
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
84
>>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
85
>>> for ix, j in enumerate(i.iter_entries()):
86
... print (j[0] == shouldbe[ix], j[1])
85
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
88
(True, RootEntry('TREE_ROOT', u'', parent_id=None, revision=None))
89
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
90
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
87
91
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
88
92
Traceback (most recent call last):
90
94
BzrError: inventory already contains entry with id {2323}
91
95
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
92
InventoryFile('2324', 'bye.c', parent_id='123')
96
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
97
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
94
InventoryDirectory('2325', 'wibble', parent_id='123')
98
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
99
>>> i.path2id('src/wibble')
99
103
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
100
InventoryFile('2326', 'wibble.c', parent_id='2325')
104
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
InventoryFile('2326', 'wibble.c', parent_id='2325')
106
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
107
>>> for path, entry in i.iter_entries():
104
... print path.replace('\\\\', '/') # for win32 os.sep
105
109
... assert i.path2id(path)
111
116
src/wibble/wibble.c
112
>>> i.id2path('2326').replace('\\\\', '/')
117
>>> i.id2path('2326')
113
118
'src/wibble/wibble.c'
121
# Constants returned by describe_change()
123
# TODO: These should probably move to some kind of FileChangeDescription
124
# class; that's like what's inside a TreeDelta but we want to be able to
125
# generate them just for one file at a time.
127
MODIFIED_AND_RENAMED = 'modified and renamed'
116
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
117
'text_id', 'parent_id', 'children', 'executable',
120
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
121
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
124
131
def detect_changes(self, old_entry):
125
132
"""Return a (text_modified, meta_modified) from this to old_entry.
150
157
output_to, reverse=False):
151
158
"""Perform a diff between two entries of the same kind."""
153
def find_previous_heads(self, previous_inventories, entry_weave):
154
"""Return the revisions and entries that directly preceed this.
160
def find_previous_heads(self, previous_inventories,
161
versioned_file_store,
164
"""Return the revisions and entries that directly precede this.
156
166
Returned as a map from revision to inventory entry.
158
168
This is a map containing the file revisions in all parents
159
169
for which the file exists, and its revision is not a parent of
160
170
any other. If the file is new, the set will be empty.
172
:param versioned_file_store: A store where ancestry data on this
173
file id can be queried.
174
:param transaction: The transaction that queries to the versioned
175
file store should be completed under.
176
:param entry_vf: The entry versioned file, if its already available.
162
178
def get_ancestors(weave, entry):
163
return set(map(weave.idx_to_name,
164
weave.inclusions([weave.lookup(entry.revision)])))
179
return set(weave.get_ancestry(entry.revision))
180
# revision:ie mapping for each ie found in previous_inventories.
182
# revision:ie mapping with one revision for each head.
184
# revision: ancestor list for each head
166
185
head_ancestors = {}
186
# identify candidate head revision ids.
167
187
for inv in previous_inventories:
168
188
if self.file_id in inv:
169
189
ie = inv[self.file_id]
170
190
assert ie.file_id == self.file_id
171
if ie.revision in heads:
172
# fixup logic, there was a bug in revision updates.
173
# with x bit support.
191
if ie.revision in candidates:
192
# same revision value in two different inventories:
193
# correct possible inconsistencies:
194
# * there was a bug in revision updates with 'x' bit
175
if heads[ie.revision].executable != ie.executable:
176
heads[ie.revision].executable = False
197
if candidates[ie.revision].executable != ie.executable:
198
candidates[ie.revision].executable = False
177
199
ie.executable = False
178
200
except AttributeError:
180
assert heads[ie.revision] == ie
202
# must now be the same.
203
assert candidates[ie.revision] == ie
182
# may want to add it.
183
# may already be covered:
184
already_present = 0 != len(
185
[head for head in heads
186
if ie.revision in head_ancestors[head]])
188
# an ancestor of a known head.
191
ancestors = get_ancestors(entry_weave, ie)
192
# may knock something else out:
193
check_heads = list(heads.keys())
194
for head in check_heads:
195
if head in ancestors:
196
# this head is not really a head
198
head_ancestors[ie.revision] = ancestors
199
heads[ie.revision] = ie
205
# add this revision as a candidate.
206
candidates[ie.revision] = ie
208
# common case optimisation
209
if len(candidates) == 1:
210
# if there is only one candidate revision found
211
# then we can opening the versioned file to access ancestry:
212
# there cannot be any ancestors to eliminate when there is
213
# only one revision available.
214
heads[ie.revision] = ie
217
# eliminate ancestors amongst the available candidates:
218
# heads are those that are not an ancestor of any other candidate
219
# - this provides convergence at a per-file level.
220
for ie in candidates.values():
221
# may be an ancestor of a known head:
222
already_present = 0 != len(
223
[head for head in heads
224
if ie.revision in head_ancestors[head]])
226
# an ancestor of an analyzed candidate.
228
# not an ancestor of a known head:
229
# load the versioned file for this file id if needed
231
entry_vf = versioned_file_store.get_weave_or_empty(
232
self.file_id, transaction)
233
ancestors = get_ancestors(entry_vf, ie)
234
# may knock something else out:
235
check_heads = list(heads.keys())
236
for head in check_heads:
237
if head in ancestors:
238
# this previously discovered 'head' is not
239
# really a head - its an ancestor of the newly
242
head_ancestors[ie.revision] = ancestors
243
heads[ie.revision] = ie
202
246
def get_tar_item(self, root, dp, now, tree):
203
247
"""Get a tarfile item and a file stream for its content."""
204
item = tarfile.TarInfo(os.path.join(root, dp))
248
item = tarfile.TarInfo(pathjoin(root, dp))
205
249
# TODO: would be cool to actually set it to the timestamp of the
206
250
# revision it was last changed
267
311
This is a template method - implement _put_on_disk in subclasses.
269
fullpath = appendpath(dest, dp)
313
fullpath = pathjoin(dest, dp)
270
314
self._put_on_disk(fullpath, tree)
271
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
315
mutter(" export {%s} kind %s to %s", self.file_id,
273
318
def _put_on_disk(self, fullpath, tree):
274
319
"""Put this entry onto disk at fullpath, from tree tree."""
275
320
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
277
322
def sorted_children(self):
278
l = self.children.items()
323
return sorted(self.children.items())
283
326
def versionable_kind(kind):
300
351
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
301
352
(self.kind, rev_id))
305
355
"""Clone this inventory entry."""
306
356
raise NotImplementedError
308
def _get_snapshot_change(self, previous_entries):
309
if len(previous_entries) > 1:
311
elif len(previous_entries) == 0:
359
def describe_change(old_entry, new_entry):
360
"""Describe the change between old_entry and this.
362
This smells of being an InterInventoryEntry situation, but as its
363
the first one, we're making it a static method for now.
365
An entry with a different parent, or different name is considered
366
to be renamed. Reparenting is an internal detail.
367
Note that renaming the parent does not trigger a rename for the
370
# TODO: Perhaps return an object rather than just a string
371
if old_entry is new_entry:
372
# also the case of both being None
374
elif old_entry is None:
314
return 'modified/renamed/reparented'
376
elif new_entry is None:
378
text_modified, meta_modified = new_entry.detect_changes(old_entry)
379
if text_modified or meta_modified:
383
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
384
if old_entry.parent_id != new_entry.parent_id:
386
elif old_entry.name != new_entry.name:
390
if renamed and not modified:
391
return InventoryEntry.RENAMED
392
if modified and not renamed:
394
if modified and renamed:
395
return InventoryEntry.MODIFIED_AND_RENAMED
316
398
def __repr__(self):
317
return ("%s(%r, %r, parent_id=%r)"
399
return ("%s(%r, %r, parent_id=%r, revision=%r)"
318
400
% (self.__class__.__name__,
323
406
def snapshot(self, revision, path, previous_entries,
324
work_tree, weave_store, transaction):
407
work_tree, commit_builder):
325
408
"""Make a snapshot of this entry which may or may not have changed.
327
410
This means that all its fields are populated, that it has its
336
421
mutter("found unchanged entry")
337
422
self.revision = parent_ie.revision
338
423
return "unchanged"
339
return self.snapshot_revision(revision, previous_entries,
340
work_tree, weave_store, transaction)
342
def snapshot_revision(self, revision, previous_entries, work_tree,
343
weave_store, transaction):
344
"""Record this revision unconditionally."""
345
mutter('new revision for {%s}', self.file_id)
424
return self._snapshot_into_revision(revision, previous_entries,
425
work_tree, commit_builder)
427
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
429
"""Record this revision unconditionally into a store.
431
The entry's last-changed revision property (`revision`) is updated to
432
that of the new revision.
434
:param revision: id of the new revision that is being recorded.
436
:returns: String description of the commit (e.g. "merged", "modified"), etc.
438
mutter('new revision {%s} for {%s}', revision, self.file_id)
346
439
self.revision = revision
347
change = self._get_snapshot_change(previous_entries)
348
self._snapshot_text(previous_entries, work_tree, weave_store,
440
self._snapshot_text(previous_entries, work_tree, commit_builder)
352
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
442
def _snapshot_text(self, file_parents, work_tree, commit_builder):
353
443
"""Record the 'text' of this entry, whatever form that takes.
355
445
This default implementation simply adds an empty text.
357
mutter('storing file {%s} in revision {%s}',
358
self.file_id, self.revision)
359
self._add_text_to_weave([], file_parents, weave_store, transaction)
447
raise NotImplementedError(self._snapshot_text)
361
449
def __eq__(self, other):
362
450
if not isinstance(other, InventoryEntry):
405
493
# first requested, or preload them if they're already known
406
494
pass # nothing to do by default
496
def _forget_tree_state(self):
409
500
class RootEntry(InventoryEntry):
502
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
503
'text_id', 'parent_id', 'children', 'executable',
504
'revision', 'symlink_target']
411
506
def _check(self, checker, rev_id, tree):
412
507
"""See InventoryEntry._check"""
429
525
class InventoryDirectory(InventoryEntry):
430
526
"""A directory in an inventory."""
528
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
529
'text_id', 'parent_id', 'children', 'executable',
530
'revision', 'symlink_target']
432
532
def _check(self, checker, rev_id, tree):
433
533
"""See InventoryEntry._check"""
434
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
534
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
435
535
raise BzrCheckError('directory {%s} has text in revision {%s}'
436
536
% (self.file_id, rev_id))
464
564
"""See InventoryEntry._put_on_disk."""
465
565
os.mkdir(fullpath)
567
def _snapshot_text(self, file_parents, work_tree, commit_builder):
568
"""See InventoryEntry._snapshot_text."""
569
commit_builder.modified_directory(self.file_id, file_parents)
468
572
class InventoryFile(InventoryEntry):
469
573
"""A file in an inventory."""
471
def _check(self, checker, rev_id, tree):
575
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
576
'text_id', 'parent_id', 'children', 'executable',
577
'revision', 'symlink_target']
579
def _check(self, checker, tree_revision_id, tree):
472
580
"""See InventoryEntry._check"""
473
revision = self.revision
474
t = (self.file_id, revision)
581
t = (self.file_id, self.revision)
475
582
if t in checker.checked_texts:
476
prev_sha = checker.checked_texts[t]
583
prev_sha = checker.checked_texts[t]
477
584
if prev_sha != self.text_sha1:
478
585
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
479
(self.file_id, rev_id))
586
(self.file_id, tree_revision_id))
481
588
checker.repeated_text_cnt += 1
483
mutter('check version {%s} of {%s}', rev_id, self.file_id)
484
file_lines = tree.get_file_lines(self.file_id)
485
checker.checked_text_cnt += 1
486
if self.text_size != sum(map(len, file_lines)):
487
raise BzrCheckError('text {%s} wrong size' % self.text_id)
488
if self.text_sha1 != sha_strings(file_lines):
489
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
591
if self.file_id not in checker.checked_weaves:
592
mutter('check weave {%s}', self.file_id)
593
w = tree.get_weave(self.file_id)
594
# Not passing a progress bar, because it creates a new
595
# progress, which overwrites the current progress,
596
# and doesn't look nice
598
checker.checked_weaves[self.file_id] = True
600
w = tree.get_weave(self.file_id)
602
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
603
checker.checked_text_cnt += 1
604
# We can't check the length, because Weave doesn't store that
605
# information, and the whole point of looking at the weave's
606
# sha1sum is that we don't have to extract the text.
607
if self.text_sha1 != w.get_sha1(self.revision):
608
raise BzrCheckError('text {%s} version {%s} wrong sha1'
609
% (self.file_id, self.revision))
490
610
checker.checked_texts[t] = self.text_sha1
509
629
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
510
630
output_to, reverse=False):
511
631
"""See InventoryEntry._diff."""
512
from_text = tree.get_file(self.file_id).readlines()
514
to_text = to_tree.get_file(to_entry.file_id).readlines()
518
text_diff(from_label, from_text,
519
to_label, to_text, output_to)
521
text_diff(to_label, to_text,
522
from_label, from_text, output_to)
633
from_text = tree.get_file(self.file_id).readlines()
635
to_text = to_tree.get_file(to_entry.file_id).readlines()
639
text_diff(from_label, from_text,
640
to_label, to_text, output_to)
642
text_diff(to_label, to_text,
643
from_label, from_text, output_to)
646
label_pair = (to_label, from_label)
648
label_pair = (from_label, to_label)
649
print >> output_to, "Binary files %s and %s differ" % label_pair
524
651
def has_text(self):
525
652
"""See InventoryEntry.has_text."""
553
680
def _read_tree_state(self, path, work_tree):
554
681
"""See InventoryEntry._read_tree_state."""
555
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
556
self.executable = work_tree.is_executable(self.file_id)
558
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
682
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
683
# FIXME: 20050930 probe for the text size when getting sha1
684
# in _read_tree_state
685
self.executable = work_tree.is_executable(self.file_id, path=path)
688
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
689
% (self.__class__.__name__,
696
def _forget_tree_state(self):
697
self.text_sha1 = None
699
def _snapshot_text(self, file_parents, work_tree, commit_builder):
559
700
"""See InventoryEntry._snapshot_text."""
560
mutter('storing file {%s} in revision {%s}',
561
self.file_id, self.revision)
562
# special case to avoid diffing on renames or
564
if (len(file_parents) == 1
565
and self.text_sha1 == file_parents.values()[0].text_sha1
566
and self.text_size == file_parents.values()[0].text_size):
567
previous_ie = file_parents.values()[0]
568
weave_store.add_identical_text(
569
self.file_id, previous_ie.revision,
570
self.revision, file_parents, transaction)
572
new_lines = work_tree.get_file(self.file_id).readlines()
573
self._add_text_to_weave(new_lines, file_parents, weave_store,
575
self.text_sha1 = sha_strings(new_lines)
576
self.text_size = sum(map(len, new_lines))
701
def get_content_byte_lines():
702
return work_tree.get_file(self.file_id).readlines()
703
self.text_sha1, self.text_size = commit_builder.modified_file_text(
704
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
579
706
def _unchanged(self, previous_ie):
580
707
"""See InventoryEntry._unchanged."""
593
720
class InventoryLink(InventoryEntry):
594
721
"""A file in an inventory."""
596
__slots__ = ['symlink_target']
723
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
724
'text_id', 'parent_id', 'children', 'executable',
725
'revision', 'symlink_target']
598
727
def _check(self, checker, rev_id, tree):
599
728
"""See InventoryEntry._check"""
600
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
729
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
601
730
raise BzrCheckError('symlink {%s} has text in revision {%s}'
602
731
% (self.file_id, rev_id))
603
if self.symlink_target == None:
732
if self.symlink_target is None:
604
733
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
605
734
% (self.file_id, rev_id))
705
842
May also look up by name:
707
844
>>> [x[0] for x in inv.iter_entries()]
709
846
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
710
847
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
711
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
848
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
713
def __init__(self, root_id=ROOT_ID):
850
def __init__(self, root_id=ROOT_ID, revision_id=None):
714
851
"""Create or read an inventory.
716
853
If a working directory is specified, the inventory is read
720
857
The inventory is created with a default root directory, with
723
# We are letting Branch.initialize() create a unique inventory
860
# We are letting Branch.create() create a unique inventory
724
861
# root id. Rather than generating a random one here.
725
862
#if root_id is None:
726
863
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
727
864
self.root = RootEntry(root_id)
865
# FIXME: this isn't ever used, changing it to self.revision may break
866
# things. TODO make everything use self.revision_id
867
self.revision_id = revision_id
728
868
self._byid = {self.root.file_id: self.root}
732
other = Inventory(self.root.file_id)
871
# TODO: jam 20051218 Should copy also copy the revision_id?
872
entries = self.iter_entries()
873
other = Inventory(entries.next()[1].file_id)
733
874
# copy recursively so we know directories will be added before
734
875
# their children. There are more efficient ways than this...
735
for path, entry in self.iter_entries():
736
if entry == self.root:
876
for path, entry in entries():
738
877
other.add(entry.copy())
742
880
def __iter__(self):
743
881
return iter(self._byid)
746
883
def __len__(self):
747
884
"""Returns number of entries."""
748
885
return len(self._byid)
751
887
def iter_entries(self, from_dir=None):
752
888
"""Return (path, entry) pairs, in order by name."""
756
elif isinstance(from_dir, basestring):
757
from_dir = self._byid[from_dir]
759
kids = from_dir.children.items()
761
for name, ie in kids:
763
if ie.kind == 'directory':
764
for cn, cie in self.iter_entries(from_dir=ie.file_id):
765
yield os.path.join(name, cn), cie
893
elif isinstance(from_dir, basestring):
894
from_dir = self._byid[from_dir]
896
# unrolling the recursive called changed the time from
897
# 440ms/663ms (inline/total) to 116ms/116ms
898
children = from_dir.children.items()
900
children = collections.deque(children)
901
stack = [(u'', children)]
903
from_dir_relpath, children = stack[-1]
906
name, ie = children.popleft()
908
# we know that from_dir_relpath never ends in a slash
909
# and 'f' doesn't begin with one, we can do a string op, rather
910
# than the checks of pathjoin(), though this means that all paths
912
path = from_dir_relpath + '/' + name
916
if ie.kind != 'directory':
919
# But do this child first
920
new_children = ie.children.items()
922
new_children = collections.deque(new_children)
923
stack.append((path, new_children))
924
# Break out of inner loop, so that we start outer loop with child
927
# if we finished all children, pop it off the stack
930
def iter_entries_by_dir(self, from_dir=None):
931
"""Iterate over the entries in a directory first order.
933
This returns all entries for a directory before returning
934
the entries for children of a directory. This is not
935
lexicographically sorted order, and is a hybrid between
936
depth-first and breadth-first.
938
:return: This yields (path, entry) pairs
940
# TODO? Perhaps this should return the from_dir so that the root is
941
# yielded? or maybe an option?
946
elif isinstance(from_dir, basestring):
947
from_dir = self._byid[from_dir]
949
stack = [(u'', from_dir)]
951
cur_relpath, cur_dir = stack.pop()
954
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
956
child_relpath = cur_relpath + child_name
958
yield child_relpath, child_ie
960
if child_ie.kind == 'directory':
961
child_dirs.append((child_relpath+'/', child_ie))
962
stack.extend(reversed(child_dirs))
768
964
def entries(self):
769
965
"""Return list of (path, ie) for all entries except the root.
797
992
for name, child_ie in kids:
798
child_path = os.path.join(parent_path, name)
993
child_path = pathjoin(parent_path, name)
799
994
descend(child_ie, child_path)
800
descend(self.root, '')
995
descend(self.root, u'')
805
998
def __contains__(self, file_id):
806
999
"""True if this entry contains a file with given id.
808
1001
>>> inv = Inventory()
809
1002
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
810
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1003
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
811
1004
>>> '123' in inv
813
1006
>>> '456' in inv
816
1009
return file_id in self._byid
819
1011
def __getitem__(self, file_id):
820
1012
"""Return the entry for given file_id.
822
1014
>>> inv = Inventory()
823
1015
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
824
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1016
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
825
1017
>>> inv['123123'].name
829
1021
return self._byid[file_id]
830
1022
except KeyError:
832
1024
raise BzrError("can't look up file_id None")
834
1026
raise BzrError("file_id {%s} not in inventory" % file_id)
837
1028
def get_file_kind(self, file_id):
838
1029
return self._byid[file_id].kind
840
1031
def get_child(self, parent_id, filename):
841
1032
return self[parent_id].children.get(filename)
844
1034
def add(self, entry):
845
1035
"""Add entry to inventory.
860
1050
except KeyError:
861
1051
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
863
if parent.children.has_key(entry.name):
1053
if entry.name in parent.children:
864
1054
raise BzrError("%s is already versioned" %
865
appendpath(self.id2path(parent.file_id), entry.name))
1055
pathjoin(self.id2path(parent.file_id), entry.name))
867
1057
self._byid[entry.file_id] = entry
868
1058
parent.children[entry.name] = entry
872
def add_path(self, relpath, kind, file_id=None):
1061
def add_path(self, relpath, kind, file_id=None, parent_id=None):
873
1062
"""Add entry from a path.
875
1064
The immediate parent must already be versioned.
877
1066
Returns the new entry object."""
878
from bzrlib.branch import gen_file_id
880
parts = bzrlib.osutils.splitpath(relpath)
1068
parts = osutils.splitpath(relpath)
881
1070
if len(parts) == 0:
882
raise BzrError("cannot re-add root of inventory")
885
file_id = gen_file_id(relpath)
887
parent_path = parts[:-1]
888
parent_id = self.path2id(parent_path)
889
if parent_id == None:
890
raise NotVersionedError(path=parent_path)
891
if kind == 'directory':
892
ie = InventoryDirectory(file_id, parts[-1], parent_id)
894
ie = InventoryFile(file_id, parts[-1], parent_id)
895
elif kind == 'symlink':
896
ie = InventoryLink(file_id, parts[-1], parent_id)
1072
file_id = bzrlib.workingtree.gen_root_id()
1073
self.root = RootEntry(file_id)
1074
self._byid = {self.root.file_id: self.root}
898
raise BzrError("unknown kind %r" % kind)
1077
parent_path = parts[:-1]
1078
parent_id = self.path2id(parent_path)
1079
if parent_id is None:
1080
raise NotVersionedError(path=parent_path)
1081
ie = make_entry(kind, parts[-1], parent_id, file_id)
899
1082
return self.add(ie)
902
1084
def __delitem__(self, file_id):
903
1085
"""Remove entry by id.
905
1087
>>> inv = Inventory()
906
1088
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
907
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1089
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
908
1090
>>> '123' in inv
910
1092
>>> del inv['123']
936
1112
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
937
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1113
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
940
1116
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
941
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1117
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
945
1121
if not isinstance(other, Inventory):
946
1122
return NotImplemented
948
if len(self._byid) != len(other._byid):
949
# shortcut: obviously not the same
952
1124
return self._byid == other._byid
955
1126
def __ne__(self, other):
956
1127
return not self.__eq__(other)
959
1129
def __hash__(self):
960
1130
raise ValueError('not hashable')
1132
def _iter_file_id_parents(self, file_id):
1133
"""Yield the parents of file_id up to the root."""
1134
while file_id is not None:
1136
ie = self._byid[file_id]
1138
raise BzrError("file_id {%s} not found in inventory" % file_id)
1140
file_id = ie.parent_id
963
1142
def get_idpath(self, file_id):
964
1143
"""Return a list of file_ids for the path to an entry.
969
1148
root directory as depth 1.
972
while file_id != None:
974
ie = self._byid[file_id]
976
raise BzrError("file_id {%s} not found in inventory" % file_id)
977
p.insert(0, ie.file_id)
978
file_id = ie.parent_id
1151
for parent in self._iter_file_id_parents(file_id):
1152
p.insert(0, parent.file_id)
982
1155
def id2path(self, file_id):
983
"""Return as a list the path to file_id.
1156
"""Return as a string the path to file_id.
985
1158
>>> i = Inventory()
986
1159
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
987
1160
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
988
>>> print i.id2path('foo-id').replace(os.sep, '/')
1161
>>> print i.id2path('foo-id')
991
1164
# get all names, skipping root
992
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
return os.sep.join(p)
1165
return '/'.join(reversed(
1166
[parent.name for parent in
1167
self._iter_file_id_parents(file_id)][:-1]))
997
1169
def path2id(self, name):
998
1170
"""Walk down through directories to return entry of last component.
1062
1231
file_ie.parent_id = new_parent_id
1234
def make_entry(kind, name, parent_id, file_id=None):
1235
"""Create an inventory entry.
1237
:param kind: the type of inventory entry to create.
1238
:param name: the basename of the entry.
1239
:param parent_id: the parent_id of the entry.
1240
:param file_id: the file_id to use. if None, one will be created.
1243
file_id = bzrlib.workingtree.gen_file_id(name)
1245
norm_name, can_access = osutils.normalized_filename(name)
1246
if norm_name != name:
1250
# TODO: jam 20060701 This would probably be more useful
1251
# if the error was raised with the full path
1252
raise errors.InvalidNormalization(name)
1254
if kind == 'directory':
1255
return InventoryDirectory(file_id, name, parent_id)
1256
elif kind == 'file':
1257
return InventoryFile(file_id, name, parent_id)
1258
elif kind == 'symlink':
1259
return InventoryLink(file_id, name, parent_id)
1261
raise BzrError("unknown kind %r" % kind)
1067
1264
_NAME_RE = None
1069
1266
def is_valid_name(name):
1070
1267
global _NAME_RE
1071
if _NAME_RE == None:
1268
if _NAME_RE is None:
1072
1269
_NAME_RE = re.compile(r'^[^/\\]+$')
1074
1271
return bool(_NAME_RE.match(name))