~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-07-12 01:30:55 UTC
  • Revision ID: mbp@sourcefrog.net-20050712013055-aafbf9db6df73c34
- fix up breakage of 'bzr log -v' by root_id patch

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