~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-05 13:46:36 UTC
  • Revision ID: mbp@sourcefrog.net-20050405134635-488e04a5092ce0faec0ff181
- New 'move' command; now separated out from rename

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
 
 
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
 
 
 
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>"
30
23
 
31
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
32
34
 
33
35
import bzrlib
34
 
from bzrlib.errors import BzrError, BzrCheckError
35
 
 
36
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
37
from bzrlib.trace import mutter
38
 
from bzrlib.errors import NotVersionedError
39
 
        
40
38
 
41
 
class InventoryEntry(object):
 
39
class InventoryEntry(XMLMixin):
42
40
    """Description of a versioned file.
43
41
 
44
42
    An InventoryEntry has the following fields, which are also
45
43
    present in the XML inventory-entry element:
46
44
 
47
 
    file_id
48
 
 
49
 
    name
50
 
        (within the parent directory)
51
 
 
52
 
    kind
53
 
        'directory' or 'file'
54
 
 
55
 
    parent_id
56
 
        file_id of the parent directory, or ROOT_ID
57
 
 
58
 
    entry_version
59
 
        the revision_id in which the name or parent of this file was
60
 
        last changed
61
 
 
62
 
    text_sha1
63
 
        sha-1 of the text of the file
64
 
        
65
 
    text_size
66
 
        size in bytes of the text of the file
67
 
        
68
 
    text_version
69
 
        the revision_id in which the text of this file was introduced
70
 
 
71
 
    (reading a version 4 tree created a text_id field.)
 
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
 
72
59
 
73
60
    >>> i = Inventory()
74
61
    >>> i.path2id('')
75
 
    'TREE_ROOT'
76
 
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
77
 
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
78
 
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
79
 
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
 
62
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
 
63
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
80
64
    >>> for j in i.iter_entries():
81
65
    ...   print j
82
66
    ... 
83
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
67
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
84
68
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
85
 
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
69
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
86
70
    Traceback (most recent call last):
87
71
    ...
88
 
    BzrError: inventory already contains entry with id {2323}
89
 
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
90
 
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
91
 
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
92
 
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
 
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'))
93
75
    >>> i.path2id('src/wibble')
94
76
    '2325'
95
77
    >>> '2325' in i
96
78
    True
97
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
98
 
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
79
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
99
80
    >>> i['2326']
100
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
101
82
    >>> for j in i.iter_entries():
109
90
    src/wibble/wibble.c
110
91
    >>> i.id2path('2326')
111
92
    '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.
112
97
    """
113
 
    
114
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
115
 
                 'text_id', 'parent_id', 'children',
116
 
                 'text_version', 'entry_version', ]
117
 
 
118
 
 
119
 
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
98
    def __init__(self, file_id, name, kind='file', text_id=None,
 
99
                 parent_id=None):
120
100
        """Create an InventoryEntry
121
101
        
122
102
        The filename must be a single component, relative to the
123
103
        parent directory; it cannot be a whole path or relative name.
124
104
 
125
 
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
105
        >>> e = InventoryEntry('123', 'hello.c')
126
106
        >>> e.name
127
107
        'hello.c'
128
108
        >>> e.file_id
129
109
        '123'
130
 
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
110
        >>> e = InventoryEntry('123', 'src/hello.c')
131
111
        Traceback (most recent call last):
132
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
112
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
133
113
        """
134
 
        assert isinstance(name, basestring), name
135
 
        if '/' in name or '\\' in name:
136
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
137
 
        
138
 
        self.text_version = None
139
 
        self.entry_version = None
140
 
        self.text_sha1 = None
141
 
        self.text_size = None
 
114
        
 
115
        if len(splitpath(name)) != 1:
 
116
            bailout('InventoryEntry name is not a simple filename: %r'
 
117
                    % name)
 
118
        
142
119
        self.file_id = file_id
143
120
        self.name = name
 
121
        assert kind in ['file', 'directory']
144
122
        self.kind = kind
145
123
        self.text_id = text_id
146
124
        self.parent_id = parent_id
 
125
        self.text_sha1 = None
 
126
        self.text_size = None
147
127
        if kind == 'directory':
148
128
            self.children = {}
149
 
        elif kind == 'file':
150
 
            pass
151
 
        else:
152
 
            raise BzrError("unhandled entry kind %r" % kind)
153
 
 
154
129
 
155
130
 
156
131
    def sorted_children(self):
161
136
 
162
137
    def copy(self):
163
138
        other = InventoryEntry(self.file_id, self.name, self.kind,
164
 
                               self.parent_id)
165
 
        other.text_id = self.text_id
 
139
                               self.text_id, self.parent_id)
166
140
        other.text_sha1 = self.text_sha1
167
141
        other.text_size = self.text_size
168
 
        other.text_version = self.text_version
169
 
        other.entry_version = self.entry_version
170
 
        # note that children are *not* copied; they're pulled across when
171
 
        # others are added
172
142
        return other
173
143
 
174
144
 
181
151
                   self.parent_id))
182
152
 
183
153
    
184
 
    def __eq__(self, other):
 
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
185
195
        if not isinstance(other, InventoryEntry):
186
196
            return NotImplemented
187
197
 
188
 
        return (self.file_id == other.file_id) \
189
 
               and (self.name == other.name) \
190
 
               and (self.text_sha1 == other.text_sha1) \
191
 
               and (self.text_size == other.text_size) \
192
 
               and (self.text_id == other.text_id) \
193
 
               and (self.parent_id == other.parent_id) \
194
 
               and (self.kind == other.kind) \
195
 
               and (self.text_version == other.text_version) \
196
 
               and (self.entry_version == other.entry_version)
197
 
 
198
 
 
199
 
    def __ne__(self, other):
200
 
        return not (self == other)
201
 
 
202
 
    def __hash__(self):
203
 
        raise ValueError('not hashable')
 
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)
204
205
 
205
206
 
206
207
 
212
213
        self.parent_id = None
213
214
        self.name = ''
214
215
 
215
 
    def __eq__(self, other):
 
216
    def __cmp__(self, other):
 
217
        if self is other:
 
218
            return 0
216
219
        if not isinstance(other, RootEntry):
217
220
            return NotImplemented
218
 
        
219
 
        return (self.file_id == other.file_id) \
220
 
               and (self.children == other.children)
221
 
 
222
 
 
223
 
 
224
 
class Inventory(object):
 
221
        return cmp(self.file_id, other.file_id) \
 
222
               or cmp(self.children, other.children)
 
223
 
 
224
 
 
225
 
 
226
class Inventory(XMLMixin):
225
227
    """Inventory of versioned files in a tree.
226
228
 
227
 
    This describes which file_id is present at each point in the tree,
228
 
    and possibly the SHA-1 or other information about the file.
229
 
    Entries can be looked up either by path or by file_id.
 
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
235
 
231
236
    The inventory represents a typical unix file tree, with
232
237
    directories containing files and subdirectories.  We never store
239
244
    inserted, other than through the Inventory API.
240
245
 
241
246
    >>> inv = Inventory()
242
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
243
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
247
    >>> inv.write_xml(sys.stdout)
 
248
    <inventory>
 
249
    </inventory>
 
250
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
244
251
    >>> inv['123-123'].name
245
252
    'hello.c'
246
253
 
255
262
 
256
263
    >>> [x[0] for x in inv.iter_entries()]
257
264
    ['hello.c']
258
 
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
259
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
260
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
265
    
 
266
    >>> inv.write_xml(sys.stdout)
 
267
    <inventory>
 
268
    <entry file_id="123-123" kind="file" name="hello.c" />
 
269
    </inventory>
 
270
 
261
271
    """
262
 
    def __init__(self, root_id=ROOT_ID):
 
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):
263
285
        """Create or read an inventory.
264
286
 
265
287
        If a working directory is specified, the inventory is read
269
291
        The inventory is created with a default root directory, with
270
292
        an id of None.
271
293
        """
272
 
        # We are letting Branch(init=True) create a unique inventory
273
 
        # root id. Rather than generating a random one here.
274
 
        #if root_id is None:
275
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
276
 
        self.root = RootEntry(root_id)
277
 
        self._byid = {self.root.file_id: self.root}
278
 
 
279
 
 
280
 
    def copy(self):
281
 
        other = Inventory(self.root.file_id)
282
 
        # copy recursively so we know directories will be added before
283
 
        # their children.  There are more efficient ways than this...
284
 
        for path, entry in self.iter_entries():
285
 
            if entry == self.root:
286
 
                continue
287
 
            other.add(entry.copy())
288
 
        return other
 
294
        self.root = RootEntry(None)
 
295
        self._byid = {None: self.root}
289
296
 
290
297
 
291
298
    def __iter__(self):
311
318
            yield name, ie
312
319
            if ie.kind == 'directory':
313
320
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
314
 
                    yield os.path.join(name, cn), cie
315
 
 
316
 
 
317
 
    def entries(self):
318
 
        """Return list of (path, ie) for all entries except the root.
319
 
 
320
 
        This may be faster than iter_entries.
 
321
                    yield '/'.join((name, cn)), cie
 
322
                    
 
323
 
 
324
 
 
325
    def directories(self, from_dir=None):
 
326
        """Return (path, entry) pairs for all directories.
321
327
        """
322
 
        accum = []
323
 
        def descend(dir_ie, dir_path):
324
 
            kids = dir_ie.children.items()
325
 
            kids.sort()
326
 
            for name, ie in kids:
327
 
                child_path = os.path.join(dir_path, name)
328
 
                accum.append((child_path, ie))
 
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():
329
335
                if ie.kind == 'directory':
330
 
                    descend(ie, child_path)
331
 
 
332
 
        descend(self.root, '')
333
 
        return accum
334
 
 
335
 
 
336
 
    def directories(self):
337
 
        """Return (path, entry) pairs for all directories, including the root.
338
 
        """
339
 
        accum = []
340
 
        def descend(parent_ie, parent_path):
341
 
            accum.append((parent_path, parent_ie))
 
336
                    dn.append((ie.name, ie))
 
337
            dn.sort()
342
338
            
343
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
344
 
            kids.sort()
 
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
345
342
 
346
 
            for name, child_ie in kids:
347
 
                child_path = os.path.join(parent_path, name)
348
 
                descend(child_ie, child_path)
349
 
        descend(self.root, '')
350
 
        return accum
 
343
        for name, ie in descend(self.root):
 
344
            yield name, ie
351
345
        
352
346
 
353
347
 
355
349
        """True if this entry contains a file with given id.
356
350
 
357
351
        >>> inv = Inventory()
358
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
359
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
352
        >>> inv.add(InventoryEntry('123', 'foo.c'))
360
353
        >>> '123' in inv
361
354
        True
362
355
        >>> '456' in inv
369
362
        """Return the entry for given file_id.
370
363
 
371
364
        >>> inv = Inventory()
372
 
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
373
 
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
365
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
374
366
        >>> inv['123123'].name
375
367
        'hello.c'
376
368
        """
377
 
        try:
378
 
            return self._byid[file_id]
379
 
        except KeyError:
380
 
            if file_id == None:
381
 
                raise BzrError("can't look up file_id None")
382
 
            else:
383
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
384
 
 
385
 
 
386
 
    def get_file_kind(self, file_id):
387
 
        return self._byid[file_id].kind
 
369
        return self._byid[file_id]
 
370
 
388
371
 
389
372
    def get_child(self, parent_id, filename):
390
373
        return self[parent_id].children.get(filename)
394
377
        """Add entry to inventory.
395
378
 
396
379
        To add  a file to a branch ready to be committed, use Branch.add,
397
 
        which calls this.
398
 
 
399
 
        Returns the new entry object.
400
 
        """
 
380
        which calls this."""
401
381
        if entry.file_id in self._byid:
402
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
403
 
 
404
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
405
 
            entry.parent_id = self.root.file_id
 
382
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
406
383
 
407
384
        try:
408
385
            parent = self._byid[entry.parent_id]
409
386
        except KeyError:
410
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
387
            bailout("parent_id %r not in inventory" % entry.parent_id)
411
388
 
412
389
        if parent.children.has_key(entry.name):
413
 
            raise BzrError("%s is already versioned" %
 
390
            bailout("%s is already versioned" %
414
391
                    appendpath(self.id2path(parent.file_id), entry.name))
415
392
 
416
393
        self._byid[entry.file_id] = entry
417
394
        parent.children[entry.name] = entry
418
 
        return entry
419
395
 
420
396
 
421
397
    def add_path(self, relpath, kind, file_id=None):
422
398
        """Add entry from a path.
423
399
 
424
 
        The immediate parent must already be versioned.
425
 
 
426
 
        Returns the new entry object."""
427
 
        from bzrlib.branch import gen_file_id
428
 
        
 
400
        The immediate parent must already be versioned"""
429
401
        parts = bzrlib.osutils.splitpath(relpath)
430
402
        if len(parts) == 0:
431
 
            raise BzrError("cannot re-add root of inventory")
432
 
 
433
 
        if file_id == None:
434
 
            file_id = gen_file_id(relpath)
435
 
 
436
 
        parent_path = parts[:-1]
437
 
        parent_id = self.path2id(parent_path)
438
 
        if parent_id == None:
439
 
            raise NotVersionedError(parent_path)
440
 
 
 
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])
441
409
        ie = InventoryEntry(file_id, parts[-1],
442
410
                            kind=kind, parent_id=parent_id)
443
411
        return self.add(ie)
447
415
        """Remove entry by id.
448
416
 
449
417
        >>> inv = Inventory()
450
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
451
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
418
        >>> inv.add(InventoryEntry('123', 'foo.c'))
452
419
        >>> '123' in inv
453
420
        True
454
421
        >>> del inv['123']
470
437
        del self[ie.parent_id].children[ie.name]
471
438
 
472
439
 
473
 
    def __eq__(self, other):
 
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):
474
473
        """Compare two sets by comparing their contents.
475
474
 
476
475
        >>> i1 = Inventory()
477
476
        >>> i2 = Inventory()
478
477
        >>> i1 == i2
479
478
        True
480
 
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
481
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
479
        >>> i1.add(InventoryEntry('123', 'foo'))
482
480
        >>> i1 == i2
483
481
        False
484
 
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
485
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
482
        >>> i2.add(InventoryEntry('123', 'foo'))
486
483
        >>> i1 == i2
487
484
        True
488
485
        """
 
486
        if self is other:
 
487
            return 0
 
488
        
489
489
        if not isinstance(other, Inventory):
490
490
            return NotImplemented
491
491
 
492
 
        if len(self._byid) != len(other._byid):
493
 
            # shortcut: obviously not the same
494
 
            return False
495
 
 
496
 
        return self._byid == other._byid
497
 
 
498
 
 
499
 
    def __ne__(self, other):
500
 
        return not self.__eq__(other)
501
 
 
502
 
 
503
 
    def __hash__(self):
504
 
        raise ValueError('not hashable')
 
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
505
500
 
506
501
 
507
502
    def get_idpath(self, file_id):
510
505
        The list contains one element for each directory followed by
511
506
        the id of the file itself.  So the length of the returned list
512
507
        is equal to the depth of the file in the tree, counting the
513
 
        root directory as depth 1.
 
508
        root directory as depth 0.
514
509
        """
515
510
        p = []
516
511
        while file_id != None:
517
 
            try:
518
 
                ie = self._byid[file_id]
519
 
            except KeyError:
520
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
512
            ie = self._byid[file_id]
521
513
            p.insert(0, ie.file_id)
522
514
            file_id = ie.parent_id
523
515
        return p
525
517
 
526
518
    def id2path(self, file_id):
527
519
        """Return as a list the path to file_id."""
528
 
 
529
 
        # get all names, skipping root
530
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
531
 
        return os.sep.join(p)
 
520
        p = []
 
521
        while file_id != None:
 
522
            ie = self._byid[file_id]
 
523
            p.insert(0, ie.name)
 
524
            file_id = ie.parent_id
 
525
        return '/'.join(p)
532
526
            
533
527
 
534
528
 
540
534
 
541
535
        This returns the entry of the last component in the path,
542
536
        which may be either a file or a directory.
543
 
 
544
 
        Returns None iff the path is not found.
545
537
        """
546
538
        if isinstance(name, types.StringTypes):
547
539
            name = splitpath(name)
548
540
 
549
 
        mutter("lookup path %r" % name)
550
 
 
551
 
        parent = self.root
 
541
        parent = self[None]
552
542
        for f in name:
553
543
            try:
554
544
                cie = parent.children[f]
555
545
                assert cie.name == f
556
 
                assert cie.parent_id == parent.file_id
557
546
                parent = cie
558
547
            except KeyError:
559
548
                # or raise an error?
577
566
 
578
567
        This does not move the working file."""
579
568
        if not is_valid_name(new_name):
580
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
569
            bailout("not an acceptable filename: %r" % new_name)
581
570
 
582
571
        new_parent = self._byid[new_parent_id]
583
572
        if new_name in new_parent.children:
584
 
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
573
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
585
574
 
586
575
        new_parent_idpath = self.get_idpath(new_parent_id)
587
576
        if file_id in new_parent_idpath:
588
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
577
            bailout("cannot move directory %r into a subdirectory of itself, %r"
589
578
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
590
579
 
591
580
        file_ie = self._byid[file_id]
602
591
 
603
592
 
604
593
 
605
 
_NAME_RE = None
 
594
_NAME_RE = re.compile(r'^[^/\\]+$')
606
595
 
607
596
def is_valid_name(name):
608
 
    global _NAME_RE
609
 
    if _NAME_RE == None:
610
 
        _NAME_RE = re.compile(r'^[^/\\]+$')
611
 
        
612
597
    return bool(_NAME_RE.match(name))
 
598
 
 
599
 
 
600
 
 
601
if __name__ == '__main__':
 
602
    import doctest, inventory
 
603
    doctest.testmod(inventory)