~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Aaron Bentley
  • Date: 2005-08-10 15:05:26 UTC
  • mto: (1092.1.41) (1185.3.4) (974.1.47)
  • mto: This revision was merged to the branch mainline in revision 1110.
  • Revision ID: abentley@panoramicfeedback.com-20050810150526-468a7e2e3dc299e4
Switched to using a set of interesting file_ids instead of SourceFile attribute

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