/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2005-10-03 14:28:37 UTC
  • mto: (1393.1.30)
  • mto: This revision was merged to the branch mainline in revision 1400.
  • Revision ID: robertc@robertcollins.net-20051003142837-78bf906d4edcbd62
factor out inventory directory logic into 'InventoryDirectory' class

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
 
 
18
# TODO: Maybe also keep the full path of the entry, and the children?
 
19
# But those depend on its position within a particular inventory, and
 
20
# it would be nice not to need to hold the backpointer here.
 
21
 
 
22
# This should really be an id randomly assigned when the tree is
 
23
# created, but it's not for now.
 
24
ROOT_ID = "TREE_ROOT"
 
25
 
 
26
 
 
27
import os.path
 
28
import re
 
29
import sys
 
30
import tarfile
 
31
import types
 
32
 
 
33
import bzrlib
 
34
from bzrlib.errors import BzrError, BzrCheckError
 
35
 
 
36
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
 
37
                            appendpath, sha_strings)
 
38
from bzrlib.trace import mutter
 
39
from bzrlib.errors import NotVersionedError
 
40
 
 
41
 
 
42
class InventoryEntry(object):
 
43
    """Description of a versioned file.
 
44
 
 
45
    An InventoryEntry has the following fields, which are also
 
46
    present in the XML inventory-entry element:
 
47
 
 
48
    file_id
 
49
 
 
50
    name
 
51
        (within the parent directory)
 
52
 
 
53
    kind
 
54
        'directory' or 'file' or 'symlink'
 
55
 
 
56
    parent_id
 
57
        file_id of the parent directory, or ROOT_ID
 
58
 
 
59
    revision
 
60
        the revision_id in which this variation of this file was 
 
61
        introduced.
 
62
 
 
63
    executable
 
64
        Indicates that this file should be executable on systems
 
65
        that support it.
 
66
 
 
67
    text_sha1
 
68
        sha-1 of the text of the file
 
69
        
 
70
    text_size
 
71
        size in bytes of the text of the file
 
72
        
 
73
    (reading a version 4 tree created a text_id field.)
 
74
 
 
75
    >>> i = Inventory()
 
76
    >>> i.path2id('')
 
77
    'TREE_ROOT'
 
78
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
 
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
 
80
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
81
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
 
82
    >>> for j in i.iter_entries():
 
83
    ...   print j
 
84
    ... 
 
85
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
86
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
87
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
88
    Traceback (most recent call last):
 
89
    ...
 
90
    BzrError: inventory already contains entry with id {2323}
 
91
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
92
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
93
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
 
94
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
95
    >>> i.path2id('src/wibble')
 
96
    '2325'
 
97
    >>> '2325' in i
 
98
    True
 
99
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
100
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
101
    >>> i['2326']
 
102
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
103
    >>> for path, entry in i.iter_entries():
 
104
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
105
    ...     assert i.path2id(path)
 
106
    ... 
 
107
    src
 
108
    src/bye.c
 
109
    src/hello.c
 
110
    src/wibble
 
111
    src/wibble/wibble.c
 
112
    >>> i.id2path('2326').replace('\\\\', '/')
 
113
    'src/wibble/wibble.c'
 
114
    """
 
115
    
 
116
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
117
                 'text_id', 'parent_id', 'children', 'executable', 
 
118
                 'revision', 'symlink_target']
 
119
 
 
120
    def _add_text_to_weave(self, new_lines, parents, weave_store):
 
121
        weave_store.add_text(self.file_id, self.revision, new_lines, parents)
 
122
 
 
123
    def detect_changes(self, old_entry):
 
124
        """Return a (text_modified, meta_modified) from this to old_entry.
 
125
        
 
126
        _read_tree_state must have been called on self and old_entry prior to 
 
127
        calling detect_changes.
 
128
        """
 
129
        if self.kind == 'file':
 
130
            assert self.text_sha1 != None
 
131
            assert old_entry.text_sha1 != None
 
132
            text_modified = (self.text_sha1 != old_entry.text_sha1)
 
133
            meta_modified = (self.executable != old_entry.executable)
 
134
        elif self.kind == 'symlink':
 
135
            # FIXME: which _modified field should we use ? RBC 20051003
 
136
            text_modified = (self.symlink_target != old_entry.symlink_target)
 
137
            if text_modified:
 
138
                mutter("    symlink target changed")
 
139
            meta_modified = False
 
140
        else:
 
141
            text_modified = False
 
142
            meta_modified = False
 
143
        return text_modified, meta_modified
 
144
 
 
145
    def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
 
146
             output_to, reverse=False):
 
147
        """Perform a diff from this to to_entry.
 
148
 
 
149
        text_diff will be used for textual difference calculation.
 
150
        """
 
151
        self._read_tree_state(tree.id2path(self.file_id), tree)
 
152
        if to_entry:
 
153
            # cannot diff from one kind to another - you must do a removal
 
154
            # and an addif they do not match.
 
155
            assert self.kind == to_entry.kind
 
156
            to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
 
157
                                      to_tree)
 
158
        if self.kind == 'file':
 
159
            from_text = tree.get_file(self.file_id).readlines()
 
160
            if to_entry:
 
161
                to_text = to_tree.get_file(to_entry.file_id).readlines()
 
162
            else:
 
163
                to_text = []
 
164
            if not reverse:
 
165
                text_diff(from_label, from_text,
 
166
                          to_label, to_text, output_to)
 
167
            else:
 
168
                text_diff(to_label, to_text,
 
169
                          from_label, from_text, output_to)
 
170
        elif self.kind == 'symlink':
 
171
            from_text = self.symlink_target
 
172
            if to_entry is not None:
 
173
                to_text = to_entry.symlink_target
 
174
                if reverse:
 
175
                    temp = from_text
 
176
                    from_text = to_text
 
177
                    to_text = temp
 
178
                print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
 
179
            else:
 
180
                if not reverse:
 
181
                    print >>output_to, '=== target was %r' % self.symlink_target
 
182
                else:
 
183
                    print >>output_to, '=== target is %r' % self.symlink_target
 
184
 
 
185
    def get_tar_item(self, root, dp, now, tree):
 
186
        """Get a tarfile item and a file stream for its content."""
 
187
        item = tarfile.TarInfo(os.path.join(root, dp))
 
188
        # TODO: would be cool to actually set it to the timestamp of the
 
189
        # revision it was last changed
 
190
        item.mtime = now
 
191
        fileobj = self._put_in_tar(item, tree)
 
192
        return item, fileobj
 
193
 
 
194
    def has_text(self):
 
195
        """Return true if the object this entry represents has textual data.
 
196
 
 
197
        Note that textual data includes binary content.
 
198
        """
 
199
        if self.kind =='file':
 
200
            return True
 
201
        else:
 
202
            return False
 
203
 
 
204
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
205
        """Create an InventoryEntry
 
206
        
 
207
        The filename must be a single component, relative to the
 
208
        parent directory; it cannot be a whole path or relative name.
 
209
 
 
210
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
211
        >>> e.name
 
212
        'hello.c'
 
213
        >>> e.file_id
 
214
        '123'
 
215
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
216
        Traceback (most recent call last):
 
217
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
218
        """
 
219
        assert isinstance(name, basestring), name
 
220
        if '/' in name or '\\' in name:
 
221
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
222
        
 
223
        self.executable = False
 
224
        self.revision = None
 
225
        self.text_sha1 = None
 
226
        self.text_size = None
 
227
        self.file_id = file_id
 
228
        self.name = name
 
229
        self.kind = kind
 
230
        self.text_id = text_id
 
231
        self.parent_id = parent_id
 
232
        self.symlink_target = None
 
233
        if kind not in ('directory', 'file', 'symlink'):
 
234
            raise BzrError("unhandled entry kind %r" % kind)
 
235
 
 
236
    def kind_character(self):
 
237
        """Return a short kind indicator useful for appending to names."""
 
238
        if self.kind == 'file':
 
239
            return ''
 
240
        if self.kind == 'symlink':
 
241
            return ''
 
242
        raise RuntimeError('unreachable code')
 
243
 
 
244
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
 
245
 
 
246
    def __new__(cls, file_id, name, kind, parent_id, text_id=None):
 
247
        """Factory method to return the appropriate concrete class."""
 
248
        return object.__new__(InventoryEntry, file_id, name, kind, parent_id,
 
249
                              text_id)
 
250
 
 
251
    def _put_in_tar(self, item, tree):
 
252
        """populate item for stashing in a tar, and return the content stream.
 
253
 
 
254
        If no content is available, return None.
 
255
        """
 
256
        if self.kind == 'file':
 
257
            item.type = tarfile.REGTYPE
 
258
            fileobj = tree.get_file(self.file_id)
 
259
            item.size = self.text_size
 
260
            if tree.is_executable(self.file_id):
 
261
                item.mode = 0755
 
262
            else:
 
263
                item.mode = 0644
 
264
        elif self.kind == 'symlink':
 
265
            iterm.type = tarfile.SYMTYPE
 
266
            fileobj = None
 
267
            item.size = 0
 
268
            item.mode = 0755
 
269
            item.linkname = self.symlink_target
 
270
        else:
 
271
            raise BzrError("don't know how to export {%s} of kind %r" %
 
272
                    (self.file_id, self.kind))
 
273
        return fileobj
 
274
 
 
275
    def put_on_disk(self, dest, dp, tree):
 
276
        """Create a representation of self on disk in the prefix dest.
 
277
        
 
278
        This is a template method - implement _put_on_disk in subclasses.
 
279
        """
 
280
        fullpath = appendpath(dest, dp)
 
281
        self._put_on_disk(fullpath, tree)
 
282
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
 
283
 
 
284
    def _put_on_disk(self, fullpath, tree):
 
285
        """Put this entry onto disk at fullpath, from tree tree."""
 
286
        if self.kind == 'file':
 
287
            pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
 
288
            if tree.is_executable(self.file_id):
 
289
                os.chmod(fullpath, 0755)
 
290
        elif self.kind == 'symlink':
 
291
            try:
 
292
                os.symlink(self.symlink_target, fullpath)
 
293
            except OSError,e:
 
294
                raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
 
295
        else:
 
296
            raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
 
297
 
 
298
    def sorted_children(self):
 
299
        l = self.children.items()
 
300
        l.sort()
 
301
        return l
 
302
 
 
303
    @staticmethod
 
304
    def versionable_kind(kind):
 
305
        return kind in ('file', 'directory', 'symlink')
 
306
 
 
307
    def check(self, checker, rev_id, inv, tree):
 
308
        """Check this inventory entry is intact.
 
309
 
 
310
        This is a template method, override _check for kind specific
 
311
        tests.
 
312
        """
 
313
        if self.parent_id != None:
 
314
            if not inv.has_id(self.parent_id):
 
315
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
 
316
                        % (self.parent_id, rev_id))
 
317
        self._check(checker, rev_id, tree)
 
318
 
 
319
    def _check(self, checker, rev_id, tree):
 
320
        """Check this inventory entry for kind specific errors."""
 
321
        if self.kind == 'file':
 
322
            revision = self.revision
 
323
            t = (self.file_id, revision)
 
324
            if t in checker.checked_texts:
 
325
                prev_sha = checker.checked_texts[t] 
 
326
                if prev_sha != self.text_sha1:
 
327
                    raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
 
328
                                        (self.file_id, rev_id))
 
329
                else:
 
330
                    checker.repeated_text_cnt += 1
 
331
                    return
 
332
            mutter('check version {%s} of {%s}', rev_id, self.file_id)
 
333
            file_lines = tree.get_file_lines(self.file_id)
 
334
            checker.checked_text_cnt += 1 
 
335
            if self.text_size != sum(map(len, file_lines)):
 
336
                raise BzrCheckError('text {%s} wrong size' % self.text_id)
 
337
            if self.text_sha1 != sha_strings(file_lines):
 
338
                raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
 
339
            checker.checked_texts[t] = self.text_sha1
 
340
        elif self.kind == 'symlink':
 
341
            if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
342
                raise BzrCheckError('symlink {%s} has text in revision {%s}'
 
343
                        % (self.file_id, rev_id))
 
344
            if self.symlink_target == None:
 
345
                raise BzrCheckError('symlink {%s} has no target in revision {%s}'
 
346
                        % (self.file_id, rev_id))
 
347
        else:
 
348
            raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
 
349
                                (self.kind, rev_id))
 
350
 
 
351
 
 
352
    def copy(self):
 
353
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
354
                               self.parent_id)
 
355
        other.executable = self.executable
 
356
        other.text_id = self.text_id
 
357
        other.text_sha1 = self.text_sha1
 
358
        other.text_size = self.text_size
 
359
        other.symlink_target = self.symlink_target
 
360
        other.revision = self.revision
 
361
        # note that children are *not* copied; they're pulled across when
 
362
        # others are added
 
363
        return other
 
364
 
 
365
    def _get_snapshot_change(self, previous_entries):
 
366
        if len(previous_entries) > 1:
 
367
            return 'merged'
 
368
        elif len(previous_entries) == 0:
 
369
            return 'added'
 
370
        else:
 
371
            return 'modified/renamed/reparented'
 
372
 
 
373
    def __repr__(self):
 
374
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
375
                % (self.__class__.__name__,
 
376
                   self.file_id,
 
377
                   self.name,
 
378
                   self.kind,
 
379
                   self.parent_id))
 
380
 
 
381
    def snapshot(self, revision, path, previous_entries, work_tree, 
 
382
                 weave_store):
 
383
        """Make a snapshot of this entry.
 
384
        
 
385
        This means that all its fields are populated, that it has its
 
386
        text stored in the text store or weave.
 
387
        """
 
388
        mutter('new parents of %s are %r', path, previous_entries)
 
389
        self._read_tree_state(path, work_tree)
 
390
        if len(previous_entries) == 1:
 
391
            # cannot be unchanged unless there is only one parent file rev.
 
392
            parent_ie = previous_entries.values()[0]
 
393
            if self._unchanged(path, parent_ie, work_tree):
 
394
                mutter("found unchanged entry")
 
395
                self.revision = parent_ie.revision
 
396
                return "unchanged"
 
397
        mutter('new revision for {%s}', self.file_id)
 
398
        self.revision = revision
 
399
        change = self._get_snapshot_change(previous_entries)
 
400
        if self.kind != 'file':
 
401
            return change
 
402
        self._snapshot_text(previous_entries, work_tree, weave_store)
 
403
        return change
 
404
 
 
405
    def _snapshot_text(self, file_parents, work_tree, weave_store): 
 
406
        mutter('storing file {%s} in revision {%s}',
 
407
               self.file_id, self.revision)
 
408
        # special case to avoid diffing on renames or 
 
409
        # reparenting
 
410
        if (len(file_parents) == 1
 
411
            and self.text_sha1 == file_parents.values()[0].text_sha1
 
412
            and self.text_size == file_parents.values()[0].text_size):
 
413
            previous_ie = file_parents.values()[0]
 
414
            weave_store.add_identical_text(
 
415
                self.file_id, previous_ie.revision, 
 
416
                self.revision, file_parents)
 
417
        else:
 
418
            new_lines = work_tree.get_file(self.file_id).readlines()
 
419
            self._add_text_to_weave(new_lines, file_parents, weave_store)
 
420
            self.text_sha1 = sha_strings(new_lines)
 
421
            self.text_size = sum(map(len, new_lines))
 
422
 
 
423
    def __eq__(self, other):
 
424
        if not isinstance(other, InventoryEntry):
 
425
            return NotImplemented
 
426
 
 
427
        return ((self.file_id == other.file_id)
 
428
                and (self.name == other.name)
 
429
                and (other.symlink_target == self.symlink_target)
 
430
                and (self.text_sha1 == other.text_sha1)
 
431
                and (self.text_size == other.text_size)
 
432
                and (self.text_id == other.text_id)
 
433
                and (self.parent_id == other.parent_id)
 
434
                and (self.kind == other.kind)
 
435
                and (self.revision == other.revision)
 
436
                and (self.executable == other.executable)
 
437
                )
 
438
 
 
439
    def __ne__(self, other):
 
440
        return not (self == other)
 
441
 
 
442
    def __hash__(self):
 
443
        raise ValueError('not hashable')
 
444
 
 
445
    def _unchanged(self, path, previous_ie, work_tree):
 
446
        compatible = True
 
447
        # different inv parent
 
448
        if previous_ie.parent_id != self.parent_id:
 
449
            compatible = False
 
450
        # renamed
 
451
        elif previous_ie.name != self.name:
 
452
            compatible = False
 
453
        if self.kind == 'symlink':
 
454
            if self.symlink_target != previous_ie.symlink_target:
 
455
                compatible = False
 
456
        if self.kind == 'file':
 
457
            if self.text_sha1 != previous_ie.text_sha1:
 
458
                compatible = False
 
459
            else:
 
460
                # FIXME: 20050930 probe for the text size when getting sha1
 
461
                # in _read_tree_state
 
462
                self.text_size = previous_ie.text_size
 
463
        return compatible
 
464
 
 
465
    def _read_tree_state(self, path, work_tree):
 
466
        if self.kind == 'symlink':
 
467
            self.symlink_target = work_tree.get_symlink_target(self.file_id)
 
468
        if self.kind == 'file':
 
469
            self.text_sha1 = work_tree.get_file_sha1(self.file_id)
 
470
            self.executable = work_tree.is_executable(self.file_id)
 
471
 
 
472
 
 
473
class RootEntry(InventoryEntry):
 
474
 
 
475
    def _check(self, checker, rev_id, tree):
 
476
        """See InventoryEntry._check"""
 
477
 
 
478
    def __init__(self, file_id):
 
479
        self.file_id = file_id
 
480
        self.children = {}
 
481
        self.kind = 'root_directory'
 
482
        self.parent_id = None
 
483
        self.name = ''
 
484
 
 
485
    def __new__(cls, file_id):
 
486
        """Only present until the InventoryEntry.__new__ can go away."""
 
487
        return object.__new__(RootEntry, file_id)
 
488
 
 
489
    def __eq__(self, other):
 
490
        if not isinstance(other, RootEntry):
 
491
            return NotImplemented
 
492
        
 
493
        return (self.file_id == other.file_id) \
 
494
               and (self.children == other.children)
 
495
 
 
496
 
 
497
class InventoryDirectory(InventoryEntry):
 
498
    """A directory in an inventory."""
 
499
 
 
500
    def _check(self, checker, rev_id, tree):
 
501
        """See InventoryEntry._check"""
 
502
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
 
503
            raise BzrCheckError('directory {%s} has text in revision {%s}'
 
504
                                % (self.file_id, rev_id))
 
505
 
 
506
    def copy(self):
 
507
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
 
508
        other.revision = self.revision
 
509
        # note that children are *not* copied; they're pulled across when
 
510
        # others are added
 
511
        return other
 
512
 
 
513
    def __init__(self, file_id, name, parent_id):
 
514
        super(InventoryDirectory, self).__init__(file_id, name, 'directory',
 
515
                                                 parent_id)
 
516
        self.children = {}
 
517
 
 
518
    def kind_character(self):
 
519
        """See InventoryEntry.kind_character."""
 
520
        return '/'
 
521
 
 
522
    def __new__(cls, file_id, name, parent_id):
 
523
        """Only present until the InventoryEntry.__new__ can go away."""
 
524
        result = object.__new__(InventoryDirectory, file_id, name, 'directory',
 
525
                                parent_id)
 
526
        # type.__call__ is strange, it doesn't __init__ when the returned type
 
527
        # differs
 
528
        #result.__init__(file_id, name, 'directory', parent_id)
 
529
        return result
 
530
 
 
531
    def _put_in_tar(self, item, tree):
 
532
        """See InventoryEntry._put_in_tar."""
 
533
        item.type = tarfile.DIRTYPE
 
534
        fileobj = None
 
535
        item.name += '/'
 
536
        item.size = 0
 
537
        item.mode = 0755
 
538
        return fileobj
 
539
 
 
540
    def _put_on_disk(self, fullpath, tree):
 
541
        """See InventoryEntry._put_on_disk."""
 
542
        os.mkdir(fullpath)
 
543
 
 
544
    def __repr__(self):
 
545
        return ("%s(%r, %r, parent_id=%r)"
 
546
                % (self.__class__.__name__,
 
547
                   self.file_id,
 
548
                   self.name,
 
549
                   self.parent_id))
 
550
 
 
551
 
 
552
class Inventory(object):
 
553
    """Inventory of versioned files in a tree.
 
554
 
 
555
    This describes which file_id is present at each point in the tree,
 
556
    and possibly the SHA-1 or other information about the file.
 
557
    Entries can be looked up either by path or by file_id.
 
558
 
 
559
    The inventory represents a typical unix file tree, with
 
560
    directories containing files and subdirectories.  We never store
 
561
    the full path to a file, because renaming a directory implicitly
 
562
    moves all of its contents.  This class internally maintains a
 
563
    lookup tree that allows the children under a directory to be
 
564
    returned quickly.
 
565
 
 
566
    InventoryEntry objects must not be modified after they are
 
567
    inserted, other than through the Inventory API.
 
568
 
 
569
    >>> inv = Inventory()
 
570
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
571
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
572
    >>> inv['123-123'].name
 
573
    'hello.c'
 
574
 
 
575
    May be treated as an iterator or set to look up file ids:
 
576
    
 
577
    >>> bool(inv.path2id('hello.c'))
 
578
    True
 
579
    >>> '123-123' in inv
 
580
    True
 
581
 
 
582
    May also look up by name:
 
583
 
 
584
    >>> [x[0] for x in inv.iter_entries()]
 
585
    ['hello.c']
 
586
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
587
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
588
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
589
    """
 
590
    def __init__(self, root_id=ROOT_ID):
 
591
        """Create or read an inventory.
 
592
 
 
593
        If a working directory is specified, the inventory is read
 
594
        from there.  If the file is specified, read from that. If not,
 
595
        the inventory is created empty.
 
596
 
 
597
        The inventory is created with a default root directory, with
 
598
        an id of None.
 
599
        """
 
600
        # We are letting Branch.initialize() create a unique inventory
 
601
        # root id. Rather than generating a random one here.
 
602
        #if root_id is None:
 
603
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
604
        self.root = RootEntry(root_id)
 
605
        self._byid = {self.root.file_id: self.root}
 
606
 
 
607
 
 
608
    def copy(self):
 
609
        other = Inventory(self.root.file_id)
 
610
        # copy recursively so we know directories will be added before
 
611
        # their children.  There are more efficient ways than this...
 
612
        for path, entry in self.iter_entries():
 
613
            if entry == self.root:
 
614
                continue
 
615
            other.add(entry.copy())
 
616
        return other
 
617
 
 
618
 
 
619
    def __iter__(self):
 
620
        return iter(self._byid)
 
621
 
 
622
 
 
623
    def __len__(self):
 
624
        """Returns number of entries."""
 
625
        return len(self._byid)
 
626
 
 
627
 
 
628
    def iter_entries(self, from_dir=None):
 
629
        """Return (path, entry) pairs, in order by name."""
 
630
        if from_dir == None:
 
631
            assert self.root
 
632
            from_dir = self.root
 
633
        elif isinstance(from_dir, basestring):
 
634
            from_dir = self._byid[from_dir]
 
635
            
 
636
        kids = from_dir.children.items()
 
637
        kids.sort()
 
638
        for name, ie in kids:
 
639
            yield name, ie
 
640
            if ie.kind == 'directory':
 
641
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
642
                    yield os.path.join(name, cn), cie
 
643
 
 
644
 
 
645
    def entries(self):
 
646
        """Return list of (path, ie) for all entries except the root.
 
647
 
 
648
        This may be faster than iter_entries.
 
649
        """
 
650
        accum = []
 
651
        def descend(dir_ie, dir_path):
 
652
            kids = dir_ie.children.items()
 
653
            kids.sort()
 
654
            for name, ie in kids:
 
655
                child_path = os.path.join(dir_path, name)
 
656
                accum.append((child_path, ie))
 
657
                if ie.kind == 'directory':
 
658
                    descend(ie, child_path)
 
659
 
 
660
        descend(self.root, '')
 
661
        return accum
 
662
 
 
663
 
 
664
    def directories(self):
 
665
        """Return (path, entry) pairs for all directories, including the root.
 
666
        """
 
667
        accum = []
 
668
        def descend(parent_ie, parent_path):
 
669
            accum.append((parent_path, parent_ie))
 
670
            
 
671
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
672
            kids.sort()
 
673
 
 
674
            for name, child_ie in kids:
 
675
                child_path = os.path.join(parent_path, name)
 
676
                descend(child_ie, child_path)
 
677
        descend(self.root, '')
 
678
        return accum
 
679
        
 
680
 
 
681
 
 
682
    def __contains__(self, file_id):
 
683
        """True if this entry contains a file with given id.
 
684
 
 
685
        >>> inv = Inventory()
 
686
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
687
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
688
        >>> '123' in inv
 
689
        True
 
690
        >>> '456' in inv
 
691
        False
 
692
        """
 
693
        return file_id in self._byid
 
694
 
 
695
 
 
696
    def __getitem__(self, file_id):
 
697
        """Return the entry for given file_id.
 
698
 
 
699
        >>> inv = Inventory()
 
700
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
701
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
702
        >>> inv['123123'].name
 
703
        'hello.c'
 
704
        """
 
705
        try:
 
706
            return self._byid[file_id]
 
707
        except KeyError:
 
708
            if file_id == None:
 
709
                raise BzrError("can't look up file_id None")
 
710
            else:
 
711
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
712
 
 
713
 
 
714
    def get_file_kind(self, file_id):
 
715
        return self._byid[file_id].kind
 
716
 
 
717
    def get_child(self, parent_id, filename):
 
718
        return self[parent_id].children.get(filename)
 
719
 
 
720
 
 
721
    def add(self, entry):
 
722
        """Add entry to inventory.
 
723
 
 
724
        To add  a file to a branch ready to be committed, use Branch.add,
 
725
        which calls this.
 
726
 
 
727
        Returns the new entry object.
 
728
        """
 
729
        if entry.file_id in self._byid:
 
730
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
731
 
 
732
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
733
            entry.parent_id = self.root.file_id
 
734
 
 
735
        try:
 
736
            parent = self._byid[entry.parent_id]
 
737
        except KeyError:
 
738
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
739
 
 
740
        if parent.children.has_key(entry.name):
 
741
            raise BzrError("%s is already versioned" %
 
742
                    appendpath(self.id2path(parent.file_id), entry.name))
 
743
 
 
744
        self._byid[entry.file_id] = entry
 
745
        parent.children[entry.name] = entry
 
746
        return entry
 
747
 
 
748
 
 
749
    def add_path(self, relpath, kind, file_id=None):
 
750
        """Add entry from a path.
 
751
 
 
752
        The immediate parent must already be versioned.
 
753
 
 
754
        Returns the new entry object."""
 
755
        from bzrlib.branch import gen_file_id
 
756
        
 
757
        parts = bzrlib.osutils.splitpath(relpath)
 
758
        if len(parts) == 0:
 
759
            raise BzrError("cannot re-add root of inventory")
 
760
 
 
761
        if file_id == None:
 
762
            file_id = gen_file_id(relpath)
 
763
 
 
764
        parent_path = parts[:-1]
 
765
        parent_id = self.path2id(parent_path)
 
766
        if parent_id == None:
 
767
            raise NotVersionedError(parent_path)
 
768
 
 
769
        if kind == 'directory':
 
770
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
 
771
        else:
 
772
            ie = InventoryEntry(file_id, parts[-1],
 
773
                                kind=kind, parent_id=parent_id)
 
774
        return self.add(ie)
 
775
 
 
776
 
 
777
    def __delitem__(self, file_id):
 
778
        """Remove entry by id.
 
779
 
 
780
        >>> inv = Inventory()
 
781
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
782
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
783
        >>> '123' in inv
 
784
        True
 
785
        >>> del inv['123']
 
786
        >>> '123' in inv
 
787
        False
 
788
        """
 
789
        ie = self[file_id]
 
790
 
 
791
        assert self[ie.parent_id].children[ie.name] == ie
 
792
        
 
793
        # TODO: Test deleting all children; maybe hoist to a separate
 
794
        # deltree method?
 
795
        if ie.kind == 'directory':
 
796
            for cie in ie.children.values():
 
797
                del self[cie.file_id]
 
798
            del ie.children
 
799
 
 
800
        del self._byid[file_id]
 
801
        del self[ie.parent_id].children[ie.name]
 
802
 
 
803
 
 
804
    def __eq__(self, other):
 
805
        """Compare two sets by comparing their contents.
 
806
 
 
807
        >>> i1 = Inventory()
 
808
        >>> i2 = Inventory()
 
809
        >>> i1 == i2
 
810
        True
 
811
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
812
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
813
        >>> i1 == i2
 
814
        False
 
815
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
816
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
817
        >>> i1 == i2
 
818
        True
 
819
        """
 
820
        if not isinstance(other, Inventory):
 
821
            return NotImplemented
 
822
 
 
823
        if len(self._byid) != len(other._byid):
 
824
            # shortcut: obviously not the same
 
825
            return False
 
826
 
 
827
        return self._byid == other._byid
 
828
 
 
829
 
 
830
    def __ne__(self, other):
 
831
        return not self.__eq__(other)
 
832
 
 
833
 
 
834
    def __hash__(self):
 
835
        raise ValueError('not hashable')
 
836
 
 
837
 
 
838
    def get_idpath(self, file_id):
 
839
        """Return a list of file_ids for the path to an entry.
 
840
 
 
841
        The list contains one element for each directory followed by
 
842
        the id of the file itself.  So the length of the returned list
 
843
        is equal to the depth of the file in the tree, counting the
 
844
        root directory as depth 1.
 
845
        """
 
846
        p = []
 
847
        while file_id != None:
 
848
            try:
 
849
                ie = self._byid[file_id]
 
850
            except KeyError:
 
851
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
852
            p.insert(0, ie.file_id)
 
853
            file_id = ie.parent_id
 
854
        return p
 
855
 
 
856
 
 
857
    def id2path(self, file_id):
 
858
        """Return as a list the path to file_id."""
 
859
 
 
860
        # get all names, skipping root
 
861
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
862
        return os.sep.join(p)
 
863
            
 
864
 
 
865
 
 
866
    def path2id(self, name):
 
867
        """Walk down through directories to return entry of last component.
 
868
 
 
869
        names may be either a list of path components, or a single
 
870
        string, in which case it is automatically split.
 
871
 
 
872
        This returns the entry of the last component in the path,
 
873
        which may be either a file or a directory.
 
874
 
 
875
        Returns None iff the path is not found.
 
876
        """
 
877
        if isinstance(name, types.StringTypes):
 
878
            name = splitpath(name)
 
879
 
 
880
        mutter("lookup path %r" % name)
 
881
 
 
882
        parent = self.root
 
883
        for f in name:
 
884
            try:
 
885
                cie = parent.children[f]
 
886
                assert cie.name == f
 
887
                assert cie.parent_id == parent.file_id
 
888
                parent = cie
 
889
            except KeyError:
 
890
                # or raise an error?
 
891
                return None
 
892
 
 
893
        return parent.file_id
 
894
 
 
895
 
 
896
    def has_filename(self, names):
 
897
        return bool(self.path2id(names))
 
898
 
 
899
 
 
900
    def has_id(self, file_id):
 
901
        return self._byid.has_key(file_id)
 
902
 
 
903
 
 
904
    def rename(self, file_id, new_parent_id, new_name):
 
905
        """Move a file within the inventory.
 
906
 
 
907
        This can change either the name, or the parent, or both.
 
908
 
 
909
        This does not move the working file."""
 
910
        if not is_valid_name(new_name):
 
911
            raise BzrError("not an acceptable filename: %r" % new_name)
 
912
 
 
913
        new_parent = self._byid[new_parent_id]
 
914
        if new_name in new_parent.children:
 
915
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
916
 
 
917
        new_parent_idpath = self.get_idpath(new_parent_id)
 
918
        if file_id in new_parent_idpath:
 
919
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
920
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
921
 
 
922
        file_ie = self._byid[file_id]
 
923
        old_parent = self._byid[file_ie.parent_id]
 
924
 
 
925
        # TODO: Don't leave things messed up if this fails
 
926
 
 
927
        del old_parent.children[file_ie.name]
 
928
        new_parent.children[new_name] = file_ie
 
929
        
 
930
        file_ie.name = new_name
 
931
        file_ie.parent_id = new_parent_id
 
932
 
 
933
 
 
934
 
 
935
 
 
936
_NAME_RE = None
 
937
 
 
938
def is_valid_name(name):
 
939
    global _NAME_RE
 
940
    if _NAME_RE == None:
 
941
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
942
        
 
943
    return bool(_NAME_RE.match(name))