95
94
>>> for ix, j in enumerate(i.iter_entries()):
96
95
... print (j[0] == shouldbe[ix], j[1])
98
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
97
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
99
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
100
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
100
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
101
Traceback (most recent call last):
103
BzrError: inventory already contains entry with id {2323}
101
104
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
102
105
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
103
106
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
163
165
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
164
166
output_to, reverse=False):
165
167
"""Perform a diff between two entries of the same kind."""
167
def parent_candidates(self, previous_inventories):
168
"""Find possible per-file graph parents.
170
This is currently defined by:
171
- Select the last changed revision in the parent inventory.
172
- Do deal with a short lived bug in bzr 0.8's development two entries
173
that have the same last changed but different 'x' bit settings are
169
def find_previous_heads(self, previous_inventories,
170
versioned_file_store,
173
"""Return the revisions and entries that directly precede this.
175
Returned as a map from revision to inventory entry.
177
This is a map containing the file revisions in all parents
178
for which the file exists, and its revision is not a parent of
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.
187
def get_ancestors(weave, entry):
188
return set(weave.get_ancestry(entry.revision))
176
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
178
195
# identify candidate head revision ids.
179
196
for inv in previous_inventories:
180
197
if self.file_id in inv:
197
214
# add this revision as a candidate.
198
215
candidates[ie.revision] = ie
201
@deprecated_method(symbol_versioning.zero_ninetyone)
202
def find_previous_heads(self, previous_inventories,
203
versioned_file_store,
206
"""Return the revisions and entries that directly precede this.
208
Returned as a map from revision to inventory entry.
210
This is a map containing the file revisions in all parents
211
for which the file exists, and its revision is not a parent of
212
any other. If the file is new, the set will be empty.
214
:param versioned_file_store: A store where ancestry data on this
215
file id can be queried.
216
:param transaction: The transaction that queries to the versioned
217
file store should be completed under.
218
:param entry_vf: The entry versioned file, if its already available.
220
candidates = self.parent_candidates(previous_inventories)
222
# revision:ie mapping with one revision for each head.
224
217
# common case optimisation
225
218
if len(candidates) == 1:
226
219
# if there is only one candidate revision found
227
# then we can avoid opening the versioned file to access ancestry:
220
# then we can opening the versioned file to access ancestry:
228
221
# there cannot be any ancestors to eliminate when there is
229
222
# only one revision available.
232
# --- what follows is now encapsulated in repository.get_graph.heads(),
233
# but that is not accessible from here as we have no repository
234
# pointer. Note that the repository.get_graph.heads() call can return
235
# different results *at the moment* because of the kind-changing check
236
# we have in parent_candidates().
223
heads[ie.revision] = ie
238
226
# eliminate ancestors amongst the available candidates:
239
227
# heads are those that are not an ancestor of any other candidate
240
228
# - this provides convergence at a per-file level.
241
def get_ancestors(weave, entry):
242
return set(weave.get_ancestry(entry.revision, topo_sorted=False))
243
# revision: ancestor list for each head
245
229
for ie in candidates.values():
246
230
# may be an ancestor of a known head:
247
231
already_present = 0 != len(
415
def snapshot(self, revision, path, previous_entries,
416
work_tree, commit_builder):
417
"""Make a snapshot of this entry which may or may not have changed.
419
This means that all its fields are populated, that it has its
420
text stored in the text store or weave.
422
# mutter('new parents of %s are %r', path, previous_entries)
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
426
if len(previous_entries) == 1:
427
# cannot be unchanged unless there is only one parent file rev.
428
parent_ie = previous_entries.values()[0]
429
if self._unchanged(parent_ie):
430
# mutter("found unchanged entry")
431
self.revision = parent_ie.revision
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)
448
self.revision = revision
449
self._snapshot_text(previous_entries, work_tree, commit_builder)
451
def _snapshot_text(self, file_parents, work_tree, commit_builder):
452
"""Record the 'text' of this entry, whatever form that takes.
454
This default implementation simply adds an empty text.
456
raise NotImplementedError(self._snapshot_text)
436
458
def __eq__(self, other):
437
459
if not isinstance(other, InventoryEntry):
438
460
return NotImplemented
490
509
class RootEntry(InventoryEntry):
492
511
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
493
'text_id', 'parent_id', 'children', 'executable',
494
'revision', 'symlink_target', 'reference_revision']
512
'text_id', 'parent_id', 'children', 'executable',
513
'revision', 'symlink_target']
496
515
def _check(self, checker, rev_id, tree):
497
516
"""See InventoryEntry._check"""
519
538
"""A directory in an inventory."""
521
540
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
522
'text_id', 'parent_id', 'children', 'executable',
523
'revision', 'symlink_target', 'reference_revision']
541
'text_id', 'parent_id', 'children', 'executable',
542
'revision', 'symlink_target']
525
544
def _check(self, checker, rev_id, tree):
526
545
"""See InventoryEntry._check"""
557
576
"""See InventoryEntry._put_on_disk."""
558
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)
561
584
class InventoryFile(InventoryEntry):
562
585
"""A file in an inventory."""
564
587
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
565
'text_id', 'parent_id', 'children', 'executable',
566
'revision', 'symlink_target', 'reference_revision']
588
'text_id', 'parent_id', 'children', 'executable',
589
'revision', 'symlink_target']
568
591
def _check(self, checker, tree_revision_id, tree):
569
592
"""See InventoryEntry._check"""
571
594
if t in checker.checked_texts:
572
595
prev_sha = checker.checked_texts[t]
573
596
if prev_sha != self.text_sha1:
575
'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
576
(self.file_id, tree_revision_id, prev_sha, self.text_sha1,
597
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
598
(self.file_id, tree_revision_id))
579
600
checker.repeated_text_cnt += 1
582
603
if self.file_id not in checker.checked_weaves:
583
604
mutter('check weave {%s}', self.file_id)
584
w = tree._get_weave(self.file_id)
605
w = tree.get_weave(self.file_id)
585
606
# Not passing a progress bar, because it creates a new
586
607
# progress, which overwrites the current progress,
587
608
# and doesn't look nice
589
610
checker.checked_weaves[self.file_id] = True
591
w = tree._get_weave(self.file_id)
612
w = tree.get_weave(self.file_id)
593
614
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
594
615
checker.checked_text_cnt += 1
620
641
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
621
642
output_to, reverse=False):
622
643
"""See InventoryEntry._diff."""
623
from bzrlib.diff import DiffText
624
from_file_id = self.file_id
626
to_file_id = to_entry.file_id
630
to_file_id, from_file_id = from_file_id, to_file_id
631
tree, to_tree = to_tree, tree
632
from_label, to_label = to_label, from_label
633
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
635
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
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
637
663
def has_text(self):
638
664
"""See InventoryEntry.has_text."""
682
708
def _forget_tree_state(self):
683
709
self.text_sha1 = None
711
def _snapshot_text(self, file_parents, work_tree, commit_builder):
712
"""See InventoryEntry._snapshot_text."""
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)
685
718
def _unchanged(self, previous_ie):
686
719
"""See InventoryEntry._unchanged."""
687
720
compatible = super(InventoryFile, self)._unchanged(previous_ie)
700
733
"""A file in an inventory."""
702
735
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
703
'text_id', 'parent_id', 'children', 'executable',
704
'revision', 'symlink_target', 'reference_revision']
736
'text_id', 'parent_id', 'children', 'executable',
737
'revision', 'symlink_target']
706
739
def _check(self, checker, rev_id, tree):
707
740
"""See InventoryEntry._check"""
730
763
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
731
764
output_to, reverse=False):
732
765
"""See InventoryEntry._diff."""
733
from bzrlib.diff import DiffSymlink
734
old_target = self.symlink_target
766
from_text = self.symlink_target
735
767
if to_entry is not None:
736
new_target = to_entry.symlink_target
745
new_target, old_target = old_target, new_target
746
differ = DiffSymlink(old_tree, new_tree, output_to)
747
return differ.diff_symlink(old_target, new_target)
768
to_text = to_entry.symlink_target
773
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
776
print >>output_to, '=== target was %r' % self.symlink_target
778
print >>output_to, '=== target is %r' % self.symlink_target
749
780
def __init__(self, file_id, name, parent_id):
750
781
super(InventoryLink, self).__init__(file_id, name, parent_id)
784
815
compatible = False
785
816
return compatible
788
class TreeReference(InventoryEntry):
790
kind = 'tree-reference'
792
def __init__(self, file_id, name, parent_id, revision=None,
793
reference_revision=None):
794
InventoryEntry.__init__(self, file_id, name, parent_id)
795
self.revision = revision
796
self.reference_revision = reference_revision
799
return TreeReference(self.file_id, self.name, self.parent_id,
800
self.revision, self.reference_revision)
802
def _read_tree_state(self, path, work_tree):
803
"""Populate fields in the inventory entry from the given tree.
805
self.reference_revision = work_tree.get_reference_revision(
808
def _forget_tree_state(self):
809
self.reference_revision = None
811
def _unchanged(self, previous_ie):
812
"""See InventoryEntry._unchanged."""
813
compatible = super(TreeReference, self)._unchanged(previous_ie)
814
if self.reference_revision != previous_ie.reference_revision:
818
def _snapshot_text(self, file_parents, work_tree, commit_builder):
819
"""See InventoryEntry._snapshot_text."""
820
commit_builder.modified_link(
821
self.file_id, file_parents, self.symlink_target)
819
824
class Inventory(object):
870
875
if root_id is not None:
871
assert root_id.__class__ == str
872
self._set_root(InventoryDirectory(root_id, u'', None))
876
self._set_root(InventoryDirectory(root_id, '', None))
876
880
self.revision_id = revision_id
879
return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
881
def apply_delta(self, delta):
882
"""Apply a delta to this inventory.
884
:param delta: A list of changes to apply. After all the changes are
885
applied the final inventory must be internally consistent, but it
886
is ok to supply changes which, if only half-applied would have an
887
invalid result - such as supplying two changes which rename two
888
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
889
('B', 'A', 'B-id', b_entry)].
891
Each change is a tuple, of the form (old_path, new_path, file_id,
894
When new_path is None, the change indicates the removal of an entry
895
from the inventory and new_entry will be ignored (using None is
896
appropriate). If new_path is not None, then new_entry must be an
897
InventoryEntry instance, which will be incorporated into the
898
inventory (and replace any existing entry with the same file id).
900
When old_path is None, the change indicates the addition of
901
a new entry to the inventory.
903
When neither new_path nor old_path are None, the change is a
904
modification to an entry, such as a rename, reparent, kind change
907
The children attribute of new_entry is ignored. This is because
908
this method preserves children automatically across alterations to
909
the parent of the children, and cases where the parent id of a
910
child is changing require the child to be passed in as a separate
911
change regardless. E.g. in the recursive deletion of a directory -
912
the directory's children must be included in the delta, or the
913
final inventory will be invalid.
916
# Remove all affected items which were in the original inventory,
917
# starting with the longest paths, thus ensuring parents are examined
918
# after their children, which means that everything we examine has no
919
# modified children remaining by the time we examine it.
920
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
921
if op is not None), reverse=True):
922
if file_id not in self:
925
# Preserve unaltered children of file_id for later reinsertion.
926
children[file_id] = getattr(self[file_id], 'children', {})
927
# Remove file_id and the unaltered children. If file_id is not
928
# being deleted it will be reinserted back later.
929
self.remove_recursive_id(file_id)
930
# Insert all affected which should be in the new inventory, reattaching
931
# their children if they had any. This is done from shortest path to
932
# longest, ensuring that items which were modified and whose parents in
933
# the resulting inventory were also modified, are inserted after their
935
for new_path, new_entry in sorted((np, e) for op, np, f, e in
936
delta if np is not None):
937
if new_entry.kind == 'directory':
938
new_entry.children = children.get(new_entry.file_id, {})
941
882
def _set_root(self, ie):
943
884
self._byid = {self.root.file_id: self.root}
1014
952
lexicographically sorted order, and is a hybrid between
1015
953
depth-first and breadth-first.
1017
:param yield_parents: If True, yield the parents from the root leading
1018
down to specific_file_ids that have been requested. This has no
1019
impact if specific_file_ids is None.
1020
955
:return: This yields (path, entry) pairs
1022
if specific_file_ids and not isinstance(specific_file_ids, set):
1023
specific_file_ids = set(specific_file_ids)
1024
957
# TODO? Perhaps this should return the from_dir so that the root is
1025
958
# yielded? or maybe an option?
1026
959
if from_dir is None:
1027
960
if self.root is None:
1029
962
# Optimize a common case
1030
if (not yield_parents and specific_file_ids is not None and
1031
len(specific_file_ids) == 1):
963
if specific_file_ids is not None and len(specific_file_ids) == 1:
1032
964
file_id = list(specific_file_ids)[0]
1033
965
if file_id in self:
1034
966
yield self.id2path(file_id), self[file_id]
1036
968
from_dir = self.root
1037
if (specific_file_ids is None or yield_parents or
969
if (specific_file_ids is None or
1038
970
self.root.file_id in specific_file_ids):
1039
yield u'', self.root
1040
972
elif isinstance(from_dir, basestring):
1041
973
from_dir = self._byid[from_dir]
1043
975
if specific_file_ids is not None:
1044
# TODO: jam 20070302 This could really be done as a loop rather
1045
# than a bunch of recursive calls.
1048
977
def add_ancestors(file_id):
1049
if file_id not in byid:
978
if file_id not in self:
1051
parent_id = byid[file_id].parent_id
980
parent_id = self[file_id].parent_id
1052
981
if parent_id is None:
1054
983
if parent_id not in parents:
1150
1074
def get_child(self, parent_id, filename):
1151
1075
return self[parent_id].children.get(filename)
1153
def _add_child(self, entry):
1154
"""Add an entry to the inventory, without adding it to its parent"""
1155
if entry.file_id in self._byid:
1156
raise BzrError("inventory already contains entry with id {%s}" %
1158
self._byid[entry.file_id] = entry
1159
for child in getattr(entry, 'children', {}).itervalues():
1160
self._add_child(child)
1163
1077
def add(self, entry):
1164
1078
"""Add entry to inventory.
1169
1083
Returns the new entry object.
1171
1085
if entry.file_id in self._byid:
1172
raise errors.DuplicateFileId(entry.file_id,
1173
self._byid[entry.file_id])
1086
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1175
1088
if entry.parent_id is None:
1176
1089
assert self.root is None and len(self._byid) == 0
1180
parent = self._byid[entry.parent_id]
1182
raise BzrError("parent_id {%s} not in inventory" %
1185
if entry.name in parent.children:
1186
raise BzrError("%s is already versioned" %
1187
osutils.pathjoin(self.id2path(parent.file_id),
1188
entry.name).encode('utf-8'))
1189
parent.children[entry.name] = entry
1190
return self._add_child(entry)
1090
self._set_root(entry)
1093
parent = self._byid[entry.parent_id]
1095
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1097
if entry.name in parent.children:
1098
raise BzrError("%s is already versioned" %
1099
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
1101
self._byid[entry.file_id] = entry
1102
parent.children[entry.name] = entry
1192
1105
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1193
1106
"""Add entry from a path.
1352
1265
for file_id in reversed(to_delete):
1353
1266
ie = self[file_id]
1354
1267
del self._byid[file_id]
1355
if ie.parent_id is not None:
1356
del self[ie.parent_id].children[ie.name]
1268
if ie.parent_id is not None:
1269
del self[ie.parent_id].children[ie.name]
1360
1271
def rename(self, file_id, new_parent_id, new_name):
1361
1272
"""Move a file within the inventory.
1363
1274
This can change either the name, or the parent, or both.
1365
This does not move the working file.
1367
new_name = ensure_normalized_name(new_name)
1276
This does not move the working file."""
1368
1277
if not is_valid_name(new_name):
1369
1278
raise BzrError("not an acceptable filename: %r" % new_name)
1410
1312
if file_id is None:
1411
1313
file_id = generate_ids.gen_file_id(name)
1412
name = ensure_normalized_name(name)
1414
factory = entry_factory[kind]
1416
raise BzrError("unknown kind %r" % kind)
1417
return factory(file_id, name, parent_id)
1420
def ensure_normalized_name(name):
1423
:raises InvalidNormalization: When name is not normalized, and cannot be
1424
accessed on this platform by the normalized path.
1425
:return: The NFC normalised version of name.
1427
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1428
# keep them synchronised.
1429
# we dont import normalized_filename directly because we want to be
1430
# able to change the implementation at runtime for tests.
1431
1315
norm_name, can_access = osutils.normalized_filename(name)
1432
1316
if norm_name != name:
1436
1320
# TODO: jam 20060701 This would probably be more useful
1437
1321
# if the error was raised with the full path
1438
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)
1442
1334
_NAME_RE = None