~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Exclude more files from dumb-rsync upload

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# (C) 2005 Canonical Ltd
2
 
 
 
2
#
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.
7
 
 
 
7
#
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.
12
 
 
 
12
#
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
16
16
 
 
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.
17
21
 
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
31
35
import types
32
36
 
33
37
import bzrlib
34
 
from bzrlib.errors import BzrError, BzrCheckError
35
 
 
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)
40
43
 
41
44
 
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():
80
 
    ...   print j
 
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])
81
85
    ... 
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):
86
90
    ...
112
116
    
113
117
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
118
                 'text_id', 'parent_id', 'children', 'executable', 
115
 
                 'revision', 'symlink_target']
 
119
                 'revision']
116
120
 
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,
 
123
                             transaction)
119
124
 
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.
125
130
        """
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)
134
 
            if text_modified:
135
 
                mutter("    symlink target changed")
136
 
            meta_modified = False
137
 
        else:
138
 
            text_modified = False
139
 
            meta_modified = False
140
 
        return text_modified, meta_modified
 
131
        return False, False
141
132
 
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.
145
136
 
146
137
        text_diff will be used for textual difference calculation.
 
138
        This is a template method, override _diff in child classes.
147
139
        """
148
140
        self._read_tree_state(tree.id2path(self.file_id), tree)
149
141
        if to_entry:
152
144
            assert self.kind == to_entry.kind
153
145
            to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
154
146
                                      to_tree)
155
 
        if self.kind == 'file':
156
 
            from_text = tree.get_file(self.file_id).readlines()
157
 
            if to_entry:
158
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
159
 
            else:
160
 
                to_text = []
161
 
            if not reverse:
162
 
                text_diff(from_label, from_text,
163
 
                          to_label, to_text, output_to)
164
 
            else:
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
171
 
                if reverse:
172
 
                    temp = from_text
173
 
                    from_text = to_text
174
 
                    to_text = temp
175
 
                print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
176
 
            else:
177
 
                if not reverse:
178
 
                    print >>output_to, '=== target was %r' % self.symlink_target
 
147
        self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
 
148
                   output_to, reverse)
 
149
 
 
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."""
 
153
 
 
154
    def find_previous_heads(self, previous_inventories, entry_weave):
 
155
        """Return the revisions and entries that directly preceed this.
 
156
 
 
157
        Returned as a map from revision to inventory entry.
 
158
 
 
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.
 
162
        """
 
163
        def get_ancestors(weave, entry):
 
164
            return set(map(weave.idx_to_name,
 
165
                           weave.inclusions([weave.lookup(entry.revision)])))
 
166
        heads = {}
 
167
        head_ancestors = {}
 
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.
 
175
                    try:
 
176
                        if heads[ie.revision].executable != ie.executable:
 
177
                            heads[ie.revision].executable = False
 
178
                            ie.executable = False
 
179
                    except AttributeError:
 
180
                        pass
 
181
                    assert heads[ie.revision] == ie
179
182
                else:
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]])
 
188
                    if already_present:
 
189
                        # an ancestor of a known head.
 
190
                        continue
 
191
                    # definately a 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
 
198
                            heads.pop(head)
 
199
                    head_ancestors[ie.revision] = ancestors
 
200
                    heads[ie.revision] = ie
 
201
        return heads
181
202
 
182
203
    def get_tar_item(self, root, dp, now, tree):
183
204
        """Get a tarfile item and a file stream for its content."""
192
213
        """Return true if the object this entry represents has textual data.
193
214
 
194
215
        Note that textual data includes binary content.
 
216
 
 
217
        Also note that all entries get weave files created for them.
 
218
        This attribute is primarily used when upgrading from old trees that
 
219
        did not have the weave index for all inventory entries.
195
220
        """
196
221
        return False
197
222
 
208
233
        '123'
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
212
237
        """
213
238
        assert isinstance(name, basestring), name
214
239
        if '/' in name or '\\' in name:
215
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
216
 
        
 
240
            raise InvalidEntryName(name=name)
217
241
        self.executable = False
218
242
        self.revision = None
219
243
        self.text_sha1 = None
226
250
 
227
251
    def kind_character(self):
228
252
        """Return a short kind indicator useful for appending to names."""
229
 
        if self.kind == 'symlink':
230
 
            return ''
231
 
        raise RuntimeError('unreachable code')
 
253
        raise BzrError('unknown kind %r' % self.kind)
232
254
 
233
255
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
234
256
 
247
269
        """
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,
 
273
                self.kind, fullpath)
251
274
 
252
275
    def _put_on_disk(self, fullpath, tree):
253
276
        """Put this entry onto disk at fullpath, from tree tree."""
299
322
                   self.name,
300
323
                   self.parent_id))
301
324
 
302
 
    def snapshot(self, revision, path, previous_entries, work_tree, 
303
 
                 weave_store):
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.
305
328
        
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)
 
343
 
 
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':
322
 
            return change
323
 
        self._snapshot_text(previous_entries, work_tree, weave_store)
 
350
        self._snapshot_text(previous_entries, work_tree, weave_store,
 
351
                            transaction)
324
352
        return change
325
353
 
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.
 
356
        
 
357
        This default implementation simply adds an empty text.
 
358
        """
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 
330
 
        # reparenting
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)
338
 
        else:
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)
343
362
 
344
363
    def __eq__(self, other):
345
364
        if not isinstance(other, InventoryEntry):
363
382
    def __hash__(self):
364
383
        raise ValueError('not hashable')
365
384
 
366
 
    def _unchanged(self, path, previous_ie, work_tree):
 
385
    def _unchanged(self, previous_ie):
 
386
        """Has this entry changed relative to previous_ie.
 
387
 
 
388
        This method should be overriden in child classes.
 
389
        """
367
390
        compatible = True
368
391
        # different inv parent
369
392
        if previous_ie.parent_id != self.parent_id:
371
394
        # renamed
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:
376
 
                compatible = False
377
 
        if self.kind == 'file':
378
 
            if self.text_sha1 != previous_ie.text_sha1:
379
 
                compatible = False
380
 
            else:
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
385
398
 
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.
 
401
        
 
402
        Note that this should be modified to be a noop on virtual trees
 
403
        as all entries created there are prepopulated.
 
404
        """
 
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
392
409
 
393
410
 
394
411
class RootEntry(InventoryEntry):
483
500
        other.revision = self.revision
484
501
        return other
485
502
 
 
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
 
510
 
 
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()
 
515
        if to_entry:
 
516
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
517
        else:
 
518
            to_text = []
 
519
        if not reverse:
 
520
            text_diff(from_label, from_text,
 
521
                      to_label, to_text, output_to)
 
522
        else:
 
523
            text_diff(to_label, to_text,
 
524
                      from_label, from_text, output_to)
 
525
 
486
526
    def has_text(self):
487
527
        """See InventoryEntry.has_text."""
488
528
        return True
512
552
        if tree.is_executable(self.file_id):
513
553
            os.chmod(fullpath, 0755)
514
554
 
 
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)
 
559
 
 
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 
 
565
        # reparenting
 
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)
 
573
        else:
 
574
            new_lines = work_tree.get_file(self.file_id).readlines()
 
575
            self._add_text_to_weave(new_lines, file_parents, weave_store,
 
576
                                    transaction)
 
577
            self.text_sha1 = sha_strings(new_lines)
 
578
            self.text_size = sum(map(len, new_lines))
 
579
 
 
580
 
 
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:
 
585
            compatible = False
 
586
        else:
 
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:
 
591
            compatible = False
 
592
        return compatible
 
593
 
515
594
 
516
595
class InventoryLink(InventoryEntry):
517
596
    """A file in an inventory."""
518
597
 
 
598
    __slots__ = ['symlink_target']
 
599
 
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
532
613
        return other
533
614
 
 
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)
 
619
        if text_modified:
 
620
            mutter("    symlink target changed")
 
621
        meta_modified = False
 
622
        return text_modified, meta_modified
 
623
 
 
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
 
630
            if reverse:
 
631
                temp = from_text
 
632
                from_text = to_text
 
633
                to_text = temp
 
634
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
 
635
        else:
 
636
            if not reverse:
 
637
                print >>output_to, '=== target was %r' % self.symlink_target
 
638
            else:
 
639
                print >>output_to, '=== target is %r' % self.symlink_target
 
640
 
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'
541
648
 
542
649
    def _put_in_tar(self, item, tree):
543
650
        """See InventoryEntry._put_in_tar."""
544
 
        iterm.type = tarfile.SYMTYPE
 
651
        item.type = tarfile.SYMTYPE
545
652
        fileobj = None
546
653
        item.size = 0
547
654
        item.mode = 0755
555
662
        except OSError,e:
556
663
            raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
557
664
 
 
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)
 
668
 
 
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:
 
673
            compatible = False
 
674
        return compatible
 
675
 
558
676
 
559
677
class Inventory(object):
560
678
    """Inventory of versioned files in a tree.
771
889
        parent_path = parts[:-1]
772
890
        parent_id = self.path2id(parent_path)
773
891
        if parent_id == None:
774
 
            raise NotVersionedError(parent_path)
775
 
 
 
892
            raise NotVersionedError(path=parent_path)
776
893
        if kind == 'directory':
777
894
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
778
895
        elif kind == 'file':
865
982
 
866
983
 
867
984
    def id2path(self, file_id):
868
 
        """Return as a list the path to file_id."""
869
 
 
 
985
        """Return as a list the path to file_id.
 
986
        
 
987
        >>> i = Inventory()
 
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, '/')
 
991
        src/foo.c
 
992
        """
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)