1
1
# (C) 2005 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
34
from bzrlib.errors import BzrError, BzrCheckError
36
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
37
39
appendpath, sha_strings)
38
40
from bzrlib.trace import mutter
39
from bzrlib.errors import NotVersionedError
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
BzrError, BzrCheckError)
42
45
class InventoryEntry(object):
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: os.path.join('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):
113
117
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
118
'text_id', 'parent_id', 'children', 'executable',
115
'revision', 'symlink_target']
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)
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,
120
125
def detect_changes(self, old_entry):
121
126
"""Return a (text_modified, meta_modified) from this to old_entry.
123
128
_read_tree_state must have been called on self and old_entry prior to
124
129
calling detect_changes.
126
if self.kind == 'file':
127
assert self.text_sha1 != None
128
assert old_entry.text_sha1 != None
129
text_modified = (self.text_sha1 != old_entry.text_sha1)
130
meta_modified = (self.executable != old_entry.executable)
131
elif self.kind == 'symlink':
132
# FIXME: which _modified field should we use ? RBC 20051003
133
text_modified = (self.symlink_target != old_entry.symlink_target)
135
mutter(" symlink target changed")
136
meta_modified = False
138
text_modified = False
139
meta_modified = False
140
return text_modified, meta_modified
142
133
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
143
134
output_to, reverse=False):
144
135
"""Perform a diff from this to to_entry.
146
137
text_diff will be used for textual difference calculation.
138
This is a template method, override _diff in child classes.
148
140
self._read_tree_state(tree.id2path(self.file_id), tree)
152
144
assert self.kind == to_entry.kind
153
145
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
155
if self.kind == 'file':
156
from_text = tree.get_file(self.file_id).readlines()
158
to_text = to_tree.get_file(to_entry.file_id).readlines()
162
text_diff(from_label, from_text,
163
to_label, to_text, output_to)
165
text_diff(to_label, to_text,
166
from_label, from_text, output_to)
167
elif self.kind == 'symlink':
168
from_text = self.symlink_target
169
if to_entry is not None:
170
to_text = to_entry.symlink_target
175
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
178
print >>output_to, '=== target was %r' % self.symlink_target
147
self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
150
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
151
output_to, reverse=False):
152
"""Perform a diff between two entries of the same kind."""
154
def find_previous_heads(self, previous_inventories, entry_weave):
155
"""Return the revisions and entries that directly preceed this.
157
Returned as a map from revision to inventory entry.
159
This is a map containing the file revisions in all parents
160
for which the file exists, and its revision is not a parent of
161
any other. If the file is new, the set will be empty.
163
def get_ancestors(weave, entry):
164
return set(map(weave.idx_to_name,
165
weave.inclusions([weave.lookup(entry.revision)])))
168
for inv in previous_inventories:
169
if self.file_id in inv:
170
ie = inv[self.file_id]
171
assert ie.file_id == self.file_id
172
if ie.revision in heads:
173
# fixup logic, there was a bug in revision updates.
174
# with x bit support.
176
if heads[ie.revision].executable != ie.executable:
177
heads[ie.revision].executable = False
178
ie.executable = False
179
except AttributeError:
181
assert heads[ie.revision] == ie
180
print >>output_to, '=== target is %r' % self.symlink_target
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
182
203
def get_tar_item(self, root, dp, now, tree):
183
204
"""Get a tarfile item and a file stream for its content."""
209
234
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
210
235
Traceback (most recent call last):
211
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
236
InvalidEntryName: Invalid entry name: src/hello.c
213
238
assert isinstance(name, basestring), name
214
239
if '/' in name or '\\' in name:
215
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
240
raise InvalidEntryName(name=name)
217
241
self.executable = False
218
242
self.revision = None
219
243
self.text_sha1 = None
248
270
fullpath = appendpath(dest, dp)
249
271
self._put_on_disk(fullpath, tree)
250
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
272
mutter(" export {%s} kind %s to %s", self.file_id,
252
275
def _put_on_disk(self, fullpath, tree):
253
276
"""Put this entry onto disk at fullpath, from tree tree."""
302
def snapshot(self, revision, path, previous_entries, work_tree,
304
"""Make a snapshot of this entry.
325
def snapshot(self, revision, path, previous_entries,
326
work_tree, weave_store, transaction):
327
"""Make a snapshot of this entry which may or may not have changed.
306
329
This means that all its fields are populated, that it has its
307
330
text stored in the text store or weave.
311
334
if len(previous_entries) == 1:
312
335
# cannot be unchanged unless there is only one parent file rev.
313
336
parent_ie = previous_entries.values()[0]
314
if self._unchanged(path, parent_ie, work_tree):
337
if self._unchanged(parent_ie):
315
338
mutter("found unchanged entry")
316
339
self.revision = parent_ie.revision
317
340
return "unchanged"
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."""
318
347
mutter('new revision for {%s}', self.file_id)
319
348
self.revision = revision
320
349
change = self._get_snapshot_change(previous_entries)
321
if self.kind != 'file':
323
self._snapshot_text(previous_entries, work_tree, weave_store)
350
self._snapshot_text(previous_entries, work_tree, weave_store,
326
def _snapshot_text(self, file_parents, work_tree, weave_store):
354
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
355
"""Record the 'text' of this entry, whatever form that takes.
357
This default implementation simply adds an empty text.
327
359
mutter('storing file {%s} in revision {%s}',
328
360
self.file_id, self.revision)
329
# special case to avoid diffing on renames or
331
if (len(file_parents) == 1
332
and self.text_sha1 == file_parents.values()[0].text_sha1
333
and self.text_size == file_parents.values()[0].text_size):
334
previous_ie = file_parents.values()[0]
335
weave_store.add_identical_text(
336
self.file_id, previous_ie.revision,
337
self.revision, file_parents)
339
new_lines = work_tree.get_file(self.file_id).readlines()
340
self._add_text_to_weave(new_lines, file_parents, weave_store)
341
self.text_sha1 = sha_strings(new_lines)
342
self.text_size = sum(map(len, new_lines))
361
self._add_text_to_weave([], file_parents, weave_store, transaction)
344
363
def __eq__(self, other):
345
364
if not isinstance(other, InventoryEntry):
372
395
elif previous_ie.name != self.name:
373
396
compatible = False
374
if self.kind == 'symlink':
375
if self.symlink_target != previous_ie.symlink_target:
377
if self.kind == 'file':
378
if self.text_sha1 != previous_ie.text_sha1:
381
# FIXME: 20050930 probe for the text size when getting sha1
382
# in _read_tree_state
383
self.text_size = previous_ie.text_size
384
397
return compatible
386
399
def _read_tree_state(self, path, work_tree):
387
if self.kind == 'symlink':
388
self.symlink_target = work_tree.get_symlink_target(self.file_id)
389
if self.kind == 'file':
390
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
391
self.executable = work_tree.is_executable(self.file_id)
400
"""Populate fields in the inventory entry from the given tree.
402
Note that this should be modified to be a noop on virtual trees
403
as all entries created there are prepopulated.
405
# TODO: Rather than running this manually, we should check the
406
# working sha1 and other expensive properties when they're
407
# first requested, or preload them if they're already known
408
pass # nothing to do by default
394
411
class RootEntry(InventoryEntry):
483
500
other.revision = self.revision
503
def detect_changes(self, old_entry):
504
"""See InventoryEntry.detect_changes."""
505
assert self.text_sha1 != None
506
assert old_entry.text_sha1 != None
507
text_modified = (self.text_sha1 != old_entry.text_sha1)
508
meta_modified = (self.executable != old_entry.executable)
509
return text_modified, meta_modified
511
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
512
output_to, reverse=False):
513
"""See InventoryEntry._diff."""
514
from_text = tree.get_file(self.file_id).readlines()
516
to_text = to_tree.get_file(to_entry.file_id).readlines()
520
text_diff(from_label, from_text,
521
to_label, to_text, output_to)
523
text_diff(to_label, to_text,
524
from_label, from_text, output_to)
486
526
def has_text(self):
487
527
"""See InventoryEntry.has_text."""
512
552
if tree.is_executable(self.file_id):
513
553
os.chmod(fullpath, 0755)
555
def _read_tree_state(self, path, work_tree):
556
"""See InventoryEntry._read_tree_state."""
557
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
558
self.executable = work_tree.is_executable(self.file_id)
560
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
561
"""See InventoryEntry._snapshot_text."""
562
mutter('storing file {%s} in revision {%s}',
563
self.file_id, self.revision)
564
# special case to avoid diffing on renames or
566
if (len(file_parents) == 1
567
and self.text_sha1 == file_parents.values()[0].text_sha1
568
and self.text_size == file_parents.values()[0].text_size):
569
previous_ie = file_parents.values()[0]
570
weave_store.add_identical_text(
571
self.file_id, previous_ie.revision,
572
self.revision, file_parents, transaction)
574
new_lines = work_tree.get_file(self.file_id).readlines()
575
self._add_text_to_weave(new_lines, file_parents, weave_store,
577
self.text_sha1 = sha_strings(new_lines)
578
self.text_size = sum(map(len, new_lines))
581
def _unchanged(self, previous_ie):
582
"""See InventoryEntry._unchanged."""
583
compatible = super(InventoryFile, self)._unchanged(previous_ie)
584
if self.text_sha1 != previous_ie.text_sha1:
587
# FIXME: 20050930 probe for the text size when getting sha1
588
# in _read_tree_state
589
self.text_size = previous_ie.text_size
590
if self.executable != previous_ie.executable:
516
595
class InventoryLink(InventoryEntry):
517
596
"""A file in an inventory."""
598
__slots__ = ['symlink_target']
519
600
def _check(self, checker, rev_id, tree):
520
601
"""See InventoryEntry._check"""
521
602
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
531
612
other.revision = self.revision
615
def detect_changes(self, old_entry):
616
"""See InventoryEntry.detect_changes."""
617
# FIXME: which _modified field should we use ? RBC 20051003
618
text_modified = (self.symlink_target != old_entry.symlink_target)
620
mutter(" symlink target changed")
621
meta_modified = False
622
return text_modified, meta_modified
624
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
625
output_to, reverse=False):
626
"""See InventoryEntry._diff."""
627
from_text = self.symlink_target
628
if to_entry is not None:
629
to_text = to_entry.symlink_target
634
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
637
print >>output_to, '=== target was %r' % self.symlink_target
639
print >>output_to, '=== target is %r' % self.symlink_target
534
641
def __init__(self, file_id, name, parent_id):
535
642
super(InventoryLink, self).__init__(file_id, name, parent_id)
536
643
self.kind = 'symlink'
555
662
except OSError,e:
556
663
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
665
def _read_tree_state(self, path, work_tree):
666
"""See InventoryEntry._read_tree_state."""
667
self.symlink_target = work_tree.get_symlink_target(self.file_id)
669
def _unchanged(self, previous_ie):
670
"""See InventoryEntry._unchanged."""
671
compatible = super(InventoryLink, self)._unchanged(previous_ie)
672
if self.symlink_target != previous_ie.symlink_target:
559
677
class Inventory(object):
560
678
"""Inventory of versioned files in a tree.
867
984
def id2path(self, file_id):
868
"""Return as a list the path to file_id."""
985
"""Return as a list the path to file_id.
988
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
989
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
990
>>> print i.id2path('foo-id').replace(os.sep, '/')
870
993
# get all names, skipping root
871
994
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
872
995
return os.sep.join(p)