~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-05-05 06:38:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050505063818-3eb3260343878325
- do upload CHANGELOG to web server, even though it's autogenerated

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
 
18
21
# This should really be an id randomly assigned when the tree is
19
22
# created, but it's not for now.
20
23
ROOT_ID = "TREE_ROOT"
21
24
 
22
25
 
23
26
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
24
36
 
25
37
import bzrlib
26
 
from bzrlib.errors import BzrError, BzrCheckError
27
 
 
28
38
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
29
39
from bzrlib.trace import mutter
30
40
 
31
 
class InventoryEntry(object):
 
41
class InventoryEntry(XMLMixin):
32
42
    """Description of a versioned file.
33
43
 
34
44
    An InventoryEntry has the following fields, which are also
62
72
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
63
73
    Traceback (most recent call last):
64
74
    ...
65
 
    BzrError: inventory already contains entry with id {2323}
 
75
    BzrError: ('inventory already contains entry with id {2323}', [])
66
76
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
67
77
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
68
78
    >>> i.path2id('src/wibble')
91
101
 
92
102
    # TODO: split InventoryEntry into subclasses for files,
93
103
    # directories, etc etc.
94
 
 
95
 
    text_sha1 = None
96
 
    text_size = None
97
104
    
98
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
99
106
        """Create an InventoryEntry
108
115
        '123'
109
116
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
110
117
        Traceback (most recent call last):
111
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
118
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
112
119
        """
113
 
        if '/' in name or '\\' in name:
114
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
120
        
 
121
        if len(splitpath(name)) != 1:
 
122
            bailout('InventoryEntry name is not a simple filename: %r'
 
123
                    % name)
115
124
        
116
125
        self.file_id = file_id
117
126
        self.name = name
118
127
        self.kind = kind
119
128
        self.text_id = text_id
120
129
        self.parent_id = parent_id
 
130
        self.text_sha1 = None
 
131
        self.text_size = None
121
132
        if kind == 'directory':
122
133
            self.children = {}
123
134
        elif kind == 'file':
138
149
                               self.parent_id, text_id=self.text_id)
139
150
        other.text_sha1 = self.text_sha1
140
151
        other.text_size = self.text_size
141
 
        # note that children are *not* copied; they're pulled across when
142
 
        # others are added
143
152
        return other
144
153
 
145
154
 
154
163
    
155
164
    def to_element(self):
156
165
        """Convert to XML element"""
157
 
        from bzrlib.xml import Element
158
 
        
159
166
        e = Element('entry')
160
167
 
161
168
        e.set('name', self.name)
206
213
 
207
214
    from_element = classmethod(from_element)
208
215
 
209
 
    def __eq__(self, other):
 
216
    def __cmp__(self, other):
 
217
        if self is other:
 
218
            return 0
210
219
        if not isinstance(other, InventoryEntry):
211
220
            return NotImplemented
212
221
 
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')
 
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)
227
229
 
228
230
 
229
231
 
235
237
        self.parent_id = None
236
238
        self.name = ''
237
239
 
238
 
    def __eq__(self, other):
 
240
    def __cmp__(self, other):
 
241
        if self is other:
 
242
            return 0
239
243
        if not isinstance(other, RootEntry):
240
244
            return NotImplemented
241
 
        
242
 
        return (self.file_id == other.file_id) \
243
 
               and (self.children == other.children)
244
 
 
245
 
 
246
 
 
247
 
class Inventory(object):
 
245
        return cmp(self.file_id, other.file_id) \
 
246
               or cmp(self.children, other.children)
 
247
 
 
248
 
 
249
 
 
250
class Inventory(XMLMixin):
248
251
    """Inventory of versioned files in a tree.
249
252
 
250
253
    This describes which file_id is present at each point in the tree,
262
265
    inserted, other than through the Inventory API.
263
266
 
264
267
    >>> inv = Inventory()
 
268
    >>> inv.write_xml(sys.stdout)
 
269
    <inventory>
 
270
    </inventory>
265
271
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
266
272
    >>> inv['123-123'].name
267
273
    'hello.c'
277
283
 
278
284
    >>> [x[0] for x in inv.iter_entries()]
279
285
    ['hello.c']
280
 
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
281
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
286
    
 
287
    >>> inv.write_xml(sys.stdout)
 
288
    <inventory>
 
289
    <entry file_id="123-123" kind="file" name="hello.c" />
 
290
    </inventory>
 
291
 
282
292
    """
283
 
    def __init__(self, root_id=ROOT_ID):
 
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):
284
310
        """Create or read an inventory.
285
311
 
286
312
        If a working directory is specified, the inventory is read
290
316
        The inventory is created with a default root directory, with
291
317
        an id of None.
292
318
        """
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)
 
319
        self.root = RootEntry(ROOT_ID)
298
320
        self._byid = {self.root.file_id: self.root}
299
321
 
300
322
 
322
344
            if ie.kind == 'directory':
323
345
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
324
346
                    yield os.path.join(name, cn), cie
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.
 
347
                    
 
348
 
 
349
 
 
350
    def directories(self):
 
351
        """Return (path, entry) pairs for all directories.
331
352
        """
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))
 
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():
339
360
                if ie.kind == 'directory':
340
 
                    descend(ie, child_path)
341
 
 
342
 
        descend(self.root, '')
343
 
        return accum
344
 
 
345
 
 
346
 
    def directories(self):
347
 
        """Return (path, entry) pairs for all directories, including the root.
348
 
        """
349
 
        accum = []
350
 
        def descend(parent_ie, parent_path):
351
 
            accum.append((parent_path, parent_ie))
 
361
                    dn.append((ie.name, ie))
 
362
            dn.sort()
352
363
            
353
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
354
 
            kids.sort()
 
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
355
367
 
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
 
368
        for name, ie in descend(self.root):
 
369
            yield name, ie
361
370
        
362
371
 
363
372
 
382
391
        >>> inv['123123'].name
383
392
        'hello.c'
384
393
        """
 
394
        if file_id == None:
 
395
            raise BzrError("can't look up file_id None")
 
396
            
385
397
        try:
386
398
            return self._byid[file_id]
387
399
        except KeyError:
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
 
400
            raise BzrError("file_id {%s} not in inventory" % file_id)
 
401
 
396
402
 
397
403
    def get_child(self, parent_id, filename):
398
404
        return self[parent_id].children.get(filename)
404
410
        To add  a file to a branch ready to be committed, use Branch.add,
405
411
        which calls this."""
406
412
        if entry.file_id in self._byid:
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
 
413
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
411
414
 
412
415
        try:
413
416
            parent = self._byid[entry.parent_id]
414
417
        except KeyError:
415
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
418
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
416
419
 
417
420
        if parent.children.has_key(entry.name):
418
 
            raise BzrError("%s is already versioned" %
 
421
            bailout("%s is already versioned" %
419
422
                    appendpath(self.id2path(parent.file_id), entry.name))
420
423
 
421
424
        self._byid[entry.file_id] = entry
426
429
        """Add entry from a path.
427
430
 
428
431
        The immediate parent must already be versioned"""
429
 
        from bzrlib.errors import NotVersionedError
430
 
        
431
432
        parts = bzrlib.osutils.splitpath(relpath)
432
433
        if len(parts) == 0:
433
 
            raise BzrError("cannot re-add root of inventory")
 
434
            bailout("cannot re-add root of inventory")
434
435
 
435
436
        if file_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
 
 
 
437
            file_id = bzrlib.branch.gen_file_id(relpath)
 
438
 
 
439
        parent_id = self.path2id(parts[:-1])
 
440
        assert parent_id != None
444
441
        ie = InventoryEntry(file_id, parts[-1],
445
442
                            kind=kind, parent_id=parent_id)
446
443
        return self.add(ie)
472
469
        del self[ie.parent_id].children[ie.name]
473
470
 
474
471
 
 
472
    def id_set(self):
 
473
        return Set(self._byid)
 
474
 
 
475
 
475
476
    def to_element(self):
476
477
        """Convert to XML Element"""
477
 
        from bzrlib.xml import Element
478
 
        
479
478
        e = Element('inventory')
480
479
        e.text = '\n'
481
 
        if self.root.file_id not in (None, ROOT_ID):
482
 
            e.set('file_id', self.root.file_id)
483
480
        for path, ie in self.iter_entries():
484
481
            e.append(ie.to_element())
485
482
        return e
487
484
 
488
485
    def from_element(cls, elt):
489
486
        """Construct from XML Element
490
 
        
 
487
 
491
488
        >>> inv = Inventory()
492
489
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
493
490
        >>> elt = inv.to_element()
495
492
        >>> inv2 == inv
496
493
        True
497
494
        """
498
 
        # XXXX: doctest doesn't run this properly under python2.3
499
495
        assert elt.tag == 'inventory'
500
 
        root_id = elt.get('file_id') or ROOT_ID
501
 
        o = cls(root_id)
 
496
        o = cls()
502
497
        for e in elt:
503
 
            ie = InventoryEntry.from_element(e)
504
 
            if ie.parent_id == ROOT_ID:
505
 
                ie.parent_id = root_id
506
 
            o.add(ie)
 
498
            o.add(InventoryEntry.from_element(e))
507
499
        return o
508
500
        
509
501
    from_element = classmethod(from_element)
510
502
 
511
503
 
512
 
    def __eq__(self, other):
 
504
    def __cmp__(self, other):
513
505
        """Compare two sets by comparing their contents.
514
506
 
515
507
        >>> i1 = Inventory()
523
515
        >>> i1 == i2
524
516
        True
525
517
        """
 
518
        if self is other:
 
519
            return 0
 
520
        
526
521
        if not isinstance(other, Inventory):
527
522
            return NotImplemented
528
523
 
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
 
 
 
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
543
532
 
544
533
 
545
534
    def get_idpath(self, file_id):
555
544
            try:
556
545
                ie = self._byid[file_id]
557
546
            except KeyError:
558
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
547
                bailout("file_id {%s} not found in inventory" % file_id)
559
548
            p.insert(0, ie.file_id)
560
549
            file_id = ie.parent_id
561
550
        return p
565
554
        """Return as a list the path to file_id."""
566
555
 
567
556
        # get all names, skipping root
568
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
557
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
569
558
        return os.sep.join(p)
570
559
            
571
560
 
615
604
 
616
605
        This does not move the working file."""
617
606
        if not is_valid_name(new_name):
618
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
607
            bailout("not an acceptable filename: %r" % new_name)
619
608
 
620
609
        new_parent = self._byid[new_parent_id]
621
610
        if new_name in new_parent.children:
622
 
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
611
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
623
612
 
624
613
        new_parent_idpath = self.get_idpath(new_parent_id)
625
614
        if file_id in new_parent_idpath:
626
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
615
            bailout("cannot move directory %r into a subdirectory of itself, %r"
627
616
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
628
617
 
629
618
        file_ie = self._byid[file_id]