~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-08-30 01:35:40 UTC
  • Revision ID: mbp@sourcefrog.net-20050830013540-34e8996a86ba25fb
- rename FunctionalTest to TestCaseInTempDir

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