/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: Martin Pool
  • Date: 2005-10-06 04:02:15 UTC
  • mto: (1185.13.3)
  • mto: This revision was merged to the branch mainline in revision 1417.
  • Revision ID: mbp@sourcefrog.net-20051006040215-50eb01ef45ddd178
- doc and small refactoring of log code

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