~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-08-26 00:10:48 UTC
  • Revision ID: mbp@sourcefrog.net-20050826001048-b84148d3ef567d0d
- fix bzr.dev branch url in tutorial
  thanks to madduck

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
69
64
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
65
    Traceback (most recent call last):
71
66
    ...
72
 
    BzrError: ('inventory already contains entry with id {2323}', [])
 
67
    BzrError: inventory already contains entry with id {2323}
73
68
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
74
69
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
75
70
    >>> i.path2id('src/wibble')
99
94
    # TODO: split InventoryEntry into subclasses for files,
100
95
    # directories, etc etc.
101
96
 
102
 
    text_sha1 = None
103
 
    text_size = None
104
 
    
 
97
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
98
                 'text_id', 'parent_id', 'children', ]
 
99
 
105
100
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
101
        """Create an InventoryEntry
107
102
        
120
115
        if '/' in name or '\\' in name:
121
116
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
122
117
        
 
118
        self.text_sha1 = None
 
119
        self.text_size = None
 
120
    
123
121
        self.file_id = file_id
124
122
        self.name = name
125
123
        self.kind = kind
161
159
    
162
160
    def to_element(self):
163
161
        """Convert to XML element"""
 
162
        from bzrlib.xml import Element
 
163
        
164
164
        e = Element('entry')
165
165
 
166
166
        e.set('name', self.name)
211
211
 
212
212
    from_element = classmethod(from_element)
213
213
 
214
 
    def __cmp__(self, other):
215
 
        if self is other:
216
 
            return 0
 
214
    def __eq__(self, other):
217
215
        if not isinstance(other, InventoryEntry):
218
216
            return NotImplemented
219
217
 
220
 
        return cmp(self.file_id, other.file_id) \
221
 
               or cmp(self.name, other.name) \
222
 
               or cmp(self.text_sha1, other.text_sha1) \
223
 
               or cmp(self.text_size, other.text_size) \
224
 
               or cmp(self.text_id, other.text_id) \
225
 
               or cmp(self.parent_id, other.parent_id) \
226
 
               or cmp(self.kind, other.kind)
 
218
        return (self.file_id == other.file_id) \
 
219
               and (self.name == other.name) \
 
220
               and (self.text_sha1 == other.text_sha1) \
 
221
               and (self.text_size == other.text_size) \
 
222
               and (self.text_id == other.text_id) \
 
223
               and (self.parent_id == other.parent_id) \
 
224
               and (self.kind == other.kind)
 
225
 
 
226
 
 
227
    def __ne__(self, other):
 
228
        return not (self == other)
 
229
 
 
230
    def __hash__(self):
 
231
        raise ValueError('not hashable')
227
232
 
228
233
 
229
234
 
235
240
        self.parent_id = None
236
241
        self.name = ''
237
242
 
238
 
    def __cmp__(self, other):
239
 
        if self is other:
240
 
            return 0
 
243
    def __eq__(self, other):
241
244
        if not isinstance(other, RootEntry):
242
245
            return NotImplemented
243
 
        return cmp(self.file_id, other.file_id) \
244
 
               or cmp(self.children, other.children)
245
 
 
246
 
 
247
 
 
248
 
class Inventory(XMLMixin):
 
246
        
 
247
        return (self.file_id == other.file_id) \
 
248
               and (self.children == other.children)
 
249
 
 
250
 
 
251
 
 
252
class Inventory(object):
249
253
    """Inventory of versioned files in a tree.
250
254
 
251
255
    This describes which file_id is present at each point in the tree,
263
267
    inserted, other than through the Inventory API.
264
268
 
265
269
    >>> inv = Inventory()
266
 
    >>> inv.write_xml(sys.stdout)
267
 
    <inventory>
268
 
    </inventory>
269
270
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
270
271
    >>> inv['123-123'].name
271
272
    'hello.c'
281
282
 
282
283
    >>> [x[0] for x in inv.iter_entries()]
283
284
    ['hello.c']
284
 
    
285
 
    >>> inv.write_xml(sys.stdout)
286
 
    <inventory>
287
 
    <entry file_id="123-123" kind="file" name="hello.c" />
288
 
    </inventory>
289
 
 
 
285
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
286
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
290
287
    """
291
 
    def __init__(self):
 
288
    def __init__(self, root_id=ROOT_ID):
292
289
        """Create or read an inventory.
293
290
 
294
291
        If a working directory is specified, the inventory is read
298
295
        The inventory is created with a default root directory, with
299
296
        an id of None.
300
297
        """
301
 
        self.root = RootEntry(ROOT_ID)
 
298
        # We are letting Branch(init=True) create a unique inventory
 
299
        # root id. Rather than generating a random one here.
 
300
        #if root_id is None:
 
301
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
302
        self.root = RootEntry(root_id)
302
303
        self._byid = {self.root.file_id: self.root}
303
304
 
304
305
 
326
327
            if ie.kind == 'directory':
327
328
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
328
329
                    yield os.path.join(name, cn), cie
329
 
                    
 
330
 
 
331
 
 
332
    def entries(self):
 
333
        """Return list of (path, ie) for all entries except the root.
 
334
 
 
335
        This may be faster than iter_entries.
 
336
        """
 
337
        accum = []
 
338
        def descend(dir_ie, dir_path):
 
339
            kids = dir_ie.children.items()
 
340
            kids.sort()
 
341
            for name, ie in kids:
 
342
                child_path = os.path.join(dir_path, name)
 
343
                accum.append((child_path, ie))
 
344
                if ie.kind == 'directory':
 
345
                    descend(ie, child_path)
 
346
 
 
347
        descend(self.root, '')
 
348
        return accum
330
349
 
331
350
 
332
351
    def directories(self):
333
 
        """Return (path, entry) pairs for all directories.
 
352
        """Return (path, entry) pairs for all directories, including the root.
334
353
        """
335
 
        def descend(parent_ie):
336
 
            parent_name = parent_ie.name
337
 
            yield parent_name, parent_ie
338
 
 
339
 
            # directory children in sorted order
340
 
            dn = []
341
 
            for ie in parent_ie.children.itervalues():
342
 
                if ie.kind == 'directory':
343
 
                    dn.append((ie.name, ie))
344
 
            dn.sort()
 
354
        accum = []
 
355
        def descend(parent_ie, parent_path):
 
356
            accum.append((parent_path, parent_ie))
345
357
            
346
 
            for name, child_ie in dn:
347
 
                for sub_name, sub_ie in descend(child_ie):
348
 
                    yield appendpath(parent_name, sub_name), sub_ie
 
358
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
359
            kids.sort()
349
360
 
350
 
        for name, ie in descend(self.root):
351
 
            yield name, ie
 
361
            for name, child_ie in kids:
 
362
                child_path = os.path.join(parent_path, name)
 
363
                descend(child_ie, child_path)
 
364
        descend(self.root, '')
 
365
        return accum
352
366
        
353
367
 
354
368
 
395
409
        To add  a file to a branch ready to be committed, use Branch.add,
396
410
        which calls this."""
397
411
        if entry.file_id in self._byid:
398
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
412
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
413
 
 
414
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
415
            entry.parent_id = self.root.file_id
399
416
 
400
417
        try:
401
418
            parent = self._byid[entry.parent_id]
402
419
        except KeyError:
403
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
420
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
404
421
 
405
422
        if parent.children.has_key(entry.name):
406
 
            bailout("%s is already versioned" %
 
423
            raise BzrError("%s is already versioned" %
407
424
                    appendpath(self.id2path(parent.file_id), entry.name))
408
425
 
409
426
        self._byid[entry.file_id] = entry
414
431
        """Add entry from a path.
415
432
 
416
433
        The immediate parent must already be versioned"""
 
434
        from bzrlib.branch import gen_file_id
 
435
        
417
436
        parts = bzrlib.osutils.splitpath(relpath)
418
437
        if len(parts) == 0:
419
 
            bailout("cannot re-add root of inventory")
 
438
            raise BzrError("cannot re-add root of inventory")
420
439
 
421
440
        if file_id == None:
422
 
            file_id = bzrlib.branch.gen_file_id(relpath)
423
 
 
424
 
        parent_id = self.path2id(parts[:-1])
425
 
        assert parent_id != None
 
441
            file_id = gen_file_id(relpath)
 
442
 
 
443
        parent_path = parts[:-1]
 
444
        parent_id = self.path2id(parent_path)
 
445
        if parent_id == None:
 
446
            raise NotVersionedError(parent_path)
 
447
 
426
448
        ie = InventoryEntry(file_id, parts[-1],
427
449
                            kind=kind, parent_id=parent_id)
428
450
        return self.add(ie)
454
476
        del self[ie.parent_id].children[ie.name]
455
477
 
456
478
 
457
 
    def id_set(self):
458
 
        return Set(self._byid)
459
 
 
460
 
 
461
479
    def to_element(self):
462
480
        """Convert to XML Element"""
 
481
        from bzrlib.xml import Element
 
482
        
463
483
        e = Element('inventory')
464
484
        e.text = '\n'
 
485
        if self.root.file_id not in (None, ROOT_ID):
 
486
            e.set('file_id', self.root.file_id)
465
487
        for path, ie in self.iter_entries():
466
488
            e.append(ie.to_element())
467
489
        return e
469
491
 
470
492
    def from_element(cls, elt):
471
493
        """Construct from XML Element
472
 
 
 
494
        
473
495
        >>> inv = Inventory()
474
496
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
475
497
        >>> elt = inv.to_element()
477
499
        >>> inv2 == inv
478
500
        True
479
501
        """
 
502
        # XXXX: doctest doesn't run this properly under python2.3
480
503
        assert elt.tag == 'inventory'
481
 
        o = cls()
 
504
        root_id = elt.get('file_id') or ROOT_ID
 
505
        o = cls(root_id)
482
506
        for e in elt:
483
 
            o.add(InventoryEntry.from_element(e))
 
507
            ie = InventoryEntry.from_element(e)
 
508
            if ie.parent_id == ROOT_ID:
 
509
                ie.parent_id = root_id
 
510
            o.add(ie)
484
511
        return o
485
512
        
486
513
    from_element = classmethod(from_element)
487
514
 
488
515
 
489
 
    def __cmp__(self, other):
 
516
    def __eq__(self, other):
490
517
        """Compare two sets by comparing their contents.
491
518
 
492
519
        >>> i1 = Inventory()
500
527
        >>> i1 == i2
501
528
        True
502
529
        """
503
 
        if self is other:
504
 
            return 0
505
 
        
506
530
        if not isinstance(other, Inventory):
507
531
            return NotImplemented
508
532
 
509
 
        if self.id_set() ^ other.id_set():
510
 
            return 1
511
 
 
512
 
        for file_id in self._byid:
513
 
            c = cmp(self[file_id], other[file_id])
514
 
            if c: return c
515
 
 
516
 
        return 0
 
533
        if len(self._byid) != len(other._byid):
 
534
            # shortcut: obviously not the same
 
535
            return False
 
536
 
 
537
        return self._byid == other._byid
 
538
 
 
539
 
 
540
    def __ne__(self, other):
 
541
        return not (self == other)
 
542
 
 
543
 
 
544
    def __hash__(self):
 
545
        raise ValueError('not hashable')
 
546
 
517
547
 
518
548
 
519
549
    def get_idpath(self, file_id):
529
559
            try:
530
560
                ie = self._byid[file_id]
531
561
            except KeyError:
532
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
562
                raise BzrError("file_id {%s} not found in inventory" % file_id)
533
563
            p.insert(0, ie.file_id)
534
564
            file_id = ie.parent_id
535
565
        return p
539
569
        """Return as a list the path to file_id."""
540
570
 
541
571
        # get all names, skipping root
542
 
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
 
572
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
543
573
        return os.sep.join(p)
544
574
            
545
575
 
589
619
 
590
620
        This does not move the working file."""
591
621
        if not is_valid_name(new_name):
592
 
            bailout("not an acceptable filename: %r" % new_name)
 
622
            raise BzrError("not an acceptable filename: %r" % new_name)
593
623
 
594
624
        new_parent = self._byid[new_parent_id]
595
625
        if new_name in new_parent.children:
596
 
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
626
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
597
627
 
598
628
        new_parent_idpath = self.get_idpath(new_parent_id)
599
629
        if file_id in new_parent_idpath:
600
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
630
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
601
631
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
602
632
 
603
633
        file_ie = self._byid[file_id]
614
644
 
615
645
 
616
646
 
617
 
_NAME_RE = re.compile(r'^[^/\\]+$')
 
647
_NAME_RE = None
618
648
 
619
649
def is_valid_name(name):
 
650
    global _NAME_RE
 
651
    if _NAME_RE == None:
 
652
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
653
        
620
654
    return bool(_NAME_RE.match(name))