~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: John Arbash Meinel
  • Date: 2005-09-29 21:13:03 UTC
  • mto: (1393.1.12)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050929211303-7f1f9bf969d65dc3
All tests pass.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
 
"""Inventories map files to their name in a revision."""
18
 
 
19
 
# TODO: Maybe store inventory_id in the file?  Not really needed.
20
 
 
21
 
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
 
__author__ = "Martin Pool <mbp@canonical.com>"
23
 
 
24
 
import sys, os.path, types, re
25
 
from sets import Set
26
 
 
27
 
try:
28
 
    from cElementTree import Element, ElementTree, SubElement
29
 
except ImportError:
30
 
    from elementtree.ElementTree import Element, ElementTree, SubElement
31
 
 
32
 
from xml import XMLMixin
33
 
from errors import bailout
 
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
# TODO: Perhaps split InventoryEntry into subclasses for files,
 
23
# directories, etc etc.
 
24
 
 
25
 
 
26
# This should really be an id randomly assigned when the tree is
 
27
# created, but it's not for now.
 
28
ROOT_ID = "TREE_ROOT"
 
29
 
 
30
 
 
31
import os.path
 
32
import re
 
33
import sys
 
34
import types
34
35
 
35
36
import bzrlib
36
 
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
37
from bzrlib.errors import BzrError, BzrCheckError
 
38
 
 
39
from bzrlib.osutils import quotefn, splitpath, joinpath, appendpath
37
40
from bzrlib.trace import mutter
38
 
 
39
 
class InventoryEntry(XMLMixin):
 
41
from bzrlib.errors import NotVersionedError
 
42
 
 
43
 
 
44
class InventoryEntry(object):
40
45
    """Description of a versioned file.
41
46
 
42
47
    An InventoryEntry has the following fields, which are also
43
48
    present in the XML inventory-entry element:
44
49
 
45
 
    * *file_id*
46
 
    * *name*: (only the basename within the directory, must not
47
 
      contain slashes)
48
 
    * *kind*: "directory" or "file"
49
 
    * *directory_id*: (if absent/null means the branch root directory)
50
 
    * *text_sha1*: only for files
51
 
    * *text_size*: in bytes, only for files 
52
 
    * *text_id*: identifier for the text version, only for files
53
 
 
54
 
    InventoryEntries can also exist inside a WorkingTree
55
 
    inventory, in which case they are not yet bound to a
56
 
    particular revision of the file.  In that case the text_sha1,
57
 
    text_size and text_id are absent.
58
 
 
 
50
    file_id
 
51
 
 
52
    name
 
53
        (within the parent directory)
 
54
 
 
55
    kind
 
56
        'directory' or 'file'
 
57
 
 
58
    parent_id
 
59
        file_id of the parent directory, or ROOT_ID
 
60
 
 
61
    name_version
 
62
        the revision_id in which the name or parent of this file was
 
63
        last changed
 
64
 
 
65
    text_sha1
 
66
        sha-1 of the text of the file
 
67
        
 
68
    text_size
 
69
        size in bytes of the text of the file
 
70
        
 
71
    text_version
 
72
        the revision_id in which the text of this file was introduced
 
73
 
 
74
    (reading a version 4 tree created a text_id field.)
59
75
 
60
76
    >>> i = Inventory()
61
77
    >>> i.path2id('')
62
 
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
 
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
 
78
    'TREE_ROOT'
 
79
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
80
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
 
81
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
82
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
64
83
    >>> for j in i.iter_entries():
65
84
    ...   print j
66
85
    ... 
67
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
 
86
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
68
87
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
 
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
 
88
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
89
    Traceback (most recent call last):
71
90
    ...
72
 
    BzrError: ('inventory already contains entry with id {2323}', [])
73
 
    >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
74
 
    >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
 
91
    BzrError: inventory already contains entry with id {2323}
 
92
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
93
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
94
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
95
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
75
96
    >>> i.path2id('src/wibble')
76
97
    '2325'
77
98
    >>> '2325' in i
78
99
    True
79
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
100
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
101
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
102
    >>> i['2326']
81
103
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
 
    >>> for j in i.iter_entries():
83
 
    ...     print j[0]
84
 
    ...     assert i.path2id(j[0])
 
104
    >>> for path, entry in i.iter_entries():
 
105
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
106
    ...     assert i.path2id(path)
85
107
    ... 
86
108
    src
87
109
    src/bye.c
88
110
    src/hello.c
89
111
    src/wibble
90
112
    src/wibble/wibble.c
91
 
    >>> i.id2path('2326')
 
113
    >>> i.id2path('2326').replace('\\\\', '/')
92
114
    'src/wibble/wibble.c'
93
 
 
94
 
    :todo: Maybe also keep the full path of the entry, and the children?
95
 
           But those depend on its position within a particular inventory, and
96
 
           it would be nice not to need to hold the backpointer here.
97
115
    """
98
 
    def __init__(self, file_id, name, kind='file', text_id=None,
99
 
                 parent_id=None):
 
116
    
 
117
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
118
                 'text_id', 'parent_id', 'children',
 
119
                 'text_version', 'name_version', ]
 
120
 
 
121
 
 
122
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
100
123
        """Create an InventoryEntry
101
124
        
102
125
        The filename must be a single component, relative to the
103
126
        parent directory; it cannot be a whole path or relative name.
104
127
 
105
 
        >>> e = InventoryEntry('123', 'hello.c')
 
128
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
106
129
        >>> e.name
107
130
        'hello.c'
108
131
        >>> e.file_id
109
132
        '123'
110
 
        >>> e = InventoryEntry('123', 'src/hello.c')
 
133
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
111
134
        Traceback (most recent call last):
112
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
135
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
113
136
        """
114
 
        
115
 
        if len(splitpath(name)) != 1:
116
 
            bailout('InventoryEntry name is not a simple filename: %r'
117
 
                    % name)
118
 
        
 
137
        assert isinstance(name, basestring), name
 
138
        if '/' in name or '\\' in name:
 
139
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
140
        
 
141
        self.text_version = None
 
142
        self.name_version = None
 
143
        self.text_sha1 = None
 
144
        self.text_size = None
119
145
        self.file_id = file_id
120
146
        self.name = name
121
 
        assert kind in ['file', 'directory']
122
147
        self.kind = kind
123
148
        self.text_id = text_id
124
149
        self.parent_id = parent_id
125
 
        self.text_sha1 = None
126
 
        self.text_size = None
127
150
        if kind == 'directory':
128
151
            self.children = {}
 
152
        elif kind == 'file':
 
153
            pass
 
154
        else:
 
155
            raise BzrError("unhandled entry kind %r" % kind)
 
156
 
129
157
 
130
158
 
131
159
    def sorted_children(self):
136
164
 
137
165
    def copy(self):
138
166
        other = InventoryEntry(self.file_id, self.name, self.kind,
139
 
                               self.text_id, self.parent_id)
 
167
                               self.parent_id)
 
168
        other.text_id = self.text_id
140
169
        other.text_sha1 = self.text_sha1
141
170
        other.text_size = self.text_size
 
171
        other.text_version = self.text_version
 
172
        other.name_version = self.name_version
 
173
        # note that children are *not* copied; they're pulled across when
 
174
        # others are added
142
175
        return other
143
176
 
144
177
 
151
184
                   self.parent_id))
152
185
 
153
186
    
154
 
    def to_element(self):
155
 
        """Convert to XML element"""
156
 
        e = Element('entry')
157
 
 
158
 
        e.set('name', self.name)
159
 
        e.set('file_id', self.file_id)
160
 
        e.set('kind', self.kind)
161
 
 
162
 
        if self.text_size is not None:
163
 
            e.set('text_size', '%d' % self.text_size)
164
 
            
165
 
        for f in ['text_id', 'text_sha1', 'parent_id']:
166
 
            v = getattr(self, f)
167
 
            if v is not None:
168
 
                e.set(f, v)
169
 
 
170
 
        e.tail = '\n'
171
 
            
172
 
        return e
173
 
 
174
 
 
175
 
    def from_element(cls, elt):
176
 
        assert elt.tag == 'entry'
177
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
178
 
        self.text_id = elt.get('text_id')
179
 
        self.text_sha1 = elt.get('text_sha1')
180
 
        self.parent_id = elt.get('parent_id')
181
 
        
182
 
        ## mutter("read inventoryentry: %r" % (elt.attrib))
183
 
 
184
 
        v = elt.get('text_size')
185
 
        self.text_size = v and int(v)
186
 
 
187
 
        return self
188
 
            
189
 
 
190
 
    from_element = classmethod(from_element)
191
 
 
192
 
    def __cmp__(self, other):
193
 
        if self is other:
194
 
            return 0
 
187
    def __eq__(self, other):
195
188
        if not isinstance(other, InventoryEntry):
196
189
            return NotImplemented
197
190
 
198
 
        return cmp(self.file_id, other.file_id) \
199
 
               or cmp(self.name, other.name) \
200
 
               or cmp(self.text_sha1, other.text_sha1) \
201
 
               or cmp(self.text_size, other.text_size) \
202
 
               or cmp(self.text_id, other.text_id) \
203
 
               or cmp(self.parent_id, other.parent_id) \
204
 
               or cmp(self.kind, other.kind)
 
191
        return (self.file_id == other.file_id) \
 
192
               and (self.name == other.name) \
 
193
               and (self.text_sha1 == other.text_sha1) \
 
194
               and (self.text_size == other.text_size) \
 
195
               and (self.text_id == other.text_id) \
 
196
               and (self.parent_id == other.parent_id) \
 
197
               and (self.kind == other.kind) \
 
198
               and (self.text_version == other.text_version) \
 
199
               and (self.name_version == other.name_version)
 
200
 
 
201
 
 
202
    def __ne__(self, other):
 
203
        return not (self == other)
 
204
 
 
205
    def __hash__(self):
 
206
        raise ValueError('not hashable')
205
207
 
206
208
 
207
209
 
213
215
        self.parent_id = None
214
216
        self.name = ''
215
217
 
216
 
    def __cmp__(self, other):
217
 
        if self is other:
218
 
            return 0
 
218
    def __eq__(self, other):
219
219
        if not isinstance(other, RootEntry):
220
220
            return NotImplemented
221
 
        return cmp(self.file_id, other.file_id) \
222
 
               or cmp(self.children, other.children)
223
 
 
224
 
 
225
 
 
226
 
class Inventory(XMLMixin):
 
221
        
 
222
        return (self.file_id == other.file_id) \
 
223
               and (self.children == other.children)
 
224
 
 
225
 
 
226
 
 
227
class Inventory(object):
227
228
    """Inventory of versioned files in a tree.
228
229
 
229
 
    An Inventory acts like a set of InventoryEntry items.  You can
230
 
    also look files up by their file_id or name.
231
 
    
232
 
    May be read from and written to a metadata file in a tree.  To
233
 
    manipulate the inventory (for example to add a file), it is read
234
 
    in, modified, and then written back out.
 
230
    This describes which file_id is present at each point in the tree,
 
231
    and possibly the SHA-1 or other information about the file.
 
232
    Entries can be looked up either by path or by file_id.
235
233
 
236
234
    The inventory represents a typical unix file tree, with
237
235
    directories containing files and subdirectories.  We never store
244
242
    inserted, other than through the Inventory API.
245
243
 
246
244
    >>> inv = Inventory()
247
 
    >>> inv.write_xml(sys.stdout)
248
 
    <inventory>
249
 
    </inventory>
250
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
245
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
246
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
251
247
    >>> inv['123-123'].name
252
248
    'hello.c'
253
249
 
262
258
 
263
259
    >>> [x[0] for x in inv.iter_entries()]
264
260
    ['hello.c']
265
 
    
266
 
    >>> inv.write_xml(sys.stdout)
267
 
    <inventory>
268
 
    <entry file_id="123-123" kind="file" name="hello.c" />
269
 
    </inventory>
270
 
 
 
261
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
262
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
263
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
271
264
    """
272
 
 
273
 
    ## TODO: Make sure only canonical filenames are stored.
274
 
 
275
 
    ## TODO: Do something sensible about the possible collisions on
276
 
    ## case-losing filesystems.  Perhaps we should just always forbid
277
 
    ## such collisions.
278
 
 
279
 
    ## TODO: No special cases for root, rather just give it a file id
280
 
    ## like everything else.
281
 
 
282
 
    ## TODO: Probably change XML serialization to use nesting
283
 
 
284
 
    def __init__(self):
 
265
    def __init__(self, root_id=ROOT_ID):
285
266
        """Create or read an inventory.
286
267
 
287
268
        If a working directory is specified, the inventory is read
291
272
        The inventory is created with a default root directory, with
292
273
        an id of None.
293
274
        """
294
 
        self.root = RootEntry(None)
295
 
        self._byid = {None: self.root}
 
275
        # We are letting Branch.initialize() create a unique inventory
 
276
        # root id. Rather than generating a random one here.
 
277
        #if root_id is None:
 
278
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
279
        self.root = RootEntry(root_id)
 
280
        self._byid = {self.root.file_id: self.root}
 
281
 
 
282
 
 
283
    def copy(self):
 
284
        other = Inventory(self.root.file_id)
 
285
        # copy recursively so we know directories will be added before
 
286
        # their children.  There are more efficient ways than this...
 
287
        for path, entry in self.iter_entries():
 
288
            if entry == self.root:
 
289
                continue
 
290
            other.add(entry.copy())
 
291
        return other
296
292
 
297
293
 
298
294
    def __iter__(self):
318
314
            yield name, ie
319
315
            if ie.kind == 'directory':
320
316
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
321
 
                    yield '/'.join((name, cn)), cie
322
 
                    
323
 
 
324
 
 
325
 
    def directories(self, from_dir=None):
326
 
        """Return (path, entry) pairs for all directories.
 
317
                    yield os.path.join(name, cn), cie
 
318
 
 
319
 
 
320
    def entries(self):
 
321
        """Return list of (path, ie) for all entries except the root.
 
322
 
 
323
        This may be faster than iter_entries.
327
324
        """
328
 
        def descend(parent_ie):
329
 
            parent_name = parent_ie.name
330
 
            yield parent_name, parent_ie
331
 
 
332
 
            # directory children in sorted order
333
 
            dn = []
334
 
            for ie in parent_ie.children.itervalues():
 
325
        accum = []
 
326
        def descend(dir_ie, dir_path):
 
327
            kids = dir_ie.children.items()
 
328
            kids.sort()
 
329
            for name, ie in kids:
 
330
                child_path = os.path.join(dir_path, name)
 
331
                accum.append((child_path, ie))
335
332
                if ie.kind == 'directory':
336
 
                    dn.append((ie.name, ie))
337
 
            dn.sort()
 
333
                    descend(ie, child_path)
 
334
 
 
335
        descend(self.root, '')
 
336
        return accum
 
337
 
 
338
 
 
339
    def directories(self):
 
340
        """Return (path, entry) pairs for all directories, including the root.
 
341
        """
 
342
        accum = []
 
343
        def descend(parent_ie, parent_path):
 
344
            accum.append((parent_path, parent_ie))
338
345
            
339
 
            for name, child_ie in dn:
340
 
                for sub_name, sub_ie in descend(child_ie):
341
 
                    yield appendpath(parent_name, sub_name), sub_ie
 
346
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
347
            kids.sort()
342
348
 
343
 
        for name, ie in descend(self.root):
344
 
            yield name, ie
 
349
            for name, child_ie in kids:
 
350
                child_path = os.path.join(parent_path, name)
 
351
                descend(child_ie, child_path)
 
352
        descend(self.root, '')
 
353
        return accum
345
354
        
346
355
 
347
356
 
349
358
        """True if this entry contains a file with given id.
350
359
 
351
360
        >>> inv = Inventory()
352
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
361
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
362
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
353
363
        >>> '123' in inv
354
364
        True
355
365
        >>> '456' in inv
362
372
        """Return the entry for given file_id.
363
373
 
364
374
        >>> inv = Inventory()
365
 
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
375
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
376
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
366
377
        >>> inv['123123'].name
367
378
        'hello.c'
368
379
        """
369
 
        return self._byid[file_id]
370
 
 
 
380
        try:
 
381
            return self._byid[file_id]
 
382
        except KeyError:
 
383
            if file_id == None:
 
384
                raise BzrError("can't look up file_id None")
 
385
            else:
 
386
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
387
 
 
388
 
 
389
    def get_file_kind(self, file_id):
 
390
        return self._byid[file_id].kind
371
391
 
372
392
    def get_child(self, parent_id, filename):
373
393
        return self[parent_id].children.get(filename)
377
397
        """Add entry to inventory.
378
398
 
379
399
        To add  a file to a branch ready to be committed, use Branch.add,
380
 
        which calls this."""
 
400
        which calls this.
 
401
 
 
402
        Returns the new entry object.
 
403
        """
381
404
        if entry.file_id in self._byid:
382
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
405
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
406
 
 
407
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
408
            entry.parent_id = self.root.file_id
383
409
 
384
410
        try:
385
411
            parent = self._byid[entry.parent_id]
386
412
        except KeyError:
387
 
            bailout("parent_id %r not in inventory" % entry.parent_id)
 
413
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
388
414
 
389
415
        if parent.children.has_key(entry.name):
390
 
            bailout("%s is already versioned" %
 
416
            raise BzrError("%s is already versioned" %
391
417
                    appendpath(self.id2path(parent.file_id), entry.name))
392
418
 
393
419
        self._byid[entry.file_id] = entry
394
420
        parent.children[entry.name] = entry
 
421
        return entry
395
422
 
396
423
 
397
424
    def add_path(self, relpath, kind, file_id=None):
398
425
        """Add entry from a path.
399
426
 
400
 
        The immediate parent must already be versioned"""
 
427
        The immediate parent must already be versioned.
 
428
 
 
429
        Returns the new entry object."""
 
430
        from bzrlib.branch import gen_file_id
 
431
        
401
432
        parts = bzrlib.osutils.splitpath(relpath)
402
433
        if len(parts) == 0:
403
 
            bailout("cannot re-add root of inventory")
404
 
 
405
 
        if file_id is None:
406
 
            file_id = bzrlib.branch.gen_file_id(relpath)
407
 
 
408
 
        parent_id = self.path2id(parts[:-1])
 
434
            raise BzrError("cannot re-add root of inventory")
 
435
 
 
436
        if file_id == None:
 
437
            file_id = gen_file_id(relpath)
 
438
 
 
439
        parent_path = parts[:-1]
 
440
        parent_id = self.path2id(parent_path)
 
441
        if parent_id == None:
 
442
            raise NotVersionedError(parent_path)
 
443
 
409
444
        ie = InventoryEntry(file_id, parts[-1],
410
445
                            kind=kind, parent_id=parent_id)
411
446
        return self.add(ie)
415
450
        """Remove entry by id.
416
451
 
417
452
        >>> inv = Inventory()
418
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
453
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
454
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
419
455
        >>> '123' in inv
420
456
        True
421
457
        >>> del inv['123']
437
473
        del self[ie.parent_id].children[ie.name]
438
474
 
439
475
 
440
 
    def id_set(self):
441
 
        return Set(self._byid)
442
 
 
443
 
 
444
 
    def to_element(self):
445
 
        """Convert to XML Element"""
446
 
        e = Element('inventory')
447
 
        e.text = '\n'
448
 
        for path, ie in self.iter_entries():
449
 
            e.append(ie.to_element())
450
 
        return e
451
 
    
452
 
 
453
 
    def from_element(cls, elt):
454
 
        """Construct from XML Element
455
 
 
456
 
        >>> inv = Inventory()
457
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
458
 
        >>> elt = inv.to_element()
459
 
        >>> inv2 = Inventory.from_element(elt)
460
 
        >>> inv2 == inv
461
 
        True
462
 
        """
463
 
        assert elt.tag == 'inventory'
464
 
        o = cls()
465
 
        for e in elt:
466
 
            o.add(InventoryEntry.from_element(e))
467
 
        return o
468
 
        
469
 
    from_element = classmethod(from_element)
470
 
 
471
 
 
472
 
    def __cmp__(self, other):
 
476
    def __eq__(self, other):
473
477
        """Compare two sets by comparing their contents.
474
478
 
475
479
        >>> i1 = Inventory()
476
480
        >>> i2 = Inventory()
477
481
        >>> i1 == i2
478
482
        True
479
 
        >>> i1.add(InventoryEntry('123', 'foo'))
 
483
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
484
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
480
485
        >>> i1 == i2
481
486
        False
482
 
        >>> i2.add(InventoryEntry('123', 'foo'))
 
487
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
488
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
483
489
        >>> i1 == i2
484
490
        True
485
491
        """
486
 
        if self is other:
487
 
            return 0
488
 
        
489
492
        if not isinstance(other, Inventory):
490
493
            return NotImplemented
491
494
 
492
 
        if self.id_set() ^ other.id_set():
493
 
            return 1
494
 
 
495
 
        for file_id in self._byid:
496
 
            c = cmp(self[file_id], other[file_id])
497
 
            if c: return c
498
 
 
499
 
        return 0
500
 
 
501
 
 
502
 
    def id2path(self, file_id):
503
 
        """Return as a list the path to file_id."""
 
495
        if len(self._byid) != len(other._byid):
 
496
            # shortcut: obviously not the same
 
497
            return False
 
498
 
 
499
        return self._byid == other._byid
 
500
 
 
501
 
 
502
    def __ne__(self, other):
 
503
        return not self.__eq__(other)
 
504
 
 
505
 
 
506
    def __hash__(self):
 
507
        raise ValueError('not hashable')
 
508
 
 
509
 
 
510
    def get_idpath(self, file_id):
 
511
        """Return a list of file_ids for the path to an entry.
 
512
 
 
513
        The list contains one element for each directory followed by
 
514
        the id of the file itself.  So the length of the returned list
 
515
        is equal to the depth of the file in the tree, counting the
 
516
        root directory as depth 1.
 
517
        """
504
518
        p = []
505
519
        while file_id != None:
506
 
            ie = self._byid[file_id]
507
 
            p.insert(0, ie.name)
 
520
            try:
 
521
                ie = self._byid[file_id]
 
522
            except KeyError:
 
523
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
524
            p.insert(0, ie.file_id)
508
525
            file_id = ie.parent_id
509
 
        return '/'.join(p)
 
526
        return p
 
527
 
 
528
 
 
529
    def id2path(self, file_id):
 
530
        """Return as a list the path to file_id."""
 
531
 
 
532
        # get all names, skipping root
 
533
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
534
        return os.sep.join(p)
510
535
            
511
536
 
512
537
 
518
543
 
519
544
        This returns the entry of the last component in the path,
520
545
        which may be either a file or a directory.
 
546
 
 
547
        Returns None iff the path is not found.
521
548
        """
522
549
        if isinstance(name, types.StringTypes):
523
550
            name = splitpath(name)
524
551
 
525
 
        parent = self[None]
 
552
        mutter("lookup path %r" % name)
 
553
 
 
554
        parent = self.root
526
555
        for f in name:
527
556
            try:
528
557
                cie = parent.children[f]
529
558
                assert cie.name == f
 
559
                assert cie.parent_id == parent.file_id
530
560
                parent = cie
531
561
            except KeyError:
532
562
                # or raise an error?
550
580
 
551
581
        This does not move the working file."""
552
582
        if not is_valid_name(new_name):
553
 
            bailout("not an acceptable filename: %r" % new_name)
 
583
            raise BzrError("not an acceptable filename: %r" % new_name)
554
584
 
555
585
        new_parent = self._byid[new_parent_id]
556
586
        if new_name in new_parent.children:
557
 
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
587
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
588
 
 
589
        new_parent_idpath = self.get_idpath(new_parent_id)
 
590
        if file_id in new_parent_idpath:
 
591
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
592
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
558
593
 
559
594
        file_ie = self._byid[file_id]
560
595
        old_parent = self._byid[file_ie.parent_id]
570
605
 
571
606
 
572
607
 
573
 
_NAME_RE = re.compile(r'^[^/\\]+$')
 
608
_NAME_RE = None
574
609
 
575
610
def is_valid_name(name):
 
611
    global _NAME_RE
 
612
    if _NAME_RE == None:
 
613
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
614
        
576
615
    return bool(_NAME_RE.match(name))
577
 
 
578
 
 
579
 
 
580
 
if __name__ == '__main__':
581
 
    import doctest, inventory
582
 
    doctest.testmod(inventory)