78
90
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
92
>>> 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')}
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
83
95
>>> for ix, j in enumerate(i.iter_entries()):
84
96
... 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'))
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
Traceback (most recent call last):
91
BzrError: inventory already contains entry with id {2323}
98
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
99
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
100
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
101
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
InventoryFile('2324', 'bye.c', parent_id='123')
102
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
103
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
InventoryDirectory('2325', 'wibble', parent_id='123')
104
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
105
>>> i.path2id('src/wibble')
100
109
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
110
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
112
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
113
>>> for path, entry in i.iter_entries():
106
... assert i.path2id(path)
132
142
return False, False
134
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
135
output_to, reverse=False):
136
"""Perform a diff from this to to_entry.
138
text_diff will be used for textual difference calculation.
139
This is a template method, override _diff in child classes.
141
self._read_tree_state(tree.id2path(self.file_id), tree)
143
# cannot diff from one kind to another - you must do a removal
144
# and an addif they do not match.
145
assert self.kind == to_entry.kind
146
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
148
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
151
144
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
152
145
output_to, reverse=False):
153
146
"""Perform a diff between two entries of the same kind."""
155
def find_previous_heads(self, previous_inventories,
156
versioned_file_store,
159
"""Return the revisions and entries that directly preceed this.
161
Returned as a map from revision to inventory entry.
163
This is a map containing the file revisions in all parents
164
for which the file exists, and its revision is not a parent of
165
any other. If the file is new, the set will be empty.
167
:param versioned_file_store: A store where ancestry data on this
168
file id can be queried.
169
:param transaction: The transaction that queries to the versioned
170
file store should be completed under.
171
:param entry_vf: The entry versioned file, if its already available.
148
def parent_candidates(self, previous_inventories):
149
"""Find possible per-file graph parents.
151
This is currently defined by:
152
- Select the last changed revision in the parent inventory.
153
- Do deal with a short lived bug in bzr 0.8's development two entries
154
that have the same last changed but different 'x' bit settings are
173
def get_ancestors(weave, entry):
174
return set(weave.get_ancestry(entry.revision))
175
157
# revision:ie mapping for each ie found in previous_inventories.
177
# revision:ie mapping with one revision for each head.
179
# revision: ancestor list for each head
181
159
# identify candidate head revision ids.
182
160
for inv in previous_inventories:
183
161
if self.file_id in inv:
184
162
ie = inv[self.file_id]
185
assert ie.file_id == self.file_id
186
163
if ie.revision in candidates:
187
164
# same revision value in two different inventories:
188
165
# correct possible inconsistencies:
194
171
ie.executable = False
195
172
except AttributeError:
197
# must now be the same.
198
assert candidates[ie.revision] == ie
200
175
# add this revision as a candidate.
201
176
candidates[ie.revision] = ie
203
# common case optimisation
204
if len(candidates) == 1:
205
# if there is only one candidate revision found
206
# then we can opening the versioned file to access ancestry:
207
# there cannot be any ancestors to eliminate when there is
208
# only one revision available.
209
heads[ie.revision] = ie
212
# eliminate ancestors amongst the available candidates:
213
# heads are those that are not an ancestor of any other candidate
214
# - this provides convergence at a per-file level.
215
for ie in candidates.values():
216
# may be an ancestor of a known head:
217
already_present = 0 != len(
218
[head for head in heads
219
if ie.revision in head_ancestors[head]])
221
# an ancestor of an analyzed candidate.
223
# not an ancestor of a known head:
224
# load the versioned file for this file id if needed
226
entry_vf = versioned_file_store.get_weave_or_empty(
227
self.file_id, transaction)
228
ancestors = get_ancestors(entry_vf, ie)
229
# may knock something else out:
230
check_heads = list(heads.keys())
231
for head in check_heads:
232
if head in ancestors:
233
# this previously discovered 'head' is not
234
# really a head - its an ancestor of the newly
237
head_ancestors[ie.revision] = ancestors
238
heads[ie.revision] = ie
241
179
def get_tar_item(self, root, dp, now, tree):
242
180
"""Get a tarfile item and a file stream for its content."""
243
item = tarfile.TarInfo(pathjoin(root, dp))
181
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
244
182
# TODO: would be cool to actually set it to the timestamp of the
245
183
# revision it was last changed
352
288
"""Clone this inventory entry."""
353
289
raise NotImplementedError
355
def _describe_snapshot_change(self, previous_entries):
356
"""Describe how this entry will have changed in a new commit.
358
:param previous_entries: Dictionary from revision_id to inventory entry.
360
:returns: One-word description: "merged", "added", "renamed", "modified".
292
def describe_change(old_entry, new_entry):
293
"""Describe the change between old_entry and this.
295
This smells of being an InterInventoryEntry situation, but as its
296
the first one, we're making it a static method for now.
298
An entry with a different parent, or different name is considered
299
to be renamed. Reparenting is an internal detail.
300
Note that renaming the parent does not trigger a rename for the
362
# XXX: This assumes that the file *has* changed -- it should probably
363
# be fused with whatever does that detection. Why not just a single
364
# thing to compare the entries?
366
# TODO: Return some kind of object describing all the possible
367
# dimensions that can change, not just a string. That can then give
368
# both old and new names for renames, etc.
370
if len(previous_entries) > 1:
372
elif len(previous_entries) == 0:
303
# TODO: Perhaps return an object rather than just a string
304
if old_entry is new_entry:
305
# also the case of both being None
307
elif old_entry is None:
374
the_parent, = previous_entries.values()
375
if self.parent_id != the_parent.parent_id:
376
# actually, moved to another directory
378
elif self.name != the_parent.name:
309
elif new_entry is None:
311
if old_entry.kind != new_entry.kind:
313
text_modified, meta_modified = new_entry.detect_changes(old_entry)
314
if text_modified or meta_modified:
318
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
319
if old_entry.parent_id != new_entry.parent_id:
321
elif old_entry.name != new_entry.name:
325
if renamed and not modified:
326
return InventoryEntry.RENAMED
327
if modified and not renamed:
329
if modified and renamed:
330
return InventoryEntry.MODIFIED_AND_RENAMED
382
333
def __repr__(self):
383
return ("%s(%r, %r, parent_id=%r)"
334
return ("%s(%r, %r, parent_id=%r, revision=%r)"
384
335
% (self.__class__.__name__,
389
def snapshot(self, revision, path, previous_entries,
390
work_tree, weave_store, transaction):
391
"""Make a snapshot of this entry which may or may not have changed.
393
This means that all its fields are populated, that it has its
394
text stored in the text store or weave.
396
mutter('new parents of %s are %r', path, previous_entries)
397
self._read_tree_state(path, work_tree)
398
if len(previous_entries) == 1:
399
# cannot be unchanged unless there is only one parent file rev.
400
parent_ie = previous_entries.values()[0]
401
if self._unchanged(parent_ie):
402
mutter("found unchanged entry")
403
self.revision = parent_ie.revision
405
return self._snapshot_into_revision(revision, previous_entries,
406
work_tree, weave_store, transaction)
408
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
409
weave_store, transaction):
410
"""Record this revision unconditionally into a store.
412
The entry's last-changed revision property (`revision`) is updated to
413
that of the new revision.
415
:param revision: id of the new revision that is being recorded.
417
:returns: String description of the commit (e.g. "merged", "modified"), etc.
419
mutter('new revision {%s} for {%s}', revision, self.file_id)
420
self.revision = revision
421
change = self._describe_snapshot_change(previous_entries)
422
self._snapshot_text(previous_entries, work_tree, weave_store,
426
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
427
"""Record the 'text' of this entry, whatever form that takes.
429
This default implementation simply adds an empty text.
431
mutter('storing file {%s} in revision {%s}',
432
self.file_id, self.revision)
433
self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
435
341
def __eq__(self, other):
436
342
if not isinstance(other, InventoryEntry):
545
466
class InventoryFile(InventoryEntry):
546
467
"""A file in an inventory."""
469
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
470
'text_id', 'parent_id', 'children', 'executable',
471
'revision', 'symlink_target', 'reference_revision']
548
473
def _check(self, checker, tree_revision_id, tree):
549
474
"""See InventoryEntry._check"""
550
475
t = (self.file_id, self.revision)
551
476
if t in checker.checked_texts:
552
477
prev_sha = checker.checked_texts[t]
553
478
if prev_sha != self.text_sha1:
554
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
555
(self.file_id, tree_revision_id))
480
'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
481
(self.file_id, tree_revision_id, prev_sha, self.text_sha1,
557
484
checker.repeated_text_cnt += 1
560
487
if self.file_id not in checker.checked_weaves:
561
488
mutter('check weave {%s}', self.file_id)
562
w = tree.get_weave(self.file_id)
489
w = tree._get_weave(self.file_id)
563
490
# Not passing a progress bar, because it creates a new
564
491
# progress, which overwrites the current progress,
565
492
# and doesn't look nice
567
494
checker.checked_weaves[self.file_id] = True
569
w = tree.get_weave(self.file_id)
496
w = tree._get_weave(self.file_id)
571
498
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
572
499
checker.checked_text_cnt += 1
573
500
# We can't check the length, because Weave doesn't store that
574
501
# information, and the whole point of looking at the weave's
575
502
# sha1sum is that we don't have to extract the text.
576
if self.text_sha1 != w.get_sha1(self.revision):
503
if self.text_sha1 != w.get_sha1s([self.revision])[0]:
577
504
raise BzrCheckError('text {%s} version {%s} wrong sha1'
578
505
% (self.file_id, self.revision))
579
506
checker.checked_texts[t] = self.text_sha1
636
563
def _put_on_disk(self, fullpath, tree):
637
564
"""See InventoryEntry._put_on_disk."""
638
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
565
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
639
566
if tree.is_executable(self.file_id):
640
567
os.chmod(fullpath, 0755)
642
569
def _read_tree_state(self, path, work_tree):
643
570
"""See InventoryEntry._read_tree_state."""
644
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
645
self.executable = work_tree.is_executable(self.file_id)
571
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
572
# FIXME: 20050930 probe for the text size when getting sha1
573
# in _read_tree_state
574
self.executable = work_tree.is_executable(self.file_id, path=path)
577
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
578
% (self.__class__.__name__,
647
585
def _forget_tree_state(self):
648
586
self.text_sha1 = None
649
self.executable = None
651
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
652
"""See InventoryEntry._snapshot_text."""
653
mutter('storing file {%s} in revision {%s}',
654
self.file_id, self.revision)
655
# special case to avoid diffing on renames or
657
if (len(file_parents) == 1
658
and self.text_sha1 == file_parents.values()[0].text_sha1
659
and self.text_size == file_parents.values()[0].text_size):
660
previous_ie = file_parents.values()[0]
661
versionedfile = weave_store.get_weave(self.file_id, transaction)
662
versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
664
new_lines = work_tree.get_file(self.file_id).readlines()
665
self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
667
self.text_sha1 = sha_strings(new_lines)
668
self.text_size = sum(map(len, new_lines))
671
588
def _unchanged(self, previous_ie):
672
589
"""See InventoryEntry._unchanged."""
815
770
The inventory is created with a default root directory, with
818
# We are letting Branch.create() create a unique inventory
819
# root id. Rather than generating a random one here.
821
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
822
self.root = RootEntry(root_id)
773
if root_id is not None:
774
self._set_root(InventoryDirectory(root_id, u'', None))
823
778
self.revision_id = revision_id
781
return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
783
def apply_delta(self, delta):
784
"""Apply a delta to this inventory.
786
:param delta: A list of changes to apply. After all the changes are
787
applied the final inventory must be internally consistent, but it
788
is ok to supply changes which, if only half-applied would have an
789
invalid result - such as supplying two changes which rename two
790
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
791
('B', 'A', 'B-id', b_entry)].
793
Each change is a tuple, of the form (old_path, new_path, file_id,
796
When new_path is None, the change indicates the removal of an entry
797
from the inventory and new_entry will be ignored (using None is
798
appropriate). If new_path is not None, then new_entry must be an
799
InventoryEntry instance, which will be incorporated into the
800
inventory (and replace any existing entry with the same file id).
802
When old_path is None, the change indicates the addition of
803
a new entry to the inventory.
805
When neither new_path nor old_path are None, the change is a
806
modification to an entry, such as a rename, reparent, kind change
809
The children attribute of new_entry is ignored. This is because
810
this method preserves children automatically across alterations to
811
the parent of the children, and cases where the parent id of a
812
child is changing require the child to be passed in as a separate
813
change regardless. E.g. in the recursive deletion of a directory -
814
the directory's children must be included in the delta, or the
815
final inventory will be invalid.
818
# Remove all affected items which were in the original inventory,
819
# starting with the longest paths, thus ensuring parents are examined
820
# after their children, which means that everything we examine has no
821
# modified children remaining by the time we examine it.
822
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
823
if op is not None), reverse=True):
824
if file_id not in self:
827
# Preserve unaltered children of file_id for later reinsertion.
828
children[file_id] = getattr(self[file_id], 'children', {})
829
# Remove file_id and the unaltered children. If file_id is not
830
# being deleted it will be reinserted back later.
831
self.remove_recursive_id(file_id)
832
# Insert all affected which should be in the new inventory, reattaching
833
# their children if they had any. This is done from shortest path to
834
# longest, ensuring that items which were modified and whose parents in
835
# the resulting inventory were also modified, are inserted after their
837
for new_path, new_entry in sorted((np, e) for op, np, f, e in
838
delta if np is not None):
839
if new_entry.kind == 'directory':
840
new_entry.children = children.get(new_entry.file_id, {})
843
def _set_root(self, ie):
824
845
self._byid = {self.root.file_id: self.root}
828
848
# TODO: jam 20051218 Should copy also copy the revision_id?
829
other = Inventory(self.root.file_id)
849
entries = self.iter_entries()
850
if self.root is None:
851
return Inventory(root_id=None)
852
other = Inventory(entries.next()[1].file_id)
830
853
# copy recursively so we know directories will be added before
831
854
# their children. There are more efficient ways than this...
832
for path, entry in self.iter_entries():
833
if entry == self.root:
855
for path, entry in entries:
835
856
other.add(entry.copy())
839
859
def __iter__(self):
840
860
return iter(self._byid)
843
862
def __len__(self):
844
863
"""Returns number of entries."""
845
864
return len(self._byid)
848
866
def iter_entries(self, from_dir=None):
849
867
"""Return (path, entry) pairs, in order by name."""
853
elif isinstance(from_dir, basestring):
854
from_dir = self._byid[from_dir]
856
kids = from_dir.children.items()
858
for name, ie in kids:
860
if ie.kind == 'directory':
861
for cn, cie in self.iter_entries(from_dir=ie.file_id):
862
yield pathjoin(name, cn), cie
869
if self.root is None:
873
elif isinstance(from_dir, basestring):
874
from_dir = self._byid[from_dir]
876
# unrolling the recursive called changed the time from
877
# 440ms/663ms (inline/total) to 116ms/116ms
878
children = from_dir.children.items()
880
children = collections.deque(children)
881
stack = [(u'', children)]
883
from_dir_relpath, children = stack[-1]
886
name, ie = children.popleft()
888
# we know that from_dir_relpath never ends in a slash
889
# and 'f' doesn't begin with one, we can do a string op, rather
890
# than the checks of pathjoin(), though this means that all paths
892
path = from_dir_relpath + '/' + name
896
if ie.kind != 'directory':
899
# But do this child first
900
new_children = ie.children.items()
902
new_children = collections.deque(new_children)
903
stack.append((path, new_children))
904
# Break out of inner loop, so that we start outer loop with child
907
# if we finished all children, pop it off the stack
910
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None,
911
yield_parents=False):
912
"""Iterate over the entries in a directory first order.
914
This returns all entries for a directory before returning
915
the entries for children of a directory. This is not
916
lexicographically sorted order, and is a hybrid between
917
depth-first and breadth-first.
919
:param yield_parents: If True, yield the parents from the root leading
920
down to specific_file_ids that have been requested. This has no
921
impact if specific_file_ids is None.
922
:return: This yields (path, entry) pairs
924
if specific_file_ids and not isinstance(specific_file_ids, set):
925
specific_file_ids = set(specific_file_ids)
926
# TODO? Perhaps this should return the from_dir so that the root is
927
# yielded? or maybe an option?
929
if self.root is None:
931
# Optimize a common case
932
if (not yield_parents and specific_file_ids is not None and
933
len(specific_file_ids) == 1):
934
file_id = list(specific_file_ids)[0]
936
yield self.id2path(file_id), self[file_id]
939
if (specific_file_ids is None or yield_parents or
940
self.root.file_id in specific_file_ids):
942
elif isinstance(from_dir, basestring):
943
from_dir = self._byid[from_dir]
945
if specific_file_ids is not None:
946
# TODO: jam 20070302 This could really be done as a loop rather
947
# than a bunch of recursive calls.
950
def add_ancestors(file_id):
951
if file_id not in byid:
953
parent_id = byid[file_id].parent_id
954
if parent_id is None:
956
if parent_id not in parents:
957
parents.add(parent_id)
958
add_ancestors(parent_id)
959
for file_id in specific_file_ids:
960
add_ancestors(file_id)
964
stack = [(u'', from_dir)]
966
cur_relpath, cur_dir = stack.pop()
969
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
971
child_relpath = cur_relpath + child_name
973
if (specific_file_ids is None or
974
child_ie.file_id in specific_file_ids or
975
(yield_parents and child_ie.file_id in parents)):
976
yield child_relpath, child_ie
978
if child_ie.kind == 'directory':
979
if parents is None or child_ie.file_id in parents:
980
child_dirs.append((child_relpath+'/', child_ie))
981
stack.extend(reversed(child_dirs))
983
def make_entry(self, kind, name, parent_id, file_id=None):
984
"""Simple thunk to bzrlib.inventory.make_entry."""
985
return make_entry(kind, name, parent_id, file_id)
865
987
def entries(self):
866
988
"""Return list of (path, ie) for all entries except the root.
894
1015
for name, child_ie in kids:
895
child_path = pathjoin(parent_path, name)
1016
child_path = osutils.pathjoin(parent_path, name)
896
1017
descend(child_ie, child_path)
897
1018
descend(self.root, u'')
902
1021
def __contains__(self, file_id):
903
1022
"""True if this entry contains a file with given id.
905
1024
>>> inv = Inventory()
906
1025
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
907
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1026
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
908
1027
>>> '123' in inv
910
1029
>>> '456' in inv
913
return file_id in self._byid
1032
return (file_id in self._byid)
916
1034
def __getitem__(self, file_id):
917
1035
"""Return the entry for given file_id.
919
1037
>>> inv = Inventory()
920
1038
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
921
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1039
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
922
1040
>>> inv['123123'].name
926
1044
return self._byid[file_id]
927
1045
except KeyError:
929
raise BzrError("can't look up file_id None")
931
raise BzrError("file_id {%s} not in inventory" % file_id)
1046
# really we're passing an inventory, not a tree...
1047
raise errors.NoSuchId(self, file_id)
934
1049
def get_file_kind(self, file_id):
935
1050
return self._byid[file_id].kind
947
1071
Returns the new entry object.
949
1073
if entry.file_id in self._byid:
950
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
952
if entry.parent_id == ROOT_ID or entry.parent_id is None:
953
entry.parent_id = self.root.file_id
956
parent = self._byid[entry.parent_id]
958
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
960
if parent.children.has_key(entry.name):
961
raise BzrError("%s is already versioned" %
962
pathjoin(self.id2path(parent.file_id), entry.name))
964
self._byid[entry.file_id] = entry
965
parent.children[entry.name] = entry
969
def add_path(self, relpath, kind, file_id=None):
1074
raise errors.DuplicateFileId(entry.file_id,
1075
self._byid[entry.file_id])
1077
if entry.parent_id is None:
1081
parent = self._byid[entry.parent_id]
1083
raise BzrError("parent_id {%s} not in inventory" %
1086
if entry.name in parent.children:
1087
raise BzrError("%s is already versioned" %
1088
osutils.pathjoin(self.id2path(parent.file_id),
1089
entry.name).encode('utf-8'))
1090
parent.children[entry.name] = entry
1091
return self._add_child(entry)
1093
def add_path(self, relpath, kind, file_id=None, parent_id=None):
970
1094
"""Add entry from a path.
972
1096
The immediate parent must already be versioned.
974
1098
Returns the new entry object."""
975
from bzrlib.workingtree import gen_file_id
977
parts = bzrlib.osutils.splitpath(relpath)
980
file_id = gen_file_id(relpath)
1100
parts = osutils.splitpath(relpath)
982
1102
if len(parts) == 0:
983
self.root = RootEntry(file_id)
1104
file_id = generate_ids.gen_root_id()
1105
self.root = InventoryDirectory(file_id, '', None)
984
1106
self._byid = {self.root.file_id: self.root}
987
1109
parent_path = parts[:-1]
988
1110
parent_id = self.path2id(parent_path)
989
if parent_id == None:
990
raise NotVersionedError(path=parent_path)
991
if kind == 'directory':
992
ie = InventoryDirectory(file_id, parts[-1], parent_id)
994
ie = InventoryFile(file_id, parts[-1], parent_id)
995
elif kind == 'symlink':
996
ie = InventoryLink(file_id, parts[-1], parent_id)
998
raise BzrError("unknown kind %r" % kind)
1111
if parent_id is None:
1112
raise errors.NotVersionedError(path=parent_path)
1113
ie = make_entry(kind, parts[-1], parent_id, file_id)
999
1114
return self.add(ie)
1002
1116
def __delitem__(self, file_id):
1003
1117
"""Remove entry by id.
1005
1119
>>> inv = Inventory()
1006
1120
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1007
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1121
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1008
1122
>>> '123' in inv
1010
1124
>>> del inv['123']
1158
1283
file_ie.name = new_name
1159
1284
file_ie.parent_id = new_parent_id
1286
def is_root(self, file_id):
1287
return self.root is not None and file_id == self.root.file_id
1291
'directory': InventoryDirectory,
1292
'file': InventoryFile,
1293
'symlink': InventoryLink,
1294
'tree-reference': TreeReference
1297
def make_entry(kind, name, parent_id, file_id=None):
1298
"""Create an inventory entry.
1300
:param kind: the type of inventory entry to create.
1301
:param name: the basename of the entry.
1302
:param parent_id: the parent_id of the entry.
1303
:param file_id: the file_id to use. if None, one will be created.
1306
file_id = generate_ids.gen_file_id(name)
1307
name = ensure_normalized_name(name)
1309
factory = entry_factory[kind]
1311
raise BzrError("unknown kind %r" % kind)
1312
return factory(file_id, name, parent_id)
1315
def ensure_normalized_name(name):
1318
:raises InvalidNormalization: When name is not normalized, and cannot be
1319
accessed on this platform by the normalized path.
1320
:return: The NFC normalised version of name.
1322
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1323
# keep them synchronised.
1324
# we dont import normalized_filename directly because we want to be
1325
# able to change the implementation at runtime for tests.
1326
norm_name, can_access = osutils.normalized_filename(name)
1327
if norm_name != name:
1331
# TODO: jam 20060701 This would probably be more useful
1332
# if the error was raised with the full path
1333
raise errors.InvalidNormalization(name)
1164
1337
_NAME_RE = None
1166
1339
def is_valid_name(name):
1167
1340
global _NAME_RE
1168
if _NAME_RE == None:
1341
if _NAME_RE is None:
1169
1342
_NAME_RE = re.compile(r'^[^/\\]+$')
1171
1344
return bool(_NAME_RE.match(name))