~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 06:49:06 UTC
  • Revision ID: mbp@sourcefrog.net-20050830064905-203cfbb58b1f5acc
- fix imports for moved errors

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
 
105
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
106
        """Create an InventoryEntry
107
107
        
115
115
        '123'
116
116
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
117
        Traceback (most recent call last):
118
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
118
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
119
119
        """
120
 
        
121
 
        if len(splitpath(name)) != 1:
122
 
            bailout('InventoryEntry name is not a simple filename: %r'
123
 
                    % name)
124
 
        
 
120
        if '/' in name or '\\' in name:
 
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
122
        
 
123
        self.text_sha1 = None
 
124
        self.text_size = None
 
125
    
125
126
        self.file_id = file_id
126
127
        self.name = name
127
128
        self.kind = kind
128
129
        self.text_id = text_id
129
130
        self.parent_id = parent_id
130
 
        self.text_sha1 = None
131
 
        self.text_size = None
132
131
        if kind == 'directory':
133
132
            self.children = {}
134
133
        elif kind == 'file':
149
148
                               self.parent_id, text_id=self.text_id)
150
149
        other.text_sha1 = self.text_sha1
151
150
        other.text_size = self.text_size
 
151
        # note that children are *not* copied; they're pulled across when
 
152
        # others are added
152
153
        return other
153
154
 
154
155
 
163
164
    
164
165
    def to_element(self):
165
166
        """Convert to XML element"""
 
167
        from bzrlib.xml import Element
 
168
        
166
169
        e = Element('entry')
167
170
 
168
171
        e.set('name', self.name)
213
216
 
214
217
    from_element = classmethod(from_element)
215
218
 
216
 
    def __cmp__(self, other):
217
 
        if self is other:
218
 
            return 0
 
219
    def __eq__(self, other):
219
220
        if not isinstance(other, InventoryEntry):
220
221
            return NotImplemented
221
222
 
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)
 
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')
229
237
 
230
238
 
231
239
 
237
245
        self.parent_id = None
238
246
        self.name = ''
239
247
 
240
 
    def __cmp__(self, other):
241
 
        if self is other:
242
 
            return 0
 
248
    def __eq__(self, other):
243
249
        if not isinstance(other, RootEntry):
244
250
            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):
 
251
        
 
252
        return (self.file_id == other.file_id) \
 
253
               and (self.children == other.children)
 
254
 
 
255
 
 
256
 
 
257
class Inventory(object):
251
258
    """Inventory of versioned files in a tree.
252
259
 
253
260
    This describes which file_id is present at each point in the tree,
265
272
    inserted, other than through the Inventory API.
266
273
 
267
274
    >>> inv = Inventory()
268
 
    >>> inv.write_xml(sys.stdout)
269
 
    <inventory>
270
 
    </inventory>
271
275
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
276
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
272
277
    >>> inv['123-123'].name
273
278
    'hello.c'
274
279
 
283
288
 
284
289
    >>> [x[0] for x in inv.iter_entries()]
285
290
    ['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
 
 
 
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')
292
294
    """
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):
 
295
    def __init__(self, root_id=ROOT_ID):
310
296
        """Create or read an inventory.
311
297
 
312
298
        If a working directory is specified, the inventory is read
316
302
        The inventory is created with a default root directory, with
317
303
        an id of None.
318
304
        """
319
 
        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)
320
310
        self._byid = {self.root.file_id: self.root}
321
311
 
322
312
 
344
334
            if ie.kind == 'directory':
345
335
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
336
                    yield os.path.join(name, cn), cie
347
 
                    
 
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
348
356
 
349
357
 
350
358
    def directories(self):
351
 
        """Return (path, entry) pairs for all directories.
 
359
        """Return (path, entry) pairs for all directories, including the root.
352
360
        """
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()
 
361
        accum = []
 
362
        def descend(parent_ie, parent_path):
 
363
            accum.append((parent_path, parent_ie))
363
364
            
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
 
365
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
366
            kids.sort()
367
367
 
368
 
        for name, ie in descend(self.root):
369
 
            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
370
373
        
371
374
 
372
375
 
375
378
 
376
379
        >>> inv = Inventory()
377
380
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
381
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
378
382
        >>> '123' in inv
379
383
        True
380
384
        >>> '456' in inv
388
392
 
389
393
        >>> inv = Inventory()
390
394
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
395
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
391
396
        >>> inv['123123'].name
392
397
        'hello.c'
393
398
        """
394
 
        if file_id == None:
395
 
            raise BzrError("can't look up file_id None")
396
 
            
397
399
        try:
398
400
            return self._byid[file_id]
399
401
        except KeyError:
400
 
            raise BzrError("file_id {%s} not in inventory" % file_id)
401
 
 
 
402
            if file_id == None:
 
403
                raise BzrError("can't look up file_id None")
 
404
            else:
 
405
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
406
 
 
407
 
 
408
    def get_file_kind(self, file_id):
 
409
        return self._byid[file_id].kind
402
410
 
403
411
    def get_child(self, parent_id, filename):
404
412
        return self[parent_id].children.get(filename)
410
418
        To add  a file to a branch ready to be committed, use Branch.add,
411
419
        which calls this."""
412
420
        if entry.file_id in self._byid:
413
 
            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
414
425
 
415
426
        try:
416
427
            parent = self._byid[entry.parent_id]
417
428
        except KeyError:
418
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
429
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
419
430
 
420
431
        if parent.children.has_key(entry.name):
421
 
            bailout("%s is already versioned" %
 
432
            raise BzrError("%s is already versioned" %
422
433
                    appendpath(self.id2path(parent.file_id), entry.name))
423
434
 
424
435
        self._byid[entry.file_id] = entry
425
436
        parent.children[entry.name] = entry
 
437
        return entry
426
438
 
427
439
 
428
440
    def add_path(self, relpath, kind, file_id=None):
429
441
        """Add entry from a path.
430
442
 
431
443
        The immediate parent must already be versioned"""
 
444
        from bzrlib.branch import gen_file_id
 
445
        
432
446
        parts = bzrlib.osutils.splitpath(relpath)
433
447
        if len(parts) == 0:
434
 
            bailout("cannot re-add root of inventory")
 
448
            raise BzrError("cannot re-add root of inventory")
435
449
 
436
450
        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
 
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
 
441
458
        ie = InventoryEntry(file_id, parts[-1],
442
459
                            kind=kind, parent_id=parent_id)
443
460
        return self.add(ie)
448
465
 
449
466
        >>> inv = Inventory()
450
467
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
468
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
451
469
        >>> '123' in inv
452
470
        True
453
471
        >>> del inv['123']
469
487
        del self[ie.parent_id].children[ie.name]
470
488
 
471
489
 
472
 
    def id_set(self):
473
 
        return Set(self._byid)
474
 
 
475
 
 
476
490
    def to_element(self):
477
491
        """Convert to XML Element"""
 
492
        from bzrlib.xml import Element
 
493
        
478
494
        e = Element('inventory')
479
495
        e.text = '\n'
 
496
        if self.root.file_id not in (None, ROOT_ID):
 
497
            e.set('file_id', self.root.file_id)
480
498
        for path, ie in self.iter_entries():
481
499
            e.append(ie.to_element())
482
500
        return e
484
502
 
485
503
    def from_element(cls, elt):
486
504
        """Construct from XML Element
487
 
 
 
505
        
488
506
        >>> inv = Inventory()
489
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')
490
509
        >>> elt = inv.to_element()
491
510
        >>> inv2 = Inventory.from_element(elt)
492
511
        >>> inv2 == inv
493
512
        True
494
513
        """
 
514
        # XXXX: doctest doesn't run this properly under python2.3
495
515
        assert elt.tag == 'inventory'
496
 
        o = cls()
 
516
        root_id = elt.get('file_id') or ROOT_ID
 
517
        o = cls(root_id)
497
518
        for e in elt:
498
 
            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)
499
523
        return o
500
524
        
501
525
    from_element = classmethod(from_element)
502
526
 
503
527
 
504
 
    def __cmp__(self, other):
 
528
    def __eq__(self, other):
505
529
        """Compare two sets by comparing their contents.
506
530
 
507
531
        >>> i1 = Inventory()
509
533
        >>> i1 == i2
510
534
        True
511
535
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
536
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
512
537
        >>> i1 == i2
513
538
        False
514
539
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
540
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
515
541
        >>> i1 == i2
516
542
        True
517
543
        """
518
 
        if self is other:
519
 
            return 0
520
 
        
521
544
        if not isinstance(other, Inventory):
522
545
            return NotImplemented
523
546
 
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
 
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
 
532
561
 
533
562
 
534
563
    def get_idpath(self, file_id):
544
573
            try:
545
574
                ie = self._byid[file_id]
546
575
            except KeyError:
547
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
576
                raise BzrError("file_id {%s} not found in inventory" % file_id)
548
577
            p.insert(0, ie.file_id)
549
578
            file_id = ie.parent_id
550
579
        return p
554
583
        """Return as a list the path to file_id."""
555
584
 
556
585
        # get all names, skipping root
557
 
        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:]]
558
587
        return os.sep.join(p)
559
588
            
560
589
 
604
633
 
605
634
        This does not move the working file."""
606
635
        if not is_valid_name(new_name):
607
 
            bailout("not an acceptable filename: %r" % new_name)
 
636
            raise BzrError("not an acceptable filename: %r" % new_name)
608
637
 
609
638
        new_parent = self._byid[new_parent_id]
610
639
        if new_name in new_parent.children:
611
 
            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)))
612
641
 
613
642
        new_parent_idpath = self.get_idpath(new_parent_id)
614
643
        if file_id in new_parent_idpath:
615
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
644
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
645
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
617
646
 
618
647
        file_ie = self._byid[file_id]
629
658
 
630
659
 
631
660
 
632
 
_NAME_RE = re.compile(r'^[^/\\]+$')
 
661
_NAME_RE = None
633
662
 
634
663
def is_valid_name(name):
 
664
    global _NAME_RE
 
665
    if _NAME_RE == None:
 
666
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
667
        
635
668
    return bool(_NAME_RE.match(name))