~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Robert Collins
  • Date: 2005-08-23 06:52:09 UTC
  • mto: (974.1.50) (1185.1.10) (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1139.
  • Revision ID: robertc@robertcollins.net-20050823065209-81cd5962c401751b
move io redirection into each test case from the global runner

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# (C) 2005 Canonical Ltd
2
 
#
 
2
 
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
#
 
7
 
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
#
 
12
 
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
# FIXME: This refactoring of the workingtree code doesn't seem to keep 
18
 
# the WorkingTree's copy of the inventory in sync with the branch.  The
19
 
# branch modifies its working inventory when it does a commit to make
20
 
# missing files permanently removed.
21
 
 
22
 
# TODO: Maybe also keep the full path of the entry, and the children?
23
 
# But those depend on its position within a particular inventory, and
24
 
# it would be nice not to need to hold the backpointer here.
25
17
 
26
18
# This should really be an id randomly assigned when the tree is
27
19
# created, but it's not for now.
28
20
ROOT_ID = "TREE_ROOT"
29
21
 
30
22
 
31
 
import os.path
32
 
import re
33
 
import sys
34
 
import tarfile
35
 
import types
 
23
import sys, os.path, types, re
36
24
 
37
25
import bzrlib
38
 
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
39
 
                            pathjoin, sha_strings)
 
26
from bzrlib.errors import BzrError, BzrCheckError
 
27
 
 
28
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
40
29
from bzrlib.trace import mutter
41
 
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
 
                           BzrError, BzrCheckError)
43
 
 
 
30
from bzrlib.errors import NotVersionedError
 
31
        
44
32
 
45
33
class InventoryEntry(object):
46
34
    """Description of a versioned file.
48
36
    An InventoryEntry has the following fields, which are also
49
37
    present in the XML inventory-entry element:
50
38
 
51
 
    file_id
52
 
 
53
 
    name
54
 
        (within the parent directory)
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.)
 
39
    * *file_id*
 
40
    * *name*: (only the basename within the directory, must not
 
41
      contain slashes)
 
42
    * *kind*: "directory" or "file"
 
43
    * *directory_id*: (if absent/null means the branch root directory)
 
44
    * *text_sha1*: only for files
 
45
    * *text_size*: in bytes, only for files 
 
46
    * *text_id*: identifier for the text version, only for files
 
47
 
 
48
    InventoryEntries can also exist inside a WorkingTree
 
49
    inventory, in which case they are not yet bound to a
 
50
    particular revision of the file.  In that case the text_sha1,
 
51
    text_size and text_id are absent.
 
52
 
74
53
 
75
54
    >>> i = Inventory()
76
55
    >>> i.path2id('')
77
56
    'TREE_ROOT'
78
 
    >>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
 
    InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
80
 
    >>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
 
    InventoryFile('2323', 'hello.c', parent_id='123')
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])
 
57
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
58
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
59
    >>> for j in i.iter_entries():
 
60
    ...   print j
85
61
    ... 
86
 
    (True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
87
 
    (True, InventoryFile('2323', 'hello.c', parent_id='123'))
88
 
    >>> i.add(InventoryFile('2323', 'bye.c', '123'))
 
62
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
63
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
64
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
89
65
    Traceback (most recent call last):
90
66
    ...
91
67
    BzrError: inventory already contains entry with id {2323}
92
 
    >>> i.add(InventoryFile('2324', 'bye.c', '123'))
93
 
    InventoryFile('2324', 'bye.c', parent_id='123')
94
 
    >>> i.add(InventoryDirectory('2325', 'wibble', '123'))
95
 
    InventoryDirectory('2325', 'wibble', parent_id='123')
 
68
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
69
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
96
70
    >>> i.path2id('src/wibble')
97
71
    '2325'
98
72
    >>> '2325' in i
99
73
    True
100
 
    >>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
101
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
 
74
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
102
75
    >>> i['2326']
103
 
    InventoryFile('2326', 'wibble.c', parent_id='2325')
104
 
    >>> for path, entry in i.iter_entries():
105
 
    ...     print path
106
 
    ...     assert i.path2id(path)
 
76
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
77
    >>> for j in i.iter_entries():
 
78
    ...     print j[0]
 
79
    ...     assert i.path2id(j[0])
107
80
    ... 
108
81
    src
109
82
    src/bye.c
112
85
    src/wibble/wibble.c
113
86
    >>> i.id2path('2326')
114
87
    'src/wibble/wibble.c'
 
88
 
 
89
    TODO: Maybe also keep the full path of the entry, and the children?
 
90
           But those depend on its position within a particular inventory, and
 
91
           it would be nice not to need to hold the backpointer here.
115
92
    """
116
 
    
 
93
 
 
94
    # TODO: split InventoryEntry into subclasses for files,
 
95
    # directories, etc etc.
 
96
 
117
97
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
118
 
                 'text_id', 'parent_id', 'children', 'executable', 
119
 
                 'revision']
120
 
 
121
 
    def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
122
 
        versionedfile = weave_store.get_weave_or_empty(self.file_id,
123
 
                                                       transaction)
124
 
        versionedfile.add_lines(self.revision, parents, new_lines)
125
 
 
126
 
    def detect_changes(self, old_entry):
127
 
        """Return a (text_modified, meta_modified) from this to old_entry.
128
 
        
129
 
        _read_tree_state must have been called on self and old_entry prior to 
130
 
        calling detect_changes.
131
 
        """
132
 
        return False, False
133
 
 
134
 
    def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
135
 
             output_to, reverse=False):
136
 
        """Perform a diff from this to to_entry.
137
 
 
138
 
        text_diff will be used for textual difference calculation.
139
 
        This is a template method, override _diff in child classes.
140
 
        """
141
 
        self._read_tree_state(tree.id2path(self.file_id), tree)
142
 
        if to_entry:
143
 
            # cannot diff from one kind to another - you must do a removal
144
 
            # and an addif they do not match.
145
 
            assert self.kind == to_entry.kind
146
 
            to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
147
 
                                      to_tree)
148
 
        self._diff(text_diff, from_label, tree, to_label, to_entry, to_tree,
149
 
                   output_to, reverse)
150
 
 
151
 
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
152
 
             output_to, reverse=False):
153
 
        """Perform a diff between two entries of the same kind."""
154
 
 
155
 
    def find_previous_heads(self, previous_inventories,
156
 
                            versioned_file_store,
157
 
                            transaction,
158
 
                            entry_vf=None):
159
 
        """Return the revisions and entries that directly preceed this.
160
 
 
161
 
        Returned as a map from revision to inventory entry.
162
 
 
163
 
        This is a map containing the file revisions in all parents
164
 
        for which the file exists, and its revision is not a parent of
165
 
        any other. If the file is new, the set will be empty.
166
 
 
167
 
        :param versioned_file_store: A store where ancestry data on this
168
 
                                     file id can be queried.
169
 
        :param transaction: The transaction that queries to the versioned 
170
 
                            file store should be completed under.
171
 
        :param entry_vf: The entry versioned file, if its already available.
172
 
        """
173
 
        def get_ancestors(weave, entry):
174
 
            return set(weave.get_ancestry(entry.revision))
175
 
        # revision:ie mapping for each ie found in previous_inventories.
176
 
        candidates = {}
177
 
        # revision:ie mapping with one revision for each head.
178
 
        heads = {}
179
 
        # revision: ancestor list for each head
180
 
        head_ancestors = {}
181
 
        # identify candidate head revision ids.
182
 
        for inv in previous_inventories:
183
 
            if self.file_id in inv:
184
 
                ie = inv[self.file_id]
185
 
                assert ie.file_id == self.file_id
186
 
                if ie.revision in candidates:
187
 
                    # same revision value in two different inventories:
188
 
                    # correct possible inconsistencies:
189
 
                    #     * there was a bug in revision updates with 'x' bit 
190
 
                    #       support.
191
 
                    try:
192
 
                        if candidates[ie.revision].executable != ie.executable:
193
 
                            candidates[ie.revision].executable = False
194
 
                            ie.executable = False
195
 
                    except AttributeError:
196
 
                        pass
197
 
                    # must now be the same.
198
 
                    assert candidates[ie.revision] == ie
199
 
                else:
200
 
                    # add this revision as a candidate.
201
 
                    candidates[ie.revision] = ie
202
 
 
203
 
        # common case optimisation
204
 
        if len(candidates) == 1:
205
 
            # if there is only one candidate revision found
206
 
            # then we can opening the versioned file to access ancestry:
207
 
            # there cannot be any ancestors to eliminate when there is 
208
 
            # only one revision available.
209
 
            heads[ie.revision] = ie
210
 
            return heads
211
 
 
212
 
        # eliminate ancestors amongst the available candidates:
213
 
        # heads are those that are not an ancestor of any other candidate
214
 
        # - this provides convergence at a per-file level.
215
 
        for ie in candidates.values():
216
 
            # may be an ancestor of a known head:
217
 
            already_present = 0 != len(
218
 
                [head for head in heads 
219
 
                 if ie.revision in head_ancestors[head]])
220
 
            if already_present:
221
 
                # an ancestor of an analyzed candidate.
222
 
                continue
223
 
            # not an ancestor of a known head:
224
 
            # load the versioned file for this file id if needed
225
 
            if entry_vf is None:
226
 
                entry_vf = versioned_file_store.get_weave_or_empty(
227
 
                    self.file_id, transaction)
228
 
            ancestors = get_ancestors(entry_vf, ie)
229
 
            # may knock something else out:
230
 
            check_heads = list(heads.keys())
231
 
            for head in check_heads:
232
 
                if head in ancestors:
233
 
                    # this previously discovered 'head' is not
234
 
                    # really a head - its an ancestor of the newly 
235
 
                    # found head,
236
 
                    heads.pop(head)
237
 
            head_ancestors[ie.revision] = ancestors
238
 
            heads[ie.revision] = ie
239
 
        return heads
240
 
 
241
 
    def get_tar_item(self, root, dp, now, tree):
242
 
        """Get a tarfile item and a file stream for its content."""
243
 
        item = tarfile.TarInfo(pathjoin(root, dp))
244
 
        # TODO: would be cool to actually set it to the timestamp of the
245
 
        # revision it was last changed
246
 
        item.mtime = now
247
 
        fileobj = self._put_in_tar(item, tree)
248
 
        return item, fileobj
249
 
 
250
 
    def has_text(self):
251
 
        """Return true if the object this entry represents has textual data.
252
 
 
253
 
        Note that textual data includes binary content.
254
 
 
255
 
        Also note that all entries get weave files created for them.
256
 
        This attribute is primarily used when upgrading from old trees that
257
 
        did not have the weave index for all inventory entries.
258
 
        """
259
 
        return False
260
 
 
261
 
    def __init__(self, file_id, name, parent_id, text_id=None):
 
98
                 'text_id', 'parent_id', 'children', ]
 
99
 
 
100
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
262
101
        """Create an InventoryEntry
263
102
        
264
103
        The filename must be a single component, relative to the
265
104
        parent directory; it cannot be a whole path or relative name.
266
105
 
267
 
        >>> e = InventoryFile('123', 'hello.c', ROOT_ID)
 
106
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
268
107
        >>> e.name
269
108
        'hello.c'
270
109
        >>> e.file_id
271
110
        '123'
272
 
        >>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
 
111
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
273
112
        Traceback (most recent call last):
274
 
        InvalidEntryName: Invalid entry name: src/hello.c
 
113
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
275
114
        """
276
 
        assert isinstance(name, basestring), name
277
115
        if '/' in name or '\\' in name:
278
 
            raise InvalidEntryName(name=name)
279
 
        self.executable = False
280
 
        self.revision = None
 
116
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
117
        
281
118
        self.text_sha1 = None
282
119
        self.text_size = None
 
120
    
283
121
        self.file_id = file_id
284
122
        self.name = name
 
123
        self.kind = kind
285
124
        self.text_id = text_id
286
125
        self.parent_id = parent_id
287
 
        self.symlink_target = None
288
 
 
289
 
    def kind_character(self):
290
 
        """Return a short kind indicator useful for appending to names."""
291
 
        raise BzrError('unknown kind %r' % self.kind)
292
 
 
293
 
    known_kinds = ('file', 'directory', 'symlink', 'root_directory')
294
 
 
295
 
    def _put_in_tar(self, item, tree):
296
 
        """populate item for stashing in a tar, and return the content stream.
297
 
 
298
 
        If no content is available, return None.
299
 
        """
300
 
        raise BzrError("don't know how to export {%s} of kind %r" %
301
 
                       (self.file_id, self.kind))
302
 
 
303
 
    def put_on_disk(self, dest, dp, tree):
304
 
        """Create a representation of self on disk in the prefix dest.
305
 
        
306
 
        This is a template method - implement _put_on_disk in subclasses.
307
 
        """
308
 
        fullpath = pathjoin(dest, dp)
309
 
        self._put_on_disk(fullpath, tree)
310
 
        mutter("  export {%s} kind %s to %s", self.file_id,
311
 
                self.kind, fullpath)
312
 
 
313
 
    def _put_on_disk(self, fullpath, tree):
314
 
        """Put this entry onto disk at fullpath, from tree tree."""
315
 
        raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
 
126
        if kind == 'directory':
 
127
            self.children = {}
 
128
        elif kind == 'file':
 
129
            pass
 
130
        else:
 
131
            raise BzrError("unhandled entry kind %r" % kind)
 
132
 
 
133
 
316
134
 
317
135
    def sorted_children(self):
318
136
        l = self.children.items()
319
137
        l.sort()
320
138
        return l
321
139
 
322
 
    @staticmethod
323
 
    def versionable_kind(kind):
324
 
        return kind in ('file', 'directory', 'symlink')
325
 
 
326
 
    def check(self, checker, rev_id, inv, tree):
327
 
        """Check this inventory entry is intact.
328
 
 
329
 
        This is a template method, override _check for kind specific
330
 
        tests.
331
 
 
332
 
        :param checker: Check object providing context for the checks; 
333
 
             can be used to find out what parts of the repository have already
334
 
             been checked.
335
 
        :param rev_id: Revision id from which this InventoryEntry was loaded.
336
 
             Not necessarily the last-changed revision for this file.
337
 
        :param inv: Inventory from which the entry was loaded.
338
 
        :param tree: RevisionTree for this entry.
339
 
        """
340
 
        if self.parent_id != None:
341
 
            if not inv.has_id(self.parent_id):
342
 
                raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
343
 
                        % (self.parent_id, rev_id))
344
 
        self._check(checker, rev_id, tree)
345
 
 
346
 
    def _check(self, checker, rev_id, tree):
347
 
        """Check this inventory entry for kind specific errors."""
348
 
        raise BzrCheckError('unknown entry kind %r in revision {%s}' % 
349
 
                            (self.kind, rev_id))
350
 
 
351
140
 
352
141
    def copy(self):
353
 
        """Clone this inventory entry."""
354
 
        raise NotImplementedError
 
142
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
143
                               self.parent_id, text_id=self.text_id)
 
144
        other.text_sha1 = self.text_sha1
 
145
        other.text_size = self.text_size
 
146
        # note that children are *not* copied; they're pulled across when
 
147
        # others are added
 
148
        return other
355
149
 
356
 
    def _get_snapshot_change(self, previous_entries):
357
 
        if len(previous_entries) > 1:
358
 
            return 'merged'
359
 
        elif len(previous_entries) == 0:
360
 
            return 'added'
361
 
        else:
362
 
            return 'modified/renamed/reparented'
363
150
 
364
151
    def __repr__(self):
365
 
        return ("%s(%r, %r, parent_id=%r)"
 
152
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
366
153
                % (self.__class__.__name__,
367
154
                   self.file_id,
368
155
                   self.name,
 
156
                   self.kind,
369
157
                   self.parent_id))
370
158
 
371
 
    def snapshot(self, revision, path, previous_entries,
372
 
                 work_tree, weave_store, transaction):
373
 
        """Make a snapshot of this entry which may or may not have changed.
374
 
        
375
 
        This means that all its fields are populated, that it has its
376
 
        text stored in the text store or weave.
377
 
        """
378
 
        mutter('new parents of %s are %r', path, previous_entries)
379
 
        self._read_tree_state(path, work_tree)
380
 
        if len(previous_entries) == 1:
381
 
            # cannot be unchanged unless there is only one parent file rev.
382
 
            parent_ie = previous_entries.values()[0]
383
 
            if self._unchanged(parent_ie):
384
 
                mutter("found unchanged entry")
385
 
                self.revision = parent_ie.revision
386
 
                return "unchanged"
387
 
        return self.snapshot_revision(revision, previous_entries, 
388
 
                                      work_tree, weave_store, transaction)
389
 
 
390
 
    def snapshot_revision(self, revision, previous_entries, work_tree,
391
 
                          weave_store, transaction):
392
 
        """Record this revision unconditionally."""
393
 
        mutter('new revision for {%s}', self.file_id)
394
 
        self.revision = revision
395
 
        change = self._get_snapshot_change(previous_entries)
396
 
        self._snapshot_text(previous_entries, work_tree, weave_store,
397
 
                            transaction)
398
 
        return change
399
 
 
400
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction): 
401
 
        """Record the 'text' of this entry, whatever form that takes.
402
 
        
403
 
        This default implementation simply adds an empty text.
404
 
        """
405
 
        mutter('storing file {%s} in revision {%s}',
406
 
               self.file_id, self.revision)
407
 
        self._add_text_to_weave([], file_parents.keys(), weave_store, transaction)
 
159
    
 
160
    def to_element(self):
 
161
        """Convert to XML element"""
 
162
        from bzrlib.xml import Element
 
163
        
 
164
        e = Element('entry')
 
165
 
 
166
        e.set('name', self.name)
 
167
        e.set('file_id', self.file_id)
 
168
        e.set('kind', self.kind)
 
169
 
 
170
        if self.text_size != None:
 
171
            e.set('text_size', '%d' % self.text_size)
 
172
            
 
173
        for f in ['text_id', 'text_sha1']:
 
174
            v = getattr(self, f)
 
175
            if v != None:
 
176
                e.set(f, v)
 
177
 
 
178
        # to be conservative, we don't externalize the root pointers
 
179
        # for now, leaving them as null in the xml form.  in a future
 
180
        # version it will be implied by nested elements.
 
181
        if self.parent_id != ROOT_ID:
 
182
            assert isinstance(self.parent_id, basestring)
 
183
            e.set('parent_id', self.parent_id)
 
184
 
 
185
        e.tail = '\n'
 
186
            
 
187
        return e
 
188
 
 
189
 
 
190
    def from_element(cls, elt):
 
191
        assert elt.tag == 'entry'
 
192
 
 
193
        ## original format inventories don't have a parent_id for
 
194
        ## nodes in the root directory, but it's cleaner to use one
 
195
        ## internally.
 
196
        parent_id = elt.get('parent_id')
 
197
        if parent_id == None:
 
198
            parent_id = ROOT_ID
 
199
 
 
200
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
 
201
        self.text_id = elt.get('text_id')
 
202
        self.text_sha1 = elt.get('text_sha1')
 
203
        
 
204
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
205
 
 
206
        v = elt.get('text_size')
 
207
        self.text_size = v and int(v)
 
208
 
 
209
        return self
 
210
            
 
211
 
 
212
    from_element = classmethod(from_element)
408
213
 
409
214
    def __eq__(self, other):
410
215
        if not isinstance(other, InventoryEntry):
411
216
            return NotImplemented
412
217
 
413
 
        return ((self.file_id == other.file_id)
414
 
                and (self.name == other.name)
415
 
                and (other.symlink_target == self.symlink_target)
416
 
                and (self.text_sha1 == other.text_sha1)
417
 
                and (self.text_size == other.text_size)
418
 
                and (self.text_id == other.text_id)
419
 
                and (self.parent_id == other.parent_id)
420
 
                and (self.kind == other.kind)
421
 
                and (self.revision == other.revision)
422
 
                and (self.executable == other.executable)
423
 
                )
 
218
        return (self.file_id == other.file_id) \
 
219
               and (self.name == other.name) \
 
220
               and (self.text_sha1 == other.text_sha1) \
 
221
               and (self.text_size == other.text_size) \
 
222
               and (self.text_id == other.text_id) \
 
223
               and (self.parent_id == other.parent_id) \
 
224
               and (self.kind == other.kind)
 
225
 
424
226
 
425
227
    def __ne__(self, other):
426
228
        return not (self == other)
428
230
    def __hash__(self):
429
231
        raise ValueError('not hashable')
430
232
 
431
 
    def _unchanged(self, previous_ie):
432
 
        """Has this entry changed relative to previous_ie.
433
 
 
434
 
        This method should be overriden in child classes.
435
 
        """
436
 
        compatible = True
437
 
        # different inv parent
438
 
        if previous_ie.parent_id != self.parent_id:
439
 
            compatible = False
440
 
        # renamed
441
 
        elif previous_ie.name != self.name:
442
 
            compatible = False
443
 
        return compatible
444
 
 
445
 
    def _read_tree_state(self, path, work_tree):
446
 
        """Populate fields in the inventory entry from the given tree.
447
 
        
448
 
        Note that this should be modified to be a noop on virtual trees
449
 
        as all entries created there are prepopulated.
450
 
        """
451
 
        # TODO: Rather than running this manually, we should check the 
452
 
        # working sha1 and other expensive properties when they're
453
 
        # first requested, or preload them if they're already known
454
 
        pass            # nothing to do by default
455
 
 
456
 
    def _forget_tree_state(self):
457
 
        pass
458
233
 
459
234
 
460
235
class RootEntry(InventoryEntry):
461
 
 
462
 
    def _check(self, checker, rev_id, tree):
463
 
        """See InventoryEntry._check"""
464
 
 
465
236
    def __init__(self, file_id):
466
237
        self.file_id = file_id
467
238
        self.children = {}
468
239
        self.kind = 'root_directory'
469
240
        self.parent_id = None
470
 
        self.name = u''
 
241
        self.name = ''
471
242
 
472
243
    def __eq__(self, other):
473
244
        if not isinstance(other, RootEntry):
477
248
               and (self.children == other.children)
478
249
 
479
250
 
480
 
class InventoryDirectory(InventoryEntry):
481
 
    """A directory in an inventory."""
482
 
 
483
 
    def _check(self, checker, rev_id, tree):
484
 
        """See InventoryEntry._check"""
485
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
486
 
            raise BzrCheckError('directory {%s} has text in revision {%s}'
487
 
                                % (self.file_id, rev_id))
488
 
 
489
 
    def copy(self):
490
 
        other = InventoryDirectory(self.file_id, self.name, self.parent_id)
491
 
        other.revision = self.revision
492
 
        # note that children are *not* copied; they're pulled across when
493
 
        # others are added
494
 
        return other
495
 
 
496
 
    def __init__(self, file_id, name, parent_id):
497
 
        super(InventoryDirectory, self).__init__(file_id, name, parent_id)
498
 
        self.children = {}
499
 
        self.kind = 'directory'
500
 
 
501
 
    def kind_character(self):
502
 
        """See InventoryEntry.kind_character."""
503
 
        return '/'
504
 
 
505
 
    def _put_in_tar(self, item, tree):
506
 
        """See InventoryEntry._put_in_tar."""
507
 
        item.type = tarfile.DIRTYPE
508
 
        fileobj = None
509
 
        item.name += '/'
510
 
        item.size = 0
511
 
        item.mode = 0755
512
 
        return fileobj
513
 
 
514
 
    def _put_on_disk(self, fullpath, tree):
515
 
        """See InventoryEntry._put_on_disk."""
516
 
        os.mkdir(fullpath)
517
 
 
518
 
 
519
 
class InventoryFile(InventoryEntry):
520
 
    """A file in an inventory."""
521
 
 
522
 
    def _check(self, checker, tree_revision_id, tree):
523
 
        """See InventoryEntry._check"""
524
 
        t = (self.file_id, self.revision)
525
 
        if t in checker.checked_texts:
526
 
            prev_sha = checker.checked_texts[t]
527
 
            if prev_sha != self.text_sha1:
528
 
                raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
529
 
                                    (self.file_id, tree_revision_id))
530
 
            else:
531
 
                checker.repeated_text_cnt += 1
532
 
                return
533
 
 
534
 
        if self.file_id not in checker.checked_weaves:
535
 
            mutter('check weave {%s}', self.file_id)
536
 
            w = tree.get_weave(self.file_id)
537
 
            # Not passing a progress bar, because it creates a new
538
 
            # progress, which overwrites the current progress,
539
 
            # and doesn't look nice
540
 
            w.check()
541
 
            checker.checked_weaves[self.file_id] = True
542
 
        else:
543
 
            w = tree.get_weave(self.file_id)
544
 
 
545
 
        mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
546
 
        checker.checked_text_cnt += 1
547
 
        # We can't check the length, because Weave doesn't store that
548
 
        # information, and the whole point of looking at the weave's
549
 
        # sha1sum is that we don't have to extract the text.
550
 
        if self.text_sha1 != w.get_sha1(self.revision):
551
 
            raise BzrCheckError('text {%s} version {%s} wrong sha1' 
552
 
                                % (self.file_id, self.revision))
553
 
        checker.checked_texts[t] = self.text_sha1
554
 
 
555
 
    def copy(self):
556
 
        other = InventoryFile(self.file_id, self.name, self.parent_id)
557
 
        other.executable = self.executable
558
 
        other.text_id = self.text_id
559
 
        other.text_sha1 = self.text_sha1
560
 
        other.text_size = self.text_size
561
 
        other.revision = self.revision
562
 
        return other
563
 
 
564
 
    def detect_changes(self, old_entry):
565
 
        """See InventoryEntry.detect_changes."""
566
 
        assert self.text_sha1 != None
567
 
        assert old_entry.text_sha1 != None
568
 
        text_modified = (self.text_sha1 != old_entry.text_sha1)
569
 
        meta_modified = (self.executable != old_entry.executable)
570
 
        return text_modified, meta_modified
571
 
 
572
 
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
573
 
             output_to, reverse=False):
574
 
        """See InventoryEntry._diff."""
575
 
        from_text = tree.get_file(self.file_id).readlines()
576
 
        if to_entry:
577
 
            to_text = to_tree.get_file(to_entry.file_id).readlines()
578
 
        else:
579
 
            to_text = []
580
 
        if not reverse:
581
 
            text_diff(from_label, from_text,
582
 
                      to_label, to_text, output_to)
583
 
        else:
584
 
            text_diff(to_label, to_text,
585
 
                      from_label, from_text, output_to)
586
 
 
587
 
    def has_text(self):
588
 
        """See InventoryEntry.has_text."""
589
 
        return True
590
 
 
591
 
    def __init__(self, file_id, name, parent_id):
592
 
        super(InventoryFile, self).__init__(file_id, name, parent_id)
593
 
        self.kind = 'file'
594
 
 
595
 
    def kind_character(self):
596
 
        """See InventoryEntry.kind_character."""
597
 
        return ''
598
 
 
599
 
    def _put_in_tar(self, item, tree):
600
 
        """See InventoryEntry._put_in_tar."""
601
 
        item.type = tarfile.REGTYPE
602
 
        fileobj = tree.get_file(self.file_id)
603
 
        item.size = self.text_size
604
 
        if tree.is_executable(self.file_id):
605
 
            item.mode = 0755
606
 
        else:
607
 
            item.mode = 0644
608
 
        return fileobj
609
 
 
610
 
    def _put_on_disk(self, fullpath, tree):
611
 
        """See InventoryEntry._put_on_disk."""
612
 
        pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
613
 
        if tree.is_executable(self.file_id):
614
 
            os.chmod(fullpath, 0755)
615
 
 
616
 
    def _read_tree_state(self, path, work_tree):
617
 
        """See InventoryEntry._read_tree_state."""
618
 
        self.text_sha1 = work_tree.get_file_sha1(self.file_id)
619
 
        self.executable = work_tree.is_executable(self.file_id)
620
 
 
621
 
    def _forget_tree_state(self):
622
 
        self.text_sha1 = None
623
 
        self.executable = None
624
 
 
625
 
    def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
626
 
        """See InventoryEntry._snapshot_text."""
627
 
        mutter('storing file {%s} in revision {%s}',
628
 
               self.file_id, self.revision)
629
 
        # special case to avoid diffing on renames or 
630
 
        # reparenting
631
 
        if (len(file_parents) == 1
632
 
            and self.text_sha1 == file_parents.values()[0].text_sha1
633
 
            and self.text_size == file_parents.values()[0].text_size):
634
 
            previous_ie = file_parents.values()[0]
635
 
            versionedfile = weave_store.get_weave(self.file_id, transaction)
636
 
            versionedfile.clone_text(self.revision, previous_ie.revision, file_parents.keys())
637
 
        else:
638
 
            new_lines = work_tree.get_file(self.file_id).readlines()
639
 
            self._add_text_to_weave(new_lines, file_parents.keys(), weave_store,
640
 
                                    transaction)
641
 
            self.text_sha1 = sha_strings(new_lines)
642
 
            self.text_size = sum(map(len, new_lines))
643
 
 
644
 
 
645
 
    def _unchanged(self, previous_ie):
646
 
        """See InventoryEntry._unchanged."""
647
 
        compatible = super(InventoryFile, self)._unchanged(previous_ie)
648
 
        if self.text_sha1 != previous_ie.text_sha1:
649
 
            compatible = False
650
 
        else:
651
 
            # FIXME: 20050930 probe for the text size when getting sha1
652
 
            # in _read_tree_state
653
 
            self.text_size = previous_ie.text_size
654
 
        if self.executable != previous_ie.executable:
655
 
            compatible = False
656
 
        return compatible
657
 
 
658
 
 
659
 
class InventoryLink(InventoryEntry):
660
 
    """A file in an inventory."""
661
 
 
662
 
    __slots__ = ['symlink_target']
663
 
 
664
 
    def _check(self, checker, rev_id, tree):
665
 
        """See InventoryEntry._check"""
666
 
        if self.text_sha1 != None or self.text_size != None or self.text_id != None:
667
 
            raise BzrCheckError('symlink {%s} has text in revision {%s}'
668
 
                    % (self.file_id, rev_id))
669
 
        if self.symlink_target == None:
670
 
            raise BzrCheckError('symlink {%s} has no target in revision {%s}'
671
 
                    % (self.file_id, rev_id))
672
 
 
673
 
    def copy(self):
674
 
        other = InventoryLink(self.file_id, self.name, self.parent_id)
675
 
        other.symlink_target = self.symlink_target
676
 
        other.revision = self.revision
677
 
        return other
678
 
 
679
 
    def detect_changes(self, old_entry):
680
 
        """See InventoryEntry.detect_changes."""
681
 
        # FIXME: which _modified field should we use ? RBC 20051003
682
 
        text_modified = (self.symlink_target != old_entry.symlink_target)
683
 
        if text_modified:
684
 
            mutter("    symlink target changed")
685
 
        meta_modified = False
686
 
        return text_modified, meta_modified
687
 
 
688
 
    def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
689
 
             output_to, reverse=False):
690
 
        """See InventoryEntry._diff."""
691
 
        from_text = self.symlink_target
692
 
        if to_entry is not None:
693
 
            to_text = to_entry.symlink_target
694
 
            if reverse:
695
 
                temp = from_text
696
 
                from_text = to_text
697
 
                to_text = temp
698
 
            print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
699
 
        else:
700
 
            if not reverse:
701
 
                print >>output_to, '=== target was %r' % self.symlink_target
702
 
            else:
703
 
                print >>output_to, '=== target is %r' % self.symlink_target
704
 
 
705
 
    def __init__(self, file_id, name, parent_id):
706
 
        super(InventoryLink, self).__init__(file_id, name, parent_id)
707
 
        self.kind = 'symlink'
708
 
 
709
 
    def kind_character(self):
710
 
        """See InventoryEntry.kind_character."""
711
 
        return ''
712
 
 
713
 
    def _put_in_tar(self, item, tree):
714
 
        """See InventoryEntry._put_in_tar."""
715
 
        item.type = tarfile.SYMTYPE
716
 
        fileobj = None
717
 
        item.size = 0
718
 
        item.mode = 0755
719
 
        item.linkname = self.symlink_target
720
 
        return fileobj
721
 
 
722
 
    def _put_on_disk(self, fullpath, tree):
723
 
        """See InventoryEntry._put_on_disk."""
724
 
        try:
725
 
            os.symlink(self.symlink_target, fullpath)
726
 
        except OSError,e:
727
 
            raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
728
 
 
729
 
    def _read_tree_state(self, path, work_tree):
730
 
        """See InventoryEntry._read_tree_state."""
731
 
        self.symlink_target = work_tree.get_symlink_target(self.file_id)
732
 
 
733
 
    def _forget_tree_state(self):
734
 
        self.symlink_target = None
735
 
 
736
 
    def _unchanged(self, previous_ie):
737
 
        """See InventoryEntry._unchanged."""
738
 
        compatible = super(InventoryLink, self)._unchanged(previous_ie)
739
 
        if self.symlink_target != previous_ie.symlink_target:
740
 
            compatible = False
741
 
        return compatible
742
 
 
743
251
 
744
252
class Inventory(object):
745
253
    """Inventory of versioned files in a tree.
759
267
    inserted, other than through the Inventory API.
760
268
 
761
269
    >>> inv = Inventory()
762
 
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
763
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
 
270
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
764
271
    >>> inv['123-123'].name
765
272
    'hello.c'
766
273
 
776
283
    >>> [x[0] for x in inv.iter_entries()]
777
284
    ['hello.c']
778
285
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
779
 
    >>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
780
 
    InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
 
286
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
781
287
    """
782
 
    def __init__(self, root_id=ROOT_ID, revision_id=None):
 
288
    def __init__(self, root_id=ROOT_ID):
783
289
        """Create or read an inventory.
784
290
 
785
291
        If a working directory is specified, the inventory is read
789
295
        The inventory is created with a default root directory, with
790
296
        an id of None.
791
297
        """
792
 
        # We are letting Branch.create() create a unique inventory
 
298
        # We are letting Branch(init=True) create a unique inventory
793
299
        # root id. Rather than generating a random one here.
794
300
        #if root_id is None:
795
301
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
796
302
        self.root = RootEntry(root_id)
797
 
        self.revision_id = revision_id
798
303
        self._byid = {self.root.file_id: self.root}
799
304
 
800
305
 
801
 
    def copy(self):
802
 
        # TODO: jam 20051218 Should copy also copy the revision_id?
803
 
        other = Inventory(self.root.file_id)
804
 
        # copy recursively so we know directories will be added before
805
 
        # their children.  There are more efficient ways than this...
806
 
        for path, entry in self.iter_entries():
807
 
            if entry == self.root:
808
 
                continue
809
 
            other.add(entry.copy())
810
 
        return other
811
 
 
812
 
 
813
306
    def __iter__(self):
814
307
        return iter(self._byid)
815
308
 
833
326
            yield name, ie
834
327
            if ie.kind == 'directory':
835
328
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
836
 
                    yield pathjoin(name, cn), cie
 
329
                    yield os.path.join(name, cn), cie
837
330
 
838
331
 
839
332
    def entries(self):
846
339
            kids = dir_ie.children.items()
847
340
            kids.sort()
848
341
            for name, ie in kids:
849
 
                child_path = pathjoin(dir_path, name)
 
342
                child_path = os.path.join(dir_path, name)
850
343
                accum.append((child_path, ie))
851
344
                if ie.kind == 'directory':
852
345
                    descend(ie, child_path)
853
346
 
854
 
        descend(self.root, u'')
 
347
        descend(self.root, '')
855
348
        return accum
856
349
 
857
350
 
866
359
            kids.sort()
867
360
 
868
361
            for name, child_ie in kids:
869
 
                child_path = pathjoin(parent_path, name)
 
362
                child_path = os.path.join(parent_path, name)
870
363
                descend(child_ie, child_path)
871
 
        descend(self.root, u'')
 
364
        descend(self.root, '')
872
365
        return accum
873
366
        
874
367
 
877
370
        """True if this entry contains a file with given id.
878
371
 
879
372
        >>> inv = Inventory()
880
 
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
881
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
373
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
882
374
        >>> '123' in inv
883
375
        True
884
376
        >>> '456' in inv
891
383
        """Return the entry for given file_id.
892
384
 
893
385
        >>> inv = Inventory()
894
 
        >>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
895
 
        InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
 
386
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
896
387
        >>> inv['123123'].name
897
388
        'hello.c'
898
389
        """
916
407
        """Add entry to inventory.
917
408
 
918
409
        To add  a file to a branch ready to be committed, use Branch.add,
919
 
        which calls this.
920
 
 
921
 
        Returns the new entry object.
922
 
        """
 
410
        which calls this."""
923
411
        if entry.file_id in self._byid:
924
412
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
925
413
 
933
421
 
934
422
        if parent.children.has_key(entry.name):
935
423
            raise BzrError("%s is already versioned" %
936
 
                    pathjoin(self.id2path(parent.file_id), entry.name))
 
424
                    appendpath(self.id2path(parent.file_id), entry.name))
937
425
 
938
426
        self._byid[entry.file_id] = entry
939
427
        parent.children[entry.name] = entry
940
 
        return entry
941
428
 
942
429
 
943
430
    def add_path(self, relpath, kind, file_id=None):
944
431
        """Add entry from a path.
945
432
 
946
 
        The immediate parent must already be versioned.
947
 
 
948
 
        Returns the new entry object."""
949
 
        from bzrlib.workingtree import gen_file_id
 
433
        The immediate parent must already be versioned"""
 
434
        from bzrlib.branch import gen_file_id
950
435
        
951
436
        parts = bzrlib.osutils.splitpath(relpath)
 
437
        if len(parts) == 0:
 
438
            raise BzrError("cannot re-add root of inventory")
952
439
 
953
440
        if file_id == None:
954
441
            file_id = gen_file_id(relpath)
955
442
 
956
 
        if len(parts) == 0:
957
 
            self.root = RootEntry(file_id)
958
 
            self._byid = {self.root.file_id: self.root}
959
 
            return
960
 
        else:
961
 
            parent_path = parts[:-1]
962
 
            parent_id = self.path2id(parent_path)
963
 
            if parent_id == None:
964
 
                raise NotVersionedError(path=parent_path)
965
 
        if kind == 'directory':
966
 
            ie = InventoryDirectory(file_id, parts[-1], parent_id)
967
 
        elif kind == 'file':
968
 
            ie = InventoryFile(file_id, parts[-1], parent_id)
969
 
        elif kind == 'symlink':
970
 
            ie = InventoryLink(file_id, parts[-1], parent_id)
971
 
        else:
972
 
            raise BzrError("unknown kind %r" % kind)
 
443
        parent_path = parts[:-1]
 
444
        parent_id = self.path2id(parent_path)
 
445
        if parent_id == None:
 
446
            raise NotVersionedError(parent_path)
 
447
 
 
448
        ie = InventoryEntry(file_id, parts[-1],
 
449
                            kind=kind, parent_id=parent_id)
973
450
        return self.add(ie)
974
451
 
975
452
 
977
454
        """Remove entry by id.
978
455
 
979
456
        >>> inv = Inventory()
980
 
        >>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
981
 
        InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
 
457
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
982
458
        >>> '123' in inv
983
459
        True
984
460
        >>> del inv['123']
987
463
        """
988
464
        ie = self[file_id]
989
465
 
990
 
        assert ie.parent_id is None or \
991
 
            self[ie.parent_id].children[ie.name] == ie
 
466
        assert self[ie.parent_id].children[ie.name] == ie
992
467
        
 
468
        # TODO: Test deleting all children; maybe hoist to a separate
 
469
        # deltree method?
 
470
        if ie.kind == 'directory':
 
471
            for cie in ie.children.values():
 
472
                del self[cie.file_id]
 
473
            del ie.children
 
474
 
993
475
        del self._byid[file_id]
994
 
        if ie.parent_id is not None:
995
 
            del self[ie.parent_id].children[ie.name]
 
476
        del self[ie.parent_id].children[ie.name]
 
477
 
 
478
 
 
479
    def to_element(self):
 
480
        """Convert to XML Element"""
 
481
        from bzrlib.xml import Element
 
482
        
 
483
        e = Element('inventory')
 
484
        e.text = '\n'
 
485
        if self.root.file_id not in (None, ROOT_ID):
 
486
            e.set('file_id', self.root.file_id)
 
487
        for path, ie in self.iter_entries():
 
488
            e.append(ie.to_element())
 
489
        return e
 
490
    
 
491
 
 
492
    def from_element(cls, elt):
 
493
        """Construct from XML Element
 
494
        
 
495
        >>> inv = Inventory()
 
496
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
497
        >>> elt = inv.to_element()
 
498
        >>> inv2 = Inventory.from_element(elt)
 
499
        >>> inv2 == inv
 
500
        True
 
501
        """
 
502
        # XXXX: doctest doesn't run this properly under python2.3
 
503
        assert elt.tag == 'inventory'
 
504
        root_id = elt.get('file_id') or ROOT_ID
 
505
        o = cls(root_id)
 
506
        for e in elt:
 
507
            ie = InventoryEntry.from_element(e)
 
508
            if ie.parent_id == ROOT_ID:
 
509
                ie.parent_id = root_id
 
510
            o.add(ie)
 
511
        return o
 
512
        
 
513
    from_element = classmethod(from_element)
996
514
 
997
515
 
998
516
    def __eq__(self, other):
1002
520
        >>> i2 = Inventory()
1003
521
        >>> i1 == i2
1004
522
        True
1005
 
        >>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1006
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
523
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
1007
524
        >>> i1 == i2
1008
525
        False
1009
 
        >>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1010
 
        InventoryFile('123', 'foo', parent_id='TREE_ROOT')
 
526
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
1011
527
        >>> i1 == i2
1012
528
        True
1013
529
        """
1022
538
 
1023
539
 
1024
540
    def __ne__(self, other):
1025
 
        return not self.__eq__(other)
 
541
        return not (self == other)
1026
542
 
1027
543
 
1028
544
    def __hash__(self):
1029
545
        raise ValueError('not hashable')
1030
546
 
1031
 
    def _iter_file_id_parents(self, file_id):
1032
 
        """Yield the parents of file_id up to the root."""
1033
 
        while file_id != None:
1034
 
            try:
1035
 
                ie = self._byid[file_id]
1036
 
            except KeyError:
1037
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
1038
 
            yield ie
1039
 
            file_id = ie.parent_id
 
547
 
1040
548
 
1041
549
    def get_idpath(self, file_id):
1042
550
        """Return a list of file_ids for the path to an entry.
1047
555
        root directory as depth 1.
1048
556
        """
1049
557
        p = []
1050
 
        for parent in self._iter_file_id_parents(file_id):
1051
 
            p.insert(0, parent.file_id)
 
558
        while file_id != None:
 
559
            try:
 
560
                ie = self._byid[file_id]
 
561
            except KeyError:
 
562
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
563
            p.insert(0, ie.file_id)
 
564
            file_id = ie.parent_id
1052
565
        return p
1053
566
 
 
567
 
1054
568
    def id2path(self, file_id):
1055
 
        """Return as a string the path to file_id.
1056
 
        
1057
 
        >>> i = Inventory()
1058
 
        >>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1059
 
        >>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1060
 
        >>> print i.id2path('foo-id')
1061
 
        src/foo.c
1062
 
        """
 
569
        """Return as a list the path to file_id."""
 
570
 
1063
571
        # get all names, skipping root
1064
 
        return '/'.join(reversed(
1065
 
            [parent.name for parent in 
1066
 
             self._iter_file_id_parents(file_id)][:-1]))
 
572
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
573
        return os.sep.join(p)
1067
574
            
 
575
 
 
576
 
1068
577
    def path2id(self, name):
1069
578
        """Walk down through directories to return entry of last component.
1070
579