~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-23 23:52:10 UTC
  • Revision ID: mbp@sourcefrog.net-20050323235210-5464746b93c39ed0
more notes on darcs

Show diffs side-by-side

added added

removed removed

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