1
# (C) 2005 Canonical Ltd
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
# FIXME: This refactoring of the workingtree code doesn't seem to keep
18
# the WorkingTree's copy of the inventory in sync with the branch. The
19
# branch modifies its working inventory when it does a commit to make
20
# missing files permanently removed.
18
22
# TODO: Maybe also keep the full path of the entry, and the children?
19
23
# But those depend on its position within a particular inventory, and
76
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
77
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
81
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> 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])
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('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'))
84
88
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
89
Traceback (most recent call last):
108
112
src/wibble/wibble.c
109
>>> i.id2path('2326').replace('\\\\', '/')
113
>>> i.id2path('2326')
110
114
'src/wibble/wibble.c'
117
# Constants returned by describe_change()
119
# TODO: These should probably move to some kind of FileChangeDescription
120
# class; that's like what's inside a TreeDelta but we want to be able to
121
# generate them just for one file at a time.
123
MODIFIED_AND_RENAMED = 'modified and renamed'
113
125
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
126
'text_id', 'parent_id', 'children', 'executable',
117
def _add_text_to_weave(self, new_lines, parents, weave_store):
118
weave_store.add_text(self.file_id, self.revision, new_lines, parents)
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()
120
135
def detect_changes(self, old_entry):
121
136
"""Return a (text_modified, meta_modified) from this to old_entry.
154
172
This is a map containing the file revisions in all parents
155
173
for which the file exists, and its revision is not a parent of
156
174
any other. If the file is new, the set will be empty.
176
:param versioned_file_store: A store where ancestry data on this
177
file id can be queried.
178
:param transaction: The transaction that queries to the versioned
179
file store should be completed under.
180
:param entry_vf: The entry versioned file, if its already available.
158
182
def get_ancestors(weave, entry):
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
183
return set(weave.get_ancestry(entry.revision))
184
# revision:ie mapping for each ie found in previous_inventories.
186
# revision:ie mapping with one revision for each head.
188
# revision: ancestor list for each head
162
189
head_ancestors = {}
190
# identify candidate head revision ids.
163
191
for inv in previous_inventories:
164
192
if self.file_id in inv:
165
193
ie = inv[self.file_id]
166
194
assert ie.file_id == self.file_id
167
if ie.revision in heads:
168
assert heads[ie.revision] == ie
195
if ie.revision in candidates:
196
# same revision value in two different inventories:
197
# correct possible inconsistencies:
198
# * there was a bug in revision updates with 'x' bit
201
if candidates[ie.revision].executable != ie.executable:
202
candidates[ie.revision].executable = False
203
ie.executable = False
204
except AttributeError:
206
# must now be the same.
207
assert candidates[ie.revision] == ie
170
# may want to add it.
171
# may already be covered:
172
already_present = 0 != len(
173
[head for head in heads
174
if ie.revision in head_ancestors[head]])
176
# an ancestor of a known head.
179
ancestors = get_ancestors(entry_weave, ie)
180
# may knock something else out:
181
check_heads = list(heads.keys())
182
for head in check_heads:
183
if head in ancestors:
184
# this head is not really a head
186
head_ancestors[ie.revision] = ancestors
187
heads[ie.revision] = ie
209
# add this revision as a candidate.
210
candidates[ie.revision] = ie
212
# common case optimisation
213
if len(candidates) == 1:
214
# if there is only one candidate revision found
215
# then we can opening the versioned file to access ancestry:
216
# there cannot be any ancestors to eliminate when there is
217
# only one revision available.
218
heads[ie.revision] = ie
221
# eliminate ancestors amongst the available candidates:
222
# heads are those that are not an ancestor of any other candidate
223
# - this provides convergence at a per-file level.
224
for ie in candidates.values():
225
# may be an ancestor of a known head:
226
already_present = 0 != len(
227
[head for head in heads
228
if ie.revision in head_ancestors[head]])
230
# an ancestor of an analyzed candidate.
232
# not an ancestor of a known head:
233
# load the versioned file for this file id if needed
235
entry_vf = versioned_file_store.get_weave_or_empty(
236
self.file_id, transaction)
237
ancestors = get_ancestors(entry_vf, ie)
238
# may knock something else out:
239
check_heads = list(heads.keys())
240
for head in check_heads:
241
if head in ancestors:
242
# this previously discovered 'head' is not
243
# really a head - its an ancestor of the newly
246
head_ancestors[ie.revision] = ancestors
247
heads[ie.revision] = ie
190
250
def get_tar_item(self, root, dp, now, tree):
191
251
"""Get a tarfile item and a file stream for its content."""
192
item = tarfile.TarInfo(os.path.join(root, dp))
252
item = tarfile.TarInfo(pathjoin(root, dp))
193
253
# TODO: would be cool to actually set it to the timestamp of the
194
254
# revision it was last changed
256
315
This is a template method - implement _put_on_disk in subclasses.
258
fullpath = appendpath(dest, dp)
317
fullpath = pathjoin(dest, dp)
259
318
self._put_on_disk(fullpath, tree)
260
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
319
mutter(" export {%s} kind %s to %s", self.file_id,
262
322
def _put_on_disk(self, fullpath, tree):
263
323
"""Put this entry onto disk at fullpath, from tree tree."""
289
357
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
290
358
(self.kind, rev_id))
294
361
"""Clone this inventory entry."""
295
362
raise NotImplementedError
297
def _get_snapshot_change(self, previous_entries):
298
if len(previous_entries) > 1:
300
elif len(previous_entries) == 0:
365
def describe_change(old_entry, new_entry):
366
"""Describe the change between old_entry and this.
368
This smells of being an InterInventoryEntry situation, but as its
369
the first one, we're making it a static method for now.
371
An entry with a different parent, or different name is considered
372
to be renamed. Reparenting is an internal detail.
373
Note that renaming the parent does not trigger a rename for the
376
# TODO: Perhaps return an object rather than just a string
377
if old_entry is new_entry:
378
# also the case of both being None
380
elif old_entry is None:
303
return 'modified/renamed/reparented'
382
elif new_entry is None:
384
text_modified, meta_modified = new_entry.detect_changes(old_entry)
385
if text_modified or meta_modified:
389
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
390
if old_entry.parent_id != new_entry.parent_id:
392
elif old_entry.name != new_entry.name:
396
if renamed and not modified:
397
return InventoryEntry.RENAMED
398
if modified and not renamed:
400
if modified and renamed:
401
return InventoryEntry.MODIFIED_AND_RENAMED
305
404
def __repr__(self):
306
405
return ("%s(%r, %r, parent_id=%r)"
325
424
mutter("found unchanged entry")
326
425
self.revision = parent_ie.revision
327
426
return "unchanged"
328
return self.snapshot_revision(revision, previous_entries,
329
work_tree, weave_store)
331
def snapshot_revision(self, revision, previous_entries, work_tree,
333
"""Record this revision unconditionally."""
334
mutter('new revision for {%s}', self.file_id)
427
return self._snapshot_into_revision(revision, previous_entries,
428
work_tree, weave_store, transaction)
430
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
431
weave_store, transaction):
432
"""Record this revision unconditionally into a store.
434
The entry's last-changed revision property (`revision`) is updated to
435
that of the new revision.
437
:param revision: id of the new revision that is being recorded.
439
:returns: String description of the commit (e.g. "merged", "modified"), etc.
441
mutter('new revision {%s} for {%s}', revision, self.file_id)
335
442
self.revision = revision
336
change = self._get_snapshot_change(previous_entries)
337
self._snapshot_text(previous_entries, work_tree, weave_store)
443
self._snapshot_text(previous_entries, work_tree, weave_store,
340
def _snapshot_text(self, file_parents, work_tree, weave_store):
446
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
341
447
"""Record the 'text' of this entry, whatever form that takes.
343
449
This default implementation simply adds an empty text.
345
451
mutter('storing file {%s} in revision {%s}',
346
452
self.file_id, self.revision)
347
self._add_text_to_weave([], file_parents, weave_store)
453
self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
349
455
def __eq__(self, other):
350
456
if not isinstance(other, InventoryEntry):
452
565
class InventoryFile(InventoryEntry):
453
566
"""A file in an inventory."""
455
def _check(self, checker, rev_id, tree):
568
def _check(self, checker, tree_revision_id, tree):
456
569
"""See InventoryEntry._check"""
457
revision = self.revision
458
t = (self.file_id, revision)
570
t = (self.file_id, self.revision)
459
571
if t in checker.checked_texts:
460
prev_sha = checker.checked_texts[t]
572
prev_sha = checker.checked_texts[t]
461
573
if prev_sha != self.text_sha1:
462
574
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
463
(self.file_id, rev_id))
575
(self.file_id, tree_revision_id))
465
577
checker.repeated_text_cnt += 1
467
mutter('check version {%s} of {%s}', rev_id, self.file_id)
468
file_lines = tree.get_file_lines(self.file_id)
469
checker.checked_text_cnt += 1
470
if self.text_size != sum(map(len, file_lines)):
471
raise BzrCheckError('text {%s} wrong size' % self.text_id)
472
if self.text_sha1 != sha_strings(file_lines):
473
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
580
if self.file_id not in checker.checked_weaves:
581
mutter('check weave {%s}', self.file_id)
582
w = tree.get_weave(self.file_id)
583
# Not passing a progress bar, because it creates a new
584
# progress, which overwrites the current progress,
585
# and doesn't look nice
587
checker.checked_weaves[self.file_id] = True
589
w = tree.get_weave(self.file_id)
591
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
592
checker.checked_text_cnt += 1
593
# We can't check the length, because Weave doesn't store that
594
# information, and the whole point of looking at the weave's
595
# sha1sum is that we don't have to extract the text.
596
if self.text_sha1 != w.get_sha1(self.revision):
597
raise BzrCheckError('text {%s} version {%s} wrong sha1'
598
% (self.file_id, self.revision))
474
599
checker.checked_texts[t] = self.text_sha1
493
618
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
494
619
output_to, reverse=False):
495
620
"""See InventoryEntry._diff."""
496
from_text = tree.get_file(self.file_id).readlines()
498
to_text = to_tree.get_file(to_entry.file_id).readlines()
502
text_diff(from_label, from_text,
503
to_label, to_text, output_to)
505
text_diff(to_label, to_text,
506
from_label, from_text, output_to)
622
from_text = tree.get_file(self.file_id).readlines()
624
to_text = to_tree.get_file(to_entry.file_id).readlines()
628
text_diff(from_label, from_text,
629
to_label, to_text, output_to)
631
text_diff(to_label, to_text,
632
from_label, from_text, output_to)
635
label_pair = (to_label, from_label)
637
label_pair = (from_label, to_label)
638
print >> output_to, "Binary files %s and %s differ" % label_pair
508
640
def has_text(self):
509
641
"""See InventoryEntry.has_text."""
539
671
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
540
672
self.executable = work_tree.is_executable(self.file_id)
542
def _snapshot_text(self, file_parents, work_tree, weave_store):
674
def _forget_tree_state(self):
675
self.text_sha1 = None
676
self.executable = None
678
def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
543
679
"""See InventoryEntry._snapshot_text."""
544
mutter('storing file {%s} in revision {%s}',
545
self.file_id, self.revision)
680
mutter('storing text of file {%s} in revision {%s} into %r',
681
self.file_id, self.revision, versionedfile_store)
546
682
# special case to avoid diffing on renames or
548
684
if (len(file_parents) == 1
549
685
and self.text_sha1 == file_parents.values()[0].text_sha1
550
686
and self.text_size == file_parents.values()[0].text_size):
551
687
previous_ie = file_parents.values()[0]
552
weave_store.add_identical_text(
553
self.file_id, previous_ie.revision,
554
self.revision, file_parents)
688
versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
689
versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
556
691
new_lines = work_tree.get_file(self.file_id).readlines()
557
self._add_text_to_weave(new_lines, file_parents, weave_store)
692
self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
558
694
self.text_sha1 = sha_strings(new_lines)
559
695
self.text_size = sum(map(len, new_lines))
701
842
The inventory is created with a default root directory, with
704
# We are letting Branch.initialize() create a unique inventory
845
# We are letting Branch.create() create a unique inventory
705
846
# root id. Rather than generating a random one here.
706
847
#if root_id is None:
707
848
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
708
849
self.root = RootEntry(root_id)
850
self.revision_id = revision_id
709
851
self._byid = {self.root.file_id: self.root}
855
# TODO: jam 20051218 Should copy also copy the revision_id?
713
856
other = Inventory(self.root.file_id)
714
857
# copy recursively so we know directories will be added before
715
858
# their children. There are more efficient ways than this...
856
999
The immediate parent must already be versioned.
858
1001
Returns the new entry object."""
859
from bzrlib.branch import gen_file_id
1002
from bzrlib.workingtree import gen_file_id
861
1004
parts = bzrlib.osutils.splitpath(relpath)
863
raise BzrError("cannot re-add root of inventory")
865
1006
if file_id == None:
866
1007
file_id = gen_file_id(relpath)
868
parent_path = parts[:-1]
869
parent_id = self.path2id(parent_path)
870
if parent_id == None:
871
raise NotVersionedError(parent_path)
1010
self.root = RootEntry(file_id)
1011
self._byid = {self.root.file_id: self.root}
1014
parent_path = parts[:-1]
1015
parent_id = self.path2id(parent_path)
1016
if parent_id == None:
1017
raise NotVersionedError(path=parent_path)
873
1018
if kind == 'directory':
874
1019
ie = InventoryDirectory(file_id, parts[-1], parent_id)
875
1020
elif kind == 'file':
951
1100
root directory as depth 1.
954
while file_id != None:
956
ie = self._byid[file_id]
958
raise BzrError("file_id {%s} not found in inventory" % file_id)
959
p.insert(0, ie.file_id)
960
file_id = ie.parent_id
1103
for parent in self._iter_file_id_parents(file_id):
1104
p.insert(0, parent.file_id)
964
1107
def id2path(self, file_id):
965
"""Return as a list the path to file_id.
1108
"""Return as a string the path to file_id.
967
1110
>>> i = Inventory()
968
1111
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
969
1112
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
970
>>> print i.id2path('foo-id').replace(os.sep, '/')
1113
>>> print i.id2path('foo-id')
973
1116
# get all names, skipping root
974
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
975
return os.sep.join(p)
1117
return '/'.join(reversed(
1118
[parent.name for parent in
1119
self._iter_file_id_parents(file_id)][:-1]))
979
1121
def path2id(self, name):
980
1122
"""Walk down through directories to return entry of last component.