~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

Merge bzr.dev.

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