~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Michael Ellerman
  • Date: 2006-02-28 14:45:51 UTC
  • mto: (1558.1.18 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1586.
  • Revision ID: michael@ellerman.id.au-20060228144551-3d9941ecde4a0b0a
Update contrib/pwk for -p1 diffs from bzr

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
 
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
28
28
ROOT_ID = "TREE_ROOT"
29
29
 
30
30
 
31
 
import collections
32
31
import os.path
33
32
import re
34
33
import sys
36
35
import types
37
36
 
38
37
import bzrlib
39
 
from bzrlib import errors, osutils
40
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
41
39
                            pathjoin, sha_strings)
 
40
from bzrlib.trace import mutter
42
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
43
 
                           BzrError, BzrCheckError, BinaryFile)
44
 
from bzrlib.trace import mutter
 
42
                           BzrError, BzrCheckError)
45
43
 
46
44
 
47
45
class InventoryEntry(object):
78
76
    >>> i.path2id('')
79
77
    'TREE_ROOT'
80
78
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
81
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
82
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
83
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
 
81
    InventoryFile('2323', 'hello.c', parent_id='123')
84
82
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
85
83
    >>> for ix, j in enumerate(i.iter_entries()):
86
84
    ...   print (j[0] == shouldbe[ix], j[1])
87
85
    ... 
88
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
89
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
86
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
87
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
90
88
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
91
89
    Traceback (most recent call last):
92
90
    ...
93
91
    BzrError: inventory already contains entry with id {2323}
94
92
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
95
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
 
93
    InventoryFile('2324', 'bye.c', parent_id='123')
96
94
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
97
 
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
95
    InventoryDirectory('2325', 'wibble', parent_id='123')
98
96
    >>> i.path2id('src/wibble')
99
97
    '2325'
100
98
    >>> '2325' in i
101
99
    True
102
100
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
101
    InventoryFile('2326', 'wibble.c', parent_id='2325')
104
102
    >>> i['2326']
105
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
106
104
    >>> for path, entry in i.iter_entries():
107
105
    ...     print path
108
106
    ...     assert i.path2id(path)
115
113
    >>> i.id2path('2326')
116
114
    'src/wibble/wibble.c'
117
115
    """
118
 
 
119
 
    # Constants returned by describe_change()
120
 
    #
121
 
    # TODO: These should probably move to some kind of FileChangeDescription 
122
 
    # class; that's like what's inside a TreeDelta but we want to be able to 
123
 
    # generate them just for one file at a time.
124
 
    RENAMED = 'renamed'
125
 
    MODIFIED_AND_RENAMED = 'modified and renamed'
126
116
    
127
 
    __slots__ = []
 
117
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
118
                 'text_id', 'parent_id', 'children', 'executable', 
 
119
                 'revision']
 
120
 
 
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)
128
124
 
129
125
    def detect_changes(self, old_entry):
130
126
        """Return a (text_modified, meta_modified) from this to old_entry.
155
151
             output_to, reverse=False):
156
152
        """Perform a diff between two entries of the same kind."""
157
153
 
158
 
    def find_previous_heads(self, previous_inventories,
159
 
                            versioned_file_store,
160
 
                            transaction,
161
 
                            entry_vf=None):
162
 
        """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
156
 
164
157
        Returned as a map from revision to inventory entry.
165
158
 
166
159
        This is a map containing the file revisions in all parents
167
160
        for which the file exists, and its revision is not a parent of
168
161
        any other. If the file is new, the set will be empty.
169
 
 
170
 
        :param versioned_file_store: A store where ancestry data on this
171
 
                                     file id can be queried.
172
 
        :param transaction: The transaction that queries to the versioned 
173
 
                            file store should be completed under.
174
 
        :param entry_vf: The entry versioned file, if its already available.
175
162
        """
176
163
        def get_ancestors(weave, entry):
177
 
            return set(weave.get_ancestry(entry.revision))
178
 
        # revision:ie mapping for each ie found in previous_inventories.
179
 
        candidates = {}
180
 
        # 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
166
        heads = {}
182
 
        # revision: ancestor list for each head
183
167
        head_ancestors = {}
184
 
        # identify candidate head revision ids.
185
168
        for inv in previous_inventories:
186
169
            if self.file_id in inv:
187
170
                ie = inv[self.file_id]
188
171
                assert ie.file_id == self.file_id
189
 
                if ie.revision in candidates:
190
 
                    # same revision value in two different inventories:
191
 
                    # correct possible inconsistencies:
192
 
                    #     * there was a bug in revision updates with 'x' bit 
193
 
                    #       support.
 
172
                if ie.revision in heads:
 
173
                    # fixup logic, there was a bug in revision updates.
 
174
                    # with x bit support.
194
175
                    try:
195
 
                        if candidates[ie.revision].executable != ie.executable:
196
 
                            candidates[ie.revision].executable = False
 
176
                        if heads[ie.revision].executable != ie.executable:
 
177
                            heads[ie.revision].executable = False
197
178
                            ie.executable = False
198
179
                    except AttributeError:
199
180
                        pass
200
 
                    # must now be the same.
201
 
                    assert candidates[ie.revision] == ie
 
181
                    assert heads[ie.revision] == ie
202
182
                else:
203
 
                    # add this revision as a candidate.
204
 
                    candidates[ie.revision] = ie
205
 
 
206
 
        # common case optimisation
207
 
        if len(candidates) == 1:
208
 
            # if there is only one candidate revision found
209
 
            # then we can opening the versioned file to access ancestry:
210
 
            # there cannot be any ancestors to eliminate when there is 
211
 
            # only one revision available.
212
 
            heads[ie.revision] = ie
213
 
            return heads
214
 
 
215
 
        # eliminate ancestors amongst the available candidates:
216
 
        # heads are those that are not an ancestor of any other candidate
217
 
        # - this provides convergence at a per-file level.
218
 
        for ie in candidates.values():
219
 
            # may be an ancestor of a known head:
220
 
            already_present = 0 != len(
221
 
                [head for head in heads 
222
 
                 if ie.revision in head_ancestors[head]])
223
 
            if already_present:
224
 
                # an ancestor of an analyzed candidate.
225
 
                continue
226
 
            # not an ancestor of a known head:
227
 
            # load the versioned file for this file id if needed
228
 
            if entry_vf is None:
229
 
                entry_vf = versioned_file_store.get_weave_or_empty(
230
 
                    self.file_id, transaction)
231
 
            ancestors = get_ancestors(entry_vf, ie)
232
 
            # may knock something else out:
233
 
            check_heads = list(heads.keys())
234
 
            for head in check_heads:
235
 
                if head in ancestors:
236
 
                    # this previously discovered 'head' is not
237
 
                    # really a head - its an ancestor of the newly 
238
 
                    # found head,
239
 
                    heads.pop(head)
240
 
            head_ancestors[ie.revision] = ancestors
241
 
            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]])
 
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
242
201
        return heads
243
202
 
244
203
    def get_tar_item(self, root, dp, now, tree):
318
277
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
319
278
 
320
279
    def sorted_children(self):
321
 
        return sorted(self.children.items())
 
280
        l = self.children.items()
 
281
        l.sort()
 
282
        return l
322
283
 
323
284
    @staticmethod
324
285
    def versionable_kind(kind):
329
290
 
330
291
        This is a template method, override _check for kind specific
331
292
        tests.
332
 
 
333
 
        :param checker: Check object providing context for the checks; 
334
 
             can be used to find out what parts of the repository have already
335
 
             been checked.
336
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
337
 
             Not necessarily the last-changed revision for this file.
338
 
        :param inv: Inventory from which the entry was loaded.
339
 
        :param tree: RevisionTree for this entry.
340
293
        """
341
 
        if self.parent_id is not None:
 
294
        if self.parent_id != None:
342
295
            if not inv.has_id(self.parent_id):
343
296
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
344
297
                        % (self.parent_id, rev_id))
349
302
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
350
303
                            (self.kind, rev_id))
351
304
 
 
305
 
352
306
    def copy(self):
353
307
        """Clone this inventory entry."""
354
308
        raise NotImplementedError
355
309
 
356
 
    @staticmethod
357
 
    def describe_change(old_entry, new_entry):
358
 
        """Describe the change between old_entry and this.
359
 
        
360
 
        This smells of being an InterInventoryEntry situation, but as its
361
 
        the first one, we're making it a static method for now.
362
 
 
363
 
        An entry with a different parent, or different name is considered 
364
 
        to be renamed. Reparenting is an internal detail.
365
 
        Note that renaming the parent does not trigger a rename for the
366
 
        child entry itself.
367
 
        """
368
 
        # TODO: Perhaps return an object rather than just a string
369
 
        if old_entry is new_entry:
370
 
            # also the case of both being None
371
 
            return 'unchanged'
372
 
        elif old_entry is None:
 
310
    def _get_snapshot_change(self, previous_entries):
 
311
        if len(previous_entries) > 1:
 
312
            return 'merged'
 
313
        elif len(previous_entries) == 0:
373
314
            return 'added'
374
 
        elif new_entry is None:
375
 
            return 'removed'
376
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
377
 
        if text_modified or meta_modified:
378
 
            modified = True
379
 
        else:
380
 
            modified = False
381
 
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
382
 
        if old_entry.parent_id != new_entry.parent_id:
383
 
            renamed = True
384
 
        elif old_entry.name != new_entry.name:
385
 
            renamed = True
386
 
        else:
387
 
            renamed = False
388
 
        if renamed and not modified:
389
 
            return InventoryEntry.RENAMED
390
 
        if modified and not renamed:
391
 
            return 'modified'
392
 
        if modified and renamed:
393
 
            return InventoryEntry.MODIFIED_AND_RENAMED
394
 
        return 'unchanged'
 
315
        else:
 
316
            return 'modified/renamed/reparented'
395
317
 
396
318
    def __repr__(self):
397
 
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
319
        return ("%s(%r, %r, parent_id=%r)"
398
320
                % (self.__class__.__name__,
399
321
                   self.file_id,
400
322
                   self.name,
401
 
                   self.parent_id,
402
 
                   self.revision))
 
323
                   self.parent_id))
403
324
 
404
325
    def snapshot(self, revision, path, previous_entries,
405
 
                 work_tree, commit_builder):
 
326
                 work_tree, weave_store, transaction):
406
327
        """Make a snapshot of this entry which may or may not have changed.
407
328
        
408
329
        This means that all its fields are populated, that it has its
410
331
        """
411
332
        mutter('new parents of %s are %r', path, previous_entries)
412
333
        self._read_tree_state(path, work_tree)
413
 
        # TODO: Where should we determine whether to reuse a
414
 
        # previous revision id or create a new revision? 20060606
415
334
        if len(previous_entries) == 1:
416
335
            # cannot be unchanged unless there is only one parent file rev.
417
336
            parent_ie = previous_entries.values()[0]
419
338
                mutter("found unchanged entry")
420
339
                self.revision = parent_ie.revision
421
340
                return "unchanged"
422
 
        return self._snapshot_into_revision(revision, previous_entries, 
423
 
                                            work_tree, commit_builder)
424
 
 
425
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
426
 
                                commit_builder):
427
 
        """Record this revision unconditionally into a store.
428
 
 
429
 
        The entry's last-changed revision property (`revision`) is updated to 
430
 
        that of the new revision.
431
 
        
432
 
        :param revision: id of the new revision that is being recorded.
433
 
 
434
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
435
 
        """
436
 
        mutter('new revision {%s} for {%s}', revision, self.file_id)
 
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."""
 
347
        mutter('new revision for {%s}', self.file_id)
437
348
        self.revision = revision
438
 
        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,
 
351
                            transaction)
 
352
        return change
439
353
 
440
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
354
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
441
355
        """Record the 'text' of this entry, whatever form that takes.
442
356
        
443
357
        This default implementation simply adds an empty text.
444
358
        """
445
 
        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)
446
362
 
447
363
    def __eq__(self, other):
448
364
        if not isinstance(other, InventoryEntry):
469
385
    def _unchanged(self, previous_ie):
470
386
        """Has this entry changed relative to previous_ie.
471
387
 
472
 
        This method should be overridden in child classes.
 
388
        This method should be overriden in child classes.
473
389
        """
474
390
        compatible = True
475
391
        # different inv parent
497
413
 
498
414
class RootEntry(InventoryEntry):
499
415
 
500
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
501
 
                 'text_id', 'parent_id', 'children', 'executable', 
502
 
                 'revision', 'symlink_target']
503
 
 
504
416
    def _check(self, checker, rev_id, tree):
505
417
        """See InventoryEntry._check"""
506
418
 
510
422
        self.kind = 'root_directory'
511
423
        self.parent_id = None
512
424
        self.name = u''
513
 
        self.revision = None
514
425
 
515
426
    def __eq__(self, other):
516
427
        if not isinstance(other, RootEntry):
523
434
class InventoryDirectory(InventoryEntry):
524
435
    """A directory in an inventory."""
525
436
 
526
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
527
 
                 'text_id', 'parent_id', 'children', 'executable', 
528
 
                 'revision', 'symlink_target']
529
 
 
530
437
    def _check(self, checker, rev_id, tree):
531
438
        """See InventoryEntry._check"""
532
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
439
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
533
440
            raise BzrCheckError('directory {%s} has text in revision {%s}'
534
441
                                % (self.file_id, rev_id))
535
442
 
562
469
        """See InventoryEntry._put_on_disk."""
563
470
        os.mkdir(fullpath)
564
471
 
565
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
566
 
        """See InventoryEntry._snapshot_text."""
567
 
        commit_builder.modified_directory(self.file_id, file_parents)
568
 
 
569
472
 
570
473
class InventoryFile(InventoryEntry):
571
474
    """A file in an inventory."""
572
475
 
573
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
574
 
                 'text_id', 'parent_id', 'children', 'executable', 
575
 
                 'revision', 'symlink_target']
576
 
 
577
 
    def _check(self, checker, tree_revision_id, tree):
 
476
    def _check(self, checker, rev_id, tree):
578
477
        """See InventoryEntry._check"""
579
 
        t = (self.file_id, self.revision)
 
478
        revision = self.revision
 
479
        t = (self.file_id, revision)
580
480
        if t in checker.checked_texts:
581
 
            prev_sha = checker.checked_texts[t]
 
481
            prev_sha = checker.checked_texts[t] 
582
482
            if prev_sha != self.text_sha1:
583
483
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
584
 
                                    (self.file_id, tree_revision_id))
 
484
                                    (self.file_id, rev_id))
585
485
            else:
586
486
                checker.repeated_text_cnt += 1
587
487
                return
595
495
            w.check()
596
496
            checker.checked_weaves[self.file_id] = True
597
497
        else:
598
 
            w = tree.get_weave(self.file_id)
 
498
            w = tree.get_weave_prelude(self.file_id)
599
499
 
600
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
601
 
        checker.checked_text_cnt += 1
 
500
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
501
        checker.checked_text_cnt += 1 
602
502
        # We can't check the length, because Weave doesn't store that
603
503
        # information, and the whole point of looking at the weave's
604
504
        # sha1sum is that we don't have to extract the text.
618
518
 
619
519
    def detect_changes(self, old_entry):
620
520
        """See InventoryEntry.detect_changes."""
621
 
        assert self.text_sha1 is not None
622
 
        assert old_entry.text_sha1 is not None
 
521
        assert self.text_sha1 != None
 
522
        assert old_entry.text_sha1 != None
623
523
        text_modified = (self.text_sha1 != old_entry.text_sha1)
624
524
        meta_modified = (self.executable != old_entry.executable)
625
525
        return text_modified, meta_modified
627
527
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
628
528
             output_to, reverse=False):
629
529
        """See InventoryEntry._diff."""
630
 
        try:
631
 
            from_text = tree.get_file(self.file_id).readlines()
632
 
            if to_entry:
633
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
634
 
            else:
635
 
                to_text = []
636
 
            if not reverse:
637
 
                text_diff(from_label, from_text,
638
 
                          to_label, to_text, output_to)
639
 
            else:
640
 
                text_diff(to_label, to_text,
641
 
                          from_label, from_text, output_to)
642
 
        except BinaryFile:
643
 
            if reverse:
644
 
                label_pair = (to_label, from_label)
645
 
            else:
646
 
                label_pair = (from_label, to_label)
647
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
530
        from_text = tree.get_file(self.file_id).readlines()
 
531
        if to_entry:
 
532
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
533
        else:
 
534
            to_text = []
 
535
        if not reverse:
 
536
            text_diff(from_label, from_text,
 
537
                      to_label, to_text, output_to)
 
538
        else:
 
539
            text_diff(to_label, to_text,
 
540
                      from_label, from_text, output_to)
648
541
 
649
542
    def has_text(self):
650
543
        """See InventoryEntry.has_text."""
677
570
 
678
571
    def _read_tree_state(self, path, work_tree):
679
572
        """See InventoryEntry._read_tree_state."""
680
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
681
 
        # FIXME: 20050930 probe for the text size when getting sha1
682
 
        # in _read_tree_state
683
 
        self.executable = work_tree.is_executable(self.file_id, path=path)
684
 
 
685
 
    def __repr__(self):
686
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
687
 
                % (self.__class__.__name__,
688
 
                   self.file_id,
689
 
                   self.name,
690
 
                   self.parent_id,
691
 
                   self.text_sha1,
692
 
                   self.text_size))
 
573
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
 
574
        self.executable = work_tree.is_executable(self.file_id)
693
575
 
694
576
    def _forget_tree_state(self):
695
577
        self.text_sha1 = None
 
578
        self.executable = None
696
579
 
697
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
580
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
698
581
        """See InventoryEntry._snapshot_text."""
699
 
        def get_content_byte_lines():
700
 
            return work_tree.get_file(self.file_id).readlines()
701
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
702
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
582
        mutter('storing file {%s} in revision {%s}',
 
583
               self.file_id, self.revision)
 
584
        # special case to avoid diffing on renames or 
 
585
        # reparenting
 
586
        if (len(file_parents) == 1
 
587
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
588
            and self.text_size == file_parents.values()[0].text_size):
 
589
            previous_ie = file_parents.values()[0]
 
590
            weave_store.add_identical_text(
 
591
                self.file_id, previous_ie.revision, 
 
592
                self.revision, file_parents, transaction)
 
593
        else:
 
594
            new_lines = work_tree.get_file(self.file_id).readlines()
 
595
            self._add_text_to_weave(new_lines, file_parents, weave_store,
 
596
                                    transaction)
 
597
            self.text_sha1 = sha_strings(new_lines)
 
598
            self.text_size = sum(map(len, new_lines))
 
599
 
703
600
 
704
601
    def _unchanged(self, previous_ie):
705
602
        """See InventoryEntry._unchanged."""
718
615
class InventoryLink(InventoryEntry):
719
616
    """A file in an inventory."""
720
617
 
721
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
722
 
                 'text_id', 'parent_id', 'children', 'executable', 
723
 
                 'revision', 'symlink_target']
 
618
    __slots__ = ['symlink_target']
724
619
 
725
620
    def _check(self, checker, rev_id, tree):
726
621
        """See InventoryEntry._check"""
727
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
622
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
728
623
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
729
624
                    % (self.file_id, rev_id))
730
 
        if self.symlink_target is None:
 
625
        if self.symlink_target == None:
731
626
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
732
627
                    % (self.file_id, rev_id))
733
628
 
801
696
            compatible = False
802
697
        return compatible
803
698
 
804
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
805
 
        """See InventoryEntry._snapshot_text."""
806
 
        commit_builder.modified_link(
807
 
            self.file_id, file_parents, self.symlink_target)
808
 
 
809
699
 
810
700
class Inventory(object):
811
701
    """Inventory of versioned files in a tree.
826
716
 
827
717
    >>> inv = Inventory()
828
718
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
829
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
719
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
830
720
    >>> inv['123-123'].name
831
721
    'hello.c'
832
722
 
840
730
    May also look up by name:
841
731
 
842
732
    >>> [x[0] for x in inv.iter_entries()]
843
 
    [u'hello.c']
 
733
    ['hello.c']
844
734
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
845
735
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
846
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
 
736
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
847
737
    """
848
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
738
    def __init__(self, root_id=ROOT_ID):
849
739
        """Create or read an inventory.
850
740
 
851
741
        If a working directory is specified, the inventory is read
860
750
        #if root_id is None:
861
751
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
862
752
        self.root = RootEntry(root_id)
863
 
        # FIXME: this isn't ever used, changing it to self.revision may break
864
 
        # things. TODO make everything use self.revision_id
865
 
        self.revision_id = revision_id
866
753
        self._byid = {self.root.file_id: self.root}
867
754
 
 
755
 
868
756
    def copy(self):
869
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
870
757
        other = Inventory(self.root.file_id)
871
758
        # copy recursively so we know directories will be added before
872
759
        # their children.  There are more efficient ways than this...
876
763
            other.add(entry.copy())
877
764
        return other
878
765
 
 
766
 
879
767
    def __iter__(self):
880
768
        return iter(self._byid)
881
769
 
 
770
 
882
771
    def __len__(self):
883
772
        """Returns number of entries."""
884
773
        return len(self._byid)
885
774
 
 
775
 
886
776
    def iter_entries(self, from_dir=None):
887
777
        """Return (path, entry) pairs, in order by name."""
888
 
        if from_dir is None:
889
 
            assert self.root
890
 
            from_dir = self.root
891
 
        elif isinstance(from_dir, basestring):
892
 
            from_dir = self._byid[from_dir]
893
 
            
894
 
        # unrolling the recursive called changed the time from
895
 
        # 440ms/663ms (inline/total) to 116ms/116ms
896
 
        children = from_dir.children.items()
897
 
        children.sort()
898
 
        children = collections.deque(children)
899
 
        stack = [(u'', children)]
900
 
        while stack:
901
 
            from_dir_relpath, children = stack[-1]
902
 
 
903
 
            while children:
904
 
                name, ie = children.popleft()
905
 
 
906
 
                # we know that from_dir_relpath never ends in a slash
907
 
                # and 'f' doesn't begin with one, we can do a string op, rather
908
 
                # than the checks of pathjoin(), though this means that all paths
909
 
                # start with a slash
910
 
                path = from_dir_relpath + '/' + name
911
 
 
912
 
                yield path[1:], ie
913
 
 
914
 
                if ie.kind != 'directory':
915
 
                    continue
916
 
 
917
 
                # But do this child first
918
 
                new_children = ie.children.items()
919
 
                new_children.sort()
920
 
                new_children = collections.deque(new_children)
921
 
                stack.append((path, new_children))
922
 
                # Break out of inner loop, so that we start outer loop with child
923
 
                break
924
 
            else:
925
 
                # if we finished all children, pop it off the stack
926
 
                stack.pop()
927
 
 
928
 
    def iter_entries_by_dir(self, from_dir=None):
929
 
        """Iterate over the entries in a directory first order.
930
 
 
931
 
        This returns all entries for a directory before returning
932
 
        the entries for children of a directory. This is not
933
 
        lexicographically sorted order, and is a hybrid between
934
 
        depth-first and breadth-first.
935
 
 
936
 
        :return: This yields (path, entry) pairs
937
 
        """
938
 
        # TODO? Perhaps this should return the from_dir so that the root is
939
 
        # yielded? or maybe an option?
940
 
        if from_dir is None:
941
 
            assert self.root
942
 
            from_dir = self.root
943
 
        elif isinstance(from_dir, basestring):
944
 
            from_dir = self._byid[from_dir]
945
 
            
946
 
        stack = [(u'', from_dir)]
947
 
        while stack:
948
 
            cur_relpath, cur_dir = stack.pop()
949
 
 
950
 
            child_dirs = []
951
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
952
 
 
953
 
                child_relpath = cur_relpath + child_name
954
 
 
955
 
                yield child_relpath, child_ie
956
 
 
957
 
                if child_ie.kind == 'directory':
958
 
                    child_dirs.append((child_relpath+'/', child_ie))
959
 
            stack.extend(reversed(child_dirs))
 
778
        if from_dir == None:
 
779
            assert self.root
 
780
            from_dir = self.root
 
781
        elif isinstance(from_dir, basestring):
 
782
            from_dir = self._byid[from_dir]
 
783
            
 
784
        kids = from_dir.children.items()
 
785
        kids.sort()
 
786
        for name, ie in kids:
 
787
            yield name, ie
 
788
            if ie.kind == 'directory':
 
789
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
790
                    yield pathjoin(name, cn), cie
 
791
 
960
792
 
961
793
    def entries(self):
962
794
        """Return list of (path, ie) for all entries except the root.
976
808
        descend(self.root, u'')
977
809
        return accum
978
810
 
 
811
 
979
812
    def directories(self):
980
813
        """Return (path, entry) pairs for all directories, including the root.
981
814
        """
992
825
        descend(self.root, u'')
993
826
        return accum
994
827
        
 
828
 
 
829
 
995
830
    def __contains__(self, file_id):
996
831
        """True if this entry contains a file with given id.
997
832
 
998
833
        >>> inv = Inventory()
999
834
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1000
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
835
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1001
836
        >>> '123' in inv
1002
837
        True
1003
838
        >>> '456' in inv
1005
840
        """
1006
841
        return file_id in self._byid
1007
842
 
 
843
 
1008
844
    def __getitem__(self, file_id):
1009
845
        """Return the entry for given file_id.
1010
846
 
1011
847
        >>> inv = Inventory()
1012
848
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1013
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
849
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1014
850
        >>> inv['123123'].name
1015
851
        'hello.c'
1016
852
        """
1017
853
        try:
1018
854
            return self._byid[file_id]
1019
855
        except KeyError:
1020
 
            if file_id is None:
 
856
            if file_id == None:
1021
857
                raise BzrError("can't look up file_id None")
1022
858
            else:
1023
859
                raise BzrError("file_id {%s} not in inventory" % file_id)
1024
860
 
 
861
 
1025
862
    def get_file_kind(self, file_id):
1026
863
        return self._byid[file_id].kind
1027
864
 
1028
865
    def get_child(self, parent_id, filename):
1029
866
        return self[parent_id].children.get(filename)
1030
867
 
 
868
 
1031
869
    def add(self, entry):
1032
870
        """Add entry to inventory.
1033
871
 
1047
885
        except KeyError:
1048
886
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1049
887
 
1050
 
        if entry.name in parent.children:
 
888
        if parent.children.has_key(entry.name):
1051
889
            raise BzrError("%s is already versioned" %
1052
890
                    pathjoin(self.id2path(parent.file_id), entry.name))
1053
891
 
1055
893
        parent.children[entry.name] = entry
1056
894
        return entry
1057
895
 
1058
 
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
896
 
 
897
    def add_path(self, relpath, kind, file_id=None):
1059
898
        """Add entry from a path.
1060
899
 
1061
900
        The immediate parent must already be versioned.
1062
901
 
1063
902
        Returns the new entry object."""
 
903
        from bzrlib.workingtree import gen_file_id
1064
904
        
1065
 
        parts = osutils.splitpath(relpath)
 
905
        parts = bzrlib.osutils.splitpath(relpath)
 
906
 
 
907
        if file_id == None:
 
908
            file_id = gen_file_id(relpath)
1066
909
 
1067
910
        if len(parts) == 0:
1068
 
            if file_id is None:
1069
 
                file_id = bzrlib.workingtree.gen_root_id()
1070
911
            self.root = RootEntry(file_id)
1071
912
            self._byid = {self.root.file_id: self.root}
1072
913
            return
1073
914
        else:
1074
915
            parent_path = parts[:-1]
1075
916
            parent_id = self.path2id(parent_path)
1076
 
            if parent_id is None:
 
917
            if parent_id == None:
1077
918
                raise NotVersionedError(path=parent_path)
1078
 
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
919
        if kind == 'directory':
 
920
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
921
        elif kind == 'file':
 
922
            ie = InventoryFile(file_id, parts[-1], parent_id)
 
923
        elif kind == 'symlink':
 
924
            ie = InventoryLink(file_id, parts[-1], parent_id)
 
925
        else:
 
926
            raise BzrError("unknown kind %r" % kind)
1079
927
        return self.add(ie)
1080
928
 
 
929
 
1081
930
    def __delitem__(self, file_id):
1082
931
        """Remove entry by id.
1083
932
 
1084
933
        >>> inv = Inventory()
1085
934
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1086
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
935
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1087
936
        >>> '123' in inv
1088
937
        True
1089
938
        >>> del inv['123']
1099
948
        if ie.parent_id is not None:
1100
949
            del self[ie.parent_id].children[ie.name]
1101
950
 
 
951
 
1102
952
    def __eq__(self, other):
1103
953
        """Compare two sets by comparing their contents.
1104
954
 
1107
957
        >>> i1 == i2
1108
958
        True
1109
959
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1110
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
960
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1111
961
        >>> i1 == i2
1112
962
        False
1113
963
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1114
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
964
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1115
965
        >>> i1 == i2
1116
966
        True
1117
967
        """
1118
968
        if not isinstance(other, Inventory):
1119
969
            return NotImplemented
1120
970
 
 
971
        if len(self._byid) != len(other._byid):
 
972
            # shortcut: obviously not the same
 
973
            return False
 
974
 
1121
975
        return self._byid == other._byid
1122
976
 
 
977
 
1123
978
    def __ne__(self, other):
1124
979
        return not self.__eq__(other)
1125
980
 
 
981
 
1126
982
    def __hash__(self):
1127
983
        raise ValueError('not hashable')
1128
984
 
1129
 
    def _iter_file_id_parents(self, file_id):
1130
 
        """Yield the parents of file_id up to the root."""
1131
 
        while file_id is not None:
1132
 
            try:
1133
 
                ie = self._byid[file_id]
1134
 
            except KeyError:
1135
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1136
 
            yield ie
1137
 
            file_id = ie.parent_id
1138
985
 
1139
986
    def get_idpath(self, file_id):
1140
987
        """Return a list of file_ids for the path to an entry.
1145
992
        root directory as depth 1.
1146
993
        """
1147
994
        p = []
1148
 
        for parent in self._iter_file_id_parents(file_id):
1149
 
            p.insert(0, parent.file_id)
 
995
        while file_id != None:
 
996
            try:
 
997
                ie = self._byid[file_id]
 
998
            except KeyError:
 
999
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1000
            p.insert(0, ie.file_id)
 
1001
            file_id = ie.parent_id
1150
1002
        return p
1151
1003
 
 
1004
 
1152
1005
    def id2path(self, file_id):
1153
 
        """Return as a string the path to file_id.
 
1006
        """Return as a list the path to file_id.
1154
1007
        
1155
1008
        >>> i = Inventory()
1156
1009
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1159
1012
        src/foo.c
1160
1013
        """
1161
1014
        # get all names, skipping root
1162
 
        return '/'.join(reversed(
1163
 
            [parent.name for parent in 
1164
 
             self._iter_file_id_parents(file_id)][:-1]))
 
1015
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
1016
        if p:
 
1017
            return pathjoin(*p)
 
1018
        else:
 
1019
            return ''
1165
1020
            
 
1021
 
 
1022
 
1166
1023
    def path2id(self, name):
1167
1024
        """Walk down through directories to return entry of last component.
1168
1025
 
1172
1029
        This returns the entry of the last component in the path,
1173
1030
        which may be either a file or a directory.
1174
1031
 
1175
 
        Returns None IFF the path is not found.
 
1032
        Returns None iff the path is not found.
1176
1033
        """
1177
1034
        if isinstance(name, types.StringTypes):
1178
1035
            name = splitpath(name)
1179
1036
 
1180
 
        # mutter("lookup path %r" % name)
 
1037
        mutter("lookup path %r" % name)
1181
1038
 
1182
1039
        parent = self.root
1183
1040
        for f in name:
1192
1049
 
1193
1050
        return parent.file_id
1194
1051
 
 
1052
 
1195
1053
    def has_filename(self, names):
1196
1054
        return bool(self.path2id(names))
1197
1055
 
 
1056
 
1198
1057
    def has_id(self, file_id):
1199
1058
        return self._byid.has_key(file_id)
1200
1059
 
 
1060
 
1201
1061
    def rename(self, file_id, new_parent_id, new_name):
1202
1062
        """Move a file within the inventory.
1203
1063
 
1228
1088
        file_ie.parent_id = new_parent_id
1229
1089
 
1230
1090
 
1231
 
def make_entry(kind, name, parent_id, file_id=None):
1232
 
    """Create an inventory entry.
1233
 
 
1234
 
    :param kind: the type of inventory entry to create.
1235
 
    :param name: the basename of the entry.
1236
 
    :param parent_id: the parent_id of the entry.
1237
 
    :param file_id: the file_id to use. if None, one will be created.
1238
 
    """
1239
 
    if file_id is None:
1240
 
        file_id = bzrlib.workingtree.gen_file_id(name)
1241
 
 
1242
 
    norm_name, can_access = osutils.normalized_filename(name)
1243
 
    if norm_name != name:
1244
 
        if can_access:
1245
 
            name = norm_name
1246
 
        else:
1247
 
            # TODO: jam 20060701 This would probably be more useful
1248
 
            #       if the error was raised with the full path
1249
 
            raise errors.InvalidNormalization(name)
1250
 
 
1251
 
    if kind == 'directory':
1252
 
        return InventoryDirectory(file_id, name, parent_id)
1253
 
    elif kind == 'file':
1254
 
        return InventoryFile(file_id, name, parent_id)
1255
 
    elif kind == 'symlink':
1256
 
        return InventoryLink(file_id, name, parent_id)
1257
 
    else:
1258
 
        raise BzrError("unknown kind %r" % kind)
1259
1091
 
1260
1092
 
1261
1093
_NAME_RE = None
1262
1094
 
1263
1095
def is_valid_name(name):
1264
1096
    global _NAME_RE
1265
 
    if _NAME_RE is None:
 
1097
    if _NAME_RE == None:
1266
1098
        _NAME_RE = re.compile(r'^[^/\\]+$')
1267
1099
        
1268
1100
    return bool(_NAME_RE.match(name))