94
95
>>> for ix, j in enumerate(i.iter_entries()):
95
96
... print (j[0] == shouldbe[ix], j[1])
97
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
98
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
99
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
100
(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}
104
101
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
105
102
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
106
103
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
146
142
return False, False
148
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
149
output_to, reverse=False):
150
"""Perform a diff from this to to_entry.
152
text_diff will be used for textual difference calculation.
153
This is a template method, override _diff in child classes.
155
self._read_tree_state(tree.id2path(self.file_id), tree)
157
# cannot diff from one kind to another - you must do a removal
158
# and an addif they do not match.
159
assert self.kind == to_entry.kind
160
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
162
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
165
144
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
166
145
output_to, reverse=False):
167
146
"""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.
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.
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
187
def get_ancestors(weave, entry):
188
return set(weave.get_ancestry(entry.revision))
189
157
# 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
195
159
# identify candidate head revision ids.
196
160
for inv in previous_inventories:
197
161
if self.file_id in inv:
198
162
ie = inv[self.file_id]
199
assert ie.file_id == self.file_id
200
163
if ie.revision in candidates:
201
164
# same revision value in two different inventories:
202
165
# correct possible inconsistencies:
208
171
ie.executable = False
209
172
except AttributeError:
211
# must now be the same.
212
assert candidates[ie.revision] == ie
214
175
# add this revision as a candidate.
215
176
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
255
179
def get_tar_item(self, root, dp, now, tree):
256
180
"""Get a tarfile item and a file stream for its content."""
417
def snapshot(self, revision, path, previous_entries,
418
work_tree, commit_builder):
419
"""Make a snapshot of this entry which may or may not have changed.
421
This means that all its fields are populated, that it has its
422
text stored in the text store or weave.
424
# mutter('new parents of %s are %r', path, previous_entries)
425
self._read_tree_state(path, work_tree)
426
# TODO: Where should we determine whether to reuse a
427
# previous revision id or create a new revision? 20060606
428
if len(previous_entries) == 1:
429
# cannot be unchanged unless there is only one parent file rev.
430
parent_ie = previous_entries.values()[0]
431
if self._unchanged(parent_ie):
432
# mutter("found unchanged entry")
433
self.revision = parent_ie.revision
435
return self._snapshot_into_revision(revision, previous_entries,
436
work_tree, commit_builder)
438
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
440
"""Record this revision unconditionally into a store.
442
The entry's last-changed revision property (`revision`) is updated to
443
that of the new revision.
445
:param revision: id of the new revision that is being recorded.
447
:returns: String description of the commit (e.g. "merged", "modified"), etc.
449
# mutter('new revision {%s} for {%s}', revision, self.file_id)
450
self.revision = revision
451
self._snapshot_text(previous_entries, work_tree, commit_builder)
453
def _snapshot_text(self, file_parents, work_tree, commit_builder):
454
"""Record the 'text' of this entry, whatever form that takes.
456
This default implementation simply adds an empty text.
458
raise NotImplementedError(self._snapshot_text)
460
341
def __eq__(self, other):
461
342
if not isinstance(other, InventoryEntry):
462
343
return NotImplemented
511
395
class RootEntry(InventoryEntry):
513
397
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
514
'text_id', 'parent_id', 'children', 'executable',
515
'revision', 'symlink_target']
398
'text_id', 'parent_id', 'children', 'executable',
399
'revision', 'symlink_target', 'reference_revision']
517
401
def _check(self, checker, rev_id, tree):
518
402
"""See InventoryEntry._check"""
540
424
"""A directory in an inventory."""
542
426
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
543
'text_id', 'parent_id', 'children', 'executable',
544
'revision', 'symlink_target']
427
'text_id', 'parent_id', 'children', 'executable',
428
'revision', 'symlink_target', 'reference_revision']
546
430
def _check(self, checker, rev_id, tree):
547
431
"""See InventoryEntry._check"""
578
462
"""See InventoryEntry._put_on_disk."""
579
463
os.mkdir(fullpath)
581
def _snapshot_text(self, file_parents, work_tree, commit_builder):
582
"""See InventoryEntry._snapshot_text."""
583
commit_builder.modified_directory(self.file_id, file_parents)
586
466
class InventoryFile(InventoryEntry):
587
467
"""A file in an inventory."""
589
469
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
590
'text_id', 'parent_id', 'children', 'executable',
591
'revision', 'symlink_target']
470
'text_id', 'parent_id', 'children', 'executable',
471
'revision', 'symlink_target', 'reference_revision']
593
473
def _check(self, checker, tree_revision_id, tree):
594
474
"""See InventoryEntry._check"""
595
t = (self.file_id, self.revision)
596
if t in checker.checked_texts:
597
prev_sha = checker.checked_texts[t]
475
key = (self.file_id, self.revision)
476
if key in checker.checked_texts:
477
prev_sha = checker.checked_texts[key]
598
478
if prev_sha != self.text_sha1:
599
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
600
(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,
602
484
checker.repeated_text_cnt += 1
605
if self.file_id not in checker.checked_weaves:
606
mutter('check weave {%s}', self.file_id)
607
w = tree.get_weave(self.file_id)
608
# Not passing a progress bar, because it creates a new
609
# progress, which overwrites the current progress,
610
# and doesn't look nice
612
checker.checked_weaves[self.file_id] = True
614
w = tree.get_weave(self.file_id)
616
487
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
617
488
checker.checked_text_cnt += 1
618
489
# We can't check the length, because Weave doesn't store that
619
490
# information, and the whole point of looking at the weave's
620
491
# sha1sum is that we don't have to extract the text.
621
if self.text_sha1 != w.get_sha1(self.revision):
622
raise BzrCheckError('text {%s} version {%s} wrong sha1'
623
% (self.file_id, self.revision))
624
checker.checked_texts[t] = self.text_sha1
492
if (self.text_sha1 != tree._repository.texts.get_sha1s([key])[key]):
493
raise BzrCheckError('text {%s} version {%s} wrong sha1' % key)
494
checker.checked_texts[key] = self.text_sha1
627
497
other = InventoryFile(self.file_id, self.name, self.parent_id)
643
511
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
644
512
output_to, reverse=False):
645
513
"""See InventoryEntry._diff."""
647
from_text = tree.get_file(self.file_id).readlines()
649
to_text = to_tree.get_file(to_entry.file_id).readlines()
653
text_diff(from_label, from_text,
654
to_label, to_text, output_to)
656
text_diff(to_label, to_text,
657
from_label, from_text, output_to)
658
except errors.BinaryFile:
660
label_pair = (to_label, from_label)
662
label_pair = (from_label, to_label)
663
print >> output_to, "Binary files %s and %s differ" % label_pair
514
from bzrlib.diff import DiffText
515
from_file_id = self.file_id
517
to_file_id = to_entry.file_id
521
to_file_id, from_file_id = from_file_id, to_file_id
522
tree, to_tree = to_tree, tree
523
from_label, to_label = to_label, from_label
524
differ = DiffText(tree, to_tree, output_to, 'utf-8', '', '',
526
return differ.diff_text(from_file_id, to_file_id, from_label, to_label)
665
528
def has_text(self):
666
529
"""See InventoryEntry.has_text."""
710
573
def _forget_tree_state(self):
711
574
self.text_sha1 = None
713
def _snapshot_text(self, file_parents, work_tree, commit_builder):
714
"""See InventoryEntry._snapshot_text."""
715
def get_content_byte_lines():
716
return work_tree.get_file(self.file_id).readlines()
717
self.text_sha1, self.text_size = commit_builder.modified_file_text(
718
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
720
576
def _unchanged(self, previous_ie):
721
577
"""See InventoryEntry._unchanged."""
722
578
compatible = super(InventoryFile, self)._unchanged(previous_ie)
735
591
"""A file in an inventory."""
737
593
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
738
'text_id', 'parent_id', 'children', 'executable',
739
'revision', 'symlink_target']
594
'text_id', 'parent_id', 'children', 'executable',
595
'revision', 'symlink_target', 'reference_revision']
741
597
def _check(self, checker, rev_id, tree):
742
598
"""See InventoryEntry._check"""
765
621
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
766
622
output_to, reverse=False):
767
623
"""See InventoryEntry._diff."""
768
from_text = self.symlink_target
624
from bzrlib.diff import DiffSymlink
625
old_target = self.symlink_target
769
626
if to_entry is not None:
770
to_text = to_entry.symlink_target
775
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
778
print >>output_to, '=== target was %r' % self.symlink_target
780
print >>output_to, '=== target is %r' % self.symlink_target
627
new_target = to_entry.symlink_target
636
new_target, old_target = old_target, new_target
637
differ = DiffSymlink(old_tree, new_tree, output_to)
638
return differ.diff_symlink(old_target, new_target)
782
640
def __init__(self, file_id, name, parent_id):
783
641
super(InventoryLink, self).__init__(file_id, name, parent_id)
817
675
compatible = False
818
676
return compatible
820
def _snapshot_text(self, file_parents, work_tree, commit_builder):
821
"""See InventoryEntry._snapshot_text."""
822
commit_builder.modified_link(
823
self.file_id, file_parents, self.symlink_target)
679
class TreeReference(InventoryEntry):
681
kind = 'tree-reference'
683
def __init__(self, file_id, name, parent_id, revision=None,
684
reference_revision=None):
685
InventoryEntry.__init__(self, file_id, name, parent_id)
686
self.revision = revision
687
self.reference_revision = reference_revision
690
return TreeReference(self.file_id, self.name, self.parent_id,
691
self.revision, self.reference_revision)
693
def _read_tree_state(self, path, work_tree):
694
"""Populate fields in the inventory entry from the given tree.
696
self.reference_revision = work_tree.get_reference_revision(
699
def _forget_tree_state(self):
700
self.reference_revision = None
702
def _unchanged(self, previous_ie):
703
"""See InventoryEntry._unchanged."""
704
compatible = super(TreeReference, self)._unchanged(previous_ie)
705
if self.reference_revision != previous_ie.reference_revision:
826
710
class Inventory(object):
877
761
if root_id is not None:
878
self._set_root(InventoryDirectory(root_id, '', None))
762
self._set_root(InventoryDirectory(root_id, u'', None))
882
766
self.revision_id = revision_id
769
return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
771
def apply_delta(self, delta):
772
"""Apply a delta to this inventory.
774
:param delta: A list of changes to apply. After all the changes are
775
applied the final inventory must be internally consistent, but it
776
is ok to supply changes which, if only half-applied would have an
777
invalid result - such as supplying two changes which rename two
778
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
779
('B', 'A', 'B-id', b_entry)].
781
Each change is a tuple, of the form (old_path, new_path, file_id,
784
When new_path is None, the change indicates the removal of an entry
785
from the inventory and new_entry will be ignored (using None is
786
appropriate). If new_path is not None, then new_entry must be an
787
InventoryEntry instance, which will be incorporated into the
788
inventory (and replace any existing entry with the same file id).
790
When old_path is None, the change indicates the addition of
791
a new entry to the inventory.
793
When neither new_path nor old_path are None, the change is a
794
modification to an entry, such as a rename, reparent, kind change
797
The children attribute of new_entry is ignored. This is because
798
this method preserves children automatically across alterations to
799
the parent of the children, and cases where the parent id of a
800
child is changing require the child to be passed in as a separate
801
change regardless. E.g. in the recursive deletion of a directory -
802
the directory's children must be included in the delta, or the
803
final inventory will be invalid.
806
# Remove all affected items which were in the original inventory,
807
# starting with the longest paths, thus ensuring parents are examined
808
# after their children, which means that everything we examine has no
809
# modified children remaining by the time we examine it.
810
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
811
if op is not None), reverse=True):
812
if file_id not in self:
815
# Preserve unaltered children of file_id for later reinsertion.
816
children[file_id] = getattr(self[file_id], 'children', {})
817
# Remove file_id and the unaltered children. If file_id is not
818
# being deleted it will be reinserted back later.
819
self.remove_recursive_id(file_id)
820
# Insert all affected which should be in the new inventory, reattaching
821
# their children if they had any. This is done from shortest path to
822
# longest, ensuring that items which were modified and whose parents in
823
# the resulting inventory were also modified, are inserted after their
825
for new_path, new_entry in sorted((np, e) for op, np, f, e in
826
delta if np is not None):
827
if new_entry.kind == 'directory':
828
new_entry.children = children.get(new_entry.file_id, {})
884
831
def _set_root(self, ie):
886
833
self._byid = {self.root.file_id: self.root}
954
904
lexicographically sorted order, and is a hybrid between
955
905
depth-first and breadth-first.
907
:param yield_parents: If True, yield the parents from the root leading
908
down to specific_file_ids that have been requested. This has no
909
impact if specific_file_ids is None.
957
910
:return: This yields (path, entry) pairs
912
if specific_file_ids and not isinstance(specific_file_ids, set):
913
specific_file_ids = set(specific_file_ids)
959
914
# TODO? Perhaps this should return the from_dir so that the root is
960
915
# yielded? or maybe an option?
961
916
if from_dir is None:
962
917
if self.root is None:
964
919
# Optimize a common case
965
if specific_file_ids is not None and len(specific_file_ids) == 1:
920
if (not yield_parents and specific_file_ids is not None and
921
len(specific_file_ids) == 1):
966
922
file_id = list(specific_file_ids)[0]
967
923
if file_id in self:
968
924
yield self.id2path(file_id), self[file_id]
970
926
from_dir = self.root
971
if (specific_file_ids is None or
927
if (specific_file_ids is None or yield_parents or
972
928
self.root.file_id in specific_file_ids):
974
930
elif isinstance(from_dir, basestring):
975
931
from_dir = self._byid[from_dir]
977
933
if specific_file_ids is not None:
934
# TODO: jam 20070302 This could really be done as a loop rather
935
# than a bunch of recursive calls.
979
938
def add_ancestors(file_id):
980
if file_id not in self:
939
if file_id not in byid:
982
parent_id = self[file_id].parent_id
941
parent_id = byid[file_id].parent_id
983
942
if parent_id is None:
985
944
if parent_id not in parents:
1076
1040
def get_child(self, parent_id, filename):
1077
1041
return self[parent_id].children.get(filename)
1043
def _add_child(self, entry):
1044
"""Add an entry to the inventory, without adding it to its parent"""
1045
if entry.file_id in self._byid:
1046
raise BzrError("inventory already contains entry with id {%s}" %
1048
self._byid[entry.file_id] = entry
1049
for child in getattr(entry, 'children', {}).itervalues():
1050
self._add_child(child)
1079
1053
def add(self, entry):
1080
1054
"""Add entry to inventory.
1085
1059
Returns the new entry object.
1087
1061
if entry.file_id in self._byid:
1088
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1062
raise errors.DuplicateFileId(entry.file_id,
1063
self._byid[entry.file_id])
1090
1065
if entry.parent_id is None:
1091
assert self.root is None and len(self._byid) == 0
1092
self._set_root(entry)
1095
parent = self._byid[entry.parent_id]
1097
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1099
if entry.name in parent.children:
1100
raise BzrError("%s is already versioned" %
1101
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
1103
self._byid[entry.file_id] = entry
1104
parent.children[entry.name] = entry
1069
parent = self._byid[entry.parent_id]
1071
raise BzrError("parent_id {%s} not in inventory" %
1074
if entry.name in parent.children:
1075
raise BzrError("%s is already versioned" %
1076
osutils.pathjoin(self.id2path(parent.file_id),
1077
entry.name).encode('utf-8'))
1078
parent.children[entry.name] = entry
1079
return self._add_child(entry)
1107
1081
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1108
1082
"""Add entry from a path.
1267
1235
for file_id in reversed(to_delete):
1268
1236
ie = self[file_id]
1269
1237
del self._byid[file_id]
1270
if ie.parent_id is not None:
1271
del self[ie.parent_id].children[ie.name]
1238
if ie.parent_id is not None:
1239
del self[ie.parent_id].children[ie.name]
1273
1243
def rename(self, file_id, new_parent_id, new_name):
1274
1244
"""Move a file within the inventory.
1276
1246
This can change either the name, or the parent, or both.
1278
This does not move the working file."""
1248
This does not move the working file.
1250
new_name = ensure_normalized_name(new_name)
1279
1251
if not is_valid_name(new_name):
1280
1252
raise BzrError("not an acceptable filename: %r" % new_name)
1314
1293
if file_id is None:
1315
1294
file_id = generate_ids.gen_file_id(name)
1295
name = ensure_normalized_name(name)
1297
factory = entry_factory[kind]
1299
raise BzrError("unknown kind %r" % kind)
1300
return factory(file_id, name, parent_id)
1303
def ensure_normalized_name(name):
1306
:raises InvalidNormalization: When name is not normalized, and cannot be
1307
accessed on this platform by the normalized path.
1308
:return: The NFC normalised version of name.
1310
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1311
# keep them synchronised.
1312
# we dont import normalized_filename directly because we want to be
1313
# able to change the implementation at runtime for tests.
1317
1314
norm_name, can_access = osutils.normalized_filename(name)
1318
1315
if norm_name != name:
1322
1319
# TODO: jam 20060701 This would probably be more useful
1323
1320
# if the error was raised with the full path
1324
1321
raise errors.InvalidNormalization(name)
1326
if kind == 'directory':
1327
return InventoryDirectory(file_id, name, parent_id)
1328
elif kind == 'file':
1329
return InventoryFile(file_id, name, parent_id)
1330
elif kind == 'symlink':
1331
return InventoryLink(file_id, name, parent_id)
1333
raise BzrError("unknown kind %r" % kind)
1336
1325
_NAME_RE = None