~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: John Arbash Meinel
  • Date: 2005-11-14 17:02:35 UTC
  • mto: (1587.1.6 bound-branches)
  • mto: This revision was merged to the branch mainline in revision 1590.
  • Revision ID: john@arbash-meinel.com-20051114170235-f85afa458bae956e
Fixing up the error message for a failed bind.

Show diffs side-by-side

added added

removed removed

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