78
80
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
81
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
82
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
InventoryFile('2323', 'hello.c', parent_id='123')
83
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
82
84
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
83
85
>>> for ix, j in enumerate(i.iter_entries()):
84
86
... 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
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
89
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
88
90
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
89
91
Traceback (most recent call last):
91
93
BzrError: inventory already contains entry with id {2323}
92
94
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
InventoryFile('2324', 'bye.c', parent_id='123')
95
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
94
96
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
InventoryDirectory('2325', 'wibble', parent_id='123')
97
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
96
98
>>> i.path2id('src/wibble')
100
102
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
103
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
105
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
104
106
>>> for path, entry in i.iter_entries():
106
108
... assert i.path2id(path)
122
124
RENAMED = 'renamed'
123
125
MODIFIED_AND_RENAMED = 'modified and renamed'
125
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
126
'text_id', 'parent_id', 'children', 'executable',
129
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
130
versionedfile = weave_store.get_weave_or_empty(self.file_id,
132
versionedfile.add_lines(self.revision, parents, new_lines)
133
versionedfile.clear_cache()
135
129
def detect_changes(self, old_entry):
136
130
"""Return a (text_modified, meta_modified) from this to old_entry.
402
394
return 'unchanged'
404
396
def __repr__(self):
405
return ("%s(%r, %r, parent_id=%r)"
397
return ("%s(%r, %r, parent_id=%r, revision=%r)"
406
398
% (self.__class__.__name__,
411
404
def snapshot(self, revision, path, previous_entries,
412
work_tree, weave_store, transaction):
405
work_tree, commit_builder):
413
406
"""Make a snapshot of this entry which may or may not have changed.
415
408
This means that all its fields are populated, that it has its
425
420
self.revision = parent_ie.revision
426
421
return "unchanged"
427
422
return self._snapshot_into_revision(revision, previous_entries,
428
work_tree, weave_store, transaction)
423
work_tree, commit_builder)
430
425
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
431
weave_store, transaction):
432
427
"""Record this revision unconditionally into a store.
434
429
The entry's last-changed revision property (`revision`) is updated to
441
436
mutter('new revision {%s} for {%s}', revision, self.file_id)
442
437
self.revision = revision
443
self._snapshot_text(previous_entries, work_tree, weave_store,
438
self._snapshot_text(previous_entries, work_tree, commit_builder)
446
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
440
def _snapshot_text(self, file_parents, work_tree, commit_builder):
447
441
"""Record the 'text' of this entry, whatever form that takes.
449
443
This default implementation simply adds an empty text.
451
mutter('storing file {%s} in revision {%s}',
452
self.file_id, self.revision)
453
self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
445
raise NotImplementedError(self._snapshot_text)
455
447
def __eq__(self, other):
456
448
if not isinstance(other, InventoryEntry):
506
498
class RootEntry(InventoryEntry):
500
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
501
'text_id', 'parent_id', 'children', 'executable',
502
'revision', 'symlink_target']
508
504
def _check(self, checker, rev_id, tree):
509
505
"""See InventoryEntry._check"""
526
523
class InventoryDirectory(InventoryEntry):
527
524
"""A directory in an inventory."""
526
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
527
'text_id', 'parent_id', 'children', 'executable',
528
'revision', 'symlink_target']
529
530
def _check(self, checker, rev_id, tree):
530
531
"""See InventoryEntry._check"""
531
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
532
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
532
533
raise BzrCheckError('directory {%s} has text in revision {%s}'
533
534
% (self.file_id, rev_id))
561
562
"""See InventoryEntry._put_on_disk."""
562
563
os.mkdir(fullpath)
565
def _snapshot_text(self, file_parents, work_tree, commit_builder):
566
"""See InventoryEntry._snapshot_text."""
567
commit_builder.modified_directory(self.file_id, file_parents)
565
570
class InventoryFile(InventoryEntry):
566
571
"""A file in an inventory."""
573
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
574
'text_id', 'parent_id', 'children', 'executable',
575
'revision', 'symlink_target']
568
577
def _check(self, checker, tree_revision_id, tree):
569
578
"""See InventoryEntry._check"""
570
579
t = (self.file_id, self.revision)
610
619
def detect_changes(self, old_entry):
611
620
"""See InventoryEntry.detect_changes."""
612
assert self.text_sha1 != None
613
assert old_entry.text_sha1 != None
621
assert self.text_sha1 is not None
622
assert old_entry.text_sha1 is not None
614
623
text_modified = (self.text_sha1 != old_entry.text_sha1)
615
624
meta_modified = (self.executable != old_entry.executable)
616
625
return text_modified, meta_modified
669
678
def _read_tree_state(self, path, work_tree):
670
679
"""See InventoryEntry._read_tree_state."""
671
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
672
self.executable = work_tree.is_executable(self.file_id)
680
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
681
# FIXME: 20050930 probe for the text size when getting sha1
682
# in _read_tree_state
683
self.executable = work_tree.is_executable(self.file_id, path=path)
686
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
687
% (self.__class__.__name__,
674
694
def _forget_tree_state(self):
675
695
self.text_sha1 = None
676
self.executable = None
678
def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
697
def _snapshot_text(self, file_parents, work_tree, commit_builder):
679
698
"""See InventoryEntry._snapshot_text."""
680
mutter('storing text of file {%s} in revision {%s} into %r',
681
self.file_id, self.revision, versionedfile_store)
682
# special case to avoid diffing on renames or
684
if (len(file_parents) == 1
685
and self.text_sha1 == file_parents.values()[0].text_sha1
686
and self.text_size == file_parents.values()[0].text_size):
687
previous_ie = file_parents.values()[0]
688
versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
689
versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
691
new_lines = work_tree.get_file(self.file_id).readlines()
692
self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
694
self.text_sha1 = sha_strings(new_lines)
695
self.text_size = sum(map(len, new_lines))
699
def get_content_byte_lines():
700
return work_tree.get_file(self.file_id).readlines()
701
self.text_sha1, self.text_size = commit_builder.modified_file_text(
702
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
698
704
def _unchanged(self, previous_ie):
699
705
"""See InventoryEntry._unchanged."""
712
718
class InventoryLink(InventoryEntry):
713
719
"""A file in an inventory."""
715
__slots__ = ['symlink_target']
721
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
722
'text_id', 'parent_id', 'children', 'executable',
723
'revision', 'symlink_target']
717
725
def _check(self, checker, rev_id, tree):
718
726
"""See InventoryEntry._check"""
719
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
727
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
720
728
raise BzrCheckError('symlink {%s} has text in revision {%s}'
721
729
% (self.file_id, rev_id))
722
if self.symlink_target == None:
730
if self.symlink_target is None:
723
731
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
724
732
% (self.file_id, rev_id))
827
840
May also look up by name:
829
842
>>> [x[0] for x in inv.iter_entries()]
831
844
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
832
845
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
833
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
846
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
835
848
def __init__(self, root_id=ROOT_ID, revision_id=None):
836
849
"""Create or read an inventory.
862
876
other.add(entry.copy())
866
879
def __iter__(self):
867
880
return iter(self._byid)
870
882
def __len__(self):
871
883
"""Returns number of entries."""
872
884
return len(self._byid)
875
886
def iter_entries(self, from_dir=None):
876
887
"""Return (path, entry) pairs, in order by name."""
880
elif isinstance(from_dir, basestring):
881
from_dir = self._byid[from_dir]
883
kids = from_dir.children.items()
885
for name, ie in kids:
887
if ie.kind == 'directory':
888
for cn, cie in self.iter_entries(from_dir=ie.file_id):
889
yield pathjoin(name, cn), cie
891
elif isinstance(from_dir, basestring):
892
from_dir = self._byid[from_dir]
894
# unrolling the recursive called changed the time from
895
# 440ms/663ms (inline/total) to 116ms/116ms
896
children = from_dir.children.items()
898
children = collections.deque(children)
899
stack = [(u'', children)]
901
from_dir_relpath, children = stack[-1]
904
name, ie = children.popleft()
906
# we know that from_dir_relpath never ends in a slash
907
# and 'f' doesn't begin with one, we can do a string op, rather
908
# than the checks of pathjoin(), though this means that all paths
910
path = from_dir_relpath + '/' + name
914
if ie.kind != 'directory':
917
# But do this child first
918
new_children = ie.children.items()
920
new_children = collections.deque(new_children)
921
stack.append((path, new_children))
922
# Break out of inner loop, so that we start outer loop with child
925
# if we finished all children, pop it off the stack
928
def iter_entries_by_dir(self, from_dir=None):
929
"""Iterate over the entries in a directory first order.
931
This returns all entries for a directory before returning
932
the entries for children of a directory. This is not
933
lexicographically sorted order, and is a hybrid between
934
depth-first and breadth-first.
936
:return: This yields (path, entry) pairs
938
# TODO? Perhaps this should return the from_dir so that the root is
939
# yielded? or maybe an option?
943
elif isinstance(from_dir, basestring):
944
from_dir = self._byid[from_dir]
946
stack = [(u'', from_dir)]
948
cur_relpath, cur_dir = stack.pop()
951
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
953
child_relpath = cur_relpath + child_name
955
yield child_relpath, child_ie
957
if child_ie.kind == 'directory':
958
child_dirs.append((child_relpath+'/', child_ie))
959
stack.extend(reversed(child_dirs))
892
961
def entries(self):
893
962
"""Return list of (path, ie) for all entries except the root.
924
992
descend(self.root, u'')
929
995
def __contains__(self, file_id):
930
996
"""True if this entry contains a file with given id.
932
998
>>> inv = Inventory()
933
999
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
934
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1000
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
935
1001
>>> '123' in inv
937
1003
>>> '456' in inv
940
1006
return file_id in self._byid
943
1008
def __getitem__(self, file_id):
944
1009
"""Return the entry for given file_id.
946
1011
>>> inv = Inventory()
947
1012
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
948
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1013
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
949
1014
>>> inv['123123'].name
953
1018
return self._byid[file_id]
954
1019
except KeyError:
956
1021
raise BzrError("can't look up file_id None")
958
1023
raise BzrError("file_id {%s} not in inventory" % file_id)
961
1025
def get_file_kind(self, file_id):
962
1026
return self._byid[file_id].kind
964
1028
def get_child(self, parent_id, filename):
965
1029
return self[parent_id].children.get(filename)
968
1031
def add(self, entry):
969
1032
"""Add entry to inventory.
992
1055
parent.children[entry.name] = entry
996
def add_path(self, relpath, kind, file_id=None):
1058
def add_path(self, relpath, kind, file_id=None, parent_id=None):
997
1059
"""Add entry from a path.
999
1061
The immediate parent must already be versioned.
1001
1063
Returns the new entry object."""
1003
parts = bzrlib.osutils.splitpath(relpath)
1065
parts = osutils.splitpath(relpath)
1005
1067
if len(parts) == 0:
1006
1068
if file_id is None:
1009
1071
self._byid = {self.root.file_id: self.root}
1013
file_id = bzrlib.workingtree.gen_file_id(parts[-1])
1014
1074
parent_path = parts[:-1]
1015
1075
parent_id = self.path2id(parent_path)
1016
if parent_id == None:
1076
if parent_id is None:
1017
1077
raise NotVersionedError(path=parent_path)
1018
if kind == 'directory':
1019
ie = InventoryDirectory(file_id, parts[-1], parent_id)
1020
elif kind == 'file':
1021
ie = InventoryFile(file_id, parts[-1], parent_id)
1022
elif kind == 'symlink':
1023
ie = InventoryLink(file_id, parts[-1], parent_id)
1025
raise BzrError("unknown kind %r" % kind)
1078
ie = make_entry(kind, parts[-1], parent_id, file_id)
1026
1079
return self.add(ie)
1029
1081
def __delitem__(self, file_id):
1030
1082
"""Remove entry by id.
1032
1084
>>> inv = Inventory()
1033
1085
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1034
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1086
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
1035
1087
>>> '123' in inv
1037
1089
>>> del inv['123']
1058
1109
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1059
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1110
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1062
1113
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1063
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1114
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1067
1118
if not isinstance(other, Inventory):
1068
1119
return NotImplemented
1070
if len(self._byid) != len(other._byid):
1071
# shortcut: obviously not the same
1074
1121
return self._byid == other._byid
1077
1123
def __ne__(self, other):
1078
1124
return not self.__eq__(other)
1081
1126
def __hash__(self):
1082
1127
raise ValueError('not hashable')
1084
1129
def _iter_file_id_parents(self, file_id):
1085
1130
"""Yield the parents of file_id up to the root."""
1086
while file_id != None:
1131
while file_id is not None:
1088
1133
ie = self._byid[file_id]
1089
1134
except KeyError:
1186
1228
file_ie.parent_id = new_parent_id
1231
def make_entry(kind, name, parent_id, file_id=None):
1232
"""Create an inventory entry.
1234
:param kind: the type of inventory entry to create.
1235
:param name: the basename of the entry.
1236
:param parent_id: the parent_id of the entry.
1237
:param file_id: the file_id to use. if None, one will be created.
1240
file_id = bzrlib.workingtree.gen_file_id(name)
1242
norm_name, can_access = osutils.normalized_filename(name)
1243
if norm_name != name:
1247
# TODO: jam 20060701 This would probably be more useful
1248
# if the error was raised with the full path
1249
raise errors.InvalidNormalization(name)
1251
if kind == 'directory':
1252
return InventoryDirectory(file_id, name, parent_id)
1253
elif kind == 'file':
1254
return InventoryFile(file_id, name, parent_id)
1255
elif kind == 'symlink':
1256
return InventoryLink(file_id, name, parent_id)
1258
raise BzrError("unknown kind %r" % kind)
1191
1261
_NAME_RE = None
1193
1263
def is_valid_name(name):
1194
1264
global _NAME_RE
1195
if _NAME_RE == None:
1265
if _NAME_RE is None:
1196
1266
_NAME_RE = re.compile(r'^[^/\\]+$')
1198
1268
return bool(_NAME_RE.match(name))