~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Fix BzrDir.create_workingtree for NULL_REVISION

Show diffs side-by-side

added added

removed removed

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