79
78
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
80
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
81
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
82
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
83
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> shouldbe = {0: 'src', 1: os.path.join('src','hello.c')}
84
83
>>> for ix, j in enumerate(i.iter_entries()):
85
84
... print (j[0] == shouldbe[ix], j[1])
87
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
88
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
86
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
(True, InventoryFile('2323', 'hello.c', parent_id='123'))
89
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
90
89
Traceback (most recent call last):
92
91
BzrError: inventory already contains entry with id {2323}
93
92
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
94
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
InventoryFile('2324', 'bye.c', parent_id='123')
95
94
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
96
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
InventoryDirectory('2325', 'wibble', parent_id='123')
97
96
>>> i.path2id('src/wibble')
101
100
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
102
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
104
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
105
104
>>> for path, entry in i.iter_entries():
105
... print path.replace('\\\\', '/') # for win32 os.sep
107
106
... assert i.path2id(path)
113
112
src/wibble/wibble.c
114
>>> i.id2path('2326')
113
>>> i.id2path('2326').replace('\\\\', '/')
115
114
'src/wibble/wibble.c'
118
# Constants returned by describe_change()
120
# TODO: These should probably move to some kind of FileChangeDescription
121
# class; that's like what's inside a TreeDelta but we want to be able to
122
# generate them just for one file at a time.
124
MODIFIED_AND_RENAMED = 'modified and renamed'
117
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
118
'text_id', 'parent_id', 'children', 'executable',
121
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
128
125
def detect_changes(self, old_entry):
129
126
"""Return a (text_modified, meta_modified) from this to old_entry.
154
151
output_to, reverse=False):
155
152
"""Perform a diff between two entries of the same kind."""
157
def find_previous_heads(self, previous_inventories,
158
versioned_file_store,
161
"""Return the revisions and entries that directly precede this.
154
def find_previous_heads(self, previous_inventories, entry_weave):
155
"""Return the revisions and entries that directly preceed this.
163
157
Returned as a map from revision to inventory entry.
165
159
This is a map containing the file revisions in all parents
166
160
for which the file exists, and its revision is not a parent of
167
161
any other. If the file is new, the set will be empty.
169
:param versioned_file_store: A store where ancestry data on this
170
file id can be queried.
171
:param transaction: The transaction that queries to the versioned
172
file store should be completed under.
173
:param entry_vf: The entry versioned file, if its already available.
175
163
def get_ancestors(weave, entry):
176
return set(weave.get_ancestry(entry.revision))
177
# revision:ie mapping for each ie found in previous_inventories.
179
# revision:ie mapping with one revision for each head.
164
return set(map(weave.idx_to_name,
165
weave.inclusions([weave.lookup(entry.revision)])))
181
# revision: ancestor list for each head
182
167
head_ancestors = {}
183
# identify candidate head revision ids.
184
168
for inv in previous_inventories:
185
169
if self.file_id in inv:
186
170
ie = inv[self.file_id]
187
171
assert ie.file_id == self.file_id
188
if ie.revision in candidates:
189
# same revision value in two different inventories:
190
# correct possible inconsistencies:
191
# * there was a bug in revision updates with 'x' bit
172
if ie.revision in heads:
173
# fixup logic, there was a bug in revision updates.
174
# with x bit support.
194
if candidates[ie.revision].executable != ie.executable:
195
candidates[ie.revision].executable = False
176
if heads[ie.revision].executable != ie.executable:
177
heads[ie.revision].executable = False
196
178
ie.executable = False
197
179
except AttributeError:
199
# must now be the same.
200
assert candidates[ie.revision] == ie
181
assert heads[ie.revision] == ie
202
# add this revision as a candidate.
203
candidates[ie.revision] = ie
205
# common case optimisation
206
if len(candidates) == 1:
207
# if there is only one candidate revision found
208
# then we can opening the versioned file to access ancestry:
209
# there cannot be any ancestors to eliminate when there is
210
# only one revision available.
211
heads[ie.revision] = ie
214
# eliminate ancestors amongst the available candidates:
215
# heads are those that are not an ancestor of any other candidate
216
# - this provides convergence at a per-file level.
217
for ie in candidates.values():
218
# may be an ancestor of a known head:
219
already_present = 0 != len(
220
[head for head in heads
221
if ie.revision in head_ancestors[head]])
223
# an ancestor of an analyzed candidate.
225
# not an ancestor of a known head:
226
# load the versioned file for this file id if needed
228
entry_vf = versioned_file_store.get_weave_or_empty(
229
self.file_id, transaction)
230
ancestors = get_ancestors(entry_vf, ie)
231
# may knock something else out:
232
check_heads = list(heads.keys())
233
for head in check_heads:
234
if head in ancestors:
235
# this previously discovered 'head' is not
236
# really a head - its an ancestor of the newly
239
head_ancestors[ie.revision] = ancestors
240
heads[ie.revision] = ie
183
# may want to add it.
184
# may already be covered:
185
already_present = 0 != len(
186
[head for head in heads
187
if ie.revision in head_ancestors[head]])
189
# an ancestor of a known head.
192
ancestors = get_ancestors(entry_weave, ie)
193
# may knock something else out:
194
check_heads = list(heads.keys())
195
for head in check_heads:
196
if head in ancestors:
197
# this head is not really a head
199
head_ancestors[ie.revision] = ancestors
200
heads[ie.revision] = ie
243
203
def get_tar_item(self, root, dp, now, tree):
244
204
"""Get a tarfile item and a file stream for its content."""
245
item = tarfile.TarInfo(pathjoin(root, dp))
205
item = tarfile.TarInfo(os.path.join(root, dp))
246
206
# TODO: would be cool to actually set it to the timestamp of the
247
207
# revision it was last changed
308
268
This is a template method - implement _put_on_disk in subclasses.
310
fullpath = pathjoin(dest, dp)
270
fullpath = appendpath(dest, dp)
311
271
self._put_on_disk(fullpath, tree)
312
mutter(" export {%s} kind %s to %s", self.file_id,
272
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
315
274
def _put_on_disk(self, fullpath, tree):
316
275
"""Put this entry onto disk at fullpath, from tree tree."""
317
276
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
319
278
def sorted_children(self):
320
return sorted(self.children.items())
279
l = self.children.items()
323
284
def versionable_kind(kind):
348
301
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
349
302
(self.kind, rev_id))
352
306
"""Clone this inventory entry."""
353
307
raise NotImplementedError
356
def describe_change(old_entry, new_entry):
357
"""Describe the change between old_entry and this.
359
This smells of being an InterInventoryEntry situation, but as its
360
the first one, we're making it a static method for now.
362
An entry with a different parent, or different name is considered
363
to be renamed. Reparenting is an internal detail.
364
Note that renaming the parent does not trigger a rename for the
367
# TODO: Perhaps return an object rather than just a string
368
if old_entry is new_entry:
369
# also the case of both being None
371
elif old_entry is None:
309
def _get_snapshot_change(self, previous_entries):
310
if len(previous_entries) > 1:
312
elif len(previous_entries) == 0:
373
elif new_entry is None:
375
text_modified, meta_modified = new_entry.detect_changes(old_entry)
376
if text_modified or meta_modified:
380
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
381
if old_entry.parent_id != new_entry.parent_id:
383
elif old_entry.name != new_entry.name:
387
if renamed and not modified:
388
return InventoryEntry.RENAMED
389
if modified and not renamed:
391
if modified and renamed:
392
return InventoryEntry.MODIFIED_AND_RENAMED
315
return 'modified/renamed/reparented'
395
317
def __repr__(self):
396
return ("%s(%r, %r, parent_id=%r, revision=%r)"
318
return ("%s(%r, %r, parent_id=%r)"
397
319
% (self.__class__.__name__,
403
324
def snapshot(self, revision, path, previous_entries,
404
work_tree, commit_builder):
325
work_tree, weave_store, transaction):
405
326
"""Make a snapshot of this entry which may or may not have changed.
407
328
This means that all its fields are populated, that it has its
418
337
mutter("found unchanged entry")
419
338
self.revision = parent_ie.revision
420
339
return "unchanged"
421
return self._snapshot_into_revision(revision, previous_entries,
422
work_tree, commit_builder)
424
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
426
"""Record this revision unconditionally into a store.
428
The entry's last-changed revision property (`revision`) is updated to
429
that of the new revision.
431
:param revision: id of the new revision that is being recorded.
433
:returns: String description of the commit (e.g. "merged", "modified"), etc.
435
mutter('new revision {%s} for {%s}', revision, self.file_id)
340
return self.snapshot_revision(revision, previous_entries,
341
work_tree, weave_store, transaction)
343
def snapshot_revision(self, revision, previous_entries, work_tree,
344
weave_store, transaction):
345
"""Record this revision unconditionally."""
346
mutter('new revision for {%s}', self.file_id)
436
347
self.revision = revision
437
self._snapshot_text(previous_entries, work_tree, commit_builder)
348
change = self._get_snapshot_change(previous_entries)
349
self._snapshot_text(previous_entries, work_tree, weave_store,
439
def _snapshot_text(self, file_parents, work_tree, commit_builder):
353
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
440
354
"""Record the 'text' of this entry, whatever form that takes.
442
356
This default implementation simply adds an empty text.
444
raise NotImplementedError(self._snapshot_text)
358
mutter('storing file {%s} in revision {%s}',
359
self.file_id, self.revision)
360
self._add_text_to_weave([], file_parents, weave_store, transaction)
446
362
def __eq__(self, other):
447
363
if not isinstance(other, InventoryEntry):
490
406
# first requested, or preload them if they're already known
491
407
pass # nothing to do by default
493
def _forget_tree_state(self):
497
410
class RootEntry(InventoryEntry):
499
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
500
'text_id', 'parent_id', 'children', 'executable',
501
'revision', 'symlink_target']
503
412
def _check(self, checker, rev_id, tree):
504
413
"""See InventoryEntry._check"""
522
430
class InventoryDirectory(InventoryEntry):
523
431
"""A directory in an inventory."""
525
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
526
'text_id', 'parent_id', 'children', 'executable',
527
'revision', 'symlink_target']
529
433
def _check(self, checker, rev_id, tree):
530
434
"""See InventoryEntry._check"""
531
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
435
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
532
436
raise BzrCheckError('directory {%s} has text in revision {%s}'
533
437
% (self.file_id, rev_id))
561
465
"""See InventoryEntry._put_on_disk."""
562
466
os.mkdir(fullpath)
564
def _snapshot_text(self, file_parents, work_tree, commit_builder):
565
"""See InventoryEntry._snapshot_text."""
566
commit_builder.modified_directory(self.file_id, file_parents)
569
469
class InventoryFile(InventoryEntry):
570
470
"""A file in an inventory."""
572
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
573
'text_id', 'parent_id', 'children', 'executable',
574
'revision', 'symlink_target']
576
def _check(self, checker, tree_revision_id, tree):
472
def _check(self, checker, rev_id, tree):
577
473
"""See InventoryEntry._check"""
578
t = (self.file_id, self.revision)
474
revision = self.revision
475
t = (self.file_id, revision)
579
476
if t in checker.checked_texts:
580
prev_sha = checker.checked_texts[t]
477
prev_sha = checker.checked_texts[t]
581
478
if prev_sha != self.text_sha1:
582
479
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
583
(self.file_id, tree_revision_id))
480
(self.file_id, rev_id))
585
482
checker.repeated_text_cnt += 1
588
if self.file_id not in checker.checked_weaves:
589
mutter('check weave {%s}', self.file_id)
590
w = tree.get_weave(self.file_id)
591
# Not passing a progress bar, because it creates a new
592
# progress, which overwrites the current progress,
593
# and doesn't look nice
595
checker.checked_weaves[self.file_id] = True
597
w = tree.get_weave(self.file_id)
599
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
600
checker.checked_text_cnt += 1
601
# We can't check the length, because Weave doesn't store that
602
# information, and the whole point of looking at the weave's
603
# sha1sum is that we don't have to extract the text.
604
if self.text_sha1 != w.get_sha1(self.revision):
605
raise BzrCheckError('text {%s} version {%s} wrong sha1'
606
% (self.file_id, self.revision))
484
mutter('check version {%s} of {%s}', rev_id, self.file_id)
485
file_lines = tree.get_file_lines(self.file_id)
486
checker.checked_text_cnt += 1
487
if self.text_size != sum(map(len, file_lines)):
488
raise BzrCheckError('text {%s} wrong size' % self.text_id)
489
if self.text_sha1 != sha_strings(file_lines):
490
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
607
491
checker.checked_texts[t] = self.text_sha1
626
510
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
627
511
output_to, reverse=False):
628
512
"""See InventoryEntry._diff."""
630
from_text = tree.get_file(self.file_id).readlines()
632
to_text = to_tree.get_file(to_entry.file_id).readlines()
636
text_diff(from_label, from_text,
637
to_label, to_text, output_to)
639
text_diff(to_label, to_text,
640
from_label, from_text, output_to)
643
label_pair = (to_label, from_label)
645
label_pair = (from_label, to_label)
646
print >> output_to, "Binary files %s and %s differ" % label_pair
513
from_text = tree.get_file(self.file_id).readlines()
515
to_text = to_tree.get_file(to_entry.file_id).readlines()
519
text_diff(from_label, from_text,
520
to_label, to_text, output_to)
522
text_diff(to_label, to_text,
523
from_label, from_text, output_to)
648
525
def has_text(self):
649
526
"""See InventoryEntry.has_text."""
677
554
def _read_tree_state(self, path, work_tree):
678
555
"""See InventoryEntry._read_tree_state."""
679
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
680
# FIXME: 20050930 probe for the text size when getting sha1
681
# in _read_tree_state
682
self.executable = work_tree.is_executable(self.file_id, path=path)
685
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
686
% (self.__class__.__name__,
693
def _forget_tree_state(self):
694
self.text_sha1 = None
696
def _snapshot_text(self, file_parents, work_tree, commit_builder):
556
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
557
self.executable = work_tree.is_executable(self.file_id)
559
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
697
560
"""See InventoryEntry._snapshot_text."""
698
def get_content_byte_lines():
699
return work_tree.get_file(self.file_id).readlines()
700
self.text_sha1, self.text_size = commit_builder.modified_file_text(
701
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
561
mutter('storing file {%s} in revision {%s}',
562
self.file_id, self.revision)
563
# special case to avoid diffing on renames or
565
if (len(file_parents) == 1
566
and self.text_sha1 == file_parents.values()[0].text_sha1
567
and self.text_size == file_parents.values()[0].text_size):
568
previous_ie = file_parents.values()[0]
569
weave_store.add_identical_text(
570
self.file_id, previous_ie.revision,
571
self.revision, file_parents, transaction)
573
new_lines = work_tree.get_file(self.file_id).readlines()
574
self._add_text_to_weave(new_lines, file_parents, weave_store,
576
self.text_sha1 = sha_strings(new_lines)
577
self.text_size = sum(map(len, new_lines))
703
580
def _unchanged(self, previous_ie):
704
581
"""See InventoryEntry._unchanged."""
717
594
class InventoryLink(InventoryEntry):
718
595
"""A file in an inventory."""
720
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
721
'text_id', 'parent_id', 'children', 'executable',
722
'revision', 'symlink_target']
597
__slots__ = ['symlink_target']
724
599
def _check(self, checker, rev_id, tree):
725
600
"""See InventoryEntry._check"""
726
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
601
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
727
602
raise BzrCheckError('symlink {%s} has text in revision {%s}'
728
603
% (self.file_id, rev_id))
729
if self.symlink_target is None:
604
if self.symlink_target == None:
730
605
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
731
606
% (self.file_id, rev_id))
839
706
May also look up by name:
841
708
>>> [x[0] for x in inv.iter_entries()]
843
710
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
844
711
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
845
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
712
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
847
def __init__(self, root_id=ROOT_ID, revision_id=None):
714
def __init__(self, root_id=ROOT_ID):
848
715
"""Create or read an inventory.
850
717
If a working directory is specified, the inventory is read
854
721
The inventory is created with a default root directory, with
857
# We are letting Branch.create() create a unique inventory
724
# We are letting Branch.initialize() create a unique inventory
858
725
# root id. Rather than generating a random one here.
859
726
#if root_id is None:
860
727
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
861
728
self.root = RootEntry(root_id)
862
# FIXME: this isn't ever used, changing it to self.revision may break
863
# things. TODO make everything use self.revision_id
864
self.revision_id = revision_id
865
729
self._byid = {self.root.file_id: self.root}
868
# TODO: jam 20051218 Should copy also copy the revision_id?
869
733
other = Inventory(self.root.file_id)
870
734
# copy recursively so we know directories will be added before
871
735
# their children. There are more efficient ways than this...
875
739
other.add(entry.copy())
878
743
def __iter__(self):
879
744
return iter(self._byid)
881
747
def __len__(self):
882
748
"""Returns number of entries."""
883
749
return len(self._byid)
885
752
def iter_entries(self, from_dir=None):
886
753
"""Return (path, entry) pairs, in order by name."""
890
elif isinstance(from_dir, basestring):
891
from_dir = self._byid[from_dir]
893
# unrolling the recursive called changed the time from
894
# 440ms/663ms (inline/total) to 116ms/116ms
895
children = from_dir.children.items()
897
children = collections.deque(children)
898
stack = [(u'', children)]
900
from_dir_relpath, children = stack[-1]
903
name, ie = children.popleft()
905
# we know that from_dir_relpath never ends in a slash
906
# and 'f' doesn't begin with one, we can do a string op, rather
907
# than the checks of pathjoin(), though this means that all paths
909
path = from_dir_relpath + '/' + name
913
if ie.kind != 'directory':
916
# But do this child first
917
new_children = ie.children.items()
919
new_children = collections.deque(new_children)
920
stack.append((path, new_children))
921
# Break out of inner loop, so that we start outer loop with child
924
# if we finished all children, pop it off the stack
927
def iter_entries_by_dir(self, from_dir=None):
928
"""Iterate over the entries in a directory first order.
930
This returns all entries for a directory before returning
931
the entries for children of a directory. This is not
932
lexicographically sorted order, and is a hybrid between
933
depth-first and breadth-first.
935
:return: This yields (path, entry) pairs
937
# TODO? Perhaps this should return the from_dir so that the root is
938
# yielded? or maybe an option?
942
elif isinstance(from_dir, basestring):
943
from_dir = self._byid[from_dir]
945
stack = [(u'', from_dir)]
947
cur_relpath, cur_dir = stack.pop()
950
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
952
child_relpath = cur_relpath + child_name
954
yield child_relpath, child_ie
956
if child_ie.kind == 'directory':
957
child_dirs.append((child_relpath+'/', child_ie))
958
stack.extend(reversed(child_dirs))
757
elif isinstance(from_dir, basestring):
758
from_dir = self._byid[from_dir]
760
kids = from_dir.children.items()
762
for name, ie in kids:
764
if ie.kind == 'directory':
765
for cn, cie in self.iter_entries(from_dir=ie.file_id):
766
yield os.path.join(name, cn), cie
960
769
def entries(self):
961
770
"""Return list of (path, ie) for all entries except the root.
988
798
for name, child_ie in kids:
989
child_path = pathjoin(parent_path, name)
799
child_path = os.path.join(parent_path, name)
990
800
descend(child_ie, child_path)
991
descend(self.root, u'')
801
descend(self.root, '')
994
806
def __contains__(self, file_id):
995
807
"""True if this entry contains a file with given id.
997
809
>>> inv = Inventory()
998
810
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
999
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
811
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1000
812
>>> '123' in inv
1002
814
>>> '456' in inv
1005
817
return file_id in self._byid
1007
820
def __getitem__(self, file_id):
1008
821
"""Return the entry for given file_id.
1010
823
>>> inv = Inventory()
1011
824
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1012
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
825
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1013
826
>>> inv['123123'].name
1017
830
return self._byid[file_id]
1018
831
except KeyError:
1020
833
raise BzrError("can't look up file_id None")
1022
835
raise BzrError("file_id {%s} not in inventory" % file_id)
1024
838
def get_file_kind(self, file_id):
1025
839
return self._byid[file_id].kind
1027
841
def get_child(self, parent_id, filename):
1028
842
return self[parent_id].children.get(filename)
1030
845
def add(self, entry):
1031
846
"""Add entry to inventory.
1046
861
except KeyError:
1047
862
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1049
if entry.name in parent.children:
864
if parent.children.has_key(entry.name):
1050
865
raise BzrError("%s is already versioned" %
1051
pathjoin(self.id2path(parent.file_id), entry.name))
866
appendpath(self.id2path(parent.file_id), entry.name))
1053
868
self._byid[entry.file_id] = entry
1054
869
parent.children[entry.name] = entry
1057
def add_path(self, relpath, kind, file_id=None, parent_id=None):
873
def add_path(self, relpath, kind, file_id=None):
1058
874
"""Add entry from a path.
1060
876
The immediate parent must already be versioned.
1062
878
Returns the new entry object."""
879
from bzrlib.branch import gen_file_id
1064
881
parts = bzrlib.osutils.splitpath(relpath)
1066
882
if len(parts) == 0:
1068
file_id = bzrlib.workingtree.gen_root_id()
1069
self.root = RootEntry(file_id)
1070
self._byid = {self.root.file_id: self.root}
883
raise BzrError("cannot re-add root of inventory")
886
file_id = gen_file_id(relpath)
888
parent_path = parts[:-1]
889
parent_id = self.path2id(parent_path)
890
if parent_id == None:
891
raise NotVersionedError(path=parent_path)
892
if kind == 'directory':
893
ie = InventoryDirectory(file_id, parts[-1], parent_id)
895
ie = InventoryFile(file_id, parts[-1], parent_id)
896
elif kind == 'symlink':
897
ie = InventoryLink(file_id, parts[-1], parent_id)
1073
parent_path = parts[:-1]
1074
parent_id = self.path2id(parent_path)
1075
if parent_id is None:
1076
raise NotVersionedError(path=parent_path)
1077
ie = make_entry(kind, parts[-1], parent_id, file_id)
899
raise BzrError("unknown kind %r" % kind)
1078
900
return self.add(ie)
1080
903
def __delitem__(self, file_id):
1081
904
"""Remove entry by id.
1083
906
>>> inv = Inventory()
1084
907
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1085
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
908
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1086
909
>>> '123' in inv
1088
911
>>> del inv['123']
1108
937
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1109
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
938
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1112
941
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1113
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
942
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1117
946
if not isinstance(other, Inventory):
1118
947
return NotImplemented
949
if len(self._byid) != len(other._byid):
950
# shortcut: obviously not the same
1120
953
return self._byid == other._byid
1122
956
def __ne__(self, other):
1123
957
return not self.__eq__(other)
1125
960
def __hash__(self):
1126
961
raise ValueError('not hashable')
1128
def _iter_file_id_parents(self, file_id):
1129
"""Yield the parents of file_id up to the root."""
1130
while file_id is not None:
1132
ie = self._byid[file_id]
1134
raise BzrError("file_id {%s} not found in inventory" % file_id)
1136
file_id = ie.parent_id
1138
964
def get_idpath(self, file_id):
1139
965
"""Return a list of file_ids for the path to an entry.
1144
970
root directory as depth 1.
1147
for parent in self._iter_file_id_parents(file_id):
1148
p.insert(0, parent.file_id)
973
while file_id != None:
975
ie = self._byid[file_id]
977
raise BzrError("file_id {%s} not found in inventory" % file_id)
978
p.insert(0, ie.file_id)
979
file_id = ie.parent_id
1151
983
def id2path(self, file_id):
1152
"""Return as a string the path to file_id.
984
"""Return as a list the path to file_id.
1154
986
>>> i = Inventory()
1155
987
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1156
988
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1157
>>> print i.id2path('foo-id')
989
>>> print i.id2path('foo-id').replace(os.sep, '/')
1160
992
# get all names, skipping root
1161
return '/'.join(reversed(
1162
[parent.name for parent in
1163
self._iter_file_id_parents(file_id)][:-1]))
993
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
994
return os.sep.join(p)
1165
998
def path2id(self, name):
1166
999
"""Walk down through directories to return entry of last component.
1227
1063
file_ie.parent_id = new_parent_id
1230
def make_entry(kind, name, parent_id, file_id=None):
1231
"""Create an inventory entry.
1233
:param kind: the type of inventory entry to create.
1234
:param name: the basename of the entry.
1235
:param parent_id: the parent_id of the entry.
1236
:param file_id: the file_id to use. if None, one will be created.
1239
file_id = bzrlib.workingtree.gen_file_id(name)
1240
if kind == 'directory':
1241
return InventoryDirectory(file_id, name, parent_id)
1242
elif kind == 'file':
1243
return InventoryFile(file_id, name, parent_id)
1244
elif kind == 'symlink':
1245
return InventoryLink(file_id, name, parent_id)
1247
raise BzrError("unknown kind %r" % kind)
1251
1068
_NAME_RE = None
1253
1070
def is_valid_name(name):
1254
1071
global _NAME_RE
1255
if _NAME_RE is None:
1072
if _NAME_RE == None:
1256
1073
_NAME_RE = re.compile(r'^[^/\\]+$')
1258
1075
return bool(_NAME_RE.match(name))