~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-09-30 00:58:02 UTC
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930005802-721cfc318e393817
- copy_branch creates destination if it doesn't exist

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
# 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
 
150
        if kind == 'directory':
 
151
            self.children = {}
 
152
        elif kind == 'file':
 
153
            pass
 
154
        else:
 
155
            raise BzrError("unhandled entry kind %r" % kind)
 
156
 
 
157
 
 
158
 
 
159
    def sorted_children(self):
 
160
        l = self.children.items()
 
161
        l.sort()
 
162
        return l
127
163
 
128
164
 
129
165
    def copy(self):
130
166
        other = InventoryEntry(self.file_id, self.name, self.kind,
131
 
                               self.text_id, self.parent_id)
 
167
                               self.parent_id)
 
168
        other.text_id = self.text_id
132
169
        other.text_sha1 = self.text_sha1
133
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
134
175
        return other
135
176
 
136
177
 
143
184
                   self.parent_id))
144
185
 
145
186
    
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
 
187
    def __eq__(self, other):
187
188
        if not isinstance(other, InventoryEntry):
188
189
            return NotImplemented
189
190
 
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):
 
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')
 
207
 
 
208
 
 
209
 
 
210
class RootEntry(InventoryEntry):
 
211
    def __init__(self, file_id):
 
212
        self.file_id = file_id
 
213
        self.children = {}
 
214
        self.kind = 'root_directory'
 
215
        self.parent_id = None
 
216
        self.name = ''
 
217
 
 
218
    def __eq__(self, other):
 
219
        if not isinstance(other, RootEntry):
 
220
            return NotImplemented
 
221
        
 
222
        return (self.file_id == other.file_id) \
 
223
               and (self.children == other.children)
 
224
 
 
225
 
 
226
 
 
227
class Inventory(object):
201
228
    """Inventory of versioned files in a tree.
202
229
 
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.
 
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.
209
233
 
210
234
    The inventory represents a typical unix file tree, with
211
235
    directories containing files and subdirectories.  We never store
215
239
    returned quickly.
216
240
 
217
241
    InventoryEntry objects must not be modified after they are
218
 
    inserted.
 
242
    inserted, other than through the Inventory API.
219
243
 
220
244
    >>> inv = Inventory()
221
 
    >>> inv.write_xml(sys.stdout)
222
 
    <inventory>
223
 
    </inventory>
224
 
    >>> 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')
225
247
    >>> inv['123-123'].name
226
248
    'hello.c'
227
 
    >>> for file_id in inv: print file_id
228
 
    ...
229
 
    123-123
230
249
 
231
250
    May be treated as an iterator or set to look up file ids:
232
251
    
239
258
 
240
259
    >>> [x[0] for x in inv.iter_entries()]
241
260
    ['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
 
 
 
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')
248
264
    """
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):
 
265
    def __init__(self, root_id=ROOT_ID):
264
266
        """Create or read an inventory.
265
267
 
266
268
        If a working directory is specified, the inventory is read
267
269
        from there.  If the file is specified, read from that. If not,
268
270
        the inventory is created empty.
 
271
 
 
272
        The inventory is created with a default root directory, with
 
273
        an id of None.
269
274
        """
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: {}}
 
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
275
292
 
276
293
 
277
294
    def __iter__(self):
283
300
        return len(self._byid)
284
301
 
285
302
 
286
 
    def iter_entries(self, parent_id=None):
 
303
    def iter_entries(self, from_dir=None):
287
304
        """Return (path, entry) pairs, in order by name."""
288
 
        kids = self._tree[parent_id].items()
 
305
        if from_dir == None:
 
306
            assert self.root
 
307
            from_dir = self.root
 
308
        elif isinstance(from_dir, basestring):
 
309
            from_dir = self._byid[from_dir]
 
310
            
 
311
        kids = from_dir.children.items()
289
312
        kids.sort()
290
313
        for name, ie in kids:
291
314
            yield name, ie
292
315
            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
 
316
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
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.
 
324
        """
 
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))
 
332
                if ie.kind == 'directory':
 
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))
 
345
            
 
346
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
347
            kids.sort()
 
348
 
 
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
305
354
        
306
355
 
307
356
 
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
357
    def __contains__(self, file_id):
318
358
        """True if this entry contains a file with given id.
319
359
 
320
360
        >>> inv = Inventory()
321
 
        >>> 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')
322
363
        >>> '123' in inv
323
364
        True
324
365
        >>> '456' in inv
331
372
        """Return the entry for given file_id.
332
373
 
333
374
        >>> inv = Inventory()
334
 
        >>> 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')
335
377
        >>> inv['123123'].name
336
378
        'hello.c'
337
379
        """
338
 
        return self._byid[file_id]
 
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
 
391
 
 
392
    def get_child(self, parent_id, filename):
 
393
        return self[parent_id].children.get(filename)
339
394
 
340
395
 
341
396
    def add(self, entry):
342
397
        """Add entry to inventory.
343
398
 
344
399
        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))
 
400
        which calls this.
 
401
 
 
402
        Returns the new entry object.
 
403
        """
 
404
        if entry.file_id in self._byid:
 
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
 
409
 
 
410
        try:
 
411
            parent = self._byid[entry.parent_id]
 
412
        except KeyError:
 
413
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
414
 
 
415
        if parent.children.has_key(entry.name):
 
416
            raise BzrError("%s is already versioned" %
 
417
                    appendpath(self.id2path(parent.file_id), entry.name))
357
418
 
358
419
        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] = {}
 
420
        parent.children[entry.name] = entry
 
421
        return entry
363
422
 
364
423
 
365
424
    def add_path(self, relpath, kind, file_id=None):
366
425
        """Add entry from a path.
367
426
 
368
 
        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
        
369
432
        parts = bzrlib.osutils.splitpath(relpath)
370
433
        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])
 
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
 
377
444
        ie = InventoryEntry(file_id, parts[-1],
378
445
                            kind=kind, parent_id=parent_id)
379
446
        return self.add(ie)
383
450
        """Remove entry by id.
384
451
 
385
452
        >>> inv = Inventory()
386
 
        >>> 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')
387
455
        >>> '123' in inv
388
456
        True
389
457
        >>> del inv['123']
392
460
        """
393
461
        ie = self[file_id]
394
462
 
395
 
        assert self._tree[ie.parent_id][ie.name] == ie
 
463
        assert self[ie.parent_id].children[ie.name] == ie
396
464
        
397
465
        # TODO: Test deleting all children; maybe hoist to a separate
398
466
        # deltree method?
399
467
        if ie.kind == 'directory':
400
 
            for cie in self._tree[file_id].values():
 
468
            for cie in ie.children.values():
401
469
                del self[cie.file_id]
402
 
            del self._tree[file_id]
 
470
            del ie.children
403
471
 
404
472
        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):
 
473
        del self[ie.parent_id].children[ie.name]
 
474
 
 
475
 
 
476
    def __eq__(self, other):
441
477
        """Compare two sets by comparing their contents.
442
478
 
443
479
        >>> i1 = Inventory()
444
480
        >>> i2 = Inventory()
445
481
        >>> i1 == i2
446
482
        True
447
 
        >>> i1.add(InventoryEntry('123', 'foo'))
 
483
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
484
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
448
485
        >>> i1 == i2
449
486
        False
450
 
        >>> i2.add(InventoryEntry('123', 'foo'))
 
487
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
488
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
451
489
        >>> i1 == i2
452
490
        True
453
491
        """
454
 
        if self is other:
455
 
            return 0
456
 
        
457
492
        if not isinstance(other, Inventory):
458
493
            return NotImplemented
459
494
 
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."""
 
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
        """
472
518
        p = []
473
519
        while file_id != None:
474
 
            ie = self[file_id]
475
 
            p = [ie.name] + p
 
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)
476
525
            file_id = ie.parent_id
477
 
        return joinpath(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)
478
535
            
479
536
 
480
537
 
486
543
 
487
544
        This returns the entry of the last component in the path,
488
545
        which may be either a file or a directory.
 
546
 
 
547
        Returns None iff the path is not found.
489
548
        """
490
549
        if isinstance(name, types.StringTypes):
491
550
            name = splitpath(name)
492
551
 
493
 
        parent_id = None
 
552
        mutter("lookup path %r" % name)
 
553
 
 
554
        parent = self.root
494
555
        for f in name:
495
556
            try:
496
 
                cie = self._tree[parent_id][f]
 
557
                cie = parent.children[f]
497
558
                assert cie.name == f
498
 
                parent_id = cie.file_id
 
559
                assert cie.parent_id == parent.file_id
 
560
                parent = cie
499
561
            except KeyError:
500
562
                # or raise an error?
501
563
                return None
502
564
 
503
 
        return parent_id
504
 
 
505
 
 
506
 
    def get_child(self, parent_id, child_name):
507
 
        return self._tree[parent_id].get(child_name)
 
565
        return parent.file_id
508
566
 
509
567
 
510
568
    def has_filename(self, names):
512
570
 
513
571
 
514
572
    def has_id(self, file_id):
515
 
        assert isinstance(file_id, str)
516
573
        return self._byid.has_key(file_id)
517
574
 
518
575
 
519
 
 
520
 
 
521
 
 
522
 
if __name__ == '__main__':
523
 
    import doctest, inventory
524
 
    doctest.testmod(inventory)
 
576
    def rename(self, file_id, new_parent_id, new_name):
 
577
        """Move a file within the inventory.
 
578
 
 
579
        This can change either the name, or the parent, or both.
 
580
 
 
581
        This does not move the working file."""
 
582
        if not is_valid_name(new_name):
 
583
            raise BzrError("not an acceptable filename: %r" % new_name)
 
584
 
 
585
        new_parent = self._byid[new_parent_id]
 
586
        if new_name in new_parent.children:
 
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)))
 
593
 
 
594
        file_ie = self._byid[file_id]
 
595
        old_parent = self._byid[file_ie.parent_id]
 
596
 
 
597
        # TODO: Don't leave things messed up if this fails
 
598
 
 
599
        del old_parent.children[file_ie.name]
 
600
        new_parent.children[new_name] = file_ie
 
601
        
 
602
        file_ie.name = new_name
 
603
        file_ie.parent_id = new_parent_id
 
604
 
 
605
 
 
606
 
 
607
 
 
608
_NAME_RE = None
 
609
 
 
610
def is_valid_name(name):
 
611
    global _NAME_RE
 
612
    if _NAME_RE == None:
 
613
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
614
        
 
615
    return bool(_NAME_RE.match(name))