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
1017
lexicographically sorted order, and is a hybrid between
947
1018
depth-first and breadth-first.
1020
:param yield_parents: If True, yield the parents from the root leading
1021
down to specific_file_ids that have been requested. This has no
1022
impact if specific_file_ids is None.
949
1023
:return: This yields (path, entry) pairs
1025
if specific_file_ids and not isinstance(specific_file_ids, set):
1026
specific_file_ids = set(specific_file_ids)
951
1027
# TODO? Perhaps this should return the from_dir so that the root is
952
1028
# yielded? or maybe an option?
953
1029
if from_dir is None:
1030
if self.root is None:
1032
# Optimize a common case
1033
if (not yield_parents and specific_file_ids is not None and
1034
len(specific_file_ids) == 1):
1035
file_id = list(specific_file_ids)[0]
1037
yield self.id2path(file_id), self[file_id]
955
1039
from_dir = self.root
1040
if (specific_file_ids is None or yield_parents or
1041
self.root.file_id in specific_file_ids):
1042
yield u'', self.root
957
1043
elif isinstance(from_dir, basestring):
958
1044
from_dir = self._byid[from_dir]
1046
if specific_file_ids is not None:
1047
# TODO: jam 20070302 This could really be done as a loop rather
1048
# than a bunch of recursive calls.
1051
def add_ancestors(file_id):
1052
if file_id not in byid:
1054
parent_id = byid[file_id].parent_id
1055
if parent_id is None:
1057
if parent_id not in parents:
1058
parents.add(parent_id)
1059
add_ancestors(parent_id)
1060
for file_id in specific_file_ids:
1061
add_ancestors(file_id)
960
1065
stack = [(u'', from_dir)]
967
1072
child_relpath = cur_relpath + child_name
969
yield child_relpath, child_ie
1074
if (specific_file_ids is None or
1075
child_ie.file_id in specific_file_ids or
1076
(yield_parents and child_ie.file_id in parents)):
1077
yield child_relpath, child_ie
971
1079
if child_ie.kind == 'directory':
972
child_dirs.append((child_relpath+'/', child_ie))
1080
if parents is None or child_ie.file_id in parents:
1081
child_dirs.append((child_relpath+'/', child_ie))
973
1082
stack.extend(reversed(child_dirs))
1084
def make_entry(self, kind, name, parent_id, file_id=None):
1085
"""Simple thunk to bzrlib.inventory.make_entry."""
1086
return make_entry(kind, name, parent_id, file_id)
975
1088
def entries(self):
976
1089
"""Return list of (path, ie) for all entries except the root.
1051
1172
Returns the new entry object.
1053
1174
if entry.file_id in self._byid:
1054
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1175
raise errors.DuplicateFileId(entry.file_id,
1176
self._byid[entry.file_id])
1056
1178
if entry.parent_id is None:
1057
1179
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
1183
parent = self._byid[entry.parent_id]
1185
raise BzrError("parent_id {%s} not in inventory" %
1188
if entry.name in parent.children:
1189
raise BzrError("%s is already versioned" %
1190
osutils.pathjoin(self.id2path(parent.file_id),
1191
entry.name).encode('utf-8'))
1192
parent.children[entry.name] = entry
1193
return self._add_child(entry)
1077
1195
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1078
1196
"""Add entry from a path.
1232
1355
for file_id in reversed(to_delete):
1233
1356
ie = self[file_id]
1234
1357
del self._byid[file_id]
1235
if ie.parent_id is not None:
1236
del self[ie.parent_id].children[ie.name]
1358
if ie.parent_id is not None:
1359
del self[ie.parent_id].children[ie.name]
1238
1363
def rename(self, file_id, new_parent_id, new_name):
1239
1364
"""Move a file within the inventory.
1241
1366
This can change either the name, or the parent, or both.
1243
This does not move the working file."""
1368
This does not move the working file.
1370
new_name = ensure_normalized_name(new_name)
1244
1371
if not is_valid_name(new_name):
1245
1372
raise BzrError("not an acceptable filename: %r" % new_name)
1274
1411
:param file_id: the file_id to use. if None, one will be created.
1276
1413
if file_id is None:
1277
file_id = bzrlib.workingtree.gen_file_id(name)
1414
file_id = generate_ids.gen_file_id(name)
1415
name = ensure_normalized_name(name)
1417
factory = entry_factory[kind]
1419
raise BzrError("unknown kind %r" % kind)
1420
return factory(file_id, name, parent_id)
1423
def ensure_normalized_name(name):
1426
:raises InvalidNormalization: When name is not normalized, and cannot be
1427
accessed on this platform by the normalized path.
1428
:return: The NFC/NFKC normalised version of name.
1430
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1431
# keep them synchronised.
1432
# we dont import normalized_filename directly because we want to be
1433
# able to change the implementation at runtime for tests.
1279
1434
norm_name, can_access = osutils.normalized_filename(name)
1280
1435
if norm_name != name:
1284
1439
# TODO: jam 20060701 This would probably be more useful
1285
1440
# if the error was raised with the full path
1286
1441
raise errors.InvalidNormalization(name)
1288
if kind == 'directory':
1289
return InventoryDirectory(file_id, name, parent_id)
1290
elif kind == 'file':
1291
return InventoryFile(file_id, name, parent_id)
1292
elif kind == 'symlink':
1293
return InventoryLink(file_id, name, parent_id)
1295
raise BzrError("unknown kind %r" % kind)
1298
1445
_NAME_RE = None