79
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
80
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> for j in i.iter_entries():
82
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
83
>>> for ix, j in enumerate(i.iter_entries()):
84
... print (j[0] == shouldbe[ix], j[1])
85
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
86
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
(True, InventoryFile('2323', 'hello.c', parent_id='123'))
87
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
88
89
Traceback (most recent call last):
120
121
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
121
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
122
versionedfile = weave_store.get_weave_or_empty(self.file_id,
124
versionedfile.add_lines(self.revision, parents, new_lines)
125
versionedfile.clear_cache()
124
127
def detect_changes(self, old_entry):
125
128
"""Return a (text_modified, meta_modified) from this to old_entry.
158
164
This is a map containing the file revisions in all parents
159
165
for which the file exists, and its revision is not a parent of
160
166
any other. If the file is new, the set will be empty.
168
:param versioned_file_store: A store where ancestry data on this
169
file id can be queried.
170
:param transaction: The transaction that queries to the versioned
171
file store should be completed under.
172
:param entry_vf: The entry versioned file, if its already available.
162
174
def get_ancestors(weave, entry):
163
return set(map(weave.idx_to_name,
164
weave.inclusions([weave.lookup(entry.revision)])))
175
return set(weave.get_ancestry(entry.revision))
176
# revision:ie mapping for each ie found in previous_inventories.
178
# revision:ie mapping with one revision for each head.
180
# revision: ancestor list for each head
166
181
head_ancestors = {}
182
# identify candidate head revision ids.
167
183
for inv in previous_inventories:
168
184
if self.file_id in inv:
169
185
ie = inv[self.file_id]
170
186
assert ie.file_id == self.file_id
171
if ie.revision in heads:
172
# fixup logic, there was a bug in revision updates.
173
# with x bit support.
187
if ie.revision in candidates:
188
# same revision value in two different inventories:
189
# correct possible inconsistencies:
190
# * there was a bug in revision updates with 'x' bit
175
if heads[ie.revision].executable != ie.executable:
176
heads[ie.revision].executable = False
193
if candidates[ie.revision].executable != ie.executable:
194
candidates[ie.revision].executable = False
177
195
ie.executable = False
178
196
except AttributeError:
180
assert heads[ie.revision] == ie
198
# must now be the same.
199
assert candidates[ie.revision] == ie
182
# may want to add it.
183
# may already be covered:
184
already_present = 0 != len(
185
[head for head in heads
186
if ie.revision in head_ancestors[head]])
188
# an ancestor of a known head.
191
ancestors = get_ancestors(entry_weave, ie)
192
# may knock something else out:
193
check_heads = list(heads.keys())
194
for head in check_heads:
195
if head in ancestors:
196
# this head is not really a head
198
head_ancestors[ie.revision] = ancestors
199
heads[ie.revision] = ie
201
# add this revision as a candidate.
202
candidates[ie.revision] = ie
204
# common case optimisation
205
if len(candidates) == 1:
206
# if there is only one candidate revision found
207
# then we can opening the versioned file to access ancestry:
208
# there cannot be any ancestors to eliminate when there is
209
# only one revision available.
210
heads[ie.revision] = ie
213
# eliminate ancestors amongst the available candidates:
214
# heads are those that are not an ancestor of any other candidate
215
# - this provides convergence at a per-file level.
216
for ie in candidates.values():
217
# may be an ancestor of a known head:
218
already_present = 0 != len(
219
[head for head in heads
220
if ie.revision in head_ancestors[head]])
222
# an ancestor of an analyzed candidate.
224
# not an ancestor of a known head:
225
# load the versioned file for this file id if needed
227
entry_vf = versioned_file_store.get_weave_or_empty(
228
self.file_id, transaction)
229
ancestors = get_ancestors(entry_vf, ie)
230
# may knock something else out:
231
check_heads = list(heads.keys())
232
for head in check_heads:
233
if head in ancestors:
234
# this previously discovered 'head' is not
235
# really a head - its an ancestor of the newly
238
head_ancestors[ie.revision] = ancestors
239
heads[ie.revision] = ie
202
242
def get_tar_item(self, root, dp, now, tree):
203
243
"""Get a tarfile item and a file stream for its content."""
204
item = tarfile.TarInfo(os.path.join(root, dp))
244
item = tarfile.TarInfo(pathjoin(root, dp))
205
245
# TODO: would be cool to actually set it to the timestamp of the
206
246
# revision it was last changed
267
307
This is a template method - implement _put_on_disk in subclasses.
269
fullpath = appendpath(dest, dp)
309
fullpath = pathjoin(dest, dp)
270
310
self._put_on_disk(fullpath, tree)
271
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
311
mutter(" export {%s} kind %s to %s", self.file_id,
273
314
def _put_on_disk(self, fullpath, tree):
274
315
"""Put this entry onto disk at fullpath, from tree tree."""
300
349
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
301
350
(self.kind, rev_id))
305
353
"""Clone this inventory entry."""
306
354
raise NotImplementedError
308
def _get_snapshot_change(self, previous_entries):
356
def _describe_snapshot_change(self, previous_entries):
357
"""Describe how this entry will have changed in a new commit.
359
:param previous_entries: Dictionary from revision_id to inventory entry.
361
:returns: One-word description: "merged", "added", "renamed", "modified".
363
# XXX: This assumes that the file *has* changed -- it should probably
364
# be fused with whatever does that detection. Why not just a single
365
# thing to compare the entries?
367
# TODO: Return some kind of object describing all the possible
368
# dimensions that can change, not just a string. That can then give
369
# both old and new names for renames, etc.
309
371
if len(previous_entries) > 1:
311
373
elif len(previous_entries) == 0:
314
return 'modified/renamed/reparented'
375
the_parent, = previous_entries.values()
376
if self.parent_id != the_parent.parent_id:
377
# actually, moved to another directory
379
elif self.name != the_parent.name:
316
383
def __repr__(self):
317
384
return ("%s(%r, %r, parent_id=%r)"
336
403
mutter("found unchanged entry")
337
404
self.revision = parent_ie.revision
338
405
return "unchanged"
339
return self.snapshot_revision(revision, previous_entries,
340
work_tree, weave_store, transaction)
342
def snapshot_revision(self, revision, previous_entries, work_tree,
343
weave_store, transaction):
344
"""Record this revision unconditionally."""
345
mutter('new revision for {%s}', self.file_id)
406
return self._snapshot_into_revision(revision, previous_entries,
407
work_tree, weave_store, transaction)
409
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
410
weave_store, transaction):
411
"""Record this revision unconditionally into a store.
413
The entry's last-changed revision property (`revision`) is updated to
414
that of the new revision.
416
:param revision: id of the new revision that is being recorded.
418
:returns: String description of the commit (e.g. "merged", "modified"), etc.
420
mutter('new revision {%s} for {%s}', revision, self.file_id)
346
421
self.revision = revision
347
change = self._get_snapshot_change(previous_entries)
422
change = self._describe_snapshot_change(previous_entries)
348
423
self._snapshot_text(previous_entries, work_tree, weave_store,
468
546
class InventoryFile(InventoryEntry):
469
547
"""A file in an inventory."""
471
def _check(self, checker, rev_id, tree):
549
def _check(self, checker, tree_revision_id, tree):
472
550
"""See InventoryEntry._check"""
473
revision = self.revision
474
t = (self.file_id, revision)
551
t = (self.file_id, self.revision)
475
552
if t in checker.checked_texts:
476
prev_sha = checker.checked_texts[t]
553
prev_sha = checker.checked_texts[t]
477
554
if prev_sha != self.text_sha1:
478
555
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
479
(self.file_id, rev_id))
556
(self.file_id, tree_revision_id))
481
558
checker.repeated_text_cnt += 1
483
mutter('check version {%s} of {%s}', rev_id, self.file_id)
484
file_lines = tree.get_file_lines(self.file_id)
485
checker.checked_text_cnt += 1
486
if self.text_size != sum(map(len, file_lines)):
487
raise BzrCheckError('text {%s} wrong size' % self.text_id)
488
if self.text_sha1 != sha_strings(file_lines):
489
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
561
if self.file_id not in checker.checked_weaves:
562
mutter('check weave {%s}', self.file_id)
563
w = tree.get_weave(self.file_id)
564
# Not passing a progress bar, because it creates a new
565
# progress, which overwrites the current progress,
566
# and doesn't look nice
568
checker.checked_weaves[self.file_id] = True
570
w = tree.get_weave(self.file_id)
572
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
573
checker.checked_text_cnt += 1
574
# We can't check the length, because Weave doesn't store that
575
# information, and the whole point of looking at the weave's
576
# sha1sum is that we don't have to extract the text.
577
if self.text_sha1 != w.get_sha1(self.revision):
578
raise BzrCheckError('text {%s} version {%s} wrong sha1'
579
% (self.file_id, self.revision))
490
580
checker.checked_texts[t] = self.text_sha1
509
599
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
510
600
output_to, reverse=False):
511
601
"""See InventoryEntry._diff."""
512
from_text = tree.get_file(self.file_id).readlines()
514
to_text = to_tree.get_file(to_entry.file_id).readlines()
518
text_diff(from_label, from_text,
519
to_label, to_text, output_to)
521
text_diff(to_label, to_text,
522
from_label, from_text, output_to)
603
from_text = tree.get_file(self.file_id).readlines()
605
to_text = to_tree.get_file(to_entry.file_id).readlines()
609
text_diff(from_label, from_text,
610
to_label, to_text, output_to)
612
text_diff(to_label, to_text,
613
from_label, from_text, output_to)
616
label_pair = (to_label, from_label)
618
label_pair = (from_label, to_label)
619
print >> output_to, "Binary files %s and %s differ" % label_pair
524
621
def has_text(self):
525
622
"""See InventoryEntry.has_text."""
565
666
and self.text_sha1 == file_parents.values()[0].text_sha1
566
667
and self.text_size == file_parents.values()[0].text_size):
567
668
previous_ie = file_parents.values()[0]
568
weave_store.add_identical_text(
569
self.file_id, previous_ie.revision,
570
self.revision, file_parents, transaction)
669
versionedfile = weave_store.get_weave(self.file_id, transaction)
670
versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
572
672
new_lines = work_tree.get_file(self.file_id).readlines()
573
self._add_text_to_weave(new_lines, file_parents, weave_store,
673
self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
575
675
self.text_sha1 = sha_strings(new_lines)
576
676
self.text_size = sum(map(len, new_lines))
720
823
The inventory is created with a default root directory, with
723
# We are letting Branch.initialize() create a unique inventory
826
# We are letting Branch.create() create a unique inventory
724
827
# root id. Rather than generating a random one here.
725
828
#if root_id is None:
726
829
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
727
830
self.root = RootEntry(root_id)
831
self.revision_id = revision_id
728
832
self._byid = {self.root.file_id: self.root}
836
# TODO: jam 20051218 Should copy also copy the revision_id?
732
837
other = Inventory(self.root.file_id)
733
838
# copy recursively so we know directories will be added before
734
839
# their children. There are more efficient ways than this...
875
980
The immediate parent must already be versioned.
877
982
Returns the new entry object."""
878
from bzrlib.branch import gen_file_id
983
from bzrlib.workingtree import gen_file_id
880
985
parts = bzrlib.osutils.splitpath(relpath)
882
raise BzrError("cannot re-add root of inventory")
884
987
if file_id == None:
885
988
file_id = gen_file_id(relpath)
887
parent_path = parts[:-1]
888
parent_id = self.path2id(parent_path)
889
if parent_id == None:
890
raise NotVersionedError(path=parent_path)
991
self.root = RootEntry(file_id)
992
self._byid = {self.root.file_id: self.root}
995
parent_path = parts[:-1]
996
parent_id = self.path2id(parent_path)
997
if parent_id == None:
998
raise NotVersionedError(path=parent_path)
891
999
if kind == 'directory':
892
1000
ie = InventoryDirectory(file_id, parts[-1], parent_id)
893
1001
elif kind == 'file':
969
1081
root directory as depth 1.
972
while file_id != None:
974
ie = self._byid[file_id]
976
raise BzrError("file_id {%s} not found in inventory" % file_id)
977
p.insert(0, ie.file_id)
978
file_id = ie.parent_id
1084
for parent in self._iter_file_id_parents(file_id):
1085
p.insert(0, parent.file_id)
982
1088
def id2path(self, file_id):
983
"""Return as a list the path to file_id.
1089
"""Return as a string the path to file_id.
985
1091
>>> i = Inventory()
986
1092
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
987
1093
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
988
>>> print i.id2path('foo-id').replace(os.sep, '/')
1094
>>> print i.id2path('foo-id')
991
1097
# get all names, skipping root
992
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
return os.sep.join(p)
1098
return '/'.join(reversed(
1099
[parent.name for parent in
1100
self._iter_file_id_parents(file_id)][:-1]))
997
1102
def path2id(self, name):
998
1103
"""Walk down through directories to return entry of last component.