~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2005-10-06 22:15:52 UTC
  • mfrom: (1185.13.2)
  • mto: This revision was merged to the branch mainline in revision 1420.
  • Revision ID: robertc@robertcollins.net-20051006221552-9b15c96fa504e0ad
mergeĀ fromĀ upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 Canonical Ltd
2
 
#
 
1
# (C) 2005 Canonical Ltd
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
# FIXME: This refactoring of the workingtree code doesn't seem to keep 
18
 
# the WorkingTree's copy of the inventory in sync with the branch.  The
19
 
# branch modifies its working inventory when it does a commit to make
20
 
# missing files permanently removed.
21
17
 
22
18
# TODO: Maybe also keep the full path of the entry, and the children?
23
19
# But those depend on its position within a particular inventory, and
27
23
# created, but it's not for now.
28
24
ROOT_ID = "TREE_ROOT"
29
25
 
30
 
import os
 
26
 
 
27
import os.path
31
28
import re
32
29
import sys
33
 
 
34
 
from bzrlib.lazy_import import lazy_import
35
 
lazy_import(globals(), """
36
 
import collections
37
30
import tarfile
 
31
import types
38
32
 
39
33
import bzrlib
40
 
from bzrlib import (
41
 
    errors,
42
 
    generate_ids,
43
 
    osutils,
44
 
    symbol_versioning,
45
 
    workingtree,
46
 
    )
47
 
""")
 
34
from bzrlib.errors import BzrError, BzrCheckError
48
35
 
49
 
from bzrlib.errors import (
50
 
    BzrCheckError,
51
 
    BzrError,
52
 
    )
 
36
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
 
37
                            appendpath, sha_strings)
53
38
from bzrlib.trace import mutter
 
39
from bzrlib.errors import NotVersionedError
54
40
 
55
41
 
56
42
class InventoryEntry(object):
87
73
    >>> i.path2id('')
88
74
    'TREE_ROOT'
89
75
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
 
76
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
77
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
 
    InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
 
    >>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
94
 
    >>> for ix, j in enumerate(i.iter_entries()):
95
 
    ...   print (j[0] == shouldbe[ix], j[1])
 
78
    InventoryFile('2323', 'hello.c', parent_id='123')
 
79
    >>> for j in i.iter_entries():
 
80
    ...   print j
96
81
    ... 
97
 
    (True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
 
82
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
83
    ('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
 
84
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
 
85
    Traceback (most recent call last):
 
86
    ...
 
87
    BzrError: inventory already contains entry with id {2323}
100
88
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
101
 
    InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
 
89
    InventoryFile('2324', 'bye.c', parent_id='123')
102
90
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
103
 
    InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
 
91
    InventoryDirectory('2325', 'wibble', parent_id='123')
104
92
    >>> i.path2id('src/wibble')
105
93
    '2325'
106
94
    >>> '2325' in i
107
95
    True
108
96
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
109
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
97
    InventoryFile('2326', 'wibble.c', parent_id='2325')
110
98
    >>> i['2326']
111
 
    InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
 
99
    InventoryFile('2326', 'wibble.c', parent_id='2325')
112
100
    >>> for path, entry in i.iter_entries():
113
 
    ...     print path
 
101
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
114
102
    ...     assert i.path2id(path)
115
103
    ... 
116
 
    <BLANKLINE>
117
104
    src
118
105
    src/bye.c
119
106
    src/hello.c
120
107
    src/wibble
121
108
    src/wibble/wibble.c
122
 
    >>> i.id2path('2326')
 
109
    >>> i.id2path('2326').replace('\\\\', '/')
123
110
    'src/wibble/wibble.c'
124
111
    """
125
 
 
126
 
    # Constants returned by describe_change()
127
 
    #
128
 
    # TODO: These should probably move to some kind of FileChangeDescription 
129
 
    # class; that's like what's inside a TreeDelta but we want to be able to 
130
 
    # generate them just for one file at a time.
131
 
    RENAMED = 'renamed'
132
 
    MODIFIED_AND_RENAMED = 'modified and renamed'
133
112
    
134
 
    __slots__ = []
 
113
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
114
                 'text_id', 'parent_id', 'children', 'executable', 
 
115
                 'revision']
 
116
 
 
117
    def _add_text_to_weave(self, new_lines, parents, weave_store):
 
118
        weave_store.add_text(self.file_id, self.revision, new_lines, parents)
135
119
 
136
120
    def detect_changes(self, old_entry):
137
121
        """Return a (text_modified, meta_modified) from this to old_entry.
162
146
             output_to, reverse=False):
163
147
        """Perform a diff between two entries of the same kind."""
164
148
 
165
 
    def find_previous_heads(self, previous_inventories,
166
 
                            versioned_file_store,
167
 
                            transaction,
168
 
                            entry_vf=None):
169
 
        """Return the revisions and entries that directly precede this.
 
149
    def find_previous_heads(self, previous_inventories, entry_weave):
 
150
        """Return the revisions and entries that directly preceed this.
170
151
 
171
152
        Returned as a map from revision to inventory entry.
172
153
 
173
154
        This is a map containing the file revisions in all parents
174
155
        for which the file exists, and its revision is not a parent of
175
156
        any other. If the file is new, the set will be empty.
176
 
 
177
 
        :param versioned_file_store: A store where ancestry data on this
178
 
                                     file id can be queried.
179
 
        :param transaction: The transaction that queries to the versioned 
180
 
                            file store should be completed under.
181
 
        :param entry_vf: The entry versioned file, if its already available.
182
157
        """
183
158
        def get_ancestors(weave, entry):
184
 
            return set(weave.get_ancestry(entry.revision))
185
 
        # revision:ie mapping for each ie found in previous_inventories.
186
 
        candidates = {}
187
 
        # revision:ie mapping with one revision for each head.
 
159
            return set(map(weave.idx_to_name,
 
160
                           weave.inclusions([weave.lookup(entry.revision)])))
188
161
        heads = {}
189
 
        # revision: ancestor list for each head
190
162
        head_ancestors = {}
191
 
        # identify candidate head revision ids.
192
163
        for inv in previous_inventories:
193
164
            if self.file_id in inv:
194
165
                ie = inv[self.file_id]
195
166
                assert ie.file_id == self.file_id
196
 
                if ie.revision in candidates:
197
 
                    # same revision value in two different inventories:
198
 
                    # correct possible inconsistencies:
199
 
                    #     * there was a bug in revision updates with 'x' bit 
200
 
                    #       support.
201
 
                    try:
202
 
                        if candidates[ie.revision].executable != ie.executable:
203
 
                            candidates[ie.revision].executable = False
204
 
                            ie.executable = False
205
 
                    except AttributeError:
206
 
                        pass
207
 
                    # must now be the same.
208
 
                    assert candidates[ie.revision] == ie
 
167
                if ie.revision in heads:
 
168
                    assert heads[ie.revision] == ie
209
169
                else:
210
 
                    # add this revision as a candidate.
211
 
                    candidates[ie.revision] = ie
212
 
 
213
 
        # common case optimisation
214
 
        if len(candidates) == 1:
215
 
            # if there is only one candidate revision found
216
 
            # then we can opening the versioned file to access ancestry:
217
 
            # there cannot be any ancestors to eliminate when there is 
218
 
            # only one revision available.
219
 
            heads[ie.revision] = ie
220
 
            return heads
221
 
 
222
 
        # eliminate ancestors amongst the available candidates:
223
 
        # heads are those that are not an ancestor of any other candidate
224
 
        # - this provides convergence at a per-file level.
225
 
        for ie in candidates.values():
226
 
            # may be an ancestor of a known head:
227
 
            already_present = 0 != len(
228
 
                [head for head in heads 
229
 
                 if ie.revision in head_ancestors[head]])
230
 
            if already_present:
231
 
                # an ancestor of an analyzed candidate.
232
 
                continue
233
 
            # not an ancestor of a known head:
234
 
            # load the versioned file for this file id if needed
235
 
            if entry_vf is None:
236
 
                entry_vf = versioned_file_store.get_weave_or_empty(
237
 
                    self.file_id, transaction)
238
 
            ancestors = get_ancestors(entry_vf, ie)
239
 
            # may knock something else out:
240
 
            check_heads = list(heads.keys())
241
 
            for head in check_heads:
242
 
                if head in ancestors:
243
 
                    # this previously discovered 'head' is not
244
 
                    # really a head - its an ancestor of the newly 
245
 
                    # found head,
246
 
                    heads.pop(head)
247
 
            head_ancestors[ie.revision] = ancestors
248
 
            heads[ie.revision] = ie
 
170
                    # may want to add it.
 
171
                    # may already be covered:
 
172
                    already_present = 0 != len(
 
173
                        [head for head in heads 
 
174
                         if ie.revision in head_ancestors[head]])
 
175
                    if already_present:
 
176
                        # an ancestor of a known head.
 
177
                        continue
 
178
                    # definately a head:
 
179
                    ancestors = get_ancestors(entry_weave, ie)
 
180
                    # may knock something else out:
 
181
                    check_heads = list(heads.keys())
 
182
                    for head in check_heads:
 
183
                        if head in ancestors:
 
184
                            # this head is not really a head
 
185
                            heads.pop(head)
 
186
                    head_ancestors[ie.revision] = ancestors
 
187
                    heads[ie.revision] = ie
249
188
        return heads
250
189
 
251
190
    def get_tar_item(self, root, dp, now, tree):
252
191
        """Get a tarfile item and a file stream for its content."""
253
 
        item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
 
192
        item = tarfile.TarInfo(os.path.join(root, dp))
254
193
        # TODO: would be cool to actually set it to the timestamp of the
255
194
        # revision it was last changed
256
195
        item.mtime = now
281
220
        '123'
282
221
        >>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
283
222
        Traceback (most recent call last):
284
 
        InvalidEntryName: Invalid entry name: src/hello.c
 
223
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
285
224
        """
286
225
        assert isinstance(name, basestring), name
287
226
        if '/' in name or '\\' in name:
288
 
            raise errors.InvalidEntryName(name=name)
 
227
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
228
        
289
229
        self.executable = False
290
230
        self.revision = None
291
231
        self.text_sha1 = None
292
232
        self.text_size = None
293
233
        self.file_id = file_id
294
 
        assert isinstance(file_id, (str, None.__class__)), \
295
 
            'bad type %r for %r' % (type(file_id), file_id)
296
234
        self.name = name
297
235
        self.text_id = text_id
298
236
        self.parent_id = parent_id
302
240
        """Return a short kind indicator useful for appending to names."""
303
241
        raise BzrError('unknown kind %r' % self.kind)
304
242
 
305
 
    known_kinds = ('file', 'directory', 'symlink')
 
243
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
306
244
 
307
245
    def _put_in_tar(self, item, tree):
308
246
        """populate item for stashing in a tar, and return the content stream.
317
255
        
318
256
        This is a template method - implement _put_on_disk in subclasses.
319
257
        """
320
 
        fullpath = osutils.pathjoin(dest, dp)
 
258
        fullpath = appendpath(dest, dp)
321
259
        self._put_on_disk(fullpath, tree)
322
 
        # mutter("  export {%s} kind %s to %s", self.file_id,
323
 
        #         self.kind, fullpath)
 
260
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
324
261
 
325
262
    def _put_on_disk(self, fullpath, tree):
326
263
        """Put this entry onto disk at fullpath, from tree tree."""
327
264
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
328
265
 
329
266
    def sorted_children(self):
330
 
        return sorted(self.children.items())
 
267
        l = self.children.items()
 
268
        l.sort()
 
269
        return l
331
270
 
332
271
    @staticmethod
333
272
    def versionable_kind(kind):
334
 
        return (kind in ('file', 'directory', 'symlink'))
 
273
        return kind in ('file', 'directory', 'symlink')
335
274
 
336
275
    def check(self, checker, rev_id, inv, tree):
337
276
        """Check this inventory entry is intact.
338
277
 
339
278
        This is a template method, override _check for kind specific
340
279
        tests.
341
 
 
342
 
        :param checker: Check object providing context for the checks; 
343
 
             can be used to find out what parts of the repository have already
344
 
             been checked.
345
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
346
 
             Not necessarily the last-changed revision for this file.
347
 
        :param inv: Inventory from which the entry was loaded.
348
 
        :param tree: RevisionTree for this entry.
349
280
        """
350
 
        if self.parent_id is not None:
 
281
        if self.parent_id != None:
351
282
            if not inv.has_id(self.parent_id):
352
283
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
353
284
                        % (self.parent_id, rev_id))
358
289
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
359
290
                            (self.kind, rev_id))
360
291
 
 
292
 
361
293
    def copy(self):
362
294
        """Clone this inventory entry."""
363
295
        raise NotImplementedError
364
296
 
365
 
    @staticmethod
366
 
    def describe_change(old_entry, new_entry):
367
 
        """Describe the change between old_entry and this.
368
 
        
369
 
        This smells of being an InterInventoryEntry situation, but as its
370
 
        the first one, we're making it a static method for now.
371
 
 
372
 
        An entry with a different parent, or different name is considered 
373
 
        to be renamed. Reparenting is an internal detail.
374
 
        Note that renaming the parent does not trigger a rename for the
375
 
        child entry itself.
376
 
        """
377
 
        # TODO: Perhaps return an object rather than just a string
378
 
        if old_entry is new_entry:
379
 
            # also the case of both being None
380
 
            return 'unchanged'
381
 
        elif old_entry is None:
 
297
    def _get_snapshot_change(self, previous_entries):
 
298
        if len(previous_entries) > 1:
 
299
            return 'merged'
 
300
        elif len(previous_entries) == 0:
382
301
            return 'added'
383
 
        elif new_entry is None:
384
 
            return 'removed'
385
 
        if old_entry.kind != new_entry.kind:
386
 
            return 'modified'
387
 
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
388
 
        if text_modified or meta_modified:
389
 
            modified = True
390
 
        else:
391
 
            modified = False
392
 
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
393
 
        if old_entry.parent_id != new_entry.parent_id:
394
 
            renamed = True
395
 
        elif old_entry.name != new_entry.name:
396
 
            renamed = True
397
 
        else:
398
 
            renamed = False
399
 
        if renamed and not modified:
400
 
            return InventoryEntry.RENAMED
401
 
        if modified and not renamed:
402
 
            return 'modified'
403
 
        if modified and renamed:
404
 
            return InventoryEntry.MODIFIED_AND_RENAMED
405
 
        return 'unchanged'
 
302
        else:
 
303
            return 'modified/renamed/reparented'
406
304
 
407
305
    def __repr__(self):
408
 
        return ("%s(%r, %r, parent_id=%r, revision=%r)"
 
306
        return ("%s(%r, %r, parent_id=%r)"
409
307
                % (self.__class__.__name__,
410
308
                   self.file_id,
411
309
                   self.name,
412
 
                   self.parent_id,
413
 
                   self.revision))
 
310
                   self.parent_id))
414
311
 
415
312
    def snapshot(self, revision, path, previous_entries,
416
 
                 work_tree, commit_builder):
 
313
                 work_tree, weave_store):
417
314
        """Make a snapshot of this entry which may or may not have changed.
418
315
        
419
316
        This means that all its fields are populated, that it has its
420
317
        text stored in the text store or weave.
421
318
        """
422
 
        # mutter('new parents of %s are %r', path, previous_entries)
 
319
        mutter('new parents of %s are %r', path, previous_entries)
423
320
        self._read_tree_state(path, work_tree)
424
 
        # TODO: Where should we determine whether to reuse a
425
 
        # previous revision id or create a new revision? 20060606
426
321
        if len(previous_entries) == 1:
427
322
            # cannot be unchanged unless there is only one parent file rev.
428
323
            parent_ie = previous_entries.values()[0]
429
324
            if self._unchanged(parent_ie):
430
 
                # mutter("found unchanged entry")
 
325
                mutter("found unchanged entry")
431
326
                self.revision = parent_ie.revision
432
327
                return "unchanged"
433
 
        return self._snapshot_into_revision(revision, previous_entries, 
434
 
                                            work_tree, commit_builder)
435
 
 
436
 
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
437
 
                                commit_builder):
438
 
        """Record this revision unconditionally into a store.
439
 
 
440
 
        The entry's last-changed revision property (`revision`) is updated to 
441
 
        that of the new revision.
442
 
        
443
 
        :param revision: id of the new revision that is being recorded.
444
 
 
445
 
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
446
 
        """
447
 
        # mutter('new revision {%s} for {%s}', revision, self.file_id)
 
328
        return self.snapshot_revision(revision, previous_entries, 
 
329
                                      work_tree, weave_store)
 
330
 
 
331
    def snapshot_revision(self, revision, previous_entries, work_tree,
 
332
                          weave_store):
 
333
        """Record this revision unconditionally."""
 
334
        mutter('new revision for {%s}', self.file_id)
448
335
        self.revision = revision
449
 
        self._snapshot_text(previous_entries, work_tree, commit_builder)
 
336
        change = self._get_snapshot_change(previous_entries)
 
337
        self._snapshot_text(previous_entries, work_tree, weave_store)
 
338
        return change
450
339
 
451
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder): 
 
340
    def _snapshot_text(self, file_parents, work_tree, weave_store): 
452
341
        """Record the 'text' of this entry, whatever form that takes.
453
342
        
454
343
        This default implementation simply adds an empty text.
455
344
        """
456
 
        raise NotImplementedError(self._snapshot_text)
 
345
        mutter('storing file {%s} in revision {%s}',
 
346
               self.file_id, self.revision)
 
347
        self._add_text_to_weave([], file_parents, weave_store)
457
348
 
458
349
    def __eq__(self, other):
459
350
        if not isinstance(other, InventoryEntry):
480
371
    def _unchanged(self, previous_ie):
481
372
        """Has this entry changed relative to previous_ie.
482
373
 
483
 
        This method should be overridden in child classes.
 
374
        This method should be overriden in child classes.
484
375
        """
485
376
        compatible = True
486
377
        # different inv parent
497
388
        Note that this should be modified to be a noop on virtual trees
498
389
        as all entries created there are prepopulated.
499
390
        """
500
 
        # TODO: Rather than running this manually, we should check the 
501
 
        # working sha1 and other expensive properties when they're
502
 
        # first requested, or preload them if they're already known
503
 
        pass            # nothing to do by default
504
 
 
505
 
    def _forget_tree_state(self):
506
 
        pass
507
391
 
508
392
 
509
393
class RootEntry(InventoryEntry):
510
394
 
511
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
512
 
                 'text_id', 'parent_id', 'children', 'executable', 
513
 
                 'revision', 'symlink_target']
514
 
 
515
395
    def _check(self, checker, rev_id, tree):
516
396
        """See InventoryEntry._check"""
517
397
 
518
398
    def __init__(self, file_id):
519
399
        self.file_id = file_id
520
400
        self.children = {}
521
 
        self.kind = 'directory'
 
401
        self.kind = 'root_directory'
522
402
        self.parent_id = None
523
 
        self.name = u''
524
 
        self.revision = None
525
 
        symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
526
 
                               '  Please use InventoryDirectory instead.',
527
 
                               DeprecationWarning, stacklevel=2)
 
403
        self.name = ''
528
404
 
529
405
    def __eq__(self, other):
530
406
        if not isinstance(other, RootEntry):
537
413
class InventoryDirectory(InventoryEntry):
538
414
    """A directory in an inventory."""
539
415
 
540
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
541
 
                 'text_id', 'parent_id', 'children', 'executable', 
542
 
                 'revision', 'symlink_target']
543
 
 
544
416
    def _check(self, checker, rev_id, tree):
545
417
        """See InventoryEntry._check"""
546
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
418
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
547
419
            raise BzrCheckError('directory {%s} has text in revision {%s}'
548
420
                                % (self.file_id, rev_id))
549
421
 
576
448
        """See InventoryEntry._put_on_disk."""
577
449
        os.mkdir(fullpath)
578
450
 
579
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
580
 
        """See InventoryEntry._snapshot_text."""
581
 
        commit_builder.modified_directory(self.file_id, file_parents)
582
 
 
583
451
 
584
452
class InventoryFile(InventoryEntry):
585
453
    """A file in an inventory."""
586
454
 
587
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
588
 
                 'text_id', 'parent_id', 'children', 'executable', 
589
 
                 'revision', 'symlink_target']
590
 
 
591
 
    def _check(self, checker, tree_revision_id, tree):
 
455
    def _check(self, checker, rev_id, tree):
592
456
        """See InventoryEntry._check"""
593
 
        t = (self.file_id, self.revision)
 
457
        revision = self.revision
 
458
        t = (self.file_id, revision)
594
459
        if t in checker.checked_texts:
595
 
            prev_sha = checker.checked_texts[t]
 
460
            prev_sha = checker.checked_texts[t] 
596
461
            if prev_sha != self.text_sha1:
597
462
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
598
 
                                    (self.file_id, tree_revision_id))
 
463
                                    (self.file_id, rev_id))
599
464
            else:
600
465
                checker.repeated_text_cnt += 1
601
466
                return
602
 
 
603
 
        if self.file_id not in checker.checked_weaves:
604
 
            mutter('check weave {%s}', self.file_id)
605
 
            w = tree.get_weave(self.file_id)
606
 
            # Not passing a progress bar, because it creates a new
607
 
            # progress, which overwrites the current progress,
608
 
            # and doesn't look nice
609
 
            w.check()
610
 
            checker.checked_weaves[self.file_id] = True
611
 
        else:
612
 
            w = tree.get_weave(self.file_id)
613
 
 
614
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
615
 
        checker.checked_text_cnt += 1
616
 
        # We can't check the length, because Weave doesn't store that
617
 
        # information, and the whole point of looking at the weave's
618
 
        # sha1sum is that we don't have to extract the text.
619
 
        if self.text_sha1 != w.get_sha1(self.revision):
620
 
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
621
 
                                % (self.file_id, self.revision))
 
467
        mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
468
        file_lines = tree.get_file_lines(self.file_id)
 
469
        checker.checked_text_cnt += 1 
 
470
        if self.text_size != sum(map(len, file_lines)):
 
471
            raise BzrCheckError('text {%s} wrong size' % self.text_id)
 
472
        if self.text_sha1 != sha_strings(file_lines):
 
473
            raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
622
474
        checker.checked_texts[t] = self.text_sha1
623
475
 
624
476
    def copy(self):
632
484
 
633
485
    def detect_changes(self, old_entry):
634
486
        """See InventoryEntry.detect_changes."""
635
 
        assert self.text_sha1 is not None
636
 
        assert old_entry.text_sha1 is not None
 
487
        assert self.text_sha1 != None
 
488
        assert old_entry.text_sha1 != None
637
489
        text_modified = (self.text_sha1 != old_entry.text_sha1)
638
490
        meta_modified = (self.executable != old_entry.executable)
639
491
        return text_modified, meta_modified
641
493
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
642
494
             output_to, reverse=False):
643
495
        """See InventoryEntry._diff."""
644
 
        try:
645
 
            from_text = tree.get_file(self.file_id).readlines()
646
 
            if to_entry:
647
 
                to_text = to_tree.get_file(to_entry.file_id).readlines()
648
 
            else:
649
 
                to_text = []
650
 
            if not reverse:
651
 
                text_diff(from_label, from_text,
652
 
                          to_label, to_text, output_to)
653
 
            else:
654
 
                text_diff(to_label, to_text,
655
 
                          from_label, from_text, output_to)
656
 
        except errors.BinaryFile:
657
 
            if reverse:
658
 
                label_pair = (to_label, from_label)
659
 
            else:
660
 
                label_pair = (from_label, to_label)
661
 
            print >> output_to, "Binary files %s and %s differ" % label_pair
 
496
        from_text = tree.get_file(self.file_id).readlines()
 
497
        if to_entry:
 
498
            to_text = to_tree.get_file(to_entry.file_id).readlines()
 
499
        else:
 
500
            to_text = []
 
501
        if not reverse:
 
502
            text_diff(from_label, from_text,
 
503
                      to_label, to_text, output_to)
 
504
        else:
 
505
            text_diff(to_label, to_text,
 
506
                      from_label, from_text, output_to)
662
507
 
663
508
    def has_text(self):
664
509
        """See InventoryEntry.has_text."""
685
530
 
686
531
    def _put_on_disk(self, fullpath, tree):
687
532
        """See InventoryEntry._put_on_disk."""
688
 
        osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
533
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
689
534
        if tree.is_executable(self.file_id):
690
535
            os.chmod(fullpath, 0755)
691
536
 
692
537
    def _read_tree_state(self, path, work_tree):
693
538
        """See InventoryEntry._read_tree_state."""
694
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
695
 
        # FIXME: 20050930 probe for the text size when getting sha1
696
 
        # in _read_tree_state
697
 
        self.executable = work_tree.is_executable(self.file_id, path=path)
698
 
 
699
 
    def __repr__(self):
700
 
        return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
701
 
                % (self.__class__.__name__,
702
 
                   self.file_id,
703
 
                   self.name,
704
 
                   self.parent_id,
705
 
                   self.text_sha1,
706
 
                   self.text_size))
707
 
 
708
 
    def _forget_tree_state(self):
709
 
        self.text_sha1 = None
710
 
 
711
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
 
539
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
 
540
        self.executable = work_tree.is_executable(self.file_id)
 
541
 
 
542
    def _snapshot_text(self, file_parents, work_tree, weave_store): 
712
543
        """See InventoryEntry._snapshot_text."""
713
 
        def get_content_byte_lines():
714
 
            return work_tree.get_file(self.file_id).readlines()
715
 
        self.text_sha1, self.text_size = commit_builder.modified_file_text(
716
 
            self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
 
544
        mutter('storing file {%s} in revision {%s}',
 
545
               self.file_id, self.revision)
 
546
        # special case to avoid diffing on renames or 
 
547
        # reparenting
 
548
        if (len(file_parents) == 1
 
549
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
550
            and self.text_size == file_parents.values()[0].text_size):
 
551
            previous_ie = file_parents.values()[0]
 
552
            weave_store.add_identical_text(
 
553
                self.file_id, previous_ie.revision, 
 
554
                self.revision, file_parents)
 
555
        else:
 
556
            new_lines = work_tree.get_file(self.file_id).readlines()
 
557
            self._add_text_to_weave(new_lines, file_parents, weave_store)
 
558
            self.text_sha1 = sha_strings(new_lines)
 
559
            self.text_size = sum(map(len, new_lines))
 
560
 
717
561
 
718
562
    def _unchanged(self, previous_ie):
719
563
        """See InventoryEntry._unchanged."""
724
568
            # FIXME: 20050930 probe for the text size when getting sha1
725
569
            # in _read_tree_state
726
570
            self.text_size = previous_ie.text_size
727
 
        if self.executable != previous_ie.executable:
728
 
            compatible = False
729
571
        return compatible
730
572
 
731
573
 
732
574
class InventoryLink(InventoryEntry):
733
575
    """A file in an inventory."""
734
576
 
735
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
736
 
                 'text_id', 'parent_id', 'children', 'executable', 
737
 
                 'revision', 'symlink_target']
 
577
    __slots__ = ['symlink_target']
738
578
 
739
579
    def _check(self, checker, rev_id, tree):
740
580
        """See InventoryEntry._check"""
741
 
        if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
 
581
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
742
582
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
743
583
                    % (self.file_id, rev_id))
744
 
        if self.symlink_target is None:
 
584
        if self.symlink_target == None:
745
585
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
746
586
                    % (self.file_id, rev_id))
747
587
 
787
627
 
788
628
    def _put_in_tar(self, item, tree):
789
629
        """See InventoryEntry._put_in_tar."""
790
 
        item.type = tarfile.SYMTYPE
 
630
        iterm.type = tarfile.SYMTYPE
791
631
        fileobj = None
792
632
        item.size = 0
793
633
        item.mode = 0755
805
645
        """See InventoryEntry._read_tree_state."""
806
646
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
807
647
 
808
 
    def _forget_tree_state(self):
809
 
        self.symlink_target = None
810
 
 
811
648
    def _unchanged(self, previous_ie):
812
649
        """See InventoryEntry._unchanged."""
813
650
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
815
652
            compatible = False
816
653
        return compatible
817
654
 
818
 
    def _snapshot_text(self, file_parents, work_tree, commit_builder):
819
 
        """See InventoryEntry._snapshot_text."""
820
 
        commit_builder.modified_link(
821
 
            self.file_id, file_parents, self.symlink_target)
822
 
 
823
655
 
824
656
class Inventory(object):
825
657
    """Inventory of versioned files in a tree.
840
672
 
841
673
    >>> inv = Inventory()
842
674
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
843
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
675
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
844
676
    >>> inv['123-123'].name
845
677
    'hello.c'
846
678
 
854
686
    May also look up by name:
855
687
 
856
688
    >>> [x[0] for x in inv.iter_entries()]
857
 
    ['', u'hello.c']
 
689
    ['hello.c']
858
690
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
859
691
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
860
 
    Traceback (most recent call last):
861
 
    BzrError: parent_id {TREE_ROOT} not in inventory
862
 
    >>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
863
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
 
692
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
864
693
    """
865
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
694
    def __init__(self, root_id=ROOT_ID):
866
695
        """Create or read an inventory.
867
696
 
868
697
        If a working directory is specified, the inventory is read
872
701
        The inventory is created with a default root directory, with
873
702
        an id of None.
874
703
        """
875
 
        if root_id is not None:
876
 
            assert root_id.__class__ == str
877
 
            self._set_root(InventoryDirectory(root_id, u'', None))
878
 
        else:
879
 
            self.root = None
880
 
            self._byid = {}
881
 
        self.revision_id = revision_id
882
 
 
883
 
    def _set_root(self, ie):
884
 
        self.root = ie
 
704
        # We are letting Branch.initialize() create a unique inventory
 
705
        # root id. Rather than generating a random one here.
 
706
        #if root_id is None:
 
707
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
708
        self.root = RootEntry(root_id)
885
709
        self._byid = {self.root.file_id: self.root}
886
710
 
 
711
 
887
712
    def copy(self):
888
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
889
 
        entries = self.iter_entries()
890
 
        other = Inventory(entries.next()[1].file_id)
 
713
        other = Inventory(self.root.file_id)
891
714
        # copy recursively so we know directories will be added before
892
715
        # their children.  There are more efficient ways than this...
893
 
        for path, entry in entries():
 
716
        for path, entry in self.iter_entries():
 
717
            if entry == self.root:
 
718
                continue
894
719
            other.add(entry.copy())
895
720
        return other
896
721
 
 
722
 
897
723
    def __iter__(self):
898
724
        return iter(self._byid)
899
725
 
 
726
 
900
727
    def __len__(self):
901
728
        """Returns number of entries."""
902
729
        return len(self._byid)
903
730
 
 
731
 
904
732
    def iter_entries(self, from_dir=None):
905
733
        """Return (path, entry) pairs, in order by name."""
906
 
        if from_dir is None:
907
 
            if self.root is None:
908
 
                return
909
 
            from_dir = self.root
910
 
            yield '', self.root
911
 
        elif isinstance(from_dir, basestring):
912
 
            from_dir = self._byid[from_dir]
913
 
            
914
 
        # unrolling the recursive called changed the time from
915
 
        # 440ms/663ms (inline/total) to 116ms/116ms
916
 
        children = from_dir.children.items()
917
 
        children.sort()
918
 
        children = collections.deque(children)
919
 
        stack = [(u'', children)]
920
 
        while stack:
921
 
            from_dir_relpath, children = stack[-1]
922
 
 
923
 
            while children:
924
 
                name, ie = children.popleft()
925
 
 
926
 
                # we know that from_dir_relpath never ends in a slash
927
 
                # and 'f' doesn't begin with one, we can do a string op, rather
928
 
                # than the checks of pathjoin(), though this means that all paths
929
 
                # start with a slash
930
 
                path = from_dir_relpath + '/' + name
931
 
 
932
 
                yield path[1:], ie
933
 
 
934
 
                if ie.kind != 'directory':
935
 
                    continue
936
 
 
937
 
                # But do this child first
938
 
                new_children = ie.children.items()
939
 
                new_children.sort()
940
 
                new_children = collections.deque(new_children)
941
 
                stack.append((path, new_children))
942
 
                # Break out of inner loop, so that we start outer loop with child
943
 
                break
944
 
            else:
945
 
                # if we finished all children, pop it off the stack
946
 
                stack.pop()
947
 
 
948
 
    def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
949
 
        """Iterate over the entries in a directory first order.
950
 
 
951
 
        This returns all entries for a directory before returning
952
 
        the entries for children of a directory. This is not
953
 
        lexicographically sorted order, and is a hybrid between
954
 
        depth-first and breadth-first.
955
 
 
956
 
        :return: This yields (path, entry) pairs
957
 
        """
958
 
        if specific_file_ids:
959
 
            specific_file_ids = [osutils.safe_file_id(fid)
960
 
                                 for fid in specific_file_ids]
961
 
        # TODO? Perhaps this should return the from_dir so that the root is
962
 
        # yielded? or maybe an option?
963
 
        if from_dir is None:
964
 
            if self.root is None:
965
 
                return
966
 
            # Optimize a common case
967
 
            if specific_file_ids is not None and len(specific_file_ids) == 1:
968
 
                file_id = list(specific_file_ids)[0]
969
 
                if file_id in self:
970
 
                    yield self.id2path(file_id), self[file_id]
971
 
                return 
972
 
            from_dir = self.root
973
 
            if (specific_file_ids is None or 
974
 
                self.root.file_id in specific_file_ids):
975
 
                yield u'', self.root
976
 
        elif isinstance(from_dir, basestring):
977
 
            from_dir = self._byid[from_dir]
978
 
 
979
 
        if specific_file_ids is not None:
980
 
            parents = set()
981
 
            def add_ancestors(file_id):
982
 
                if file_id not in self:
983
 
                    return
984
 
                parent_id = self[file_id].parent_id
985
 
                if parent_id is None:
986
 
                    return
987
 
                if parent_id not in parents:
988
 
                    parents.add(parent_id)
989
 
                    add_ancestors(parent_id)
990
 
            for file_id in specific_file_ids:
991
 
                add_ancestors(file_id)
992
 
        else:
993
 
            parents = None
994
 
            
995
 
        stack = [(u'', from_dir)]
996
 
        while stack:
997
 
            cur_relpath, cur_dir = stack.pop()
998
 
 
999
 
            child_dirs = []
1000
 
            for child_name, child_ie in sorted(cur_dir.children.iteritems()):
1001
 
 
1002
 
                child_relpath = cur_relpath + child_name
1003
 
 
1004
 
                if (specific_file_ids is None or 
1005
 
                    child_ie.file_id in specific_file_ids):
1006
 
                    yield child_relpath, child_ie
1007
 
 
1008
 
                if child_ie.kind == 'directory':
1009
 
                    if parents is None or child_ie.file_id in parents:
1010
 
                        child_dirs.append((child_relpath+'/', child_ie))
1011
 
            stack.extend(reversed(child_dirs))
 
734
        if from_dir == None:
 
735
            assert self.root
 
736
            from_dir = self.root
 
737
        elif isinstance(from_dir, basestring):
 
738
            from_dir = self._byid[from_dir]
 
739
            
 
740
        kids = from_dir.children.items()
 
741
        kids.sort()
 
742
        for name, ie in kids:
 
743
            yield name, ie
 
744
            if ie.kind == 'directory':
 
745
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
746
                    yield os.path.join(name, cn), cie
 
747
 
1012
748
 
1013
749
    def entries(self):
1014
750
        """Return list of (path, ie) for all entries except the root.
1020
756
            kids = dir_ie.children.items()
1021
757
            kids.sort()
1022
758
            for name, ie in kids:
1023
 
                child_path = osutils.pathjoin(dir_path, name)
 
759
                child_path = os.path.join(dir_path, name)
1024
760
                accum.append((child_path, ie))
1025
761
                if ie.kind == 'directory':
1026
762
                    descend(ie, child_path)
1027
763
 
1028
 
        descend(self.root, u'')
 
764
        descend(self.root, '')
1029
765
        return accum
1030
766
 
 
767
 
1031
768
    def directories(self):
1032
769
        """Return (path, entry) pairs for all directories, including the root.
1033
770
        """
1039
776
            kids.sort()
1040
777
 
1041
778
            for name, child_ie in kids:
1042
 
                child_path = osutils.pathjoin(parent_path, name)
 
779
                child_path = os.path.join(parent_path, name)
1043
780
                descend(child_ie, child_path)
1044
 
        descend(self.root, u'')
 
781
        descend(self.root, '')
1045
782
        return accum
1046
783
        
 
784
 
 
785
 
1047
786
    def __contains__(self, file_id):
1048
787
        """True if this entry contains a file with given id.
1049
788
 
1050
789
        >>> inv = Inventory()
1051
790
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1052
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
791
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1053
792
        >>> '123' in inv
1054
793
        True
1055
794
        >>> '456' in inv
1056
795
        False
1057
796
        """
1058
 
        file_id = osutils.safe_file_id(file_id)
1059
 
        return (file_id in self._byid)
 
797
        return file_id in self._byid
 
798
 
1060
799
 
1061
800
    def __getitem__(self, file_id):
1062
801
        """Return the entry for given file_id.
1063
802
 
1064
803
        >>> inv = Inventory()
1065
804
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1066
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
805
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1067
806
        >>> inv['123123'].name
1068
807
        'hello.c'
1069
808
        """
1070
 
        file_id = osutils.safe_file_id(file_id)
1071
809
        try:
1072
810
            return self._byid[file_id]
1073
811
        except KeyError:
1074
 
            # really we're passing an inventory, not a tree...
1075
 
            raise errors.NoSuchId(self, file_id)
 
812
            if file_id == None:
 
813
                raise BzrError("can't look up file_id None")
 
814
            else:
 
815
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
816
 
1076
817
 
1077
818
    def get_file_kind(self, file_id):
1078
 
        file_id = osutils.safe_file_id(file_id)
1079
819
        return self._byid[file_id].kind
1080
820
 
1081
821
    def get_child(self, parent_id, filename):
1082
 
        parent_id = osutils.safe_file_id(parent_id)
1083
822
        return self[parent_id].children.get(filename)
1084
823
 
 
824
 
1085
825
    def add(self, entry):
1086
826
        """Add entry to inventory.
1087
827
 
1091
831
        Returns the new entry object.
1092
832
        """
1093
833
        if entry.file_id in self._byid:
1094
 
            raise errors.DuplicateFileId(entry.file_id,
1095
 
                                         self._byid[entry.file_id])
1096
 
 
1097
 
        if entry.parent_id is None:
1098
 
            assert self.root is None and len(self._byid) == 0
1099
 
            self._set_root(entry)
1100
 
            return entry
 
834
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
835
 
 
836
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
837
            entry.parent_id = self.root.file_id
 
838
 
1101
839
        try:
1102
840
            parent = self._byid[entry.parent_id]
1103
841
        except KeyError:
1104
842
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1105
843
 
1106
 
        if entry.name in parent.children:
 
844
        if parent.children.has_key(entry.name):
1107
845
            raise BzrError("%s is already versioned" %
1108
 
                    osutils.pathjoin(self.id2path(parent.file_id), entry.name))
 
846
                    appendpath(self.id2path(parent.file_id), entry.name))
1109
847
 
1110
848
        self._byid[entry.file_id] = entry
1111
849
        parent.children[entry.name] = entry
1112
850
        return entry
1113
851
 
1114
 
    def add_path(self, relpath, kind, file_id=None, parent_id=None):
 
852
 
 
853
    def add_path(self, relpath, kind, file_id=None):
1115
854
        """Add entry from a path.
1116
855
 
1117
856
        The immediate parent must already be versioned.
1118
857
 
1119
858
        Returns the new entry object."""
 
859
        from bzrlib.branch import gen_file_id
1120
860
        
1121
 
        parts = osutils.splitpath(relpath)
1122
 
 
 
861
        parts = bzrlib.osutils.splitpath(relpath)
1123
862
        if len(parts) == 0:
1124
 
            if file_id is None:
1125
 
                file_id = generate_ids.gen_root_id()
1126
 
            else:
1127
 
                file_id = osutils.safe_file_id(file_id)
1128
 
            self.root = InventoryDirectory(file_id, '', None)
1129
 
            self._byid = {self.root.file_id: self.root}
1130
 
            return self.root
 
863
            raise BzrError("cannot re-add root of inventory")
 
864
 
 
865
        if file_id == None:
 
866
            file_id = gen_file_id(relpath)
 
867
 
 
868
        parent_path = parts[:-1]
 
869
        parent_id = self.path2id(parent_path)
 
870
        if parent_id == None:
 
871
            raise NotVersionedError(parent_path)
 
872
 
 
873
        if kind == 'directory':
 
874
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
875
        elif kind == 'file':
 
876
            ie = InventoryFile(file_id, parts[-1], parent_id)
 
877
        elif kind == 'symlink':
 
878
            ie = InventoryLink(file_id, parts[-1], parent_id)
1131
879
        else:
1132
 
            parent_path = parts[:-1]
1133
 
            parent_id = self.path2id(parent_path)
1134
 
            if parent_id is None:
1135
 
                raise errors.NotVersionedError(path=parent_path)
1136
 
        ie = make_entry(kind, parts[-1], parent_id, file_id)
 
880
            raise BzrError("unknown kind %r" % kind)
1137
881
        return self.add(ie)
1138
882
 
 
883
 
1139
884
    def __delitem__(self, file_id):
1140
885
        """Remove entry by id.
1141
886
 
1142
887
        >>> inv = Inventory()
1143
888
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1144
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
 
889
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1145
890
        >>> '123' in inv
1146
891
        True
1147
892
        >>> del inv['123']
1148
893
        >>> '123' in inv
1149
894
        False
1150
895
        """
1151
 
        file_id = osutils.safe_file_id(file_id)
1152
896
        ie = self[file_id]
1153
897
 
1154
 
        assert ie.parent_id is None or \
1155
 
            self[ie.parent_id].children[ie.name] == ie
 
898
        assert self[ie.parent_id].children[ie.name] == ie
1156
899
        
 
900
        # TODO: Test deleting all children; maybe hoist to a separate
 
901
        # deltree method?
 
902
        if ie.kind == 'directory':
 
903
            for cie in ie.children.values():
 
904
                del self[cie.file_id]
 
905
            del ie.children
 
906
 
1157
907
        del self._byid[file_id]
1158
 
        if ie.parent_id is not None:
1159
 
            del self[ie.parent_id].children[ie.name]
 
908
        del self[ie.parent_id].children[ie.name]
 
909
 
1160
910
 
1161
911
    def __eq__(self, other):
1162
912
        """Compare two sets by comparing their contents.
1166
916
        >>> i1 == i2
1167
917
        True
1168
918
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1169
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
919
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1170
920
        >>> i1 == i2
1171
921
        False
1172
922
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1173
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
 
923
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1174
924
        >>> i1 == i2
1175
925
        True
1176
926
        """
1177
927
        if not isinstance(other, Inventory):
1178
928
            return NotImplemented
1179
929
 
 
930
        if len(self._byid) != len(other._byid):
 
931
            # shortcut: obviously not the same
 
932
            return False
 
933
 
1180
934
        return self._byid == other._byid
1181
935
 
 
936
 
1182
937
    def __ne__(self, other):
1183
938
        return not self.__eq__(other)
1184
939
 
 
940
 
1185
941
    def __hash__(self):
1186
942
        raise ValueError('not hashable')
1187
943
 
1188
 
    def _iter_file_id_parents(self, file_id):
1189
 
        """Yield the parents of file_id up to the root."""
1190
 
        file_id = osutils.safe_file_id(file_id)
1191
 
        while file_id is not None:
1192
 
            try:
1193
 
                ie = self._byid[file_id]
1194
 
            except KeyError:
1195
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1196
 
            yield ie
1197
 
            file_id = ie.parent_id
1198
944
 
1199
945
    def get_idpath(self, file_id):
1200
946
        """Return a list of file_ids for the path to an entry.
1204
950
        is equal to the depth of the file in the tree, counting the
1205
951
        root directory as depth 1.
1206
952
        """
1207
 
        file_id = osutils.safe_file_id(file_id)
1208
953
        p = []
1209
 
        for parent in self._iter_file_id_parents(file_id):
1210
 
            p.insert(0, parent.file_id)
 
954
        while file_id != None:
 
955
            try:
 
956
                ie = self._byid[file_id]
 
957
            except KeyError:
 
958
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
959
            p.insert(0, ie.file_id)
 
960
            file_id = ie.parent_id
1211
961
        return p
1212
962
 
 
963
 
1213
964
    def id2path(self, file_id):
1214
 
        """Return as a string the path to file_id.
 
965
        """Return as a list the path to file_id.
1215
966
        
1216
967
        >>> i = Inventory()
1217
968
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1218
969
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1219
 
        >>> print i.id2path('foo-id')
 
970
        >>> print i.id2path('foo-id').replace(os.sep, '/')
1220
971
        src/foo.c
1221
972
        """
1222
 
        file_id = osutils.safe_file_id(file_id)
1223
973
        # get all names, skipping root
1224
 
        return '/'.join(reversed(
1225
 
            [parent.name for parent in 
1226
 
             self._iter_file_id_parents(file_id)][:-1]))
 
974
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
975
        return os.sep.join(p)
1227
976
            
 
977
 
 
978
 
1228
979
    def path2id(self, name):
1229
980
        """Walk down through directories to return entry of last component.
1230
981
 
1234
985
        This returns the entry of the last component in the path,
1235
986
        which may be either a file or a directory.
1236
987
 
1237
 
        Returns None IFF the path is not found.
 
988
        Returns None iff the path is not found.
1238
989
        """
1239
 
        if isinstance(name, basestring):
1240
 
            name = osutils.splitpath(name)
 
990
        if isinstance(name, types.StringTypes):
 
991
            name = splitpath(name)
1241
992
 
1242
 
        # mutter("lookup path %r" % name)
 
993
        mutter("lookup path %r" % name)
1243
994
 
1244
995
        parent = self.root
1245
 
        if parent is None:
1246
 
            return None
1247
996
        for f in name:
1248
997
            try:
1249
 
                children = getattr(parent, 'children', None)
1250
 
                if children is None:
1251
 
                    return None
1252
 
                cie = children[f]
 
998
                cie = parent.children[f]
1253
999
                assert cie.name == f
1254
1000
                assert cie.parent_id == parent.file_id
1255
1001
                parent = cie
1259
1005
 
1260
1006
        return parent.file_id
1261
1007
 
 
1008
 
1262
1009
    def has_filename(self, names):
1263
1010
        return bool(self.path2id(names))
1264
1011
 
 
1012
 
1265
1013
    def has_id(self, file_id):
1266
 
        file_id = osutils.safe_file_id(file_id)
1267
 
        return (file_id in self._byid)
 
1014
        return self._byid.has_key(file_id)
1268
1015
 
1269
 
    def remove_recursive_id(self, file_id):
1270
 
        """Remove file_id, and children, from the inventory.
1271
 
        
1272
 
        :param file_id: A file_id to remove.
1273
 
        """
1274
 
        file_id = osutils.safe_file_id(file_id)
1275
 
        to_find_delete = [self._byid[file_id]]
1276
 
        to_delete = []
1277
 
        while to_find_delete:
1278
 
            ie = to_find_delete.pop()
1279
 
            to_delete.append(ie.file_id)
1280
 
            if ie.kind == 'directory':
1281
 
                to_find_delete.extend(ie.children.values())
1282
 
        for file_id in reversed(to_delete):
1283
 
            ie = self[file_id]
1284
 
            del self._byid[file_id]
1285
 
            if ie.parent_id is not None:
1286
 
                del self[ie.parent_id].children[ie.name]
1287
1016
 
1288
1017
    def rename(self, file_id, new_parent_id, new_name):
1289
1018
        """Move a file within the inventory.
1290
1019
 
1291
1020
        This can change either the name, or the parent, or both.
1292
1021
 
1293
 
        This does not move the working file.
1294
 
        """
1295
 
        file_id = osutils.safe_file_id(file_id)
 
1022
        This does not move the working file."""
1296
1023
        if not is_valid_name(new_name):
1297
1024
            raise BzrError("not an acceptable filename: %r" % new_name)
1298
1025
 
1316
1043
        file_ie.name = new_name
1317
1044
        file_ie.parent_id = new_parent_id
1318
1045
 
1319
 
    def is_root(self, file_id):
1320
 
        file_id = osutils.safe_file_id(file_id)
1321
 
        return self.root is not None and file_id == self.root.file_id
1322
 
 
1323
 
 
1324
 
entry_factory = {
1325
 
    'directory':InventoryDirectory,
1326
 
    'file':InventoryFile,
1327
 
    'symlink':InventoryLink,
1328
 
}
1329
 
 
1330
 
def make_entry(kind, name, parent_id, file_id=None):
1331
 
    """Create an inventory entry.
1332
 
 
1333
 
    :param kind: the type of inventory entry to create.
1334
 
    :param name: the basename of the entry.
1335
 
    :param parent_id: the parent_id of the entry.
1336
 
    :param file_id: the file_id to use. if None, one will be created.
1337
 
    """
1338
 
    if file_id is None:
1339
 
        file_id = generate_ids.gen_file_id(name)
1340
 
    else:
1341
 
        file_id = osutils.safe_file_id(file_id)
1342
 
 
1343
 
    #------- This has been copied to bzrlib.dirstate.DirState.add, please
1344
 
    # keep them synchronised.
1345
 
    # we dont import normalized_filename directly because we want to be
1346
 
    # able to change the implementation at runtime for tests.
1347
 
    norm_name, can_access = osutils.normalized_filename(name)
1348
 
    if norm_name != name:
1349
 
        if can_access:
1350
 
            name = norm_name
1351
 
        else:
1352
 
            # TODO: jam 20060701 This would probably be more useful
1353
 
            #       if the error was raised with the full path
1354
 
            raise errors.InvalidNormalization(name)
1355
 
 
1356
 
    try:
1357
 
        factory = entry_factory[kind]
1358
 
    except KeyError:
1359
 
        raise BzrError("unknown kind %r" % kind)
1360
 
    return factory(file_id, name, parent_id)
 
1046
 
1361
1047
 
1362
1048
 
1363
1049
_NAME_RE = None
1364
1050
 
1365
1051
def is_valid_name(name):
1366
1052
    global _NAME_RE
1367
 
    if _NAME_RE is None:
 
1053
    if _NAME_RE == None:
1368
1054
        _NAME_RE = re.compile(r'^[^/\\]+$')
1369
1055
        
1370
1056
    return bool(_NAME_RE.match(name))