78
89
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
91
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
83
94
>>> for ix, j in enumerate(i.iter_entries()):
84
95
... print (j[0] == shouldbe[ix], j[1])
86
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
(True, InventoryFile('2323', 'hello.c', parent_id='123'))
97
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
88
100
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
101
Traceback (most recent call last):
91
103
BzrError: inventory already contains entry with id {2323}
92
104
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
InventoryFile('2324', 'bye.c', parent_id='123')
105
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
106
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
InventoryDirectory('2325', 'wibble', parent_id='123')
107
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
108
>>> i.path2id('src/wibble')
100
112
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
113
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
115
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
116
>>> for path, entry in i.iter_entries():
106
118
... assert i.path2id(path)
113
126
>>> i.id2path('2326')
114
127
'src/wibble/wibble.c'
130
# Constants returned by describe_change()
132
# TODO: These should probably move to some kind of FileChangeDescription
133
# class; that's like what's inside a TreeDelta but we want to be able to
134
# generate them just for one file at a time.
136
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,
125
140
def detect_changes(self, old_entry):
126
141
"""Return a (text_modified, meta_modified) from this to old_entry.
151
166
output_to, reverse=False):
152
167
"""Perform a diff between two entries of the same kind."""
154
def find_previous_heads(self, previous_inventories, entry_weave):
155
"""Return the revisions and entries that directly preceed this.
169
def find_previous_heads(self, previous_inventories,
170
versioned_file_store,
173
"""Return the revisions and entries that directly precede this.
157
175
Returned as a map from revision to inventory entry.
159
177
This is a map containing the file revisions in all parents
160
178
for which the file exists, and its revision is not a parent of
161
179
any other. If the file is new, the set will be empty.
181
:param versioned_file_store: A store where ancestry data on this
182
file id can be queried.
183
:param transaction: The transaction that queries to the versioned
184
file store should be completed under.
185
:param entry_vf: The entry versioned file, if its already available.
163
187
def get_ancestors(weave, entry):
164
return set(map(weave.idx_to_name,
165
weave.inclusions([weave.lookup(entry.revision)])))
188
return set(weave.get_ancestry(entry.revision))
189
# revision:ie mapping for each ie found in previous_inventories.
191
# revision:ie mapping with one revision for each head.
193
# revision: ancestor list for each head
167
194
head_ancestors = {}
195
# identify candidate head revision ids.
168
196
for inv in previous_inventories:
169
197
if self.file_id in inv:
170
198
ie = inv[self.file_id]
171
199
assert ie.file_id == self.file_id
172
if ie.revision in heads:
173
# fixup logic, there was a bug in revision updates.
174
# with x bit support.
200
if ie.revision in candidates:
201
# same revision value in two different inventories:
202
# correct possible inconsistencies:
203
# * there was a bug in revision updates with 'x' bit
176
if heads[ie.revision].executable != ie.executable:
177
heads[ie.revision].executable = False
206
if candidates[ie.revision].executable != ie.executable:
207
candidates[ie.revision].executable = False
178
208
ie.executable = False
179
209
except AttributeError:
181
assert heads[ie.revision] == ie
211
# must now be the same.
212
assert candidates[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
214
# add this revision as a candidate.
215
candidates[ie.revision] = ie
217
# common case optimisation
218
if len(candidates) == 1:
219
# if there is only one candidate revision found
220
# then we can opening the versioned file to access ancestry:
221
# there cannot be any ancestors to eliminate when there is
222
# only one revision available.
223
heads[ie.revision] = ie
226
# eliminate ancestors amongst the available candidates:
227
# heads are those that are not an ancestor of any other candidate
228
# - this provides convergence at a per-file level.
229
for ie in candidates.values():
230
# may be an ancestor of a known head:
231
already_present = 0 != len(
232
[head for head in heads
233
if ie.revision in head_ancestors[head]])
235
# an ancestor of an analyzed candidate.
237
# not an ancestor of a known head:
238
# load the versioned file for this file id if needed
240
entry_vf = versioned_file_store.get_weave_or_empty(
241
self.file_id, transaction)
242
ancestors = get_ancestors(entry_vf, ie)
243
# may knock something else out:
244
check_heads = list(heads.keys())
245
for head in check_heads:
246
if head in ancestors:
247
# this previously discovered 'head' is not
248
# really a head - its an ancestor of the newly
251
head_ancestors[ie.revision] = ancestors
252
heads[ie.revision] = ie
203
255
def get_tar_item(self, root, dp, now, tree):
204
256
"""Get a tarfile item and a file stream for its content."""
205
item = tarfile.TarInfo(pathjoin(root, dp))
257
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
206
258
# TODO: would be cool to actually set it to the timestamp of the
207
259
# revision it was last changed
268
320
This is a template method - implement _put_on_disk in subclasses.
270
fullpath = pathjoin(dest, dp)
322
fullpath = osutils.pathjoin(dest, dp)
271
323
self._put_on_disk(fullpath, tree)
272
mutter(" export {%s} kind %s to %s", self.file_id,
324
# mutter(" export {%s} kind %s to %s", self.file_id,
325
# self.kind, fullpath)
275
327
def _put_on_disk(self, fullpath, tree):
276
328
"""Put this entry onto disk at fullpath, from tree tree."""
277
329
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
279
331
def sorted_children(self):
280
l = self.children.items()
332
return sorted(self.children.items())
285
335
def versionable_kind(kind):
286
return kind in ('file', 'directory', 'symlink')
336
return (kind in ('file', 'directory', 'symlink'))
288
338
def check(self, checker, rev_id, inv, tree):
289
339
"""Check this inventory entry is intact.
291
341
This is a template method, override _check for kind specific
344
:param checker: Check object providing context for the checks;
345
can be used to find out what parts of the repository have already
347
:param rev_id: Revision id from which this InventoryEntry was loaded.
348
Not necessarily the last-changed revision for this file.
349
:param inv: Inventory from which the entry was loaded.
350
:param tree: RevisionTree for this entry.
294
if self.parent_id != None:
352
if self.parent_id is not None:
295
353
if not inv.has_id(self.parent_id):
296
354
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
297
355
% (self.parent_id, rev_id))
302
360
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
303
361
(self.kind, rev_id))
307
364
"""Clone this inventory entry."""
308
365
raise NotImplementedError
310
def _get_snapshot_change(self, previous_entries):
311
if len(previous_entries) > 1:
313
elif len(previous_entries) == 0:
368
def describe_change(old_entry, new_entry):
369
"""Describe the change between old_entry and this.
371
This smells of being an InterInventoryEntry situation, but as its
372
the first one, we're making it a static method for now.
374
An entry with a different parent, or different name is considered
375
to be renamed. Reparenting is an internal detail.
376
Note that renaming the parent does not trigger a rename for the
379
# TODO: Perhaps return an object rather than just a string
380
if old_entry is new_entry:
381
# also the case of both being None
383
elif old_entry is None:
316
return 'modified/renamed/reparented'
385
elif new_entry is None:
387
text_modified, meta_modified = new_entry.detect_changes(old_entry)
388
if text_modified or meta_modified:
392
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
393
if old_entry.parent_id != new_entry.parent_id:
395
elif old_entry.name != new_entry.name:
399
if renamed and not modified:
400
return InventoryEntry.RENAMED
401
if modified and not renamed:
403
if modified and renamed:
404
return InventoryEntry.MODIFIED_AND_RENAMED
318
407
def __repr__(self):
319
return ("%s(%r, %r, parent_id=%r)"
408
return ("%s(%r, %r, parent_id=%r, revision=%r)"
320
409
% (self.__class__.__name__,
325
415
def snapshot(self, revision, path, previous_entries,
326
work_tree, weave_store, transaction):
416
work_tree, commit_builder):
327
417
"""Make a snapshot of this entry which may or may not have changed.
329
419
This means that all its fields are populated, that it has its
330
420
text stored in the text store or weave.
332
mutter('new parents of %s are %r', path, previous_entries)
422
# mutter('new parents of %s are %r', path, previous_entries)
333
423
self._read_tree_state(path, work_tree)
424
# TODO: Where should we determine whether to reuse a
425
# previous revision id or create a new revision? 20060606
334
426
if len(previous_entries) == 1:
335
427
# cannot be unchanged unless there is only one parent file rev.
336
428
parent_ie = previous_entries.values()[0]
337
429
if self._unchanged(parent_ie):
338
mutter("found unchanged entry")
430
# mutter("found unchanged entry")
339
431
self.revision = parent_ie.revision
340
432
return "unchanged"
341
return self.snapshot_revision(revision, previous_entries,
342
work_tree, weave_store, transaction)
344
def snapshot_revision(self, revision, previous_entries, work_tree,
345
weave_store, transaction):
346
"""Record this revision unconditionally."""
347
mutter('new revision for {%s}', self.file_id)
433
return self._snapshot_into_revision(revision, previous_entries,
434
work_tree, commit_builder)
436
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
438
"""Record this revision unconditionally into a store.
440
The entry's last-changed revision property (`revision`) is updated to
441
that of the new revision.
443
:param revision: id of the new revision that is being recorded.
445
:returns: String description of the commit (e.g. "merged", "modified"), etc.
447
# mutter('new revision {%s} for {%s}', revision, self.file_id)
348
448
self.revision = revision
349
change = self._get_snapshot_change(previous_entries)
350
self._snapshot_text(previous_entries, work_tree, weave_store,
449
self._snapshot_text(previous_entries, work_tree, commit_builder)
354
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
451
def _snapshot_text(self, file_parents, work_tree, commit_builder):
355
452
"""Record the 'text' of this entry, whatever form that takes.
357
454
This default implementation simply adds an empty text.
359
mutter('storing file {%s} in revision {%s}',
360
self.file_id, self.revision)
361
self._add_text_to_weave([], file_parents, weave_store, transaction)
456
raise NotImplementedError(self._snapshot_text)
363
458
def __eq__(self, other):
364
459
if not isinstance(other, InventoryEntry):
407
502
# first requested, or preload them if they're already known
408
503
pass # nothing to do by default
505
def _forget_tree_state(self):
411
509
class RootEntry(InventoryEntry):
511
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
512
'text_id', 'parent_id', 'children', 'executable',
513
'revision', 'symlink_target']
413
515
def _check(self, checker, rev_id, tree):
414
516
"""See InventoryEntry._check"""
416
518
def __init__(self, file_id):
417
519
self.file_id = file_id
418
520
self.children = {}
419
self.kind = 'root_directory'
521
self.kind = 'directory'
420
522
self.parent_id = None
525
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
526
' Please use InventoryDirectory instead.',
527
DeprecationWarning, stacklevel=2)
423
529
def __eq__(self, other):
424
530
if not isinstance(other, RootEntry):
466
576
"""See InventoryEntry._put_on_disk."""
467
577
os.mkdir(fullpath)
579
def _snapshot_text(self, file_parents, work_tree, commit_builder):
580
"""See InventoryEntry._snapshot_text."""
581
commit_builder.modified_directory(self.file_id, file_parents)
470
584
class InventoryFile(InventoryEntry):
471
585
"""A file in an inventory."""
473
def _check(self, checker, rev_id, tree):
587
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
588
'text_id', 'parent_id', 'children', 'executable',
589
'revision', 'symlink_target']
591
def _check(self, checker, tree_revision_id, tree):
474
592
"""See InventoryEntry._check"""
475
revision = self.revision
476
t = (self.file_id, revision)
593
t = (self.file_id, self.revision)
477
594
if t in checker.checked_texts:
478
prev_sha = checker.checked_texts[t]
595
prev_sha = checker.checked_texts[t]
479
596
if prev_sha != self.text_sha1:
480
597
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
481
(self.file_id, rev_id))
598
(self.file_id, tree_revision_id))
483
600
checker.repeated_text_cnt += 1
524
641
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
525
642
output_to, reverse=False):
526
643
"""See InventoryEntry._diff."""
527
from_text = tree.get_file(self.file_id).readlines()
529
to_text = to_tree.get_file(to_entry.file_id).readlines()
533
text_diff(from_label, from_text,
534
to_label, to_text, output_to)
536
text_diff(to_label, to_text,
537
from_label, from_text, output_to)
645
from_text = tree.get_file(self.file_id).readlines()
647
to_text = to_tree.get_file(to_entry.file_id).readlines()
651
text_diff(from_label, from_text,
652
to_label, to_text, output_to)
654
text_diff(to_label, to_text,
655
from_label, from_text, output_to)
656
except errors.BinaryFile:
658
label_pair = (to_label, from_label)
660
label_pair = (from_label, to_label)
661
print >> output_to, "Binary files %s and %s differ" % label_pair
539
663
def has_text(self):
540
664
"""See InventoryEntry.has_text."""
562
686
def _put_on_disk(self, fullpath, tree):
563
687
"""See InventoryEntry._put_on_disk."""
564
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
688
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
565
689
if tree.is_executable(self.file_id):
566
690
os.chmod(fullpath, 0755)
568
692
def _read_tree_state(self, path, work_tree):
569
693
"""See InventoryEntry._read_tree_state."""
570
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
571
self.executable = work_tree.is_executable(self.file_id)
573
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
694
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
695
# FIXME: 20050930 probe for the text size when getting sha1
696
# in _read_tree_state
697
self.executable = work_tree.is_executable(self.file_id, path=path)
700
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
701
% (self.__class__.__name__,
708
def _forget_tree_state(self):
709
self.text_sha1 = None
711
def _snapshot_text(self, file_parents, work_tree, commit_builder):
574
712
"""See InventoryEntry._snapshot_text."""
575
mutter('storing file {%s} in revision {%s}',
576
self.file_id, self.revision)
577
# special case to avoid diffing on renames or
579
if (len(file_parents) == 1
580
and self.text_sha1 == file_parents.values()[0].text_sha1
581
and self.text_size == file_parents.values()[0].text_size):
582
previous_ie = file_parents.values()[0]
583
weave_store.add_identical_text(
584
self.file_id, previous_ie.revision,
585
self.revision, file_parents, transaction)
587
new_lines = work_tree.get_file(self.file_id).readlines()
588
self._add_text_to_weave(new_lines, file_parents, weave_store,
590
self.text_sha1 = sha_strings(new_lines)
591
self.text_size = sum(map(len, new_lines))
713
def get_content_byte_lines():
714
return work_tree.get_file(self.file_id).readlines()
715
self.text_sha1, self.text_size = commit_builder.modified_file_text(
716
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
594
718
def _unchanged(self, previous_ie):
595
719
"""See InventoryEntry._unchanged."""
608
732
class InventoryLink(InventoryEntry):
609
733
"""A file in an inventory."""
611
__slots__ = ['symlink_target']
735
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
736
'text_id', 'parent_id', 'children', 'executable',
737
'revision', 'symlink_target']
613
739
def _check(self, checker, rev_id, tree):
614
740
"""See InventoryEntry._check"""
615
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
741
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
616
742
raise BzrCheckError('symlink {%s} has text in revision {%s}'
617
743
% (self.file_id, rev_id))
618
if self.symlink_target == None:
744
if self.symlink_target is None:
619
745
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
620
746
% (self.file_id, rev_id))
720
854
May also look up by name:
722
856
>>> [x[0] for x in inv.iter_entries()]
724
858
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
725
859
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
726
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
860
Traceback (most recent call last):
861
BzrError: parent_id {TREE_ROOT} not in inventory
862
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
863
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
728
def __init__(self, root_id=ROOT_ID):
865
def __init__(self, root_id=ROOT_ID, revision_id=None):
729
866
"""Create or read an inventory.
731
868
If a working directory is specified, the inventory is read
735
872
The inventory is created with a default root directory, with
738
# We are letting Branch.initialize() create a unique inventory
739
# root id. Rather than generating a random one here.
741
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
742
self.root = RootEntry(root_id)
875
if root_id is not None:
876
self._set_root(InventoryDirectory(root_id, '', None))
880
self.revision_id = revision_id
882
def _set_root(self, ie):
743
884
self._byid = {self.root.file_id: self.root}
747
other = Inventory(self.root.file_id)
887
# TODO: jam 20051218 Should copy also copy the revision_id?
888
entries = self.iter_entries()
889
other = Inventory(entries.next()[1].file_id)
748
890
# copy recursively so we know directories will be added before
749
891
# their children. There are more efficient ways than this...
750
for path, entry in self.iter_entries():
751
if entry == self.root:
892
for path, entry in entries():
753
893
other.add(entry.copy())
757
896
def __iter__(self):
758
897
return iter(self._byid)
761
899
def __len__(self):
762
900
"""Returns number of entries."""
763
901
return len(self._byid)
766
903
def iter_entries(self, from_dir=None):
767
904
"""Return (path, entry) pairs, in order by name."""
771
elif isinstance(from_dir, basestring):
772
from_dir = self._byid[from_dir]
774
kids = from_dir.children.items()
776
for name, ie in kids:
778
if ie.kind == 'directory':
779
for cn, cie in self.iter_entries(from_dir=ie.file_id):
780
yield pathjoin(name, cn), cie
906
if self.root is None:
910
elif isinstance(from_dir, basestring):
911
from_dir = self._byid[from_dir]
913
# unrolling the recursive called changed the time from
914
# 440ms/663ms (inline/total) to 116ms/116ms
915
children = from_dir.children.items()
917
children = collections.deque(children)
918
stack = [(u'', children)]
920
from_dir_relpath, children = stack[-1]
923
name, ie = children.popleft()
925
# we know that from_dir_relpath never ends in a slash
926
# and 'f' doesn't begin with one, we can do a string op, rather
927
# than the checks of pathjoin(), though this means that all paths
929
path = from_dir_relpath + '/' + name
933
if ie.kind != 'directory':
936
# But do this child first
937
new_children = ie.children.items()
939
new_children = collections.deque(new_children)
940
stack.append((path, new_children))
941
# Break out of inner loop, so that we start outer loop with child
944
# if we finished all children, pop it off the stack
947
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
948
"""Iterate over the entries in a directory first order.
950
This returns all entries for a directory before returning
951
the entries for children of a directory. This is not
952
lexicographically sorted order, and is a hybrid between
953
depth-first and breadth-first.
955
:return: This yields (path, entry) pairs
957
# TODO? Perhaps this should return the from_dir so that the root is
958
# yielded? or maybe an option?
960
if self.root is None:
962
# Optimize a common case
963
if specific_file_ids is not None and len(specific_file_ids) == 1:
964
file_id = list(specific_file_ids)[0]
966
yield self.id2path(file_id), self[file_id]
969
if (specific_file_ids is None or
970
self.root.file_id in specific_file_ids):
972
elif isinstance(from_dir, basestring):
973
from_dir = self._byid[from_dir]
975
if specific_file_ids is not None:
977
def add_ancestors(file_id):
978
if file_id not in self:
980
parent_id = self[file_id].parent_id
981
if parent_id is None:
983
if parent_id not in parents:
984
parents.add(parent_id)
985
add_ancestors(parent_id)
986
for file_id in specific_file_ids:
987
add_ancestors(file_id)
991
stack = [(u'', from_dir)]
993
cur_relpath, cur_dir = stack.pop()
996
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
998
child_relpath = cur_relpath + child_name
1000
if (specific_file_ids is None or
1001
child_ie.file_id in specific_file_ids):
1002
yield child_relpath, child_ie
1004
if child_ie.kind == 'directory':
1005
if parents is None or child_ie.file_id in parents:
1006
child_dirs.append((child_relpath+'/', child_ie))
1007
stack.extend(reversed(child_dirs))
783
1009
def entries(self):
784
1010
"""Return list of (path, ie) for all entries except the root.
812
1037
for name, child_ie in kids:
813
child_path = pathjoin(parent_path, name)
1038
child_path = osutils.pathjoin(parent_path, name)
814
1039
descend(child_ie, child_path)
815
1040
descend(self.root, u'')
820
1043
def __contains__(self, file_id):
821
1044
"""True if this entry contains a file with given id.
823
1046
>>> inv = Inventory()
824
1047
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
825
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1048
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
826
1049
>>> '123' in inv
828
1051
>>> '456' in inv
831
return file_id in self._byid
1054
return (file_id in self._byid)
834
1056
def __getitem__(self, file_id):
835
1057
"""Return the entry for given file_id.
837
1059
>>> inv = Inventory()
838
1060
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
839
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1061
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
840
1062
>>> inv['123123'].name
844
1066
return self._byid[file_id]
845
1067
except KeyError:
847
raise BzrError("can't look up file_id None")
849
raise BzrError("file_id {%s} not in inventory" % file_id)
1068
# really we're passing an inventory, not a tree...
1069
raise errors.NoSuchId(self, file_id)
852
1071
def get_file_kind(self, file_id):
853
1072
return self._byid[file_id].kind
867
1085
if entry.file_id in self._byid:
868
1086
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
870
if entry.parent_id == ROOT_ID or entry.parent_id is None:
871
entry.parent_id = self.root.file_id
1088
if entry.parent_id is None:
1089
assert self.root is None and len(self._byid) == 0
1090
self._set_root(entry)
874
1093
parent = self._byid[entry.parent_id]
875
1094
except KeyError:
876
1095
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
878
if parent.children.has_key(entry.name):
1097
if entry.name in parent.children:
879
1098
raise BzrError("%s is already versioned" %
880
pathjoin(self.id2path(parent.file_id), entry.name))
1099
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
882
1101
self._byid[entry.file_id] = entry
883
1102
parent.children[entry.name] = entry
887
def add_path(self, relpath, kind, file_id=None):
1105
def add_path(self, relpath, kind, file_id=None, parent_id=None):
888
1106
"""Add entry from a path.
890
1108
The immediate parent must already be versioned.
892
1110
Returns the new entry object."""
893
from bzrlib.workingtree import gen_file_id
895
parts = bzrlib.osutils.splitpath(relpath)
1112
parts = osutils.splitpath(relpath)
896
1114
if len(parts) == 0:
897
raise BzrError("cannot re-add root of inventory")
900
file_id = gen_file_id(relpath)
902
parent_path = parts[:-1]
903
parent_id = self.path2id(parent_path)
904
if parent_id == None:
905
raise NotVersionedError(path=parent_path)
906
if kind == 'directory':
907
ie = InventoryDirectory(file_id, parts[-1], parent_id)
909
ie = InventoryFile(file_id, parts[-1], parent_id)
910
elif kind == 'symlink':
911
ie = InventoryLink(file_id, parts[-1], parent_id)
1116
file_id = generate_ids.gen_root_id()
1117
self.root = InventoryDirectory(file_id, '', None)
1118
self._byid = {self.root.file_id: self.root}
913
raise BzrError("unknown kind %r" % kind)
1121
parent_path = parts[:-1]
1122
parent_id = self.path2id(parent_path)
1123
if parent_id is None:
1124
raise errors.NotVersionedError(path=parent_path)
1125
ie = make_entry(kind, parts[-1], parent_id, file_id)
914
1126
return self.add(ie)
917
1128
def __delitem__(self, file_id):
918
1129
"""Remove entry by id.
920
1131
>>> inv = Inventory()
921
1132
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
922
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1133
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
923
1134
>>> '123' in inv
925
1136
>>> del inv['123']
951
1156
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
952
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1157
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
955
1160
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
956
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1161
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
960
1165
if not isinstance(other, Inventory):
961
1166
return NotImplemented
963
if len(self._byid) != len(other._byid):
964
# shortcut: obviously not the same
967
1168
return self._byid == other._byid
970
1170
def __ne__(self, other):
971
1171
return not self.__eq__(other)
974
1173
def __hash__(self):
975
1174
raise ValueError('not hashable')
1176
def _iter_file_id_parents(self, file_id):
1177
"""Yield the parents of file_id up to the root."""
1178
while file_id is not None:
1180
ie = self._byid[file_id]
1182
raise BzrError("file_id {%s} not found in inventory" % file_id)
1184
file_id = ie.parent_id
978
1186
def get_idpath(self, file_id):
979
1187
"""Return a list of file_ids for the path to an entry.
1042
1245
return parent.file_id
1045
1247
def has_filename(self, names):
1046
1248
return bool(self.path2id(names))
1049
1250
def has_id(self, file_id):
1050
return self._byid.has_key(file_id)
1251
return (file_id in self._byid)
1253
def remove_recursive_id(self, file_id):
1254
"""Remove file_id, and children, from the inventory.
1256
:param file_id: A file_id to remove.
1258
to_find_delete = [self._byid[file_id]]
1260
while to_find_delete:
1261
ie = to_find_delete.pop()
1262
to_delete.append(ie.file_id)
1263
if ie.kind == 'directory':
1264
to_find_delete.extend(ie.children.values())
1265
for file_id in reversed(to_delete):
1267
del self._byid[file_id]
1268
if ie.parent_id is not None:
1269
del self[ie.parent_id].children[ie.name]
1053
1271
def rename(self, file_id, new_parent_id, new_name):
1054
1272
"""Move a file within the inventory.
1079
1297
file_ie.name = new_name
1080
1298
file_ie.parent_id = new_parent_id
1300
def is_root(self, file_id):
1301
return self.root is not None and file_id == self.root.file_id
1304
def make_entry(kind, name, parent_id, file_id=None):
1305
"""Create an inventory entry.
1307
:param kind: the type of inventory entry to create.
1308
:param name: the basename of the entry.
1309
:param parent_id: the parent_id of the entry.
1310
:param file_id: the file_id to use. if None, one will be created.
1313
file_id = generate_ids.gen_file_id(name)
1315
norm_name, can_access = osutils.normalized_filename(name)
1316
if norm_name != name:
1320
# TODO: jam 20060701 This would probably be more useful
1321
# if the error was raised with the full path
1322
raise errors.InvalidNormalization(name)
1324
if kind == 'directory':
1325
return InventoryDirectory(file_id, name, parent_id)
1326
elif kind == 'file':
1327
return InventoryFile(file_id, name, parent_id)
1328
elif kind == 'symlink':
1329
return InventoryLink(file_id, name, parent_id)
1331
raise BzrError("unknown kind %r" % kind)
1085
1334
_NAME_RE = None
1087
1336
def is_valid_name(name):
1088
1337
global _NAME_RE
1089
if _NAME_RE == None:
1338
if _NAME_RE is None:
1090
1339
_NAME_RE = re.compile(r'^[^/\\]+$')
1092
1341
return bool(_NAME_RE.match(name))