~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-08-03 14:16:04 UTC
  • Revision ID: mbp@sourcefrog.net-20050803141604-b69a03512e094f37
- better summary help screen

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
72
64
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
73
65
    Traceback (most recent call last):
74
66
    ...
75
 
    BzrError: ('inventory already contains entry with id {2323}', [])
 
67
    BzrError: inventory already contains entry with id {2323}
76
68
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
77
69
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
78
70
    >>> i.path2id('src/wibble')
101
93
 
102
94
    # TODO: split InventoryEntry into subclasses for files,
103
95
    # directories, etc etc.
104
 
    
 
96
 
 
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
        
115
110
        '123'
116
111
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
112
        Traceback (most recent call last):
118
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
113
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
119
114
        """
120
 
        
121
 
        if len(splitpath(name)) != 1:
122
 
            bailout('InventoryEntry name is not a simple filename: %r'
123
 
                    % name)
124
 
        
 
115
        if '/' in name or '\\' in name:
 
116
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
117
        
 
118
        self.text_sha1 = None
 
119
        self.text_size = None
 
120
    
125
121
        self.file_id = file_id
126
122
        self.name = name
127
123
        self.kind = kind
128
124
        self.text_id = text_id
129
125
        self.parent_id = parent_id
130
 
        self.text_sha1 = None
131
 
        self.text_size = None
132
126
        if kind == 'directory':
133
127
            self.children = {}
134
128
        elif kind == 'file':
149
143
                               self.parent_id, text_id=self.text_id)
150
144
        other.text_sha1 = self.text_sha1
151
145
        other.text_size = self.text_size
 
146
        # note that children are *not* copied; they're pulled across when
 
147
        # others are added
152
148
        return other
153
149
 
154
150
 
163
159
    
164
160
    def to_element(self):
165
161
        """Convert to XML element"""
 
162
        from bzrlib.xml import Element
 
163
        
166
164
        e = Element('entry')
167
165
 
168
166
        e.set('name', self.name)
213
211
 
214
212
    from_element = classmethod(from_element)
215
213
 
216
 
    def __cmp__(self, other):
217
 
        if self is other:
218
 
            return 0
 
214
    def __eq__(self, other):
219
215
        if not isinstance(other, InventoryEntry):
220
216
            return NotImplemented
221
217
 
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)
 
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')
229
232
 
230
233
 
231
234
 
237
240
        self.parent_id = None
238
241
        self.name = ''
239
242
 
240
 
    def __cmp__(self, other):
241
 
        if self is other:
242
 
            return 0
 
243
    def __eq__(self, other):
243
244
        if not isinstance(other, RootEntry):
244
245
            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):
 
246
        
 
247
        return (self.file_id == other.file_id) \
 
248
               and (self.children == other.children)
 
249
 
 
250
 
 
251
 
 
252
class Inventory(object):
251
253
    """Inventory of versioned files in a tree.
252
254
 
253
255
    This describes which file_id is present at each point in the tree,
265
267
    inserted, other than through the Inventory API.
266
268
 
267
269
    >>> inv = Inventory()
268
 
    >>> inv.write_xml(sys.stdout)
269
 
    <inventory>
270
 
    </inventory>
271
270
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
272
271
    >>> inv['123-123'].name
273
272
    'hello.c'
283
282
 
284
283
    >>> [x[0] for x in inv.iter_entries()]
285
284
    ['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
 
 
 
285
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
286
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
292
287
    """
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):
 
288
    def __init__(self, root_id=ROOT_ID):
310
289
        """Create or read an inventory.
311
290
 
312
291
        If a working directory is specified, the inventory is read
316
295
        The inventory is created with a default root directory, with
317
296
        an id of None.
318
297
        """
319
 
        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)
320
303
        self._byid = {self.root.file_id: self.root}
321
304
 
322
305
 
344
327
            if ie.kind == 'directory':
345
328
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
329
                    yield os.path.join(name, cn), cie
347
 
                    
 
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
348
349
 
349
350
 
350
351
    def directories(self):
351
 
        """Return (path, entry) pairs for all directories.
 
352
        """Return (path, entry) pairs for all directories, including the root.
352
353
        """
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()
 
354
        accum = []
 
355
        def descend(parent_ie, parent_path):
 
356
            accum.append((parent_path, parent_ie))
363
357
            
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
 
358
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
359
            kids.sort()
367
360
 
368
 
        for name, ie in descend(self.root):
369
 
            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
370
366
        
371
367
 
372
368
 
391
387
        >>> inv['123123'].name
392
388
        'hello.c'
393
389
        """
394
 
        if file_id == None:
395
 
            raise BzrError("can't look up file_id None")
396
 
            
397
390
        try:
398
391
            return self._byid[file_id]
399
392
        except KeyError:
400
 
            raise BzrError("file_id {%s} not in inventory" % file_id)
401
 
 
 
393
            if file_id == None:
 
394
                raise BzrError("can't look up file_id None")
 
395
            else:
 
396
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
397
 
 
398
 
 
399
    def get_file_kind(self, file_id):
 
400
        return self._byid[file_id].kind
402
401
 
403
402
    def get_child(self, parent_id, filename):
404
403
        return self[parent_id].children.get(filename)
410
409
        To add  a file to a branch ready to be committed, use Branch.add,
411
410
        which calls this."""
412
411
        if entry.file_id in self._byid:
413
 
            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
414
416
 
415
417
        try:
416
418
            parent = self._byid[entry.parent_id]
417
419
        except KeyError:
418
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
420
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
419
421
 
420
422
        if parent.children.has_key(entry.name):
421
 
            bailout("%s is already versioned" %
 
423
            raise BzrError("%s is already versioned" %
422
424
                    appendpath(self.id2path(parent.file_id), entry.name))
423
425
 
424
426
        self._byid[entry.file_id] = entry
429
431
        """Add entry from a path.
430
432
 
431
433
        The immediate parent must already be versioned"""
 
434
        from bzrlib.branch import gen_file_id
 
435
        
432
436
        parts = bzrlib.osutils.splitpath(relpath)
433
437
        if len(parts) == 0:
434
 
            bailout("cannot re-add root of inventory")
 
438
            raise BzrError("cannot re-add root of inventory")
435
439
 
436
440
        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
 
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
 
441
448
        ie = InventoryEntry(file_id, parts[-1],
442
449
                            kind=kind, parent_id=parent_id)
443
450
        return self.add(ie)
469
476
        del self[ie.parent_id].children[ie.name]
470
477
 
471
478
 
472
 
    def id_set(self):
473
 
        return Set(self._byid)
474
 
 
475
 
 
476
479
    def to_element(self):
477
480
        """Convert to XML Element"""
 
481
        from bzrlib.xml import Element
 
482
        
478
483
        e = Element('inventory')
479
484
        e.text = '\n'
 
485
        if self.root.file_id not in (None, ROOT_ID):
 
486
            e.set('file_id', self.root.file_id)
480
487
        for path, ie in self.iter_entries():
481
488
            e.append(ie.to_element())
482
489
        return e
484
491
 
485
492
    def from_element(cls, elt):
486
493
        """Construct from XML Element
487
 
 
 
494
        
488
495
        >>> inv = Inventory()
489
496
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
497
        >>> elt = inv.to_element()
492
499
        >>> inv2 == inv
493
500
        True
494
501
        """
 
502
        # XXXX: doctest doesn't run this properly under python2.3
495
503
        assert elt.tag == 'inventory'
496
 
        o = cls()
 
504
        root_id = elt.get('file_id') or ROOT_ID
 
505
        o = cls(root_id)
497
506
        for e in elt:
498
 
            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)
499
511
        return o
500
512
        
501
513
    from_element = classmethod(from_element)
502
514
 
503
515
 
504
 
    def __cmp__(self, other):
 
516
    def __eq__(self, other):
505
517
        """Compare two sets by comparing their contents.
506
518
 
507
519
        >>> i1 = Inventory()
515
527
        >>> i1 == i2
516
528
        True
517
529
        """
518
 
        if self is other:
519
 
            return 0
520
 
        
521
530
        if not isinstance(other, Inventory):
522
531
            return NotImplemented
523
532
 
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
 
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
 
532
547
 
533
548
 
534
549
    def get_idpath(self, file_id):
544
559
            try:
545
560
                ie = self._byid[file_id]
546
561
            except KeyError:
547
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
562
                raise BzrError("file_id {%s} not found in inventory" % file_id)
548
563
            p.insert(0, ie.file_id)
549
564
            file_id = ie.parent_id
550
565
        return p
554
569
        """Return as a list the path to file_id."""
555
570
 
556
571
        # get all names, skipping root
557
 
        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:]]
558
573
        return os.sep.join(p)
559
574
            
560
575
 
604
619
 
605
620
        This does not move the working file."""
606
621
        if not is_valid_name(new_name):
607
 
            bailout("not an acceptable filename: %r" % new_name)
 
622
            raise BzrError("not an acceptable filename: %r" % new_name)
608
623
 
609
624
        new_parent = self._byid[new_parent_id]
610
625
        if new_name in new_parent.children:
611
 
            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)))
612
627
 
613
628
        new_parent_idpath = self.get_idpath(new_parent_id)
614
629
        if file_id in new_parent_idpath:
615
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
630
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
631
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
617
632
 
618
633
        file_ie = self._byid[file_id]