114
113
>>> i.id2path('2326')
115
114
'src/wibble/wibble.c'
118
# Constants returned by describe_change()
120
# TODO: These should probably move to some kind of FileChangeDescription
121
# class; that's like what's inside a TreeDelta but we want to be able to
122
# generate them just for one file at a time.
124
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
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
128
125
def detect_changes(self, old_entry):
129
126
"""Return a (text_modified, meta_modified) from this to old_entry.
154
151
output_to, reverse=False):
155
152
"""Perform a diff between two entries of the same kind."""
157
def find_previous_heads(self, previous_inventories,
158
versioned_file_store,
161
"""Return the revisions and entries that directly precede this.
154
def find_previous_heads(self, previous_inventories, entry_weave):
155
"""Return the revisions and entries that directly preceed this.
163
157
Returned as a map from revision to inventory entry.
165
159
This is a map containing the file revisions in all parents
166
160
for which the file exists, and its revision is not a parent of
167
161
any other. If the file is new, the set will be empty.
169
:param versioned_file_store: A store where ancestry data on this
170
file id can be queried.
171
:param transaction: The transaction that queries to the versioned
172
file store should be completed under.
173
:param entry_vf: The entry versioned file, if its already available.
175
163
def get_ancestors(weave, entry):
176
return set(weave.get_ancestry(entry.revision))
177
# revision:ie mapping for each ie found in previous_inventories.
179
# revision:ie mapping with one revision for each head.
164
return set(map(weave.idx_to_name,
165
weave.inclusions([weave.lookup(entry.revision)])))
181
# revision: ancestor list for each head
182
167
head_ancestors = {}
183
# identify candidate head revision ids.
184
168
for inv in previous_inventories:
185
169
if self.file_id in inv:
186
170
ie = inv[self.file_id]
187
171
assert ie.file_id == self.file_id
188
if ie.revision in candidates:
189
# same revision value in two different inventories:
190
# correct possible inconsistencies:
191
# * there was a bug in revision updates with 'x' bit
172
if ie.revision in heads:
173
# fixup logic, there was a bug in revision updates.
174
# with x bit support.
194
if candidates[ie.revision].executable != ie.executable:
195
candidates[ie.revision].executable = False
176
if heads[ie.revision].executable != ie.executable:
177
heads[ie.revision].executable = False
196
178
ie.executable = False
197
179
except AttributeError:
199
# must now be the same.
200
assert candidates[ie.revision] == ie
181
assert heads[ie.revision] == ie
202
# add this revision as a candidate.
203
candidates[ie.revision] = ie
205
# common case optimisation
206
if len(candidates) == 1:
207
# if there is only one candidate revision found
208
# then we can opening the versioned file to access ancestry:
209
# there cannot be any ancestors to eliminate when there is
210
# only one revision available.
211
heads[ie.revision] = ie
214
# eliminate ancestors amongst the available candidates:
215
# heads are those that are not an ancestor of any other candidate
216
# - this provides convergence at a per-file level.
217
for ie in candidates.values():
218
# may be an ancestor of a known head:
219
already_present = 0 != len(
220
[head for head in heads
221
if ie.revision in head_ancestors[head]])
223
# an ancestor of an analyzed candidate.
225
# not an ancestor of a known head:
226
# load the versioned file for this file id if needed
228
entry_vf = versioned_file_store.get_weave_or_empty(
229
self.file_id, transaction)
230
ancestors = get_ancestors(entry_vf, ie)
231
# may knock something else out:
232
check_heads = list(heads.keys())
233
for head in check_heads:
234
if head in ancestors:
235
# this previously discovered 'head' is not
236
# really a head - its an ancestor of the newly
239
head_ancestors[ie.revision] = ancestors
240
heads[ie.revision] = ie
183
# may want to add it.
184
# may already be covered:
185
already_present = 0 != len(
186
[head for head in heads
187
if ie.revision in head_ancestors[head]])
189
# an ancestor of a known head.
192
ancestors = get_ancestors(entry_weave, ie)
193
# may knock something else out:
194
check_heads = list(heads.keys())
195
for head in check_heads:
196
if head in ancestors:
197
# this head is not really a head
199
head_ancestors[ie.revision] = ancestors
200
heads[ie.revision] = ie
243
203
def get_tar_item(self, root, dp, now, tree):
329
291
This is a template method, override _check for kind specific
332
:param checker: Check object providing context for the checks;
333
can be used to find out what parts of the repository have already
335
:param rev_id: Revision id from which this InventoryEntry was loaded.
336
Not necessarily the last-changed revision for this file.
337
:param inv: Inventory from which the entry was loaded.
338
:param tree: RevisionTree for this entry.
340
if self.parent_id is not None:
294
if self.parent_id != None:
341
295
if not inv.has_id(self.parent_id):
342
296
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
343
297
% (self.parent_id, rev_id))
348
302
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
349
303
(self.kind, rev_id))
352
307
"""Clone this inventory entry."""
353
308
raise NotImplementedError
356
def describe_change(old_entry, new_entry):
357
"""Describe the change between old_entry and this.
359
This smells of being an InterInventoryEntry situation, but as its
360
the first one, we're making it a static method for now.
362
An entry with a different parent, or different name is considered
363
to be renamed. Reparenting is an internal detail.
364
Note that renaming the parent does not trigger a rename for the
367
# TODO: Perhaps return an object rather than just a string
368
if old_entry is new_entry:
369
# also the case of both being None
371
elif old_entry is None:
310
def _get_snapshot_change(self, previous_entries):
311
if len(previous_entries) > 1:
313
elif len(previous_entries) == 0:
373
elif new_entry is None:
375
text_modified, meta_modified = new_entry.detect_changes(old_entry)
376
if text_modified or meta_modified:
380
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
381
if old_entry.parent_id != new_entry.parent_id:
383
elif old_entry.name != new_entry.name:
387
if renamed and not modified:
388
return InventoryEntry.RENAMED
389
if modified and not renamed:
391
if modified and renamed:
392
return InventoryEntry.MODIFIED_AND_RENAMED
316
return 'modified/renamed/reparented'
395
318
def __repr__(self):
396
319
return ("%s(%r, %r, parent_id=%r)"
417
338
mutter("found unchanged entry")
418
339
self.revision = parent_ie.revision
419
340
return "unchanged"
420
return self._snapshot_into_revision(revision, previous_entries,
421
work_tree, commit_builder)
423
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
425
"""Record this revision unconditionally into a store.
427
The entry's last-changed revision property (`revision`) is updated to
428
that of the new revision.
430
:param revision: id of the new revision that is being recorded.
432
:returns: String description of the commit (e.g. "merged", "modified"), etc.
434
mutter('new revision {%s} for {%s}', revision, self.file_id)
341
return self.snapshot_revision(revision, previous_entries,
342
work_tree, weave_store, transaction)
344
def snapshot_revision(self, revision, previous_entries, work_tree,
345
weave_store, transaction):
346
"""Record this revision unconditionally."""
347
mutter('new revision for {%s}', self.file_id)
435
348
self.revision = revision
436
self._snapshot_text(previous_entries, work_tree, commit_builder)
349
change = self._get_snapshot_change(previous_entries)
350
self._snapshot_text(previous_entries, work_tree, weave_store,
438
def _snapshot_text(self, file_parents, work_tree, commit_builder):
354
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
439
355
"""Record the 'text' of this entry, whatever form that takes.
441
357
This default implementation simply adds an empty text.
443
raise NotImplementedError(self._snapshot_text)
359
mutter('storing file {%s} in revision {%s}',
360
self.file_id, self.revision)
361
self._add_text_to_weave([], file_parents, weave_store, transaction)
445
363
def __eq__(self, other):
446
364
if not isinstance(other, InventoryEntry):
489
407
# first requested, or preload them if they're already known
490
408
pass # nothing to do by default
492
def _forget_tree_state(self):
496
411
class RootEntry(InventoryEntry):
498
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
499
'text_id', 'parent_id', 'children', 'executable',
500
'revision', 'symlink_target']
502
413
def _check(self, checker, rev_id, tree):
503
414
"""See InventoryEntry._check"""
520
431
class InventoryDirectory(InventoryEntry):
521
432
"""A directory in an inventory."""
523
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
524
'text_id', 'parent_id', 'children', 'executable',
525
'revision', 'symlink_target']
527
434
def _check(self, checker, rev_id, tree):
528
435
"""See InventoryEntry._check"""
529
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
436
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
530
437
raise BzrCheckError('directory {%s} has text in revision {%s}'
531
438
% (self.file_id, rev_id))
559
466
"""See InventoryEntry._put_on_disk."""
560
467
os.mkdir(fullpath)
562
def _snapshot_text(self, file_parents, work_tree, commit_builder):
563
"""See InventoryEntry._snapshot_text."""
564
commit_builder.modified_directory(self.file_id, file_parents)
567
470
class InventoryFile(InventoryEntry):
568
471
"""A file in an inventory."""
570
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
571
'text_id', 'parent_id', 'children', 'executable',
572
'revision', 'symlink_target']
574
def _check(self, checker, tree_revision_id, tree):
473
def _check(self, checker, rev_id, tree):
575
474
"""See InventoryEntry._check"""
576
t = (self.file_id, self.revision)
475
revision = self.revision
476
t = (self.file_id, revision)
577
477
if t in checker.checked_texts:
578
prev_sha = checker.checked_texts[t]
478
prev_sha = checker.checked_texts[t]
579
479
if prev_sha != self.text_sha1:
580
480
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
581
(self.file_id, tree_revision_id))
481
(self.file_id, rev_id))
583
483
checker.repeated_text_cnt += 1
593
493
checker.checked_weaves[self.file_id] = True
595
w = tree.get_weave(self.file_id)
495
w = tree.get_weave_prelude(self.file_id)
597
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
598
checker.checked_text_cnt += 1
497
mutter('check version {%s} of {%s}', rev_id, self.file_id)
498
checker.checked_text_cnt += 1
599
499
# We can't check the length, because Weave doesn't store that
600
500
# information, and the whole point of looking at the weave's
601
501
# sha1sum is that we don't have to extract the text.
624
524
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
625
525
output_to, reverse=False):
626
526
"""See InventoryEntry._diff."""
628
from_text = tree.get_file(self.file_id).readlines()
630
to_text = to_tree.get_file(to_entry.file_id).readlines()
634
text_diff(from_label, from_text,
635
to_label, to_text, output_to)
637
text_diff(to_label, to_text,
638
from_label, from_text, output_to)
641
label_pair = (to_label, from_label)
643
label_pair = (from_label, to_label)
644
print >> output_to, "Binary files %s and %s differ" % label_pair
527
from_text = tree.get_file(self.file_id).readlines()
529
to_text = to_tree.get_file(to_entry.file_id).readlines()
533
text_diff(from_label, from_text,
534
to_label, to_text, output_to)
536
text_diff(to_label, to_text,
537
from_label, from_text, output_to)
646
539
def has_text(self):
647
540
"""See InventoryEntry.has_text."""
675
568
def _read_tree_state(self, path, work_tree):
676
569
"""See InventoryEntry._read_tree_state."""
677
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
678
# FIXME: 20050930 probe for the text size when getting sha1
679
# in _read_tree_state
680
self.executable = work_tree.is_executable(self.file_id, path=path)
682
def _forget_tree_state(self):
683
self.text_sha1 = None
684
self.executable = None
686
def _snapshot_text(self, file_parents, work_tree, commit_builder):
570
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
571
self.executable = work_tree.is_executable(self.file_id)
573
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
687
574
"""See InventoryEntry._snapshot_text."""
688
def get_content_byte_lines():
689
return work_tree.get_file(self.file_id).readlines()
690
self.text_sha1, self.text_size = commit_builder.modified_file_text(
691
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
575
mutter('storing file {%s} in revision {%s}',
576
self.file_id, self.revision)
577
# special case to avoid diffing on renames or
579
if (len(file_parents) == 1
580
and self.text_sha1 == file_parents.values()[0].text_sha1
581
and self.text_size == file_parents.values()[0].text_size):
582
previous_ie = file_parents.values()[0]
583
weave_store.add_identical_text(
584
self.file_id, previous_ie.revision,
585
self.revision, file_parents, transaction)
587
new_lines = work_tree.get_file(self.file_id).readlines()
588
self._add_text_to_weave(new_lines, file_parents, weave_store,
590
self.text_sha1 = sha_strings(new_lines)
591
self.text_size = sum(map(len, new_lines))
693
594
def _unchanged(self, previous_ie):
694
595
"""See InventoryEntry._unchanged."""
707
608
class InventoryLink(InventoryEntry):
708
609
"""A file in an inventory."""
710
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
711
'text_id', 'parent_id', 'children', 'executable',
712
'revision', 'symlink_target']
611
__slots__ = ['symlink_target']
714
613
def _check(self, checker, rev_id, tree):
715
614
"""See InventoryEntry._check"""
716
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
615
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
717
616
raise BzrCheckError('symlink {%s} has text in revision {%s}'
718
617
% (self.file_id, rev_id))
719
if self.symlink_target is None:
618
if self.symlink_target == None:
720
619
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
721
620
% (self.file_id, rev_id))
829
720
May also look up by name:
831
722
>>> [x[0] for x in inv.iter_entries()]
833
724
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
834
725
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
835
726
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
837
def __init__(self, root_id=ROOT_ID, revision_id=None):
728
def __init__(self, root_id=ROOT_ID):
838
729
"""Create or read an inventory.
840
731
If a working directory is specified, the inventory is read
863
753
other.add(entry.copy())
866
757
def __iter__(self):
867
758
return iter(self._byid)
869
761
def __len__(self):
870
762
"""Returns number of entries."""
871
763
return len(self._byid)
873
766
def iter_entries(self, from_dir=None):
874
767
"""Return (path, entry) pairs, in order by name."""
878
elif isinstance(from_dir, basestring):
879
from_dir = self._byid[from_dir]
881
# unrolling the recursive called changed the time from
882
# 440ms/663ms (inline/total) to 116ms/116ms
883
children = from_dir.children.items()
885
children = collections.deque(children)
886
stack = [(u'', children)]
888
from_dir_relpath, children = stack[-1]
891
name, ie = children.popleft()
893
# we know that from_dir_relpath never ends in a slash
894
# and 'f' doesn't begin with one, we can do a string op, rather
895
# than the checks of pathjoin(), though this means that all paths
897
path = from_dir_relpath + '/' + name
901
if ie.kind != 'directory':
904
# But do this child first
905
new_children = ie.children.items()
907
new_children = collections.deque(new_children)
908
stack.append((path, new_children))
909
# Break out of inner loop, so that we start outer loop with child
912
# if we finished all children, pop it off the stack
915
def iter_entries_by_dir(self, from_dir=None):
916
"""Iterate over the entries in a directory first order.
918
This returns all entries for a directory before returning
919
the entries for children of a directory. This is not
920
lexicographically sorted order, and is a hybrid between
921
depth-first and breadth-first.
923
:return: This yields (path, entry) pairs
925
# TODO? Perhaps this should return the from_dir so that the root is
926
# yielded? or maybe an option?
930
elif isinstance(from_dir, basestring):
931
from_dir = self._byid[from_dir]
933
stack = [(u'', from_dir)]
935
cur_relpath, cur_dir = stack.pop()
938
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
940
child_relpath = cur_relpath + child_name
942
yield child_relpath, child_ie
944
if child_ie.kind == 'directory':
945
child_dirs.append((child_relpath+'/', child_ie))
946
stack.extend(reversed(child_dirs))
771
elif isinstance(from_dir, basestring):
772
from_dir = self._byid[from_dir]
774
kids = from_dir.children.items()
776
for name, ie in kids:
778
if ie.kind == 'directory':
779
for cn, cie in self.iter_entries(from_dir=ie.file_id):
780
yield pathjoin(name, cn), cie
948
783
def entries(self):
949
784
"""Return list of (path, ie) for all entries except the root.
1042
883
parent.children[entry.name] = entry
1045
def add_path(self, relpath, kind, file_id=None, parent_id=None):
887
def add_path(self, relpath, kind, file_id=None):
1046
888
"""Add entry from a path.
1048
890
The immediate parent must already be versioned.
1050
892
Returns the new entry object."""
893
from bzrlib.workingtree import gen_file_id
1052
895
parts = bzrlib.osutils.splitpath(relpath)
1054
896
if len(parts) == 0:
1056
file_id = bzrlib.workingtree.gen_root_id()
1057
self.root = RootEntry(file_id)
1058
self._byid = {self.root.file_id: self.root}
897
raise BzrError("cannot re-add root of inventory")
900
file_id = gen_file_id(relpath)
902
parent_path = parts[:-1]
903
parent_id = self.path2id(parent_path)
904
if parent_id == None:
905
raise NotVersionedError(path=parent_path)
906
if kind == 'directory':
907
ie = InventoryDirectory(file_id, parts[-1], parent_id)
909
ie = InventoryFile(file_id, parts[-1], parent_id)
910
elif kind == 'symlink':
911
ie = InventoryLink(file_id, parts[-1], parent_id)
1061
parent_path = parts[:-1]
1062
parent_id = self.path2id(parent_path)
1063
if parent_id is None:
1064
raise NotVersionedError(path=parent_path)
1065
ie = make_entry(kind, parts[-1], parent_id, file_id)
913
raise BzrError("unknown kind %r" % kind)
1066
914
return self.add(ie)
1068
917
def __delitem__(self, file_id):
1069
918
"""Remove entry by id.
1219
1080
file_ie.parent_id = new_parent_id
1222
def make_entry(kind, name, parent_id, file_id=None):
1223
"""Create an inventory entry.
1225
:param kind: the type of inventory entry to create.
1226
:param name: the basename of the entry.
1227
:param parent_id: the parent_id of the entry.
1228
:param file_id: the file_id to use. if None, one will be created.
1231
file_id = bzrlib.workingtree.gen_file_id(name)
1232
if kind == 'directory':
1233
return InventoryDirectory(file_id, name, parent_id)
1234
elif kind == 'file':
1235
return InventoryFile(file_id, name, parent_id)
1236
elif kind == 'symlink':
1237
return InventoryLink(file_id, name, parent_id)
1239
raise BzrError("unknown kind %r" % kind)
1243
1085
_NAME_RE = None
1245
1087
def is_valid_name(name):
1246
1088
global _NAME_RE
1247
if _NAME_RE is None:
1089
if _NAME_RE == None:
1248
1090
_NAME_RE = re.compile(r'^[^/\\]+$')
1250
1092
return bool(_NAME_RE.match(name))