~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2006-05-18 07:10:08 UTC
  • mto: (1713.1.3 integration)
  • mto: This revision was merged to the branch mainline in revision 1714.
  • Revision ID: robertc@robertcollins.net-20060518071008-96bc24f18116be36
'bzr add' is now less verbose in telling you what ignore globs were
matched by files being ignored. Instead it just tells you how many were ignored
(because you might reasonably be expecting none to be ignored). 'bzr add -v' is
unchanged and will report every ignored file. (Robert Collins).

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# (C) 2005 Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005, 2006 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.
17
21
 
18
22
# TODO: Maybe also keep the full path of the entry, and the children?
19
23
# But those depend on its position within a particular inventory, and
31
35
import types
32
36
 
33
37
import bzrlib
34
 
from bzrlib.errors import BzrError, BzrCheckError
35
 
 
36
38
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
37
 
                            appendpath, sha_strings)
 
39
                            pathjoin, sha_strings)
 
40
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
 
41
                           BzrError, BzrCheckError, BinaryFile)
38
42
from bzrlib.trace import mutter
39
 
from bzrlib.errors import NotVersionedError
40
43
 
41
44
 
42
45
class InventoryEntry(object):
76
79
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
77
80
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
81
    InventoryFile('2323', 'hello.c', parent_id='123')
79
 
    >>> for j in i.iter_entries():
80
 
    ...   print j
 
82
    >>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
 
83
    >>> for ix, j in enumerate(i.iter_entries()):
 
84
    ...   print (j[0] == shouldbe[ix], j[1])
81
85
    ... 
82
 
    ('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
 
    ('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
 
86
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
 
87
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
84
88
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
89
    Traceback (most recent call last):
86
90
    ...
98
102
    >>> i['2326']
99
103
    InventoryFile('2326', 'wibble.c', parent_id='2325')
100
104
    >>> for path, entry in i.iter_entries():
101
 
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
105
    ...     print path
102
106
    ...     assert i.path2id(path)
103
107
    ... 
104
108
    src
106
110
    src/hello.c
107
111
    src/wibble
108
112
    src/wibble/wibble.c
109
 
    >>> i.id2path('2326').replace('\\\\', '/')
 
113
    >>> i.id2path('2326')
110
114
    'src/wibble/wibble.c'
111
115
    """
 
116
 
 
117
    # Constants returned by describe_change()
 
118
    #
 
119
    # TODO: These should probably move to some kind of FileChangeDescription 
 
120
    # class; that's like what's inside a TreeDelta but we want to be able to 
 
121
    # generate them just for one file at a time.
 
122
    RENAMED = 'renamed'
 
123
    MODIFIED_AND_RENAMED = 'modified and renamed'
112
124
    
113
125
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
126
                 'text_id', 'parent_id', 'children', 'executable', 
115
127
                 'revision']
116
128
 
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)
 
129
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
 
130
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
 
131
                                                       transaction)
 
132
        versionedfile.add_lines(self.revision, parents, new_lines)
 
133
        versionedfile.clear_cache()
119
134
 
120
135
    def detect_changes(self, old_entry):
121
136
        """Return a (text_modified, meta_modified) from this to old_entry.
146
161
             output_to, reverse=False):
147
162
        """Perform a diff between two entries of the same kind."""
148
163
 
149
 
    def find_previous_heads(self, previous_inventories, entry_weave):
 
164
    def find_previous_heads(self, previous_inventories,
 
165
                            versioned_file_store,
 
166
                            transaction,
 
167
                            entry_vf=None):
150
168
        """Return the revisions and entries that directly preceed this.
151
169
 
152
170
        Returned as a map from revision to inventory entry.
154
172
        This is a map containing the file revisions in all parents
155
173
        for which the file exists, and its revision is not a parent of
156
174
        any other. If the file is new, the set will be empty.
 
175
 
 
176
        :param versioned_file_store: A store where ancestry data on this
 
177
                                     file id can be queried.
 
178
        :param transaction: The transaction that queries to the versioned 
 
179
                            file store should be completed under.
 
180
        :param entry_vf: The entry versioned file, if its already available.
157
181
        """
158
182
        def get_ancestors(weave, entry):
159
 
            return set(map(weave.idx_to_name,
160
 
                           weave.inclusions([weave.lookup(entry.revision)])))
 
183
            return set(weave.get_ancestry(entry.revision))
 
184
        # revision:ie mapping for each ie found in previous_inventories.
 
185
        candidates = {}
 
186
        # revision:ie mapping with one revision for each head.
161
187
        heads = {}
 
188
        # revision: ancestor list for each head
162
189
        head_ancestors = {}
 
190
        # identify candidate head revision ids.
163
191
        for inv in previous_inventories:
164
192
            if self.file_id in inv:
165
193
                ie = inv[self.file_id]
166
194
                assert ie.file_id == self.file_id
167
 
                if ie.revision in heads:
168
 
                    assert heads[ie.revision] == ie
 
195
                if ie.revision in candidates:
 
196
                    # same revision value in two different inventories:
 
197
                    # correct possible inconsistencies:
 
198
                    #     * there was a bug in revision updates with 'x' bit 
 
199
                    #       support.
 
200
                    try:
 
201
                        if candidates[ie.revision].executable != ie.executable:
 
202
                            candidates[ie.revision].executable = False
 
203
                            ie.executable = False
 
204
                    except AttributeError:
 
205
                        pass
 
206
                    # must now be the same.
 
207
                    assert candidates[ie.revision] == ie
169
208
                else:
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
 
209
                    # add this revision as a candidate.
 
210
                    candidates[ie.revision] = ie
 
211
 
 
212
        # common case optimisation
 
213
        if len(candidates) == 1:
 
214
            # if there is only one candidate revision found
 
215
            # then we can opening the versioned file to access ancestry:
 
216
            # there cannot be any ancestors to eliminate when there is 
 
217
            # only one revision available.
 
218
            heads[ie.revision] = ie
 
219
            return heads
 
220
 
 
221
        # eliminate ancestors amongst the available candidates:
 
222
        # heads are those that are not an ancestor of any other candidate
 
223
        # - this provides convergence at a per-file level.
 
224
        for ie in candidates.values():
 
225
            # may be an ancestor of a known head:
 
226
            already_present = 0 != len(
 
227
                [head for head in heads 
 
228
                 if ie.revision in head_ancestors[head]])
 
229
            if already_present:
 
230
                # an ancestor of an analyzed candidate.
 
231
                continue
 
232
            # not an ancestor of a known head:
 
233
            # load the versioned file for this file id if needed
 
234
            if entry_vf is None:
 
235
                entry_vf = versioned_file_store.get_weave_or_empty(
 
236
                    self.file_id, transaction)
 
237
            ancestors = get_ancestors(entry_vf, ie)
 
238
            # may knock something else out:
 
239
            check_heads = list(heads.keys())
 
240
            for head in check_heads:
 
241
                if head in ancestors:
 
242
                    # this previously discovered 'head' is not
 
243
                    # really a head - its an ancestor of the newly 
 
244
                    # found head,
 
245
                    heads.pop(head)
 
246
            head_ancestors[ie.revision] = ancestors
 
247
            heads[ie.revision] = ie
188
248
        return heads
189
249
 
190
250
    def get_tar_item(self, root, dp, now, tree):
191
251
        """Get a tarfile item and a file stream for its content."""
192
 
        item = tarfile.TarInfo(os.path.join(root, dp))
 
252
        item = tarfile.TarInfo(pathjoin(root, dp))
193
253
        # TODO: would be cool to actually set it to the timestamp of the
194
254
        # revision it was last changed
195
255
        item.mtime = now
220
280
        '123'
221
281
        >>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
222
282
        Traceback (most recent call last):
223
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
283
        InvalidEntryName: Invalid entry name: src/hello.c
224
284
        """
225
285
        assert isinstance(name, basestring), name
226
286
        if '/' in name or '\\' in name:
227
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
228
 
        
 
287
            raise InvalidEntryName(name=name)
229
288
        self.executable = False
230
289
        self.revision = None
231
290
        self.text_sha1 = None
255
314
        
256
315
        This is a template method - implement _put_on_disk in subclasses.
257
316
        """
258
 
        fullpath = appendpath(dest, dp)
 
317
        fullpath = pathjoin(dest, dp)
259
318
        self._put_on_disk(fullpath, tree)
260
 
        mutter("  export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
 
319
        mutter("  export {%s} kind %s to %s", self.file_id,
 
320
                self.kind, fullpath)
261
321
 
262
322
    def _put_on_disk(self, fullpath, tree):
263
323
        """Put this entry onto disk at fullpath, from tree tree."""
277
337
 
278
338
        This is a template method, override _check for kind specific
279
339
        tests.
 
340
 
 
341
        :param checker: Check object providing context for the checks; 
 
342
             can be used to find out what parts of the repository have already
 
343
             been checked.
 
344
        :param rev_id: Revision id from which this InventoryEntry was loaded.
 
345
             Not necessarily the last-changed revision for this file.
 
346
        :param inv: Inventory from which the entry was loaded.
 
347
        :param tree: RevisionTree for this entry.
280
348
        """
281
349
        if self.parent_id != None:
282
350
            if not inv.has_id(self.parent_id):
289
357
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
290
358
                            (self.kind, rev_id))
291
359
 
292
 
 
293
360
    def copy(self):
294
361
        """Clone this inventory entry."""
295
362
        raise NotImplementedError
296
363
 
297
 
    def _get_snapshot_change(self, previous_entries):
298
 
        if len(previous_entries) > 1:
299
 
            return 'merged'
300
 
        elif len(previous_entries) == 0:
 
364
    @staticmethod
 
365
    def describe_change(old_entry, new_entry):
 
366
        """Describe the change between old_entry and this.
 
367
        
 
368
        This smells of being an InterInventoryEntry situation, but as its
 
369
        the first one, we're making it a static method for now.
 
370
 
 
371
        An entry with a different parent, or different name is considered 
 
372
        to be renamed. Reparenting is an internal detail.
 
373
        Note that renaming the parent does not trigger a rename for the
 
374
        child entry itself.
 
375
        """
 
376
        # TODO: Perhaps return an object rather than just a string
 
377
        if old_entry is new_entry:
 
378
            # also the case of both being None
 
379
            return 'unchanged'
 
380
        elif old_entry is None:
301
381
            return 'added'
302
 
        else:
303
 
            return 'modified/renamed/reparented'
 
382
        elif new_entry is None:
 
383
            return 'removed'
 
384
        text_modified, meta_modified = new_entry.detect_changes(old_entry)
 
385
        if text_modified or meta_modified:
 
386
            modified = True
 
387
        else:
 
388
            modified = False
 
389
        # TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
 
390
        if old_entry.parent_id != new_entry.parent_id:
 
391
            renamed = True
 
392
        elif old_entry.name != new_entry.name:
 
393
            renamed = True
 
394
        else:
 
395
            renamed = False
 
396
        if renamed and not modified:
 
397
            return InventoryEntry.RENAMED
 
398
        if modified and not renamed:
 
399
            return 'modified'
 
400
        if modified and renamed:
 
401
            return InventoryEntry.MODIFIED_AND_RENAMED
 
402
        return 'unchanged'
304
403
 
305
404
    def __repr__(self):
306
405
        return ("%s(%r, %r, parent_id=%r)"
310
409
                   self.parent_id))
311
410
 
312
411
    def snapshot(self, revision, path, previous_entries,
313
 
                 work_tree, weave_store):
 
412
                 work_tree, weave_store, transaction):
314
413
        """Make a snapshot of this entry which may or may not have changed.
315
414
        
316
415
        This means that all its fields are populated, that it has its
325
424
                mutter("found unchanged entry")
326
425
                self.revision = parent_ie.revision
327
426
                return "unchanged"
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)
 
427
        return self._snapshot_into_revision(revision, previous_entries, 
 
428
                                            work_tree, weave_store, transaction)
 
429
 
 
430
    def _snapshot_into_revision(self, revision, previous_entries, work_tree,
 
431
                                weave_store, transaction):
 
432
        """Record this revision unconditionally into a store.
 
433
 
 
434
        The entry's last-changed revision property (`revision`) is updated to 
 
435
        that of the new revision.
 
436
        
 
437
        :param revision: id of the new revision that is being recorded.
 
438
 
 
439
        :returns: String description of the commit (e.g. "merged", "modified"), etc.
 
440
        """
 
441
        mutter('new revision {%s} for {%s}', revision, self.file_id)
335
442
        self.revision = revision
336
 
        change = self._get_snapshot_change(previous_entries)
337
 
        self._snapshot_text(previous_entries, work_tree, weave_store)
338
 
        return change
 
443
        self._snapshot_text(previous_entries, work_tree, weave_store,
 
444
                            transaction)
339
445
 
340
 
    def _snapshot_text(self, file_parents, work_tree, weave_store): 
 
446
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
341
447
        """Record the 'text' of this entry, whatever form that takes.
342
448
        
343
449
        This default implementation simply adds an empty text.
344
450
        """
345
451
        mutter('storing file {%s} in revision {%s}',
346
452
               self.file_id, self.revision)
347
 
        self._add_text_to_weave([], file_parents, weave_store)
 
453
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
348
454
 
349
455
    def __eq__(self, other):
350
456
        if not isinstance(other, InventoryEntry):
388
494
        Note that this should be modified to be a noop on virtual trees
389
495
        as all entries created there are prepopulated.
390
496
        """
 
497
        # TODO: Rather than running this manually, we should check the 
 
498
        # working sha1 and other expensive properties when they're
 
499
        # first requested, or preload them if they're already known
 
500
        pass            # nothing to do by default
 
501
 
 
502
    def _forget_tree_state(self):
 
503
        pass
391
504
 
392
505
 
393
506
class RootEntry(InventoryEntry):
400
513
        self.children = {}
401
514
        self.kind = 'root_directory'
402
515
        self.parent_id = None
403
 
        self.name = ''
 
516
        self.name = u''
404
517
 
405
518
    def __eq__(self, other):
406
519
        if not isinstance(other, RootEntry):
452
565
class InventoryFile(InventoryEntry):
453
566
    """A file in an inventory."""
454
567
 
455
 
    def _check(self, checker, rev_id, tree):
 
568
    def _check(self, checker, tree_revision_id, tree):
456
569
        """See InventoryEntry._check"""
457
 
        revision = self.revision
458
 
        t = (self.file_id, revision)
 
570
        t = (self.file_id, self.revision)
459
571
        if t in checker.checked_texts:
460
 
            prev_sha = checker.checked_texts[t] 
 
572
            prev_sha = checker.checked_texts[t]
461
573
            if prev_sha != self.text_sha1:
462
574
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
463
 
                                    (self.file_id, rev_id))
 
575
                                    (self.file_id, tree_revision_id))
464
576
            else:
465
577
                checker.repeated_text_cnt += 1
466
578
                return
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)
 
579
 
 
580
        if self.file_id not in checker.checked_weaves:
 
581
            mutter('check weave {%s}', self.file_id)
 
582
            w = tree.get_weave(self.file_id)
 
583
            # Not passing a progress bar, because it creates a new
 
584
            # progress, which overwrites the current progress,
 
585
            # and doesn't look nice
 
586
            w.check()
 
587
            checker.checked_weaves[self.file_id] = True
 
588
        else:
 
589
            w = tree.get_weave(self.file_id)
 
590
 
 
591
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
 
592
        checker.checked_text_cnt += 1
 
593
        # We can't check the length, because Weave doesn't store that
 
594
        # information, and the whole point of looking at the weave's
 
595
        # sha1sum is that we don't have to extract the text.
 
596
        if self.text_sha1 != w.get_sha1(self.revision):
 
597
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
 
598
                                % (self.file_id, self.revision))
474
599
        checker.checked_texts[t] = self.text_sha1
475
600
 
476
601
    def copy(self):
493
618
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
494
619
             output_to, reverse=False):
495
620
        """See InventoryEntry._diff."""
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)
 
621
        try:
 
622
            from_text = tree.get_file(self.file_id).readlines()
 
623
            if to_entry:
 
624
                to_text = to_tree.get_file(to_entry.file_id).readlines()
 
625
            else:
 
626
                to_text = []
 
627
            if not reverse:
 
628
                text_diff(from_label, from_text,
 
629
                          to_label, to_text, output_to)
 
630
            else:
 
631
                text_diff(to_label, to_text,
 
632
                          from_label, from_text, output_to)
 
633
        except BinaryFile:
 
634
            if reverse:
 
635
                label_pair = (to_label, from_label)
 
636
            else:
 
637
                label_pair = (from_label, to_label)
 
638
            print >> output_to, "Binary files %s and %s differ" % label_pair
507
639
 
508
640
    def has_text(self):
509
641
        """See InventoryEntry.has_text."""
539
671
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
540
672
        self.executable = work_tree.is_executable(self.file_id)
541
673
 
542
 
    def _snapshot_text(self, file_parents, work_tree, weave_store): 
 
674
    def _forget_tree_state(self):
 
675
        self.text_sha1 = None
 
676
        self.executable = None
 
677
 
 
678
    def _snapshot_text(self, file_parents, work_tree, versionedfile_store, transaction):
543
679
        """See InventoryEntry._snapshot_text."""
544
 
        mutter('storing file {%s} in revision {%s}',
545
 
               self.file_id, self.revision)
 
680
        mutter('storing text of file {%s} in revision {%s} into %r',
 
681
               self.file_id, self.revision, versionedfile_store)
546
682
        # special case to avoid diffing on renames or 
547
683
        # reparenting
548
684
        if (len(file_parents) == 1
549
685
            and self.text_sha1 == file_parents.values()[0].text_sha1
550
686
            and self.text_size == file_parents.values()[0].text_size):
551
687
            previous_ie = file_parents.values()[0]
552
 
            weave_store.add_identical_text(
553
 
                self.file_id, previous_ie.revision, 
554
 
                self.revision, file_parents)
 
688
            versionedfile = versionedfile_store.get_weave(self.file_id, transaction)
 
689
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
555
690
        else:
556
691
            new_lines = work_tree.get_file(self.file_id).readlines()
557
 
            self._add_text_to_weave(new_lines, file_parents, weave_store)
 
692
            self._add_text_to_weave(new_lines, file_parents.keys(), versionedfile_store,
 
693
                                    transaction)
558
694
            self.text_sha1 = sha_strings(new_lines)
559
695
            self.text_size = sum(map(len, new_lines))
560
696
 
568
704
            # FIXME: 20050930 probe for the text size when getting sha1
569
705
            # in _read_tree_state
570
706
            self.text_size = previous_ie.text_size
 
707
        if self.executable != previous_ie.executable:
 
708
            compatible = False
571
709
        return compatible
572
710
 
573
711
 
627
765
 
628
766
    def _put_in_tar(self, item, tree):
629
767
        """See InventoryEntry._put_in_tar."""
630
 
        iterm.type = tarfile.SYMTYPE
 
768
        item.type = tarfile.SYMTYPE
631
769
        fileobj = None
632
770
        item.size = 0
633
771
        item.mode = 0755
645
783
        """See InventoryEntry._read_tree_state."""
646
784
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
647
785
 
 
786
    def _forget_tree_state(self):
 
787
        self.symlink_target = None
 
788
 
648
789
    def _unchanged(self, previous_ie):
649
790
        """See InventoryEntry._unchanged."""
650
791
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
691
832
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
692
833
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
693
834
    """
694
 
    def __init__(self, root_id=ROOT_ID):
 
835
    def __init__(self, root_id=ROOT_ID, revision_id=None):
695
836
        """Create or read an inventory.
696
837
 
697
838
        If a working directory is specified, the inventory is read
701
842
        The inventory is created with a default root directory, with
702
843
        an id of None.
703
844
        """
704
 
        # We are letting Branch.initialize() create a unique inventory
 
845
        # We are letting Branch.create() create a unique inventory
705
846
        # root id. Rather than generating a random one here.
706
847
        #if root_id is None:
707
848
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
708
849
        self.root = RootEntry(root_id)
 
850
        self.revision_id = revision_id
709
851
        self._byid = {self.root.file_id: self.root}
710
852
 
711
853
 
712
854
    def copy(self):
 
855
        # TODO: jam 20051218 Should copy also copy the revision_id?
713
856
        other = Inventory(self.root.file_id)
714
857
        # copy recursively so we know directories will be added before
715
858
        # their children.  There are more efficient ways than this...
743
886
            yield name, ie
744
887
            if ie.kind == 'directory':
745
888
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
746
 
                    yield os.path.join(name, cn), cie
 
889
                    yield pathjoin(name, cn), cie
747
890
 
748
891
 
749
892
    def entries(self):
756
899
            kids = dir_ie.children.items()
757
900
            kids.sort()
758
901
            for name, ie in kids:
759
 
                child_path = os.path.join(dir_path, name)
 
902
                child_path = pathjoin(dir_path, name)
760
903
                accum.append((child_path, ie))
761
904
                if ie.kind == 'directory':
762
905
                    descend(ie, child_path)
763
906
 
764
 
        descend(self.root, '')
 
907
        descend(self.root, u'')
765
908
        return accum
766
909
 
767
910
 
776
919
            kids.sort()
777
920
 
778
921
            for name, child_ie in kids:
779
 
                child_path = os.path.join(parent_path, name)
 
922
                child_path = pathjoin(parent_path, name)
780
923
                descend(child_ie, child_path)
781
 
        descend(self.root, '')
 
924
        descend(self.root, u'')
782
925
        return accum
783
926
        
784
927
 
843
986
 
844
987
        if parent.children.has_key(entry.name):
845
988
            raise BzrError("%s is already versioned" %
846
 
                    appendpath(self.id2path(parent.file_id), entry.name))
 
989
                    pathjoin(self.id2path(parent.file_id), entry.name))
847
990
 
848
991
        self._byid[entry.file_id] = entry
849
992
        parent.children[entry.name] = entry
856
999
        The immediate parent must already be versioned.
857
1000
 
858
1001
        Returns the new entry object."""
859
 
        from bzrlib.branch import gen_file_id
 
1002
        from bzrlib.workingtree import gen_file_id
860
1003
        
861
1004
        parts = bzrlib.osutils.splitpath(relpath)
862
 
        if len(parts) == 0:
863
 
            raise BzrError("cannot re-add root of inventory")
864
1005
 
865
1006
        if file_id == None:
866
1007
            file_id = gen_file_id(relpath)
867
1008
 
868
 
        parent_path = parts[:-1]
869
 
        parent_id = self.path2id(parent_path)
870
 
        if parent_id == None:
871
 
            raise NotVersionedError(parent_path)
872
 
 
 
1009
        if len(parts) == 0:
 
1010
            self.root = RootEntry(file_id)
 
1011
            self._byid = {self.root.file_id: self.root}
 
1012
            return
 
1013
        else:
 
1014
            parent_path = parts[:-1]
 
1015
            parent_id = self.path2id(parent_path)
 
1016
            if parent_id == None:
 
1017
                raise NotVersionedError(path=parent_path)
873
1018
        if kind == 'directory':
874
1019
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
875
1020
        elif kind == 'file':
895
1040
        """
896
1041
        ie = self[file_id]
897
1042
 
898
 
        assert self[ie.parent_id].children[ie.name] == ie
 
1043
        assert ie.parent_id is None or \
 
1044
            self[ie.parent_id].children[ie.name] == ie
899
1045
        
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
 
 
907
1046
        del self._byid[file_id]
908
 
        del self[ie.parent_id].children[ie.name]
 
1047
        if ie.parent_id is not None:
 
1048
            del self[ie.parent_id].children[ie.name]
909
1049
 
910
1050
 
911
1051
    def __eq__(self, other):
941
1081
    def __hash__(self):
942
1082
        raise ValueError('not hashable')
943
1083
 
 
1084
    def _iter_file_id_parents(self, file_id):
 
1085
        """Yield the parents of file_id up to the root."""
 
1086
        while file_id != None:
 
1087
            try:
 
1088
                ie = self._byid[file_id]
 
1089
            except KeyError:
 
1090
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
1091
            yield ie
 
1092
            file_id = ie.parent_id
944
1093
 
945
1094
    def get_idpath(self, file_id):
946
1095
        """Return a list of file_ids for the path to an entry.
951
1100
        root directory as depth 1.
952
1101
        """
953
1102
        p = []
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
 
1103
        for parent in self._iter_file_id_parents(file_id):
 
1104
            p.insert(0, parent.file_id)
961
1105
        return p
962
1106
 
963
 
 
964
1107
    def id2path(self, file_id):
965
 
        """Return as a list the path to file_id.
 
1108
        """Return as a string the path to file_id.
966
1109
        
967
1110
        >>> i = Inventory()
968
1111
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
969
1112
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
970
 
        >>> print i.id2path('foo-id').replace(os.sep, '/')
 
1113
        >>> print i.id2path('foo-id')
971
1114
        src/foo.c
972
1115
        """
973
1116
        # get all names, skipping root
974
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
975
 
        return os.sep.join(p)
 
1117
        return '/'.join(reversed(
 
1118
            [parent.name for parent in 
 
1119
             self._iter_file_id_parents(file_id)][:-1]))
976
1120
            
977
 
 
978
 
 
979
1121
    def path2id(self, name):
980
1122
        """Walk down through directories to return entry of last component.
981
1123