~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

merge up with martin I hope... default merge base told me I was adding builtins.py ..

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
# TODO: Maybe store inventory_id in the file?  Not really needed.
19
 
 
20
 
 
21
18
# This should really be an id randomly assigned when the tree is
22
19
# created, but it's not for now.
23
20
ROOT_ID = "TREE_ROOT"
24
21
 
25
22
 
26
23
import sys, os.path, types, re
27
 
from sets import Set
28
 
 
29
 
try:
30
 
    from cElementTree import Element, ElementTree, SubElement
31
 
except ImportError:
32
 
    from elementtree.ElementTree import Element, ElementTree, SubElement
33
 
 
34
 
from xml import XMLMixin
35
 
from errors import bailout, BzrError
36
24
 
37
25
import bzrlib
 
26
from bzrlib.errors import BzrError, BzrCheckError
 
27
 
38
28
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
39
29
from bzrlib.trace import mutter
 
30
from bzrlib.errors import NotVersionedError
 
31
        
40
32
 
41
 
class InventoryEntry(XMLMixin):
 
33
class InventoryEntry(object):
42
34
    """Description of a versioned file.
43
35
 
44
36
    An InventoryEntry has the following fields, which are also
63
55
    >>> i.path2id('')
64
56
    'TREE_ROOT'
65
57
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
58
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
66
59
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
60
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
67
61
    >>> for j in i.iter_entries():
68
62
    ...   print j
69
63
    ... 
72
66
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
73
67
    Traceback (most recent call last):
74
68
    ...
75
 
    BzrError: ('inventory already contains entry with id {2323}', [])
 
69
    BzrError: inventory already contains entry with id {2323}
76
70
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
71
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
77
72
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
73
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
78
74
    >>> i.path2id('src/wibble')
79
75
    '2325'
80
76
    >>> '2325' in i
81
77
    True
82
78
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
79
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
83
80
    >>> i['2326']
84
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
85
82
    >>> for j in i.iter_entries():
101
98
 
102
99
    # TODO: split InventoryEntry into subclasses for files,
103
100
    # directories, etc etc.
104
 
    
 
101
 
 
102
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
103
                 'text_id', 'parent_id', 'children',
 
104
                 'text_version', 'entry_version', ]
 
105
 
 
106
 
105
107
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
108
        """Create an InventoryEntry
107
109
        
115
117
        '123'
116
118
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
119
        Traceback (most recent call last):
118
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
120
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
119
121
        """
120
 
        
121
 
        if len(splitpath(name)) != 1:
122
 
            bailout('InventoryEntry name is not a simple filename: %r'
123
 
                    % name)
124
 
        
 
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
125
130
        self.file_id = file_id
126
131
        self.name = name
127
132
        self.kind = kind
128
133
        self.text_id = text_id
129
134
        self.parent_id = parent_id
130
 
        self.text_sha1 = None
131
 
        self.text_size = None
132
135
        if kind == 'directory':
133
136
            self.children = {}
134
137
        elif kind == 'file':
149
152
                               self.parent_id, text_id=self.text_id)
150
153
        other.text_sha1 = self.text_sha1
151
154
        other.text_size = self.text_size
 
155
        # note that children are *not* copied; they're pulled across when
 
156
        # others are added
152
157
        return other
153
158
 
154
159
 
161
166
                   self.parent_id))
162
167
 
163
168
    
164
 
    def to_element(self):
165
 
        """Convert to XML element"""
166
 
        e = Element('entry')
167
 
 
168
 
        e.set('name', self.name)
169
 
        e.set('file_id', self.file_id)
170
 
        e.set('kind', self.kind)
171
 
 
172
 
        if self.text_size != None:
173
 
            e.set('text_size', '%d' % self.text_size)
174
 
            
175
 
        for f in ['text_id', 'text_sha1']:
176
 
            v = getattr(self, f)
177
 
            if v != None:
178
 
                e.set(f, v)
179
 
 
180
 
        # to be conservative, we don't externalize the root pointers
181
 
        # for now, leaving them as null in the xml form.  in a future
182
 
        # version it will be implied by nested elements.
183
 
        if self.parent_id != ROOT_ID:
184
 
            assert isinstance(self.parent_id, basestring)
185
 
            e.set('parent_id', self.parent_id)
186
 
 
187
 
        e.tail = '\n'
188
 
            
189
 
        return e
190
 
 
191
 
 
192
 
    def from_element(cls, elt):
193
 
        assert elt.tag == 'entry'
194
 
 
195
 
        ## original format inventories don't have a parent_id for
196
 
        ## nodes in the root directory, but it's cleaner to use one
197
 
        ## internally.
198
 
        parent_id = elt.get('parent_id')
199
 
        if parent_id == None:
200
 
            parent_id = ROOT_ID
201
 
 
202
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
203
 
        self.text_id = elt.get('text_id')
204
 
        self.text_sha1 = elt.get('text_sha1')
205
 
        
206
 
        ## mutter("read inventoryentry: %r" % (elt.attrib))
207
 
 
208
 
        v = elt.get('text_size')
209
 
        self.text_size = v and int(v)
210
 
 
211
 
        return self
212
 
            
213
 
 
214
 
    from_element = classmethod(from_element)
215
 
 
216
 
    def __cmp__(self, other):
217
 
        if self is other:
218
 
            return 0
 
169
    def __eq__(self, other):
219
170
        if not isinstance(other, InventoryEntry):
220
171
            return NotImplemented
221
172
 
222
 
        return cmp(self.file_id, other.file_id) \
223
 
               or cmp(self.name, other.name) \
224
 
               or cmp(self.text_sha1, other.text_sha1) \
225
 
               or cmp(self.text_size, other.text_size) \
226
 
               or cmp(self.text_id, other.text_id) \
227
 
               or cmp(self.parent_id, other.parent_id) \
228
 
               or cmp(self.kind, other.kind)
 
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')
229
189
 
230
190
 
231
191
 
237
197
        self.parent_id = None
238
198
        self.name = ''
239
199
 
240
 
    def __cmp__(self, other):
241
 
        if self is other:
242
 
            return 0
 
200
    def __eq__(self, other):
243
201
        if not isinstance(other, RootEntry):
244
202
            return NotImplemented
245
 
        return cmp(self.file_id, other.file_id) \
246
 
               or cmp(self.children, other.children)
247
 
 
248
 
 
249
 
 
250
 
class Inventory(XMLMixin):
 
203
        
 
204
        return (self.file_id == other.file_id) \
 
205
               and (self.children == other.children)
 
206
 
 
207
 
 
208
 
 
209
class Inventory(object):
251
210
    """Inventory of versioned files in a tree.
252
211
 
253
212
    This describes which file_id is present at each point in the tree,
265
224
    inserted, other than through the Inventory API.
266
225
 
267
226
    >>> inv = Inventory()
268
 
    >>> inv.write_xml(sys.stdout)
269
 
    <inventory>
270
 
    </inventory>
271
227
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
228
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
272
229
    >>> inv['123-123'].name
273
230
    'hello.c'
274
231
 
283
240
 
284
241
    >>> [x[0] for x in inv.iter_entries()]
285
242
    ['hello.c']
286
 
    
287
 
    >>> inv.write_xml(sys.stdout)
288
 
    <inventory>
289
 
    <entry file_id="123-123" kind="file" name="hello.c" />
290
 
    </inventory>
291
 
 
 
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')
292
246
    """
293
 
 
294
 
    ## TODO: Make sure only canonical filenames are stored.
295
 
 
296
 
    ## TODO: Do something sensible about the possible collisions on
297
 
    ## case-losing filesystems.  Perhaps we should just always forbid
298
 
    ## such collisions.
299
 
 
300
 
    ## TODO: No special cases for root, rather just give it a file id
301
 
    ## like everything else.
302
 
 
303
 
    ## TODO: Probably change XML serialization to use nesting rather
304
 
    ## than parent_id pointers.
305
 
 
306
 
    ## TODO: Perhaps hold the ElementTree in memory and work directly
307
 
    ## on that rather than converting into Python objects every time?
308
 
 
309
 
    def __init__(self):
 
247
    def __init__(self, root_id=ROOT_ID):
310
248
        """Create or read an inventory.
311
249
 
312
250
        If a working directory is specified, the inventory is read
316
254
        The inventory is created with a default root directory, with
317
255
        an id of None.
318
256
        """
319
 
        self.root = RootEntry(ROOT_ID)
 
257
        # We are letting Branch.initialize() 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)
320
262
        self._byid = {self.root.file_id: self.root}
321
263
 
322
264
 
344
286
            if ie.kind == 'directory':
345
287
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
288
                    yield os.path.join(name, cn), cie
347
 
                    
 
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
348
308
 
349
309
 
350
310
    def directories(self):
351
 
        """Return (path, entry) pairs for all directories.
 
311
        """Return (path, entry) pairs for all directories, including the root.
352
312
        """
353
 
        def descend(parent_ie):
354
 
            parent_name = parent_ie.name
355
 
            yield parent_name, parent_ie
356
 
 
357
 
            # directory children in sorted order
358
 
            dn = []
359
 
            for ie in parent_ie.children.itervalues():
360
 
                if ie.kind == 'directory':
361
 
                    dn.append((ie.name, ie))
362
 
            dn.sort()
 
313
        accum = []
 
314
        def descend(parent_ie, parent_path):
 
315
            accum.append((parent_path, parent_ie))
363
316
            
364
 
            for name, child_ie in dn:
365
 
                for sub_name, sub_ie in descend(child_ie):
366
 
                    yield appendpath(parent_name, sub_name), sub_ie
 
317
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
318
            kids.sort()
367
319
 
368
 
        for name, ie in descend(self.root):
369
 
            yield name, ie
 
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
370
325
        
371
326
 
372
327
 
375
330
 
376
331
        >>> inv = Inventory()
377
332
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
333
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
378
334
        >>> '123' in inv
379
335
        True
380
336
        >>> '456' in inv
388
344
 
389
345
        >>> inv = Inventory()
390
346
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
347
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
391
348
        >>> inv['123123'].name
392
349
        'hello.c'
393
350
        """
394
 
        if file_id == None:
395
 
            raise BzrError("can't look up file_id None")
396
 
            
397
351
        try:
398
352
            return self._byid[file_id]
399
353
        except KeyError:
400
 
            raise BzrError("file_id {%s} not in inventory" % file_id)
401
 
 
 
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
402
362
 
403
363
    def get_child(self, parent_id, filename):
404
364
        return self[parent_id].children.get(filename)
408
368
        """Add entry to inventory.
409
369
 
410
370
        To add  a file to a branch ready to be committed, use Branch.add,
411
 
        which calls this."""
 
371
        which calls this.
 
372
 
 
373
        Returns the new entry object.
 
374
        """
412
375
        if entry.file_id in self._byid:
413
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
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
414
380
 
415
381
        try:
416
382
            parent = self._byid[entry.parent_id]
417
383
        except KeyError:
418
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
384
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
419
385
 
420
386
        if parent.children.has_key(entry.name):
421
 
            bailout("%s is already versioned" %
 
387
            raise BzrError("%s is already versioned" %
422
388
                    appendpath(self.id2path(parent.file_id), entry.name))
423
389
 
424
390
        self._byid[entry.file_id] = entry
425
391
        parent.children[entry.name] = entry
 
392
        return entry
426
393
 
427
394
 
428
395
    def add_path(self, relpath, kind, file_id=None):
429
396
        """Add entry from a path.
430
397
 
431
 
        The immediate parent must already be versioned"""
 
398
        The immediate parent must already be versioned.
 
399
 
 
400
        Returns the new entry object."""
 
401
        from bzrlib.branch import gen_file_id
 
402
        
432
403
        parts = bzrlib.osutils.splitpath(relpath)
433
404
        if len(parts) == 0:
434
 
            bailout("cannot re-add root of inventory")
 
405
            raise BzrError("cannot re-add root of inventory")
435
406
 
436
407
        if file_id == None:
437
 
            file_id = bzrlib.branch.gen_file_id(relpath)
438
 
 
439
 
        parent_id = self.path2id(parts[:-1])
440
 
        assert parent_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
 
441
415
        ie = InventoryEntry(file_id, parts[-1],
442
416
                            kind=kind, parent_id=parent_id)
443
417
        return self.add(ie)
448
422
 
449
423
        >>> inv = Inventory()
450
424
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
425
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
451
426
        >>> '123' in inv
452
427
        True
453
428
        >>> del inv['123']
469
444
        del self[ie.parent_id].children[ie.name]
470
445
 
471
446
 
472
 
    def id_set(self):
473
 
        return Set(self._byid)
474
 
 
475
 
 
476
 
    def to_element(self):
477
 
        """Convert to XML Element"""
478
 
        e = Element('inventory')
479
 
        e.text = '\n'
480
 
        for path, ie in self.iter_entries():
481
 
            e.append(ie.to_element())
482
 
        return e
483
 
    
484
 
 
485
 
    def from_element(cls, elt):
486
 
        """Construct from XML Element
487
 
 
488
 
        >>> inv = Inventory()
489
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
 
        >>> elt = inv.to_element()
491
 
        >>> inv2 = Inventory.from_element(elt)
492
 
        >>> inv2 == inv
493
 
        True
494
 
        """
495
 
        assert elt.tag == 'inventory'
496
 
        o = cls()
497
 
        for e in elt:
498
 
            o.add(InventoryEntry.from_element(e))
499
 
        return o
500
 
        
501
 
    from_element = classmethod(from_element)
502
 
 
503
 
 
504
 
    def __cmp__(self, other):
 
447
    def __eq__(self, other):
505
448
        """Compare two sets by comparing their contents.
506
449
 
507
450
        >>> i1 = Inventory()
509
452
        >>> i1 == i2
510
453
        True
511
454
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
455
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
512
456
        >>> i1 == i2
513
457
        False
514
458
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
459
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
515
460
        >>> i1 == i2
516
461
        True
517
462
        """
518
 
        if self is other:
519
 
            return 0
520
 
        
521
463
        if not isinstance(other, Inventory):
522
464
            return NotImplemented
523
465
 
524
 
        if self.id_set() ^ other.id_set():
525
 
            return 1
526
 
 
527
 
        for file_id in self._byid:
528
 
            c = cmp(self[file_id], other[file_id])
529
 
            if c: return c
530
 
 
531
 
        return 0
 
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')
532
479
 
533
480
 
534
481
    def get_idpath(self, file_id):
544
491
            try:
545
492
                ie = self._byid[file_id]
546
493
            except KeyError:
547
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
494
                raise BzrError("file_id {%s} not found in inventory" % file_id)
548
495
            p.insert(0, ie.file_id)
549
496
            file_id = ie.parent_id
550
497
        return p
554
501
        """Return as a list the path to file_id."""
555
502
 
556
503
        # get all names, skipping root
557
 
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
 
504
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
558
505
        return os.sep.join(p)
559
506
            
560
507
 
604
551
 
605
552
        This does not move the working file."""
606
553
        if not is_valid_name(new_name):
607
 
            bailout("not an acceptable filename: %r" % new_name)
 
554
            raise BzrError("not an acceptable filename: %r" % new_name)
608
555
 
609
556
        new_parent = self._byid[new_parent_id]
610
557
        if new_name in new_parent.children:
611
 
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
558
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
612
559
 
613
560
        new_parent_idpath = self.get_idpath(new_parent_id)
614
561
        if file_id in new_parent_idpath:
615
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
562
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
563
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
617
564
 
618
565
        file_ie = self._byid[file_id]
629
576
 
630
577
 
631
578
 
632
 
_NAME_RE = re.compile(r'^[^/\\]+$')
 
579
_NAME_RE = None
633
580
 
634
581
def is_valid_name(name):
 
582
    global _NAME_RE
 
583
    if _NAME_RE == None:
 
584
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
585
        
635
586
    return bool(_NAME_RE.match(name))