80
78
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
81
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
82
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
83
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
84
>>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
85
83
>>> for ix, j in enumerate(i.iter_entries()):
86
84
... print (j[0] == shouldbe[ix], j[1])
88
(True, RootEntry('TREE_ROOT', u'', parent_id=None, revision=None))
89
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
90
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
86
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
(True, InventoryFile('2323', 'hello.c', parent_id='123'))
91
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
92
89
Traceback (most recent call last):
94
91
BzrError: inventory already contains entry with id {2323}
95
92
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
96
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
InventoryFile('2324', 'bye.c', parent_id='123')
97
94
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
98
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
InventoryDirectory('2325', 'wibble', parent_id='123')
99
96
>>> i.path2id('src/wibble')
103
100
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
104
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
101
InventoryFile('2326', 'wibble.c', parent_id='2325')
106
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
InventoryFile('2326', 'wibble.c', parent_id='2325')
107
104
>>> for path, entry in i.iter_entries():
109
106
... assert i.path2id(path)
117
113
>>> i.id2path('2326')
118
114
'src/wibble/wibble.c'
121
# Constants returned by describe_change()
123
# TODO: These should probably move to some kind of FileChangeDescription
124
# class; that's like what's inside a TreeDelta but we want to be able to
125
# generate them just for one file at a time.
127
MODIFIED_AND_RENAMED = 'modified and renamed'
117
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
118
'text_id', 'parent_id', 'children', 'executable',
121
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
versionedfile = weave_store.get_weave_or_empty(self.file_id,
124
versionedfile.add_lines(self.revision, parents, new_lines)
131
126
def detect_changes(self, old_entry):
132
127
"""Return a (text_modified, meta_modified) from this to old_entry.
351
348
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
352
349
(self.kind, rev_id))
355
353
"""Clone this inventory entry."""
356
354
raise NotImplementedError
359
def describe_change(old_entry, new_entry):
360
"""Describe the change between old_entry and this.
362
This smells of being an InterInventoryEntry situation, but as its
363
the first one, we're making it a static method for now.
365
An entry with a different parent, or different name is considered
366
to be renamed. Reparenting is an internal detail.
367
Note that renaming the parent does not trigger a rename for the
370
# TODO: Perhaps return an object rather than just a string
371
if old_entry is new_entry:
372
# also the case of both being None
374
elif old_entry is None:
356
def _get_snapshot_change(self, previous_entries):
357
if len(previous_entries) > 1:
359
elif len(previous_entries) == 0:
376
elif new_entry is None:
378
text_modified, meta_modified = new_entry.detect_changes(old_entry)
379
if text_modified or meta_modified:
383
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
384
if old_entry.parent_id != new_entry.parent_id:
386
elif old_entry.name != new_entry.name:
390
if renamed and not modified:
391
return InventoryEntry.RENAMED
392
if modified and not renamed:
394
if modified and renamed:
395
return InventoryEntry.MODIFIED_AND_RENAMED
362
return 'modified/renamed/reparented'
398
364
def __repr__(self):
399
return ("%s(%r, %r, parent_id=%r, revision=%r)"
365
return ("%s(%r, %r, parent_id=%r)"
400
366
% (self.__class__.__name__,
406
371
def snapshot(self, revision, path, previous_entries,
407
work_tree, commit_builder):
372
work_tree, weave_store, transaction):
408
373
"""Make a snapshot of this entry which may or may not have changed.
410
375
This means that all its fields are populated, that it has its
411
376
text stored in the text store or weave.
413
# mutter('new parents of %s are %r', path, previous_entries)
378
mutter('new parents of %s are %r', path, previous_entries)
414
379
self._read_tree_state(path, work_tree)
415
# TODO: Where should we determine whether to reuse a
416
# previous revision id or create a new revision? 20060606
417
380
if len(previous_entries) == 1:
418
381
# cannot be unchanged unless there is only one parent file rev.
419
382
parent_ie = previous_entries.values()[0]
420
383
if self._unchanged(parent_ie):
421
# mutter("found unchanged entry")
384
mutter("found unchanged entry")
422
385
self.revision = parent_ie.revision
423
386
return "unchanged"
424
return self._snapshot_into_revision(revision, previous_entries,
425
work_tree, commit_builder)
427
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
429
"""Record this revision unconditionally into a store.
431
The entry's last-changed revision property (`revision`) is updated to
432
that of the new revision.
434
:param revision: id of the new revision that is being recorded.
436
:returns: String description of the commit (e.g. "merged", "modified"), etc.
438
# mutter('new revision {%s} for {%s}', revision, self.file_id)
387
return self.snapshot_revision(revision, previous_entries,
388
work_tree, weave_store, transaction)
390
def snapshot_revision(self, revision, previous_entries, work_tree,
391
weave_store, transaction):
392
"""Record this revision unconditionally."""
393
mutter('new revision for {%s}', self.file_id)
439
394
self.revision = revision
440
self._snapshot_text(previous_entries, work_tree, commit_builder)
395
change = self._get_snapshot_change(previous_entries)
396
self._snapshot_text(previous_entries, work_tree, weave_store,
442
def _snapshot_text(self, file_parents, work_tree, commit_builder):
400
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
443
401
"""Record the 'text' of this entry, whatever form that takes.
445
403
This default implementation simply adds an empty text.
447
raise NotImplementedError(self._snapshot_text)
405
mutter('storing file {%s} in revision {%s}',
406
self.file_id, self.revision)
407
self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
449
409
def __eq__(self, other):
450
410
if not isinstance(other, InventoryEntry):
564
515
"""See InventoryEntry._put_on_disk."""
565
516
os.mkdir(fullpath)
567
def _snapshot_text(self, file_parents, work_tree, commit_builder):
568
"""See InventoryEntry._snapshot_text."""
569
commit_builder.modified_directory(self.file_id, file_parents)
572
519
class InventoryFile(InventoryEntry):
573
520
"""A file in an inventory."""
575
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
576
'text_id', 'parent_id', 'children', 'executable',
577
'revision', 'symlink_target']
579
522
def _check(self, checker, tree_revision_id, tree):
580
523
"""See InventoryEntry._check"""
581
524
t = (self.file_id, self.revision)
629
572
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
630
573
output_to, reverse=False):
631
574
"""See InventoryEntry._diff."""
633
from_text = tree.get_file(self.file_id).readlines()
635
to_text = to_tree.get_file(to_entry.file_id).readlines()
639
text_diff(from_label, from_text,
640
to_label, to_text, output_to)
642
text_diff(to_label, to_text,
643
from_label, from_text, output_to)
646
label_pair = (to_label, from_label)
648
label_pair = (from_label, to_label)
649
print >> output_to, "Binary files %s and %s differ" % label_pair
575
from_text = tree.get_file(self.file_id).readlines()
577
to_text = to_tree.get_file(to_entry.file_id).readlines()
581
text_diff(from_label, from_text,
582
to_label, to_text, output_to)
584
text_diff(to_label, to_text,
585
from_label, from_text, output_to)
651
587
def has_text(self):
652
588
"""See InventoryEntry.has_text."""
680
616
def _read_tree_state(self, path, work_tree):
681
617
"""See InventoryEntry._read_tree_state."""
682
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
683
# FIXME: 20050930 probe for the text size when getting sha1
684
# in _read_tree_state
685
self.executable = work_tree.is_executable(self.file_id, path=path)
688
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
689
% (self.__class__.__name__,
618
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
619
self.executable = work_tree.is_executable(self.file_id)
696
621
def _forget_tree_state(self):
697
622
self.text_sha1 = None
623
self.executable = None
699
def _snapshot_text(self, file_parents, work_tree, commit_builder):
625
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
700
626
"""See InventoryEntry._snapshot_text."""
701
def get_content_byte_lines():
702
return work_tree.get_file(self.file_id).readlines()
703
self.text_sha1, self.text_size = commit_builder.modified_file_text(
704
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
627
mutter('storing file {%s} in revision {%s}',
628
self.file_id, self.revision)
629
# special case to avoid diffing on renames or
631
if (len(file_parents) == 1
632
and self.text_sha1 == file_parents.values()[0].text_sha1
633
and self.text_size == file_parents.values()[0].text_size):
634
previous_ie = file_parents.values()[0]
635
versionedfile = weave_store.get_weave(self.file_id, transaction)
636
versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
638
new_lines = work_tree.get_file(self.file_id).readlines()
639
self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
641
self.text_sha1 = sha_strings(new_lines)
642
self.text_size = sum(map(len, new_lines))
706
645
def _unchanged(self, previous_ie):
707
646
"""See InventoryEntry._unchanged."""
720
659
class InventoryLink(InventoryEntry):
721
660
"""A file in an inventory."""
723
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
724
'text_id', 'parent_id', 'children', 'executable',
725
'revision', 'symlink_target']
662
__slots__ = ['symlink_target']
727
664
def _check(self, checker, rev_id, tree):
728
665
"""See InventoryEntry._check"""
729
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
666
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
730
667
raise BzrCheckError('symlink {%s} has text in revision {%s}'
731
668
% (self.file_id, rev_id))
732
if self.symlink_target is None:
669
if self.symlink_target == None:
733
670
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
734
671
% (self.file_id, rev_id))
862
794
#if root_id is None:
863
795
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
864
796
self.root = RootEntry(root_id)
865
# FIXME: this isn't ever used, changing it to self.revision may break
866
# things. TODO make everything use self.revision_id
867
797
self.revision_id = revision_id
868
798
self._byid = {self.root.file_id: self.root}
871
802
# TODO: jam 20051218 Should copy also copy the revision_id?
872
entries = self.iter_entries()
873
other = Inventory(entries.next()[1].file_id)
803
other = Inventory(self.root.file_id)
874
804
# copy recursively so we know directories will be added before
875
805
# their children. There are more efficient ways than this...
876
for path, entry in entries():
806
for path, entry in self.iter_entries():
807
if entry == self.root:
877
809
other.add(entry.copy())
880
813
def __iter__(self):
881
814
return iter(self._byid)
883
817
def __len__(self):
884
818
"""Returns number of entries."""
885
819
return len(self._byid)
887
822
def iter_entries(self, from_dir=None):
888
823
"""Return (path, entry) pairs, in order by name."""
893
elif isinstance(from_dir, basestring):
894
from_dir = self._byid[from_dir]
896
# unrolling the recursive called changed the time from
897
# 440ms/663ms (inline/total) to 116ms/116ms
898
children = from_dir.children.items()
900
children = collections.deque(children)
901
stack = [(u'', children)]
903
from_dir_relpath, children = stack[-1]
906
name, ie = children.popleft()
908
# we know that from_dir_relpath never ends in a slash
909
# and 'f' doesn't begin with one, we can do a string op, rather
910
# than the checks of pathjoin(), though this means that all paths
912
path = from_dir_relpath + '/' + name
916
if ie.kind != 'directory':
919
# But do this child first
920
new_children = ie.children.items()
922
new_children = collections.deque(new_children)
923
stack.append((path, new_children))
924
# Break out of inner loop, so that we start outer loop with child
927
# if we finished all children, pop it off the stack
930
def iter_entries_by_dir(self, from_dir=None):
931
"""Iterate over the entries in a directory first order.
933
This returns all entries for a directory before returning
934
the entries for children of a directory. This is not
935
lexicographically sorted order, and is a hybrid between
936
depth-first and breadth-first.
938
:return: This yields (path, entry) pairs
940
# TODO? Perhaps this should return the from_dir so that the root is
941
# yielded? or maybe an option?
946
elif isinstance(from_dir, basestring):
947
from_dir = self._byid[from_dir]
949
stack = [(u'', from_dir)]
951
cur_relpath, cur_dir = stack.pop()
954
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
956
child_relpath = cur_relpath + child_name
958
yield child_relpath, child_ie
960
if child_ie.kind == 'directory':
961
child_dirs.append((child_relpath+'/', child_ie))
962
stack.extend(reversed(child_dirs))
827
elif isinstance(from_dir, basestring):
828
from_dir = self._byid[from_dir]
830
kids = from_dir.children.items()
832
for name, ie in kids:
834
if ie.kind == 'directory':
835
for cn, cie in self.iter_entries(from_dir=ie.file_id):
836
yield pathjoin(name, cn), cie
964
839
def entries(self):
965
840
"""Return list of (path, ie) for all entries except the root.
1009
887
return file_id in self._byid
1011
890
def __getitem__(self, file_id):
1012
891
"""Return the entry for given file_id.
1014
893
>>> inv = Inventory()
1015
894
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1016
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
895
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1017
896
>>> inv['123123'].name
1021
900
return self._byid[file_id]
1022
901
except KeyError:
1024
903
raise BzrError("can't look up file_id None")
1026
905
raise BzrError("file_id {%s} not in inventory" % file_id)
1028
908
def get_file_kind(self, file_id):
1029
909
return self._byid[file_id].kind
1031
911
def get_child(self, parent_id, filename):
1032
912
return self[parent_id].children.get(filename)
1034
915
def add(self, entry):
1035
916
"""Add entry to inventory.
1058
939
parent.children[entry.name] = entry
1061
def add_path(self, relpath, kind, file_id=None, parent_id=None):
943
def add_path(self, relpath, kind, file_id=None):
1062
944
"""Add entry from a path.
1064
946
The immediate parent must already be versioned.
1066
948
Returns the new entry object."""
949
from bzrlib.workingtree import gen_file_id
1068
parts = osutils.splitpath(relpath)
951
parts = bzrlib.osutils.splitpath(relpath)
954
file_id = gen_file_id(relpath)
1070
956
if len(parts) == 0:
1072
file_id = bzrlib.workingtree.gen_root_id()
1073
957
self.root = RootEntry(file_id)
1074
958
self._byid = {self.root.file_id: self.root}
1077
961
parent_path = parts[:-1]
1078
962
parent_id = self.path2id(parent_path)
1079
if parent_id is None:
963
if parent_id == None:
1080
964
raise NotVersionedError(path=parent_path)
1081
ie = make_entry(kind, parts[-1], parent_id, file_id)
965
if kind == 'directory':
966
ie = InventoryDirectory(file_id, parts[-1], parent_id)
968
ie = InventoryFile(file_id, parts[-1], parent_id)
969
elif kind == 'symlink':
970
ie = InventoryLink(file_id, parts[-1], parent_id)
972
raise BzrError("unknown kind %r" % kind)
1082
973
return self.add(ie)
1084
976
def __delitem__(self, file_id):
1085
977
"""Remove entry by id.
1087
979
>>> inv = Inventory()
1088
980
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1089
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
981
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1090
982
>>> '123' in inv
1092
984
>>> del inv['123']
1112
1005
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1113
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1006
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1116
1009
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1117
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
1010
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1121
1014
if not isinstance(other, Inventory):
1122
1015
return NotImplemented
1017
if len(self._byid) != len(other._byid):
1018
# shortcut: obviously not the same
1124
1021
return self._byid == other._byid
1126
1024
def __ne__(self, other):
1127
1025
return not self.__eq__(other)
1129
1028
def __hash__(self):
1130
1029
raise ValueError('not hashable')
1132
1031
def _iter_file_id_parents(self, file_id):
1133
1032
"""Yield the parents of file_id up to the root."""
1134
while file_id is not None:
1033
while file_id != None:
1136
1035
ie = self._byid[file_id]
1137
1036
except KeyError:
1231
1133
file_ie.parent_id = new_parent_id
1234
def make_entry(kind, name, parent_id, file_id=None):
1235
"""Create an inventory entry.
1237
:param kind: the type of inventory entry to create.
1238
:param name: the basename of the entry.
1239
:param parent_id: the parent_id of the entry.
1240
:param file_id: the file_id to use. if None, one will be created.
1243
file_id = bzrlib.workingtree.gen_file_id(name)
1245
norm_name, can_access = osutils.normalized_filename(name)
1246
if norm_name != name:
1250
# TODO: jam 20060701 This would probably be more useful
1251
# if the error was raised with the full path
1252
raise errors.InvalidNormalization(name)
1254
if kind == 'directory':
1255
return InventoryDirectory(file_id, name, parent_id)
1256
elif kind == 'file':
1257
return InventoryFile(file_id, name, parent_id)
1258
elif kind == 'symlink':
1259
return InventoryLink(file_id, name, parent_id)
1261
raise BzrError("unknown kind %r" % kind)
1264
1138
_NAME_RE = None
1266
1140
def is_valid_name(name):
1267
1141
global _NAME_RE
1268
if _NAME_RE is None:
1142
if _NAME_RE == None:
1269
1143
_NAME_RE = re.compile(r'^[^/\\]+$')
1271
1145
return bool(_NAME_RE.match(name))