~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-05-11 08:12:23 UTC
  • Revision ID: mbp@sourcefrog.net-20050511081223-7c935881e8145274
todo

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
 
23
23
import sys, os.path, types, re
 
24
from sets import Set
 
25
 
 
26
try:
 
27
    from cElementTree import Element, ElementTree, SubElement
 
28
except ImportError:
 
29
    from elementtree.ElementTree import Element, ElementTree, SubElement
 
30
 
 
31
from xml import XMLMixin
 
32
from errors import bailout, BzrError, BzrCheckError
24
33
 
25
34
import bzrlib
26
 
from bzrlib.errors import BzrError, BzrCheckError
27
 
 
28
35
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
29
36
from bzrlib.trace import mutter
30
 
from bzrlib.errors import NotVersionedError
31
 
        
32
37
 
33
 
class InventoryEntry(object):
 
38
class InventoryEntry(XMLMixin):
34
39
    """Description of a versioned file.
35
40
 
36
41
    An InventoryEntry has the following fields, which are also
55
60
    >>> i.path2id('')
56
61
    'TREE_ROOT'
57
62
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
58
 
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
59
63
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
60
 
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
61
64
    >>> for j in i.iter_entries():
62
65
    ...   print j
63
66
    ... 
66
69
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
67
70
    Traceback (most recent call last):
68
71
    ...
69
 
    BzrError: inventory already contains entry with id {2323}
 
72
    BzrError: ('inventory already contains entry with id {2323}', [])
70
73
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
71
 
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
72
74
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
 
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
74
75
    >>> i.path2id('src/wibble')
75
76
    '2325'
76
77
    >>> '2325' in i
77
78
    True
78
79
    >>> 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():
99
99
    # TODO: split InventoryEntry into subclasses for files,
100
100
    # directories, etc etc.
101
101
 
102
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
103
 
                 'text_id', 'parent_id', 'children', ]
104
 
 
 
102
    text_sha1 = None
 
103
    text_size = None
 
104
    
105
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
106
        """Create an InventoryEntry
107
107
        
120
120
        if '/' in name or '\\' in name:
121
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
122
122
        
123
 
        self.text_sha1 = None
124
 
        self.text_size = None
125
 
    
126
123
        self.file_id = file_id
127
124
        self.name = name
128
125
        self.kind = kind
148
145
                               self.parent_id, text_id=self.text_id)
149
146
        other.text_sha1 = self.text_sha1
150
147
        other.text_size = self.text_size
151
 
        # note that children are *not* copied; they're pulled across when
152
 
        # others are added
153
148
        return other
154
149
 
155
150
 
164
159
    
165
160
    def to_element(self):
166
161
        """Convert to XML element"""
167
 
        from bzrlib.xml import Element
168
 
        
169
162
        e = Element('entry')
170
163
 
171
164
        e.set('name', self.name)
216
209
 
217
210
    from_element = classmethod(from_element)
218
211
 
219
 
    def __eq__(self, other):
 
212
    def __cmp__(self, other):
 
213
        if self is other:
 
214
            return 0
220
215
        if not isinstance(other, InventoryEntry):
221
216
            return NotImplemented
222
217
 
223
 
        return (self.file_id == other.file_id) \
224
 
               and (self.name == other.name) \
225
 
               and (self.text_sha1 == other.text_sha1) \
226
 
               and (self.text_size == other.text_size) \
227
 
               and (self.text_id == other.text_id) \
228
 
               and (self.parent_id == other.parent_id) \
229
 
               and (self.kind == other.kind)
230
 
 
231
 
 
232
 
    def __ne__(self, other):
233
 
        return not (self == other)
234
 
 
235
 
    def __hash__(self):
236
 
        raise ValueError('not hashable')
 
218
        return cmp(self.file_id, other.file_id) \
 
219
               or cmp(self.name, other.name) \
 
220
               or cmp(self.text_sha1, other.text_sha1) \
 
221
               or cmp(self.text_size, other.text_size) \
 
222
               or cmp(self.text_id, other.text_id) \
 
223
               or cmp(self.parent_id, other.parent_id) \
 
224
               or cmp(self.kind, other.kind)
237
225
 
238
226
 
239
227
 
245
233
        self.parent_id = None
246
234
        self.name = ''
247
235
 
248
 
    def __eq__(self, other):
 
236
    def __cmp__(self, other):
 
237
        if self is other:
 
238
            return 0
249
239
        if not isinstance(other, RootEntry):
250
240
            return NotImplemented
251
 
        
252
 
        return (self.file_id == other.file_id) \
253
 
               and (self.children == other.children)
254
 
 
255
 
 
256
 
 
257
 
class Inventory(object):
 
241
        return cmp(self.file_id, other.file_id) \
 
242
               or cmp(self.children, other.children)
 
243
 
 
244
 
 
245
 
 
246
class Inventory(XMLMixin):
258
247
    """Inventory of versioned files in a tree.
259
248
 
260
249
    This describes which file_id is present at each point in the tree,
272
261
    inserted, other than through the Inventory API.
273
262
 
274
263
    >>> inv = Inventory()
 
264
    >>> inv.write_xml(sys.stdout)
 
265
    <inventory>
 
266
    </inventory>
275
267
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
276
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
277
268
    >>> inv['123-123'].name
278
269
    'hello.c'
279
270
 
288
279
 
289
280
    >>> [x[0] for x in inv.iter_entries()]
290
281
    ['hello.c']
291
 
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
292
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
293
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
282
    
 
283
    >>> inv.write_xml(sys.stdout)
 
284
    <inventory>
 
285
    <entry file_id="123-123" kind="file" name="hello.c" />
 
286
    </inventory>
 
287
 
294
288
    """
295
 
    def __init__(self, root_id=ROOT_ID):
 
289
    def __init__(self):
296
290
        """Create or read an inventory.
297
291
 
298
292
        If a working directory is specified, the inventory is read
302
296
        The inventory is created with a default root directory, with
303
297
        an id of None.
304
298
        """
305
 
        # We are letting Branch(init=True) create a unique inventory
306
 
        # root id. Rather than generating a random one here.
307
 
        #if root_id is None:
308
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
309
 
        self.root = RootEntry(root_id)
 
299
        self.root = RootEntry(ROOT_ID)
310
300
        self._byid = {self.root.file_id: self.root}
311
301
 
312
302
 
334
324
            if ie.kind == 'directory':
335
325
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
336
326
                    yield os.path.join(name, cn), cie
337
 
 
338
 
 
339
 
    def entries(self):
340
 
        """Return list of (path, ie) for all entries except the root.
341
 
 
342
 
        This may be faster than iter_entries.
 
327
                    
 
328
 
 
329
 
 
330
    def directories(self):
 
331
        """Return (path, entry) pairs for all directories.
343
332
        """
344
 
        accum = []
345
 
        def descend(dir_ie, dir_path):
346
 
            kids = dir_ie.children.items()
347
 
            kids.sort()
348
 
            for name, ie in kids:
349
 
                child_path = os.path.join(dir_path, name)
350
 
                accum.append((child_path, ie))
 
333
        def descend(parent_ie):
 
334
            parent_name = parent_ie.name
 
335
            yield parent_name, parent_ie
 
336
 
 
337
            # directory children in sorted order
 
338
            dn = []
 
339
            for ie in parent_ie.children.itervalues():
351
340
                if ie.kind == 'directory':
352
 
                    descend(ie, child_path)
353
 
 
354
 
        descend(self.root, '')
355
 
        return accum
356
 
 
357
 
 
358
 
    def directories(self):
359
 
        """Return (path, entry) pairs for all directories, including the root.
360
 
        """
361
 
        accum = []
362
 
        def descend(parent_ie, parent_path):
363
 
            accum.append((parent_path, parent_ie))
 
341
                    dn.append((ie.name, ie))
 
342
            dn.sort()
364
343
            
365
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
366
 
            kids.sort()
 
344
            for name, child_ie in dn:
 
345
                for sub_name, sub_ie in descend(child_ie):
 
346
                    yield appendpath(parent_name, sub_name), sub_ie
367
347
 
368
 
            for name, child_ie in kids:
369
 
                child_path = os.path.join(parent_path, name)
370
 
                descend(child_ie, child_path)
371
 
        descend(self.root, '')
372
 
        return accum
 
348
        for name, ie in descend(self.root):
 
349
            yield name, ie
373
350
        
374
351
 
375
352
 
378
355
 
379
356
        >>> inv = Inventory()
380
357
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
381
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
382
358
        >>> '123' in inv
383
359
        True
384
360
        >>> '456' in inv
392
368
 
393
369
        >>> inv = Inventory()
394
370
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
395
 
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
396
371
        >>> inv['123123'].name
397
372
        'hello.c'
398
373
        """
418
393
        To add  a file to a branch ready to be committed, use Branch.add,
419
394
        which calls this."""
420
395
        if entry.file_id in self._byid:
421
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
422
 
 
423
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
424
 
            entry.parent_id = self.root.file_id
 
396
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
425
397
 
426
398
        try:
427
399
            parent = self._byid[entry.parent_id]
428
400
        except KeyError:
429
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
401
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
430
402
 
431
403
        if parent.children.has_key(entry.name):
432
 
            raise BzrError("%s is already versioned" %
 
404
            bailout("%s is already versioned" %
433
405
                    appendpath(self.id2path(parent.file_id), entry.name))
434
406
 
435
407
        self._byid[entry.file_id] = entry
436
408
        parent.children[entry.name] = entry
437
 
        return entry
438
409
 
439
410
 
440
411
    def add_path(self, relpath, kind, file_id=None):
441
412
        """Add entry from a path.
442
413
 
443
414
        The immediate parent must already be versioned"""
444
 
        from bzrlib.branch import gen_file_id
445
 
        
446
415
        parts = bzrlib.osutils.splitpath(relpath)
447
416
        if len(parts) == 0:
448
 
            raise BzrError("cannot re-add root of inventory")
 
417
            bailout("cannot re-add root of inventory")
449
418
 
450
419
        if file_id == None:
451
 
            file_id = gen_file_id(relpath)
452
 
 
453
 
        parent_path = parts[:-1]
454
 
        parent_id = self.path2id(parent_path)
455
 
        if parent_id == None:
456
 
            raise NotVersionedError(parent_path)
457
 
 
 
420
            file_id = bzrlib.branch.gen_file_id(relpath)
 
421
 
 
422
        parent_id = self.path2id(parts[:-1])
 
423
        assert parent_id != None
458
424
        ie = InventoryEntry(file_id, parts[-1],
459
425
                            kind=kind, parent_id=parent_id)
460
426
        return self.add(ie)
465
431
 
466
432
        >>> inv = Inventory()
467
433
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
468
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
469
434
        >>> '123' in inv
470
435
        True
471
436
        >>> del inv['123']
487
452
        del self[ie.parent_id].children[ie.name]
488
453
 
489
454
 
 
455
    def id_set(self):
 
456
        return Set(self._byid)
 
457
 
 
458
 
490
459
    def to_element(self):
491
460
        """Convert to XML Element"""
492
 
        from bzrlib.xml import Element
493
 
        
494
461
        e = Element('inventory')
495
462
        e.text = '\n'
496
 
        if self.root.file_id not in (None, ROOT_ID):
497
 
            e.set('file_id', self.root.file_id)
498
463
        for path, ie in self.iter_entries():
499
464
            e.append(ie.to_element())
500
465
        return e
502
467
 
503
468
    def from_element(cls, elt):
504
469
        """Construct from XML Element
505
 
        
 
470
 
506
471
        >>> inv = Inventory()
507
472
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
508
 
        InventoryEntry('foo.c-123981239', 'foo.c', kind='file', parent_id='TREE_ROOT')
509
473
        >>> elt = inv.to_element()
510
474
        >>> inv2 = Inventory.from_element(elt)
511
475
        >>> inv2 == inv
512
476
        True
513
477
        """
514
 
        # XXXX: doctest doesn't run this properly under python2.3
515
478
        assert elt.tag == 'inventory'
516
 
        root_id = elt.get('file_id') or ROOT_ID
517
 
        o = cls(root_id)
 
479
        o = cls()
518
480
        for e in elt:
519
 
            ie = InventoryEntry.from_element(e)
520
 
            if ie.parent_id == ROOT_ID:
521
 
                ie.parent_id = root_id
522
 
            o.add(ie)
 
481
            o.add(InventoryEntry.from_element(e))
523
482
        return o
524
483
        
525
484
    from_element = classmethod(from_element)
526
485
 
527
486
 
528
 
    def __eq__(self, other):
 
487
    def __cmp__(self, other):
529
488
        """Compare two sets by comparing their contents.
530
489
 
531
490
        >>> i1 = Inventory()
533
492
        >>> i1 == i2
534
493
        True
535
494
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
536
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
537
495
        >>> i1 == i2
538
496
        False
539
497
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
540
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
541
498
        >>> i1 == i2
542
499
        True
543
500
        """
 
501
        if self is other:
 
502
            return 0
 
503
        
544
504
        if not isinstance(other, Inventory):
545
505
            return NotImplemented
546
506
 
547
 
        if len(self._byid) != len(other._byid):
548
 
            # shortcut: obviously not the same
549
 
            return False
550
 
 
551
 
        return self._byid == other._byid
552
 
 
553
 
 
554
 
    def __ne__(self, other):
555
 
        return not (self == other)
556
 
 
557
 
 
558
 
    def __hash__(self):
559
 
        raise ValueError('not hashable')
560
 
 
 
507
        if self.id_set() ^ other.id_set():
 
508
            return 1
 
509
 
 
510
        for file_id in self._byid:
 
511
            c = cmp(self[file_id], other[file_id])
 
512
            if c: return c
 
513
 
 
514
        return 0
561
515
 
562
516
 
563
517
    def get_idpath(self, file_id):
573
527
            try:
574
528
                ie = self._byid[file_id]
575
529
            except KeyError:
576
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
530
                bailout("file_id {%s} not found in inventory" % file_id)
577
531
            p.insert(0, ie.file_id)
578
532
            file_id = ie.parent_id
579
533
        return p
583
537
        """Return as a list the path to file_id."""
584
538
 
585
539
        # get all names, skipping root
586
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
540
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
587
541
        return os.sep.join(p)
588
542
            
589
543
 
633
587
 
634
588
        This does not move the working file."""
635
589
        if not is_valid_name(new_name):
636
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
590
            bailout("not an acceptable filename: %r" % new_name)
637
591
 
638
592
        new_parent = self._byid[new_parent_id]
639
593
        if new_name in new_parent.children:
640
 
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
594
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
641
595
 
642
596
        new_parent_idpath = self.get_idpath(new_parent_id)
643
597
        if file_id in new_parent_idpath:
644
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
598
            bailout("cannot move directory %r into a subdirectory of itself, %r"
645
599
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
646
600
 
647
601
        file_ie = self._byid[file_id]
658
612
 
659
613
 
660
614
 
661
 
_NAME_RE = None
 
615
_NAME_RE = re.compile(r'^[^/\\]+$')
662
616
 
663
617
def is_valid_name(name):
664
 
    global _NAME_RE
665
 
    if _NAME_RE == None:
666
 
        _NAME_RE = re.compile(r'^[^/\\]+$')
667
 
        
668
618
    return bool(_NAME_RE.match(name))