27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
34
from bzrlib.lazy_import import lazy_import
35
lazy_import(globals(), """
37
from warnings import warn
40
from bzrlib import errors, osutils
41
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
42
pathjoin, sha_strings)
43
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
44
BzrError, BzrCheckError, BinaryFile)
49
from bzrlib.errors import (
53
from bzrlib.symbol_versioning import deprecated_method, zero_ninetyone
45
54
from bzrlib.trace import mutter
82
91
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
83
92
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
84
93
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
85
>>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
94
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
86
95
>>> for ix, j in enumerate(i.iter_entries()):
87
96
... print (j[0] == shouldbe[ix], j[1])
89
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
98
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
90
99
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
91
100
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
93
Traceback (most recent call last):
95
BzrError: inventory already contains entry with id {2323}
96
101
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
97
102
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
98
103
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
157
162
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
158
163
output_to, reverse=False):
159
164
"""Perform a diff between two entries of the same kind."""
161
def find_previous_heads(self, previous_inventories,
162
versioned_file_store,
165
"""Return the revisions and entries that directly precede this.
167
Returned as a map from revision to inventory entry.
169
This is a map containing the file revisions in all parents
170
for which the file exists, and its revision is not a parent of
171
any other. If the file is new, the set will be empty.
173
:param versioned_file_store: A store where ancestry data on this
174
file id can be queried.
175
:param transaction: The transaction that queries to the versioned
176
file store should be completed under.
177
:param entry_vf: The entry versioned file, if its already available.
166
def parent_candidates(self, previous_inventories):
167
"""Find possible per-file graph parents.
169
This is currently defined by:
170
- Select the last changed revision in the parent inventory.
171
- Do deal with a short lived bug in bzr 0.8's development two entries
172
that have the same last changed but different 'x' bit settings are
179
def get_ancestors(weave, entry):
180
return set(weave.get_ancestry(entry.revision))
181
175
# revision:ie mapping for each ie found in previous_inventories.
183
# revision:ie mapping with one revision for each head.
185
# revision: ancestor list for each head
187
177
# identify candidate head revision ids.
188
178
for inv in previous_inventories:
189
179
if self.file_id in inv:
206
196
# add this revision as a candidate.
207
197
candidates[ie.revision] = ie
200
@deprecated_method(zero_ninetyone)
201
def find_previous_heads(self, previous_inventories,
202
versioned_file_store,
205
"""Return the revisions and entries that directly precede this.
207
Returned as a map from revision to inventory entry.
209
This is a map containing the file revisions in all parents
210
for which the file exists, and its revision is not a parent of
211
any other. If the file is new, the set will be empty.
213
:param versioned_file_store: A store where ancestry data on this
214
file id can be queried.
215
:param transaction: The transaction that queries to the versioned
216
file store should be completed under.
217
:param entry_vf: The entry versioned file, if its already available.
219
candidates = self.parent_candidates(previous_inventories)
221
# revision:ie mapping with one revision for each head.
209
223
# common case optimisation
210
224
if len(candidates) == 1:
211
225
# if there is only one candidate revision found
212
# then we can opening the versioned file to access ancestry:
226
# then we can avoid opening the versioned file to access ancestry:
213
227
# there cannot be any ancestors to eliminate when there is
214
228
# only one revision available.
215
heads[ie.revision] = ie
231
# --- what follows is now encapsulated in repository.get_graph.heads(),
232
# but that is not accessible from here as we have no repository
233
# pointer. Note that the repository.get_graph.heads() call can return
234
# different results *at the moment* because of the kind-changing check
235
# we have in parent_candidates().
218
237
# eliminate ancestors amongst the available candidates:
219
238
# heads are those that are not an ancestor of any other candidate
220
239
# - this provides convergence at a per-file level.
240
def get_ancestors(weave, entry):
241
return set(weave.get_ancestry(entry.revision, topo_sorted=False))
242
# revision: ancestor list for each head
221
244
for ie in candidates.values():
222
245
# may be an ancestor of a known head:
223
246
already_present = 0 != len(
282
305
assert isinstance(name, basestring), name
283
306
if '/' in name or '\\' in name:
284
raise InvalidEntryName(name=name)
307
raise errors.InvalidEntryName(name=name)
285
308
self.executable = False
286
309
self.revision = None
287
310
self.text_sha1 = None
288
311
self.text_size = None
289
312
self.file_id = file_id
313
assert isinstance(file_id, (str, None.__class__)), \
314
'bad type %r for %r' % (type(file_id), file_id)
291
316
self.text_id = text_id
292
317
self.parent_id = parent_id
293
318
self.symlink_target = None
319
self.reference_revision = None
295
321
def kind_character(self):
296
322
"""Return a short kind indicator useful for appending to names."""
407
def snapshot(self, revision, path, previous_entries,
408
work_tree, commit_builder):
409
"""Make a snapshot of this entry which may or may not have changed.
411
This means that all its fields are populated, that it has its
412
text stored in the text store or weave.
414
# mutter('new parents of %s are %r', path, previous_entries)
415
self._read_tree_state(path, work_tree)
416
# TODO: Where should we determine whether to reuse a
417
# previous revision id or create a new revision? 20060606
418
if len(previous_entries) == 1:
419
# cannot be unchanged unless there is only one parent file rev.
420
parent_ie = previous_entries.values()[0]
421
if self._unchanged(parent_ie):
422
# mutter("found unchanged entry")
423
self.revision = parent_ie.revision
425
return self._snapshot_into_revision(revision, previous_entries,
426
work_tree, commit_builder)
428
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
430
"""Record this revision unconditionally into a store.
432
The entry's last-changed revision property (`revision`) is updated to
433
that of the new revision.
435
:param revision: id of the new revision that is being recorded.
437
:returns: String description of the commit (e.g. "merged", "modified"), etc.
439
# mutter('new revision {%s} for {%s}', revision, self.file_id)
440
self.revision = revision
441
self._snapshot_text(previous_entries, work_tree, commit_builder)
443
def _snapshot_text(self, file_parents, work_tree, commit_builder):
444
"""Record the 'text' of this entry, whatever form that takes.
446
This default implementation simply adds an empty text.
448
raise NotImplementedError(self._snapshot_text)
450
435
def __eq__(self, other):
451
436
if not isinstance(other, InventoryEntry):
452
437
return NotImplemented
501
489
class RootEntry(InventoryEntry):
503
491
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
504
'text_id', 'parent_id', 'children', 'executable',
505
'revision', 'symlink_target']
492
'text_id', 'parent_id', 'children', 'executable',
493
'revision', 'symlink_target', 'reference_revision']
507
495
def _check(self, checker, rev_id, tree):
508
496
"""See InventoryEntry._check"""
530
518
"""A directory in an inventory."""
532
520
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
533
'text_id', 'parent_id', 'children', 'executable',
534
'revision', 'symlink_target']
521
'text_id', 'parent_id', 'children', 'executable',
522
'revision', 'symlink_target', 'reference_revision']
536
524
def _check(self, checker, rev_id, tree):
537
525
"""See InventoryEntry._check"""
568
556
"""See InventoryEntry._put_on_disk."""
569
557
os.mkdir(fullpath)
571
def _snapshot_text(self, file_parents, work_tree, commit_builder):
572
"""See InventoryEntry._snapshot_text."""
573
commit_builder.modified_directory(self.file_id, file_parents)
576
560
class InventoryFile(InventoryEntry):
577
561
"""A file in an inventory."""
579
563
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
580
'text_id', 'parent_id', 'children', 'executable',
581
'revision', 'symlink_target']
564
'text_id', 'parent_id', 'children', 'executable',
565
'revision', 'symlink_target', 'reference_revision']
583
567
def _check(self, checker, tree_revision_id, tree):
584
568
"""See InventoryEntry._check"""
586
570
if t in checker.checked_texts:
587
571
prev_sha = checker.checked_texts[t]
588
572
if prev_sha != self.text_sha1:
589
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
590
(self.file_id, tree_revision_id))
574
'mismatched sha1 on {%s} in {%s} (%s != %s) %r' %
575
(self.file_id, tree_revision_id, prev_sha, self.text_sha1,
592
578
checker.repeated_text_cnt += 1
595
581
if self.file_id not in checker.checked_weaves:
596
582
mutter('check weave {%s}', self.file_id)
597
w = tree.get_weave(self.file_id)
583
w = tree._get_weave(self.file_id)
598
584
# Not passing a progress bar, because it creates a new
599
585
# progress, which overwrites the current progress,
600
586
# and doesn't look nice
602
588
checker.checked_weaves[self.file_id] = True
604
w = tree.get_weave(self.file_id)
590
w = tree._get_weave(self.file_id)
606
592
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
607
593
checker.checked_text_cnt += 1
700
687
def _forget_tree_state(self):
701
688
self.text_sha1 = None
703
def _snapshot_text(self, file_parents, work_tree, commit_builder):
704
"""See InventoryEntry._snapshot_text."""
705
def get_content_byte_lines():
706
return work_tree.get_file(self.file_id).readlines()
707
self.text_sha1, self.text_size = commit_builder.modified_file_text(
708
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
710
690
def _unchanged(self, previous_ie):
711
691
"""See InventoryEntry._unchanged."""
712
692
compatible = super(InventoryFile, self)._unchanged(previous_ie)
725
705
"""A file in an inventory."""
727
707
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
728
'text_id', 'parent_id', 'children', 'executable',
729
'revision', 'symlink_target']
708
'text_id', 'parent_id', 'children', 'executable',
709
'revision', 'symlink_target', 'reference_revision']
731
711
def _check(self, checker, rev_id, tree):
732
712
"""See InventoryEntry._check"""
763
743
from_text = to_text
765
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
745
output_to.write('=== target changed %r => %r\n' % (from_text, to_text))
768
print >>output_to, '=== target was %r' % self.symlink_target
748
output_to.write('=== target was %r\n' % self.symlink_target)
770
print >>output_to, '=== target is %r' % self.symlink_target
750
output_to.write('=== target is %r\n' % self.symlink_target)
772
752
def __init__(self, file_id, name, parent_id):
773
753
super(InventoryLink, self).__init__(file_id, name, parent_id)
807
787
compatible = False
808
788
return compatible
810
def _snapshot_text(self, file_parents, work_tree, commit_builder):
811
"""See InventoryEntry._snapshot_text."""
812
commit_builder.modified_link(
813
self.file_id, file_parents, self.symlink_target)
791
class TreeReference(InventoryEntry):
793
kind = 'tree-reference'
795
def __init__(self, file_id, name, parent_id, revision=None,
796
reference_revision=None):
797
InventoryEntry.__init__(self, file_id, name, parent_id)
798
self.revision = revision
799
self.reference_revision = reference_revision
802
return TreeReference(self.file_id, self.name, self.parent_id,
803
self.revision, self.reference_revision)
805
def _read_tree_state(self, path, work_tree):
806
"""Populate fields in the inventory entry from the given tree.
808
self.reference_revision = work_tree.get_reference_revision(
811
def _forget_tree_state(self):
812
self.reference_revision = None
814
def _unchanged(self, previous_ie):
815
"""See InventoryEntry._unchanged."""
816
compatible = super(TreeReference, self)._unchanged(previous_ie)
817
if self.reference_revision != previous_ie.reference_revision:
816
822
class Inventory(object):
861
870
The inventory is created with a default root directory, with
864
# We are letting Branch.create() create a unique inventory
865
# root id. Rather than generating a random one here.
867
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
868
873
if root_id is not None:
869
self._set_root(InventoryDirectory(root_id, '', None))
874
assert root_id.__class__ == str
875
self._set_root(InventoryDirectory(root_id, u'', None))
873
# FIXME: this isn't ever used, changing it to self.revision may break
874
# things. TODO make everything use self.revision_id
875
879
self.revision_id = revision_id
882
return "<Inventory object at %x, contents=%r>" % (id(self), self._byid)
884
def apply_delta(self, delta):
885
"""Apply a delta to this inventory.
887
:param delta: A list of changes to apply. After all the changes are
888
applied the final inventory must be internally consistent, but it
889
is ok to supply changes which, if only half-applied would have an
890
invalid result - such as supplying two changes which rename two
891
files, 'A' and 'B' with each other : [('A', 'B', 'A-id', a_entry),
892
('B', 'A', 'B-id', b_entry)].
894
Each change is a tuple, of the form (old_path, new_path, file_id,
897
When new_path is None, the change indicates the removal of an entry
898
from the inventory and new_entry will be ignored (using None is
899
appropriate). If new_path is not None, then new_entry must be an
900
InventoryEntry instance, which will be incorporated into the
901
inventory (and replace any existing entry with the same file id).
903
When old_path is None, the change indicates the addition of
904
a new entry to the inventory.
906
When neither new_path nor old_path are None, the change is a
907
modification to an entry, such as a rename, reparent, kind change
910
The children attribute of new_entry is ignored. This is because
911
this method preserves children automatically across alterations to
912
the parent of the children, and cases where the parent id of a
913
child is changing require the child to be passed in as a separate
914
change regardless. E.g. in the recursive deletion of a directory -
915
the directory's children must be included in the delta, or the
916
final inventory will be invalid.
919
# Remove all affected items which were in the original inventory,
920
# starting with the longest paths, thus ensuring parents are examined
921
# after their children, which means that everything we examine has no
922
# modified children remaining by the time we examine it.
923
for old_path, file_id in sorted(((op, f) for op, np, f, e in delta
924
if op is not None), reverse=True):
925
if file_id not in self:
928
# Preserve unaltered children of file_id for later reinsertion.
929
children[file_id] = getattr(self[file_id], 'children', {})
930
# Remove file_id and the unaltered children. If file_id is not
931
# being deleted it will be reinserted back later.
932
self.remove_recursive_id(file_id)
933
# Insert all affected which should be in the new inventory, reattaching
934
# their children if they had any. This is done from shortest path to
935
# longest, ensuring that items which were modified and whose parents in
936
# the resulting inventory were also modified, are inserted after their
938
for new_path, new_entry in sorted((np, e) for op, np, f, e in
939
delta if np is not None):
940
if new_entry.kind == 'directory':
941
new_entry.children = children.get(new_entry.file_id, {})
877
944
def _set_root(self, ie):
879
946
self._byid = {self.root.file_id: self.root}
946
1015
lexicographically sorted order, and is a hybrid between
947
1016
depth-first and breadth-first.
1018
:param yield_parents: If True, yield the parents from the root leading
1019
down to specific_file_ids that have been requested. This has no
1020
impact if specific_file_ids is None.
949
1021
:return: This yields (path, entry) pairs
1023
if specific_file_ids and not isinstance(specific_file_ids, set):
1024
specific_file_ids = set(specific_file_ids)
951
1025
# TODO? Perhaps this should return the from_dir so that the root is
952
1026
# yielded? or maybe an option?
953
1027
if from_dir is None:
1028
if self.root is None:
1030
# Optimize a common case
1031
if (not yield_parents and specific_file_ids is not None and
1032
len(specific_file_ids) == 1):
1033
file_id = list(specific_file_ids)[0]
1035
yield self.id2path(file_id), self[file_id]
955
1037
from_dir = self.root
1038
if (specific_file_ids is None or yield_parents or
1039
self.root.file_id in specific_file_ids):
1040
yield u'', self.root
957
1041
elif isinstance(from_dir, basestring):
958
1042
from_dir = self._byid[from_dir]
1044
if specific_file_ids is not None:
1045
# TODO: jam 20070302 This could really be done as a loop rather
1046
# than a bunch of recursive calls.
1049
def add_ancestors(file_id):
1050
if file_id not in byid:
1052
parent_id = byid[file_id].parent_id
1053
if parent_id is None:
1055
if parent_id not in parents:
1056
parents.add(parent_id)
1057
add_ancestors(parent_id)
1058
for file_id in specific_file_ids:
1059
add_ancestors(file_id)
960
1063
stack = [(u'', from_dir)]
967
1070
child_relpath = cur_relpath + child_name
969
yield child_relpath, child_ie
1072
if (specific_file_ids is None or
1073
child_ie.file_id in specific_file_ids or
1074
(yield_parents and child_ie.file_id in parents)):
1075
yield child_relpath, child_ie
971
1077
if child_ie.kind == 'directory':
972
child_dirs.append((child_relpath+'/', child_ie))
1078
if parents is None or child_ie.file_id in parents:
1079
child_dirs.append((child_relpath+'/', child_ie))
973
1080
stack.extend(reversed(child_dirs))
1082
def make_entry(self, kind, name, parent_id, file_id=None):
1083
"""Simple thunk to bzrlib.inventory.make_entry."""
1084
return make_entry(kind, name, parent_id, file_id)
975
1086
def entries(self):
976
1087
"""Return list of (path, ie) for all entries except the root.
1051
1170
Returns the new entry object.
1053
1172
if entry.file_id in self._byid:
1054
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1173
raise errors.DuplicateFileId(entry.file_id,
1174
self._byid[entry.file_id])
1056
1176
if entry.parent_id is None:
1057
1177
assert self.root is None and len(self._byid) == 0
1058
self._set_root(entry)
1060
if entry.parent_id == ROOT_ID:
1061
assert self.root is not None, self
1062
entry.parent_id = self.root.file_id
1065
parent = self._byid[entry.parent_id]
1067
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1069
if entry.name in parent.children:
1070
raise BzrError("%s is already versioned" %
1071
pathjoin(self.id2path(parent.file_id), entry.name))
1073
self._byid[entry.file_id] = entry
1074
parent.children[entry.name] = entry
1181
parent = self._byid[entry.parent_id]
1183
raise BzrError("parent_id {%s} not in inventory" %
1186
if entry.name in parent.children:
1187
raise BzrError("%s is already versioned" %
1188
osutils.pathjoin(self.id2path(parent.file_id),
1189
entry.name).encode('utf-8'))
1190
parent.children[entry.name] = entry
1191
return self._add_child(entry)
1077
1193
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1078
1194
"""Add entry from a path.
1215
1336
return bool(self.path2id(names))
1217
1338
def has_id(self, file_id):
1218
return self._byid.has_key(file_id)
1339
return (file_id in self._byid)
1341
def remove_recursive_id(self, file_id):
1342
"""Remove file_id, and children, from the inventory.
1344
:param file_id: A file_id to remove.
1346
to_find_delete = [self._byid[file_id]]
1348
while to_find_delete:
1349
ie = to_find_delete.pop()
1350
to_delete.append(ie.file_id)
1351
if ie.kind == 'directory':
1352
to_find_delete.extend(ie.children.values())
1353
for file_id in reversed(to_delete):
1355
del self._byid[file_id]
1356
if ie.parent_id is not None:
1357
del self[ie.parent_id].children[ie.name]
1220
1361
def rename(self, file_id, new_parent_id, new_name):
1221
1362
"""Move a file within the inventory.
1223
1364
This can change either the name, or the parent, or both.
1225
This does not move the working file."""
1366
This does not move the working file.
1368
new_name = ensure_normalized_name(new_name)
1226
1369
if not is_valid_name(new_name):
1227
1370
raise BzrError("not an acceptable filename: %r" % new_name)
1256
1409
:param file_id: the file_id to use. if None, one will be created.
1258
1411
if file_id is None:
1259
file_id = bzrlib.workingtree.gen_file_id(name)
1412
file_id = generate_ids.gen_file_id(name)
1413
name = ensure_normalized_name(name)
1415
factory = entry_factory[kind]
1417
raise BzrError("unknown kind %r" % kind)
1418
return factory(file_id, name, parent_id)
1421
def ensure_normalized_name(name):
1424
:raises InvalidNormalization: When name is not normalized, and cannot be
1425
accessed on this platform by the normalized path.
1426
:return: The NFC/NFKC normalised version of name.
1428
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1429
# keep them synchronised.
1430
# we dont import normalized_filename directly because we want to be
1431
# able to change the implementation at runtime for tests.
1261
1432
norm_name, can_access = osutils.normalized_filename(name)
1262
1433
if norm_name != name:
1266
1437
# TODO: jam 20060701 This would probably be more useful
1267
1438
# if the error was raised with the full path
1268
1439
raise errors.InvalidNormalization(name)
1270
if kind == 'directory':
1271
return InventoryDirectory(file_id, name, parent_id)
1272
elif kind == 'file':
1273
return InventoryFile(file_id, name, parent_id)
1274
elif kind == 'symlink':
1275
return InventoryLink(file_id, name, parent_id)
1277
raise BzrError("unknown kind %r" % kind)
1280
1443
_NAME_RE = None