~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-09-05 09:11:03 UTC
  • Revision ID: mbp@sourcefrog.net-20050905091103-1e51e146be0f08b4
- add test for deserialization from a canned XML inventory

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# (C) 2005 Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
15
14
# along with this program; if not, write to the Free Software
16
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
16
 
18
 
"""Inventories map files to their name in a revision."""
19
 
 
20
 
 
21
 
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
 
__author__ = "Martin Pool <mbp@canonical.com>"
23
 
 
24
 
import sys, os.path, types
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
# This should really be an id randomly assigned when the tree is
 
19
# created, but it's not for now.
 
20
ROOT_ID = "TREE_ROOT"
 
21
 
 
22
 
 
23
import sys, os.path, types, re
34
24
 
35
25
import bzrlib
 
26
from bzrlib.errors import BzrError, BzrCheckError
 
27
 
36
28
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
29
from bzrlib.trace import mutter
 
30
from bzrlib.errors import NotVersionedError
 
31
        
38
32
 
39
 
class InventoryEntry(XMLMixin):
 
33
class InventoryEntry(object):
40
34
    """Description of a versioned file.
41
35
 
42
36
    An InventoryEntry has the following fields, which are also
59
53
 
60
54
    >>> i = Inventory()
61
55
    >>> i.path2id('')
62
 
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
 
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
 
56
    'TREE_ROOT'
 
57
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
58
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
 
59
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
60
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
64
61
    >>> for j in i.iter_entries():
65
62
    ...   print j
66
63
    ... 
67
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
 
64
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
68
65
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
 
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
 
66
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
67
    Traceback (most recent call last):
71
68
    ...
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'))
 
69
    BzrError: inventory already contains entry with id {2323}
 
70
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
71
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
72
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
73
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
75
74
    >>> i.path2id('src/wibble')
76
75
    '2325'
77
76
    >>> '2325' in i
78
77
    True
79
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
78
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
79
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
80
    >>> i['2326']
81
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
82
    >>> for j in i.iter_entries():
91
91
    >>> i.id2path('2326')
92
92
    'src/wibble/wibble.c'
93
93
 
94
 
    :todo: Maybe also keep the full path of the entry, and the children?
 
94
    TODO: Maybe also keep the full path of the entry, and the children?
95
95
           But those depend on its position within a particular inventory, and
96
96
           it would be nice not to need to hold the backpointer here.
97
97
    """
98
 
    def __init__(self, file_id, name, kind='file', text_id=None,
99
 
                 parent_id=None):
 
98
 
 
99
    # TODO: split InventoryEntry into subclasses for files,
 
100
    # directories, etc etc.
 
101
 
 
102
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
103
                 'text_id', 'parent_id', 'children', ]
 
104
 
 
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
100
106
        """Create an InventoryEntry
101
107
        
102
108
        The filename must be a single component, relative to the
103
109
        parent directory; it cannot be a whole path or relative name.
104
110
 
105
 
        >>> e = InventoryEntry('123', 'hello.c')
 
111
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
106
112
        >>> e.name
107
113
        'hello.c'
108
114
        >>> e.file_id
109
115
        '123'
110
 
        >>> e = InventoryEntry('123', 'src/hello.c')
 
116
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
111
117
        Traceback (most recent call last):
112
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
118
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
113
119
        """
114
 
        
115
 
        if len(splitpath(name)) != 1:
116
 
            bailout('InventoryEntry name is not a simple filename: %r'
117
 
                    % name)
118
 
        
 
120
        if '/' in name or '\\' in name:
 
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
122
        
 
123
        self.text_sha1 = None
 
124
        self.text_size = None
 
125
    
119
126
        self.file_id = file_id
120
127
        self.name = name
121
 
        assert kind in ['file', 'directory']
122
128
        self.kind = kind
123
129
        self.text_id = text_id
124
130
        self.parent_id = parent_id
125
 
        self.text_sha1 = None
126
 
        self.text_size = None
 
131
        if kind == 'directory':
 
132
            self.children = {}
 
133
        elif kind == 'file':
 
134
            pass
 
135
        else:
 
136
            raise BzrError("unhandled entry kind %r" % kind)
 
137
 
 
138
 
 
139
 
 
140
    def sorted_children(self):
 
141
        l = self.children.items()
 
142
        l.sort()
 
143
        return l
127
144
 
128
145
 
129
146
    def copy(self):
130
147
        other = InventoryEntry(self.file_id, self.name, self.kind,
131
 
                               self.text_id, self.parent_id)
 
148
                               self.parent_id, text_id=self.text_id)
132
149
        other.text_sha1 = self.text_sha1
133
150
        other.text_size = self.text_size
 
151
        # note that children are *not* copied; they're pulled across when
 
152
        # others are added
134
153
        return other
135
154
 
136
155
 
143
162
                   self.parent_id))
144
163
 
145
164
    
146
 
    def to_element(self):
147
 
        """Convert to XML element"""
148
 
        e = Element('entry')
149
 
 
150
 
        e.set('name', self.name)
151
 
        e.set('file_id', self.file_id)
152
 
        e.set('kind', self.kind)
153
 
 
154
 
        if self.text_size is not None:
155
 
            e.set('text_size', '%d' % self.text_size)
156
 
            
157
 
        for f in ['text_id', 'text_sha1', 'parent_id']:
158
 
            v = getattr(self, f)
159
 
            if v is not None:
160
 
                e.set(f, v)
161
 
 
162
 
        e.tail = '\n'
163
 
            
164
 
        return e
165
 
 
166
 
 
167
 
    def from_element(cls, elt):
168
 
        assert elt.tag == 'entry'
169
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
170
 
        self.text_id = elt.get('text_id')
171
 
        self.text_sha1 = elt.get('text_sha1')
172
 
        self.parent_id = elt.get('parent_id')
173
 
        
174
 
        ## mutter("read inventoryentry: %r" % (elt.attrib))
175
 
 
176
 
        v = elt.get('text_size')
177
 
        self.text_size = v and int(v)
178
 
 
179
 
        return self
180
 
            
181
 
 
182
 
    from_element = classmethod(from_element)
183
 
 
184
 
    def __cmp__(self, other):
185
 
        if self is other:
186
 
            return 0
 
165
    def __eq__(self, other):
187
166
        if not isinstance(other, InventoryEntry):
188
167
            return NotImplemented
189
168
 
190
 
        return cmp(self.file_id, other.file_id) \
191
 
               or cmp(self.name, other.name) \
192
 
               or cmp(self.text_sha1, other.text_sha1) \
193
 
               or cmp(self.text_size, other.text_size) \
194
 
               or cmp(self.text_id, other.text_id) \
195
 
               or cmp(self.parent_id, other.parent_id) \
196
 
               or cmp(self.kind, other.kind)
197
 
 
198
 
 
199
 
 
200
 
class Inventory(XMLMixin):
 
169
        return (self.file_id == other.file_id) \
 
170
               and (self.name == other.name) \
 
171
               and (self.text_sha1 == other.text_sha1) \
 
172
               and (self.text_size == other.text_size) \
 
173
               and (self.text_id == other.text_id) \
 
174
               and (self.parent_id == other.parent_id) \
 
175
               and (self.kind == other.kind)
 
176
 
 
177
 
 
178
    def __ne__(self, other):
 
179
        return not (self == other)
 
180
 
 
181
    def __hash__(self):
 
182
        raise ValueError('not hashable')
 
183
 
 
184
 
 
185
 
 
186
class RootEntry(InventoryEntry):
 
187
    def __init__(self, file_id):
 
188
        self.file_id = file_id
 
189
        self.children = {}
 
190
        self.kind = 'root_directory'
 
191
        self.parent_id = None
 
192
        self.name = ''
 
193
 
 
194
    def __eq__(self, other):
 
195
        if not isinstance(other, RootEntry):
 
196
            return NotImplemented
 
197
        
 
198
        return (self.file_id == other.file_id) \
 
199
               and (self.children == other.children)
 
200
 
 
201
 
 
202
 
 
203
class Inventory(object):
201
204
    """Inventory of versioned files in a tree.
202
205
 
203
 
    An Inventory acts like a set of InventoryEntry items.  You can
204
 
    also look files up by their file_id or name.
205
 
    
206
 
    May be read from and written to a metadata file in a tree.  To
207
 
    manipulate the inventory (for example to add a file), it is read
208
 
    in, modified, and then written back out.
 
206
    This describes which file_id is present at each point in the tree,
 
207
    and possibly the SHA-1 or other information about the file.
 
208
    Entries can be looked up either by path or by file_id.
209
209
 
210
210
    The inventory represents a typical unix file tree, with
211
211
    directories containing files and subdirectories.  We never store
215
215
    returned quickly.
216
216
 
217
217
    InventoryEntry objects must not be modified after they are
218
 
    inserted.
 
218
    inserted, other than through the Inventory API.
219
219
 
220
220
    >>> inv = Inventory()
221
 
    >>> inv.write_xml(sys.stdout)
222
 
    <inventory>
223
 
    </inventory>
224
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
221
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
222
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
225
223
    >>> inv['123-123'].name
226
224
    'hello.c'
227
 
    >>> for file_id in inv: print file_id
228
 
    ...
229
 
    123-123
230
225
 
231
226
    May be treated as an iterator or set to look up file ids:
232
227
    
239
234
 
240
235
    >>> [x[0] for x in inv.iter_entries()]
241
236
    ['hello.c']
242
 
    
243
 
    >>> inv.write_xml(sys.stdout)
244
 
    <inventory>
245
 
    <entry file_id="123-123" kind="file" name="hello.c" />
246
 
    </inventory>
247
 
 
 
237
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
238
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
239
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
248
240
    """
249
 
 
250
 
    ## TODO: Clear up handling of files in subdirectories; we probably
251
 
    ## do want to be able to just look them up by name but this
252
 
    ## probably means gradually walking down the path, looking up as we go.
253
 
 
254
 
    ## TODO: Make sure only canonical filenames are stored.
255
 
 
256
 
    ## TODO: Do something sensible about the possible collisions on
257
 
    ## case-losing filesystems.  Perhaps we should just always forbid
258
 
    ## such collisions.
259
 
 
260
 
    ## _tree should probably just be stored as
261
 
    ## InventoryEntry._children on each directory.
262
 
 
263
 
    def __init__(self):
 
241
    def __init__(self, root_id=ROOT_ID):
264
242
        """Create or read an inventory.
265
243
 
266
244
        If a working directory is specified, the inventory is read
267
245
        from there.  If the file is specified, read from that. If not,
268
246
        the inventory is created empty.
 
247
 
 
248
        The inventory is created with a default root directory, with
 
249
        an id of None.
269
250
        """
270
 
        self._byid = dict()
271
 
 
272
 
        # _tree is indexed by parent_id; at each level a map from name
273
 
        # to ie.  The None entry is the root.
274
 
        self._tree = {None: {}}
 
251
        # We are letting Branch(init=True) create a unique inventory
 
252
        # root id. Rather than generating a random one here.
 
253
        #if root_id is None:
 
254
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
255
        self.root = RootEntry(root_id)
 
256
        self._byid = {self.root.file_id: self.root}
275
257
 
276
258
 
277
259
    def __iter__(self):
283
265
        return len(self._byid)
284
266
 
285
267
 
286
 
    def iter_entries(self, parent_id=None):
 
268
    def iter_entries(self, from_dir=None):
287
269
        """Return (path, entry) pairs, in order by name."""
288
 
        kids = self._tree[parent_id].items()
 
270
        if from_dir == None:
 
271
            assert self.root
 
272
            from_dir = self.root
 
273
        elif isinstance(from_dir, basestring):
 
274
            from_dir = self._byid[from_dir]
 
275
            
 
276
        kids = from_dir.children.items()
289
277
        kids.sort()
290
278
        for name, ie in kids:
291
279
            yield name, ie
292
280
            if ie.kind == 'directory':
293
 
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
294
 
                    yield joinpath([name, cn]), cie
295
 
 
296
 
 
297
 
    def directories(self, include_root=True):
298
 
        """Return (path, entry) pairs for all directories.
299
 
        """
300
 
        if include_root:
301
 
            yield '', None
302
 
        for path, entry in self.iter_entries():
303
 
            if entry.kind == 'directory':
304
 
                yield path, entry
 
281
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
282
                    yield os.path.join(name, cn), cie
 
283
 
 
284
 
 
285
    def entries(self):
 
286
        """Return list of (path, ie) for all entries except the root.
 
287
 
 
288
        This may be faster than iter_entries.
 
289
        """
 
290
        accum = []
 
291
        def descend(dir_ie, dir_path):
 
292
            kids = dir_ie.children.items()
 
293
            kids.sort()
 
294
            for name, ie in kids:
 
295
                child_path = os.path.join(dir_path, name)
 
296
                accum.append((child_path, ie))
 
297
                if ie.kind == 'directory':
 
298
                    descend(ie, child_path)
 
299
 
 
300
        descend(self.root, '')
 
301
        return accum
 
302
 
 
303
 
 
304
    def directories(self):
 
305
        """Return (path, entry) pairs for all directories, including the root.
 
306
        """
 
307
        accum = []
 
308
        def descend(parent_ie, parent_path):
 
309
            accum.append((parent_path, parent_ie))
 
310
            
 
311
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
312
            kids.sort()
 
313
 
 
314
            for name, child_ie in kids:
 
315
                child_path = os.path.join(parent_path, name)
 
316
                descend(child_ie, child_path)
 
317
        descend(self.root, '')
 
318
        return accum
305
319
        
306
320
 
307
321
 
308
 
    def children(self, parent_id):
309
 
        """Return entries that are direct children of parent_id."""
310
 
        return self._tree[parent_id]
311
 
                    
312
 
 
313
 
 
314
 
    # TODO: return all paths and entries
315
 
 
316
 
 
317
322
    def __contains__(self, file_id):
318
323
        """True if this entry contains a file with given id.
319
324
 
320
325
        >>> inv = Inventory()
321
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
326
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
327
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
322
328
        >>> '123' in inv
323
329
        True
324
330
        >>> '456' in inv
331
337
        """Return the entry for given file_id.
332
338
 
333
339
        >>> inv = Inventory()
334
 
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
340
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
341
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
335
342
        >>> inv['123123'].name
336
343
        'hello.c'
337
344
        """
338
 
        return self._byid[file_id]
 
345
        try:
 
346
            return self._byid[file_id]
 
347
        except KeyError:
 
348
            if file_id == None:
 
349
                raise BzrError("can't look up file_id None")
 
350
            else:
 
351
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
352
 
 
353
 
 
354
    def get_file_kind(self, file_id):
 
355
        return self._byid[file_id].kind
 
356
 
 
357
    def get_child(self, parent_id, filename):
 
358
        return self[parent_id].children.get(filename)
339
359
 
340
360
 
341
361
    def add(self, entry):
342
362
        """Add entry to inventory.
343
363
 
344
364
        To add  a file to a branch ready to be committed, use Branch.add,
345
 
        which calls this."""
346
 
        if entry.file_id in self:
347
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
348
 
 
349
 
        if entry.parent_id != None:
350
 
            if entry.parent_id not in self:
351
 
                bailout("parent_id %s of new entry not found in inventory"
352
 
                        % entry.parent_id)
353
 
            
354
 
        if self._tree[entry.parent_id].has_key(entry.name):
355
 
            bailout("%s is already versioned"
356
 
                    % appendpath(self.id2path(entry.parent_id), entry.name))
 
365
        which calls this.
 
366
 
 
367
        Returns the new entry object.
 
368
        """
 
369
        if entry.file_id in self._byid:
 
370
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
371
 
 
372
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
373
            entry.parent_id = self.root.file_id
 
374
 
 
375
        try:
 
376
            parent = self._byid[entry.parent_id]
 
377
        except KeyError:
 
378
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
379
 
 
380
        if parent.children.has_key(entry.name):
 
381
            raise BzrError("%s is already versioned" %
 
382
                    appendpath(self.id2path(parent.file_id), entry.name))
357
383
 
358
384
        self._byid[entry.file_id] = entry
359
 
        self._tree[entry.parent_id][entry.name] = entry
360
 
 
361
 
        if entry.kind == 'directory':
362
 
            self._tree[entry.file_id] = {}
 
385
        parent.children[entry.name] = entry
 
386
        return entry
363
387
 
364
388
 
365
389
    def add_path(self, relpath, kind, file_id=None):
366
390
        """Add entry from a path.
367
391
 
368
 
        The immediate parent must already be versioned"""
 
392
        The immediate parent must already be versioned.
 
393
 
 
394
        Returns the new entry object."""
 
395
        from bzrlib.branch import gen_file_id
 
396
        
369
397
        parts = bzrlib.osutils.splitpath(relpath)
370
398
        if len(parts) == 0:
371
 
            bailout("cannot re-add root of inventory")
372
 
 
373
 
        if file_id is None:
374
 
            file_id = bzrlib.branch.gen_file_id(relpath)
375
 
 
376
 
        parent_id = self.path2id(parts[:-1])
 
399
            raise BzrError("cannot re-add root of inventory")
 
400
 
 
401
        if file_id == None:
 
402
            file_id = gen_file_id(relpath)
 
403
 
 
404
        parent_path = parts[:-1]
 
405
        parent_id = self.path2id(parent_path)
 
406
        if parent_id == None:
 
407
            raise NotVersionedError(parent_path)
 
408
 
377
409
        ie = InventoryEntry(file_id, parts[-1],
378
410
                            kind=kind, parent_id=parent_id)
379
411
        return self.add(ie)
383
415
        """Remove entry by id.
384
416
 
385
417
        >>> inv = Inventory()
386
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
418
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
419
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
387
420
        >>> '123' in inv
388
421
        True
389
422
        >>> del inv['123']
392
425
        """
393
426
        ie = self[file_id]
394
427
 
395
 
        assert self._tree[ie.parent_id][ie.name] == ie
 
428
        assert self[ie.parent_id].children[ie.name] == ie
396
429
        
397
430
        # TODO: Test deleting all children; maybe hoist to a separate
398
431
        # deltree method?
399
432
        if ie.kind == 'directory':
400
 
            for cie in self._tree[file_id].values():
 
433
            for cie in ie.children.values():
401
434
                del self[cie.file_id]
402
 
            del self._tree[file_id]
 
435
            del ie.children
403
436
 
404
437
        del self._byid[file_id]
405
 
        del self._tree[ie.parent_id][ie.name]
406
 
 
407
 
 
408
 
    def id_set(self):
409
 
        return Set(self._byid)
410
 
 
411
 
 
412
 
    def to_element(self):
413
 
        """Convert to XML Element"""
414
 
        e = Element('inventory')
415
 
        e.text = '\n'
416
 
        for path, ie in self.iter_entries():
417
 
            e.append(ie.to_element())
418
 
        return e
419
 
    
420
 
 
421
 
    def from_element(cls, elt):
422
 
        """Construct from XML Element
423
 
 
424
 
        >>> inv = Inventory()
425
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
426
 
        >>> elt = inv.to_element()
427
 
        >>> inv2 = Inventory.from_element(elt)
428
 
        >>> inv2 == inv
429
 
        True
430
 
        """
431
 
        assert elt.tag == 'inventory'
432
 
        o = cls()
433
 
        for e in elt:
434
 
            o.add(InventoryEntry.from_element(e))
435
 
        return o
436
 
        
437
 
    from_element = classmethod(from_element)
438
 
 
439
 
 
440
 
    def __cmp__(self, other):
 
438
        del self[ie.parent_id].children[ie.name]
 
439
 
 
440
 
 
441
    def __eq__(self, other):
441
442
        """Compare two sets by comparing their contents.
442
443
 
443
444
        >>> i1 = Inventory()
444
445
        >>> i2 = Inventory()
445
446
        >>> i1 == i2
446
447
        True
447
 
        >>> i1.add(InventoryEntry('123', 'foo'))
 
448
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
449
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
448
450
        >>> i1 == i2
449
451
        False
450
 
        >>> i2.add(InventoryEntry('123', 'foo'))
 
452
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
453
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
451
454
        >>> i1 == i2
452
455
        True
453
456
        """
454
 
        if self is other:
455
 
            return 0
456
 
        
457
457
        if not isinstance(other, Inventory):
458
458
            return NotImplemented
459
459
 
460
 
        if self.id_set() ^ other.id_set():
461
 
            return 1
462
 
 
463
 
        for file_id in self._byid:
464
 
            c = cmp(self[file_id], other[file_id])
465
 
            if c: return c
466
 
 
467
 
        return 0
468
 
 
469
 
 
470
 
    def id2path(self, file_id):
471
 
        """Return as a list the path to file_id."""
 
460
        if len(self._byid) != len(other._byid):
 
461
            # shortcut: obviously not the same
 
462
            return False
 
463
 
 
464
        return self._byid == other._byid
 
465
 
 
466
 
 
467
    def __ne__(self, other):
 
468
        return not (self == other)
 
469
 
 
470
 
 
471
    def __hash__(self):
 
472
        raise ValueError('not hashable')
 
473
 
 
474
 
 
475
    def get_idpath(self, file_id):
 
476
        """Return a list of file_ids for the path to an entry.
 
477
 
 
478
        The list contains one element for each directory followed by
 
479
        the id of the file itself.  So the length of the returned list
 
480
        is equal to the depth of the file in the tree, counting the
 
481
        root directory as depth 1.
 
482
        """
472
483
        p = []
473
484
        while file_id != None:
474
 
            ie = self[file_id]
475
 
            p = [ie.name] + p
 
485
            try:
 
486
                ie = self._byid[file_id]
 
487
            except KeyError:
 
488
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
489
            p.insert(0, ie.file_id)
476
490
            file_id = ie.parent_id
477
 
        return joinpath(p)
 
491
        return p
 
492
 
 
493
 
 
494
    def id2path(self, file_id):
 
495
        """Return as a list the path to file_id."""
 
496
 
 
497
        # get all names, skipping root
 
498
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
499
        return os.sep.join(p)
478
500
            
479
501
 
480
502
 
486
508
 
487
509
        This returns the entry of the last component in the path,
488
510
        which may be either a file or a directory.
 
511
 
 
512
        Returns None iff the path is not found.
489
513
        """
490
514
        if isinstance(name, types.StringTypes):
491
515
            name = splitpath(name)
492
516
 
493
 
        parent_id = None
 
517
        mutter("lookup path %r" % name)
 
518
 
 
519
        parent = self.root
494
520
        for f in name:
495
521
            try:
496
 
                cie = self._tree[parent_id][f]
 
522
                cie = parent.children[f]
497
523
                assert cie.name == f
498
 
                parent_id = cie.file_id
 
524
                assert cie.parent_id == parent.file_id
 
525
                parent = cie
499
526
            except KeyError:
500
527
                # or raise an error?
501
528
                return None
502
529
 
503
 
        return parent_id
504
 
 
505
 
 
506
 
    def get_child(self, parent_id, child_name):
507
 
        return self._tree[parent_id].get(child_name)
 
530
        return parent.file_id
508
531
 
509
532
 
510
533
    def has_filename(self, names):
512
535
 
513
536
 
514
537
    def has_id(self, file_id):
515
 
        assert isinstance(file_id, str)
516
538
        return self._byid.has_key(file_id)
517
539
 
518
540
 
519
 
 
520
 
 
521
 
 
522
 
if __name__ == '__main__':
523
 
    import doctest, inventory
524
 
    doctest.testmod(inventory)
 
541
    def rename(self, file_id, new_parent_id, new_name):
 
542
        """Move a file within the inventory.
 
543
 
 
544
        This can change either the name, or the parent, or both.
 
545
 
 
546
        This does not move the working file."""
 
547
        if not is_valid_name(new_name):
 
548
            raise BzrError("not an acceptable filename: %r" % new_name)
 
549
 
 
550
        new_parent = self._byid[new_parent_id]
 
551
        if new_name in new_parent.children:
 
552
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
553
 
 
554
        new_parent_idpath = self.get_idpath(new_parent_id)
 
555
        if file_id in new_parent_idpath:
 
556
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
557
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
558
 
 
559
        file_ie = self._byid[file_id]
 
560
        old_parent = self._byid[file_ie.parent_id]
 
561
 
 
562
        # TODO: Don't leave things messed up if this fails
 
563
 
 
564
        del old_parent.children[file_ie.name]
 
565
        new_parent.children[new_name] = file_ie
 
566
        
 
567
        file_ie.name = new_name
 
568
        file_ie.parent_id = new_parent_id
 
569
 
 
570
 
 
571
 
 
572
 
 
573
_NAME_RE = None
 
574
 
 
575
def is_valid_name(name):
 
576
    global _NAME_RE
 
577
    if _NAME_RE == None:
 
578
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
579
        
 
580
    return bool(_NAME_RE.match(name))