89
78
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
94
83
>>> for ix, j in enumerate(i.iter_entries()):
95
84
... print (j[0] == shouldbe[ix], j[1])
97
(True, InventoryDirectory('TREE_ROOT', u'', 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))
86
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
(True, InventoryFile('2323', 'hello.c', parent_id='123'))
100
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
101
89
Traceback (most recent call last):
103
91
BzrError: inventory already contains entry with id {2323}
104
92
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
105
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
InventoryFile('2324', 'bye.c', parent_id='123')
106
94
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
107
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
InventoryDirectory('2325', 'wibble', parent_id='123')
108
96
>>> i.path2id('src/wibble')
112
100
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
113
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
115
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
116
104
>>> for path, entry in i.iter_entries():
118
106
... assert i.path2id(path)
126
113
>>> i.id2path('2326')
127
114
'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,
140
125
def detect_changes(self, old_entry):
141
126
"""Return a (text_modified, meta_modified) from this to old_entry.
166
151
output_to, reverse=False):
167
152
"""Perform a diff between two entries of the same kind."""
169
def find_previous_heads(self, previous_inventories,
170
versioned_file_store,
173
"""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.
175
157
Returned as a map from revision to inventory entry.
177
159
This is a map containing the file revisions in all parents
178
160
for which the file exists, and its revision is not a parent of
179
161
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.
187
163
def get_ancestors(weave, entry):
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.
164
return set(map(weave.idx_to_name,
165
weave.inclusions([weave.lookup(entry.revision)])))
193
# revision: ancestor list for each head
194
167
head_ancestors = {}
195
# identify candidate head revision ids.
196
168
for inv in previous_inventories:
197
169
if self.file_id in inv:
198
170
ie = inv[self.file_id]
199
171
assert ie.file_id == self.file_id
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
172
if ie.revision in heads:
173
# fixup logic, there was a bug in revision updates.
174
# with x bit support.
206
if candidates[ie.revision].executable != ie.executable:
207
candidates[ie.revision].executable = False
176
if heads[ie.revision].executable != ie.executable:
177
heads[ie.revision].executable = False
208
178
ie.executable = False
209
179
except AttributeError:
211
# must now be the same.
212
assert candidates[ie.revision] == ie
181
assert 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
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
255
203
def get_tar_item(self, root, dp, now, tree):
256
204
"""Get a tarfile item and a file stream for its content."""
257
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
205
item = tarfile.TarInfo(pathjoin(root, dp))
258
206
# TODO: would be cool to actually set it to the timestamp of the
259
207
# revision it was last changed
322
268
This is a template method - implement _put_on_disk in subclasses.
324
fullpath = osutils.pathjoin(dest, dp)
270
fullpath = pathjoin(dest, dp)
325
271
self._put_on_disk(fullpath, tree)
326
# mutter(" export {%s} kind %s to %s", self.file_id,
327
# self.kind, fullpath)
272
mutter(" export {%s} kind %s to %s", self.file_id,
329
275
def _put_on_disk(self, fullpath, tree):
330
276
"""Put this entry onto disk at fullpath, from tree tree."""
331
277
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
333
279
def sorted_children(self):
334
return sorted(self.children.items())
280
l = self.children.items()
337
285
def versionable_kind(kind):
338
return (kind in ('file', 'directory', 'symlink'))
286
return kind in ('file', 'directory', 'symlink')
340
288
def check(self, checker, rev_id, inv, tree):
341
289
"""Check this inventory entry is intact.
343
291
This is a template method, override _check for kind specific
346
:param checker: Check object providing context for the checks;
347
can be used to find out what parts of the repository have already
349
:param rev_id: Revision id from which this InventoryEntry was loaded.
350
Not necessarily the last-changed revision for this file.
351
:param inv: Inventory from which the entry was loaded.
352
:param tree: RevisionTree for this entry.
354
if self.parent_id is not None:
294
if self.parent_id != None:
355
295
if not inv.has_id(self.parent_id):
356
296
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
357
297
% (self.parent_id, rev_id))
362
302
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
363
303
(self.kind, rev_id))
366
307
"""Clone this inventory entry."""
367
308
raise NotImplementedError
370
def describe_change(old_entry, new_entry):
371
"""Describe the change between old_entry and this.
373
This smells of being an InterInventoryEntry situation, but as its
374
the first one, we're making it a static method for now.
376
An entry with a different parent, or different name is considered
377
to be renamed. Reparenting is an internal detail.
378
Note that renaming the parent does not trigger a rename for the
381
# TODO: Perhaps return an object rather than just a string
382
if old_entry is new_entry:
383
# also the case of both being None
385
elif old_entry is None:
310
def _get_snapshot_change(self, previous_entries):
311
if len(previous_entries) > 1:
313
elif len(previous_entries) == 0:
387
elif new_entry is None:
389
if old_entry.kind != new_entry.kind:
391
text_modified, meta_modified = new_entry.detect_changes(old_entry)
392
if text_modified or meta_modified:
396
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
397
if old_entry.parent_id != new_entry.parent_id:
399
elif old_entry.name != new_entry.name:
403
if renamed and not modified:
404
return InventoryEntry.RENAMED
405
if modified and not renamed:
407
if modified and renamed:
408
return InventoryEntry.MODIFIED_AND_RENAMED
316
return 'modified/renamed/reparented'
411
318
def __repr__(self):
412
return ("%s(%r, %r, parent_id=%r, revision=%r)"
319
return ("%s(%r, %r, parent_id=%r)"
413
320
% (self.__class__.__name__,
419
325
def snapshot(self, revision, path, previous_entries,
420
work_tree, commit_builder):
326
work_tree, weave_store, transaction):
421
327
"""Make a snapshot of this entry which may or may not have changed.
423
329
This means that all its fields are populated, that it has its
424
330
text stored in the text store or weave.
426
# mutter('new parents of %s are %r', path, previous_entries)
332
mutter('new parents of %s are %r', path, previous_entries)
427
333
self._read_tree_state(path, work_tree)
428
# TODO: Where should we determine whether to reuse a
429
# previous revision id or create a new revision? 20060606
430
334
if len(previous_entries) == 1:
431
335
# cannot be unchanged unless there is only one parent file rev.
432
336
parent_ie = previous_entries.values()[0]
433
337
if self._unchanged(parent_ie):
434
# mutter("found unchanged entry")
338
mutter("found unchanged entry")
435
339
self.revision = parent_ie.revision
436
340
return "unchanged"
437
return self._snapshot_into_revision(revision, previous_entries,
438
work_tree, commit_builder)
440
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
442
"""Record this revision unconditionally into a store.
444
The entry's last-changed revision property (`revision`) is updated to
445
that of the new revision.
447
:param revision: id of the new revision that is being recorded.
449
:returns: String description of the commit (e.g. "merged", "modified"), etc.
451
# mutter('new revision {%s} for {%s}', revision, self.file_id)
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)
452
348
self.revision = revision
453
self._snapshot_text(previous_entries, work_tree, commit_builder)
349
change = self._get_snapshot_change(previous_entries)
350
self._snapshot_text(previous_entries, work_tree, weave_store,
455
def _snapshot_text(self, file_parents, work_tree, commit_builder):
354
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
456
355
"""Record the 'text' of this entry, whatever form that takes.
458
357
This default implementation simply adds an empty text.
460
raise NotImplementedError(self._snapshot_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)
462
363
def __eq__(self, other):
463
364
if not isinstance(other, InventoryEntry):
506
407
# first requested, or preload them if they're already known
507
408
pass # nothing to do by default
509
def _forget_tree_state(self):
513
411
class RootEntry(InventoryEntry):
515
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
516
'text_id', 'parent_id', 'children', 'executable',
517
'revision', 'symlink_target']
519
413
def _check(self, checker, rev_id, tree):
520
414
"""See InventoryEntry._check"""
522
416
def __init__(self, file_id):
523
417
self.file_id = file_id
524
418
self.children = {}
525
self.kind = 'directory'
419
self.kind = 'root_directory'
526
420
self.parent_id = None
529
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
530
' Please use InventoryDirectory instead.',
531
DeprecationWarning, stacklevel=2)
533
423
def __eq__(self, other):
534
424
if not isinstance(other, RootEntry):
580
466
"""See InventoryEntry._put_on_disk."""
581
467
os.mkdir(fullpath)
583
def _snapshot_text(self, file_parents, work_tree, commit_builder):
584
"""See InventoryEntry._snapshot_text."""
585
commit_builder.modified_directory(self.file_id, file_parents)
588
470
class InventoryFile(InventoryEntry):
589
471
"""A file in an inventory."""
591
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
592
'text_id', 'parent_id', 'children', 'executable',
593
'revision', 'symlink_target']
595
def _check(self, checker, tree_revision_id, tree):
473
def _check(self, checker, rev_id, tree):
596
474
"""See InventoryEntry._check"""
597
t = (self.file_id, self.revision)
475
revision = self.revision
476
t = (self.file_id, revision)
598
477
if t in checker.checked_texts:
599
prev_sha = checker.checked_texts[t]
478
prev_sha = checker.checked_texts[t]
600
479
if prev_sha != self.text_sha1:
601
480
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
602
(self.file_id, tree_revision_id))
481
(self.file_id, rev_id))
604
483
checker.repeated_text_cnt += 1
645
524
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
646
525
output_to, reverse=False):
647
526
"""See InventoryEntry._diff."""
649
from_text = tree.get_file(self.file_id).readlines()
651
to_text = to_tree.get_file(to_entry.file_id).readlines()
655
text_diff(from_label, from_text,
656
to_label, to_text, output_to)
658
text_diff(to_label, to_text,
659
from_label, from_text, output_to)
660
except errors.BinaryFile:
662
label_pair = (to_label, from_label)
664
label_pair = (from_label, to_label)
665
print >> output_to, "Binary files %s and %s differ" % label_pair
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)
667
539
def has_text(self):
668
540
"""See InventoryEntry.has_text."""
690
562
def _put_on_disk(self, fullpath, tree):
691
563
"""See InventoryEntry._put_on_disk."""
692
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
564
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
693
565
if tree.is_executable(self.file_id):
694
566
os.chmod(fullpath, 0755)
696
568
def _read_tree_state(self, path, work_tree):
697
569
"""See InventoryEntry._read_tree_state."""
698
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
699
# FIXME: 20050930 probe for the text size when getting sha1
700
# in _read_tree_state
701
self.executable = work_tree.is_executable(self.file_id, path=path)
704
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
705
% (self.__class__.__name__,
712
def _forget_tree_state(self):
713
self.text_sha1 = None
715
def _snapshot_text(self, file_parents, work_tree, commit_builder):
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):
716
574
"""See InventoryEntry._snapshot_text."""
717
def get_content_byte_lines():
718
return work_tree.get_file(self.file_id).readlines()
719
self.text_sha1, self.text_size = commit_builder.modified_file_text(
720
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
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))
722
594
def _unchanged(self, previous_ie):
723
595
"""See InventoryEntry._unchanged."""
736
608
class InventoryLink(InventoryEntry):
737
609
"""A file in an inventory."""
739
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
740
'text_id', 'parent_id', 'children', 'executable',
741
'revision', 'symlink_target']
611
__slots__ = ['symlink_target']
743
613
def _check(self, checker, rev_id, tree):
744
614
"""See InventoryEntry._check"""
745
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
615
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
746
616
raise BzrCheckError('symlink {%s} has text in revision {%s}'
747
617
% (self.file_id, rev_id))
748
if self.symlink_target is None:
618
if self.symlink_target == None:
749
619
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
750
620
% (self.file_id, rev_id))
858
720
May also look up by name:
860
722
>>> [x[0] for x in inv.iter_entries()]
862
724
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
863
725
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
864
Traceback (most recent call last):
865
BzrError: parent_id {TREE_ROOT} not in inventory
866
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
867
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
726
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
869
def __init__(self, root_id=ROOT_ID, revision_id=None):
728
def __init__(self, root_id=ROOT_ID):
870
729
"""Create or read an inventory.
872
731
If a working directory is specified, the inventory is read
876
735
The inventory is created with a default root directory, with
879
if root_id is not None:
880
assert root_id.__class__ == str
881
self._set_root(InventoryDirectory(root_id, u'', None))
885
self.revision_id = revision_id
887
def _set_root(self, ie):
738
# We are letting Branch.create() 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)
889
743
self._byid = {self.root.file_id: self.root}
892
# TODO: jam 20051218 Should copy also copy the revision_id?
893
entries = self.iter_entries()
894
other = Inventory(entries.next()[1].file_id)
747
other = Inventory(self.root.file_id)
895
748
# copy recursively so we know directories will be added before
896
749
# their children. There are more efficient ways than this...
897
for path, entry in entries():
750
for path, entry in self.iter_entries():
751
if entry == self.root:
898
753
other.add(entry.copy())
901
757
def __iter__(self):
902
758
return iter(self._byid)
904
761
def __len__(self):
905
762
"""Returns number of entries."""
906
763
return len(self._byid)
908
766
def iter_entries(self, from_dir=None):
909
767
"""Return (path, entry) pairs, in order by name."""
911
if self.root is None:
915
elif isinstance(from_dir, basestring):
916
from_dir = self._byid[from_dir]
918
# unrolling the recursive called changed the time from
919
# 440ms/663ms (inline/total) to 116ms/116ms
920
children = from_dir.children.items()
922
children = collections.deque(children)
923
stack = [(u'', children)]
925
from_dir_relpath, children = stack[-1]
928
name, ie = children.popleft()
930
# we know that from_dir_relpath never ends in a slash
931
# and 'f' doesn't begin with one, we can do a string op, rather
932
# than the checks of pathjoin(), though this means that all paths
934
path = from_dir_relpath + '/' + name
938
if ie.kind != 'directory':
941
# But do this child first
942
new_children = ie.children.items()
944
new_children = collections.deque(new_children)
945
stack.append((path, new_children))
946
# Break out of inner loop, so that we start outer loop with child
949
# if we finished all children, pop it off the stack
952
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
953
"""Iterate over the entries in a directory first order.
955
This returns all entries for a directory before returning
956
the entries for children of a directory. This is not
957
lexicographically sorted order, and is a hybrid between
958
depth-first and breadth-first.
960
:return: This yields (path, entry) pairs
962
if specific_file_ids:
963
specific_file_ids = [osutils.safe_file_id(fid)
964
for fid in specific_file_ids]
965
# TODO? Perhaps this should return the from_dir so that the root is
966
# yielded? or maybe an option?
968
if self.root is None:
970
# Optimize a common case
971
if specific_file_ids is not None and len(specific_file_ids) == 1:
972
file_id = list(specific_file_ids)[0]
974
yield self.id2path(file_id), self[file_id]
977
if (specific_file_ids is None or
978
self.root.file_id in specific_file_ids):
980
elif isinstance(from_dir, basestring):
981
from_dir = self._byid[from_dir]
983
if specific_file_ids is not None:
985
def add_ancestors(file_id):
986
if file_id not in self:
988
parent_id = self[file_id].parent_id
989
if parent_id is None:
991
if parent_id not in parents:
992
parents.add(parent_id)
993
add_ancestors(parent_id)
994
for file_id in specific_file_ids:
995
add_ancestors(file_id)
999
stack = [(u'', from_dir)]
1001
cur_relpath, cur_dir = stack.pop()
1004
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
1006
child_relpath = cur_relpath + child_name
1008
if (specific_file_ids is None or
1009
child_ie.file_id in specific_file_ids):
1010
yield child_relpath, child_ie
1012
if child_ie.kind == 'directory':
1013
if parents is None or child_ie.file_id in parents:
1014
child_dirs.append((child_relpath+'/', child_ie))
1015
stack.extend(reversed(child_dirs))
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
1017
783
def entries(self):
1018
784
"""Return list of (path, ie) for all entries except the root.
1045
812
for name, child_ie in kids:
1046
child_path = osutils.pathjoin(parent_path, name)
813
child_path = pathjoin(parent_path, name)
1047
814
descend(child_ie, child_path)
1048
815
descend(self.root, u'')
1051
820
def __contains__(self, file_id):
1052
821
"""True if this entry contains a file with given id.
1054
823
>>> inv = Inventory()
1055
824
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1056
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
825
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1057
826
>>> '123' in inv
1059
828
>>> '456' in inv
1062
file_id = osutils.safe_file_id(file_id)
1063
return (file_id in self._byid)
831
return file_id in self._byid
1065
834
def __getitem__(self, file_id):
1066
835
"""Return the entry for given file_id.
1068
837
>>> inv = Inventory()
1069
838
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1070
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
839
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1071
840
>>> inv['123123'].name
1074
file_id = osutils.safe_file_id(file_id)
1076
844
return self._byid[file_id]
1077
845
except KeyError:
1078
# really we're passing an inventory, not a tree...
1079
raise errors.NoSuchId(self, file_id)
847
raise BzrError("can't look up file_id None")
849
raise BzrError("file_id {%s} not in inventory" % file_id)
1081
852
def get_file_kind(self, file_id):
1082
file_id = osutils.safe_file_id(file_id)
1083
853
return self._byid[file_id].kind
1085
855
def get_child(self, parent_id, filename):
1086
parent_id = osutils.safe_file_id(parent_id)
1087
856
return self[parent_id].children.get(filename)
1089
859
def add(self, entry):
1090
860
"""Add entry to inventory.
1097
867
if entry.file_id in self._byid:
1098
868
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1100
if entry.parent_id is None:
1101
assert self.root is None and len(self._byid) == 0
1102
self._set_root(entry)
870
if entry.parent_id == ROOT_ID or entry.parent_id is None:
871
entry.parent_id = self.root.file_id
1105
874
parent = self._byid[entry.parent_id]
1106
875
except KeyError:
1107
876
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1109
if entry.name in parent.children:
878
if parent.children.has_key(entry.name):
1110
879
raise BzrError("%s is already versioned" %
1111
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
880
pathjoin(self.id2path(parent.file_id), entry.name))
1113
882
self._byid[entry.file_id] = entry
1114
883
parent.children[entry.name] = entry
1117
def add_path(self, relpath, kind, file_id=None, parent_id=None):
887
def add_path(self, relpath, kind, file_id=None):
1118
888
"""Add entry from a path.
1120
890
The immediate parent must already be versioned.
1122
892
Returns the new entry object."""
893
from bzrlib.workingtree import gen_file_id
1124
parts = osutils.splitpath(relpath)
895
parts = bzrlib.osutils.splitpath(relpath)
1126
896
if len(parts) == 0:
1128
file_id = generate_ids.gen_root_id()
1130
file_id = osutils.safe_file_id(file_id)
1131
self.root = InventoryDirectory(file_id, '', None)
1132
self._byid = {self.root.file_id: self.root}
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)
1135
parent_path = parts[:-1]
1136
parent_id = self.path2id(parent_path)
1137
if parent_id is None:
1138
raise errors.NotVersionedError(path=parent_path)
1139
ie = make_entry(kind, parts[-1], parent_id, file_id)
913
raise BzrError("unknown kind %r" % kind)
1140
914
return self.add(ie)
1142
917
def __delitem__(self, file_id):
1143
918
"""Remove entry by id.
1145
920
>>> inv = Inventory()
1146
921
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1147
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
922
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1148
923
>>> '123' in inv
1150
925
>>> del inv['123']
1151
926
>>> '123' in inv
1154
file_id = osutils.safe_file_id(file_id)
1155
929
ie = self[file_id]
1157
assert ie.parent_id is None or \
1158
self[ie.parent_id].children[ie.name] == ie
931
assert self[ie.parent_id].children[ie.name] == ie
933
# TODO: Test deleting all children; maybe hoist to a separate
935
if ie.kind == 'directory':
936
for cie in ie.children.values():
937
del self[cie.file_id]
1160
940
del self._byid[file_id]
1161
if ie.parent_id is not None:
1162
del self[ie.parent_id].children[ie.name]
941
del self[ie.parent_id].children[ie.name]
1164
944
def __eq__(self, other):
1165
945
"""Compare two sets by comparing their contents.
1171
951
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1172
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
952
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1175
955
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1176
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
956
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1180
960
if not isinstance(other, Inventory):
1181
961
return NotImplemented
963
if len(self._byid) != len(other._byid):
964
# shortcut: obviously not the same
1183
967
return self._byid == other._byid
1185
970
def __ne__(self, other):
1186
971
return not self.__eq__(other)
1188
974
def __hash__(self):
1189
975
raise ValueError('not hashable')
1191
def _iter_file_id_parents(self, file_id):
1192
"""Yield the parents of file_id up to the root."""
1193
file_id = osutils.safe_file_id(file_id)
1194
while file_id is not None:
1196
ie = self._byid[file_id]
1198
raise BzrError("file_id {%s} not found in inventory" % file_id)
1200
file_id = ie.parent_id
1202
978
def get_idpath(self, file_id):
1203
979
"""Return a list of file_ids for the path to an entry.
1263
1042
return parent.file_id
1265
1045
def has_filename(self, names):
1266
1046
return bool(self.path2id(names))
1268
1049
def has_id(self, file_id):
1269
file_id = osutils.safe_file_id(file_id)
1270
return (file_id in self._byid)
1050
return self._byid.has_key(file_id)
1272
def remove_recursive_id(self, file_id):
1273
"""Remove file_id, and children, from the inventory.
1275
:param file_id: A file_id to remove.
1277
file_id = osutils.safe_file_id(file_id)
1278
to_find_delete = [self._byid[file_id]]
1280
while to_find_delete:
1281
ie = to_find_delete.pop()
1282
to_delete.append(ie.file_id)
1283
if ie.kind == 'directory':
1284
to_find_delete.extend(ie.children.values())
1285
for file_id in reversed(to_delete):
1287
del self._byid[file_id]
1288
if ie.parent_id is not None:
1289
del self[ie.parent_id].children[ie.name]
1291
1053
def rename(self, file_id, new_parent_id, new_name):
1292
1054
"""Move a file within the inventory.
1294
1056
This can change either the name, or the parent, or both.
1296
This does not move the working file.
1298
file_id = osutils.safe_file_id(file_id)
1058
This does not move the working file."""
1299
1059
if not is_valid_name(new_name):
1300
1060
raise BzrError("not an acceptable filename: %r" % new_name)
1319
1079
file_ie.name = new_name
1320
1080
file_ie.parent_id = new_parent_id
1322
def is_root(self, file_id):
1323
file_id = osutils.safe_file_id(file_id)
1324
return self.root is not None and file_id == self.root.file_id
1328
'directory':InventoryDirectory,
1329
'file':InventoryFile,
1330
'symlink':InventoryLink,
1333
def make_entry(kind, name, parent_id, file_id=None):
1334
"""Create an inventory entry.
1336
:param kind: the type of inventory entry to create.
1337
:param name: the basename of the entry.
1338
:param parent_id: the parent_id of the entry.
1339
:param file_id: the file_id to use. if None, one will be created.
1342
file_id = generate_ids.gen_file_id(name)
1344
file_id = osutils.safe_file_id(file_id)
1346
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1347
# keep them synchronised.
1348
# we dont import normalized_filename directly because we want to be
1349
# able to change the implementation at runtime for tests.
1350
norm_name, can_access = osutils.normalized_filename(name)
1351
if norm_name != name:
1355
# TODO: jam 20060701 This would probably be more useful
1356
# if the error was raised with the full path
1357
raise errors.InvalidNormalization(name)
1360
factory = entry_factory[kind]
1362
raise BzrError("unknown kind %r" % kind)
1363
return factory(file_id, name, parent_id)
1366
1085
_NAME_RE = None
1368
1087
def is_valid_name(name):
1369
1088
global _NAME_RE
1370
if _NAME_RE is None:
1089
if _NAME_RE == None:
1371
1090
_NAME_RE = re.compile(r'^[^/\\]+$')
1373
1092
return bool(_NAME_RE.match(name))