~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-09-05 08:00:35 UTC
  • Revision ID: mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9
- start splitting code for xml (de)serialization away from objects
  preparatory to supporting multiple formats by a single library

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
 
23
23
import sys, os.path, types, re
24
 
from sets import Set
25
 
 
26
 
try:
27
 
    from cElementTree import Element, ElementTree, SubElement
28
 
except ImportError:
29
 
    from elementtree.ElementTree import Element, ElementTree, SubElement
30
 
 
31
 
from xml import XMLMixin
32
 
from errors import bailout, BzrError, BzrCheckError
33
24
 
34
25
import bzrlib
 
26
from bzrlib.errors import BzrError, BzrCheckError
 
27
 
35
28
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
36
29
from bzrlib.trace import mutter
 
30
from bzrlib.errors import NotVersionedError
 
31
        
37
32
 
38
 
class InventoryEntry(XMLMixin):
 
33
class InventoryEntry(object):
39
34
    """Description of a versioned file.
40
35
 
41
36
    An InventoryEntry has the following fields, which are also
60
55
    >>> i.path2id('')
61
56
    'TREE_ROOT'
62
57
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
58
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
63
59
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
60
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
64
61
    >>> for j in i.iter_entries():
65
62
    ...   print j
66
63
    ... 
69
66
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
67
    Traceback (most recent call last):
71
68
    ...
72
 
    BzrError: ('inventory already contains entry with id {2323}', [])
 
69
    BzrError: inventory already contains entry with id {2323}
73
70
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
71
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
74
72
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
73
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
75
74
    >>> i.path2id('src/wibble')
76
75
    '2325'
77
76
    >>> '2325' in i
78
77
    True
79
78
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
79
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
80
    >>> i['2326']
81
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
82
    >>> for j in i.iter_entries():
99
99
    # TODO: split InventoryEntry into subclasses for files,
100
100
    # directories, etc etc.
101
101
 
102
 
    text_sha1 = None
103
 
    text_size = None
104
 
    
 
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
        
120
120
        if '/' in name or '\\' in name:
121
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
122
122
        
 
123
        self.text_sha1 = None
 
124
        self.text_size = None
 
125
    
123
126
        self.file_id = file_id
124
127
        self.name = name
125
128
        self.kind = kind
145
148
                               self.parent_id, text_id=self.text_id)
146
149
        other.text_sha1 = self.text_sha1
147
150
        other.text_size = self.text_size
 
151
        # note that children are *not* copied; they're pulled across when
 
152
        # others are added
148
153
        return other
149
154
 
150
155
 
157
162
                   self.parent_id))
158
163
 
159
164
    
160
 
    def to_element(self):
161
 
        """Convert to XML element"""
162
 
        e = Element('entry')
163
 
 
164
 
        e.set('name', self.name)
165
 
        e.set('file_id', self.file_id)
166
 
        e.set('kind', self.kind)
167
 
 
168
 
        if self.text_size != None:
169
 
            e.set('text_size', '%d' % self.text_size)
170
 
            
171
 
        for f in ['text_id', 'text_sha1']:
172
 
            v = getattr(self, f)
173
 
            if v != None:
174
 
                e.set(f, v)
175
 
 
176
 
        # to be conservative, we don't externalize the root pointers
177
 
        # for now, leaving them as null in the xml form.  in a future
178
 
        # version it will be implied by nested elements.
179
 
        if self.parent_id != ROOT_ID:
180
 
            assert isinstance(self.parent_id, basestring)
181
 
            e.set('parent_id', self.parent_id)
182
 
 
183
 
        e.tail = '\n'
184
 
            
185
 
        return e
186
 
 
187
 
 
188
 
    def from_element(cls, elt):
189
 
        assert elt.tag == 'entry'
190
 
 
191
 
        ## original format inventories don't have a parent_id for
192
 
        ## nodes in the root directory, but it's cleaner to use one
193
 
        ## internally.
194
 
        parent_id = elt.get('parent_id')
195
 
        if parent_id == None:
196
 
            parent_id = ROOT_ID
197
 
 
198
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
199
 
        self.text_id = elt.get('text_id')
200
 
        self.text_sha1 = elt.get('text_sha1')
201
 
        
202
 
        ## mutter("read inventoryentry: %r" % (elt.attrib))
203
 
 
204
 
        v = elt.get('text_size')
205
 
        self.text_size = v and int(v)
206
 
 
207
 
        return self
208
 
            
209
 
 
210
 
    from_element = classmethod(from_element)
211
 
 
212
 
    def __cmp__(self, other):
213
 
        if self is other:
214
 
            return 0
 
165
    def __eq__(self, other):
215
166
        if not isinstance(other, InventoryEntry):
216
167
            return NotImplemented
217
168
 
218
 
        return cmp(self.file_id, other.file_id) \
219
 
               or cmp(self.name, other.name) \
220
 
               or cmp(self.text_sha1, other.text_sha1) \
221
 
               or cmp(self.text_size, other.text_size) \
222
 
               or cmp(self.text_id, other.text_id) \
223
 
               or cmp(self.parent_id, other.parent_id) \
224
 
               or cmp(self.kind, other.kind)
 
169
        return (self.file_id == other.file_id) \
 
170
               and (self.name == other.name) \
 
171
               and (self.text_sha1 == other.text_sha1) \
 
172
               and (self.text_size == other.text_size) \
 
173
               and (self.text_id == other.text_id) \
 
174
               and (self.parent_id == other.parent_id) \
 
175
               and (self.kind == other.kind)
 
176
 
 
177
 
 
178
    def __ne__(self, other):
 
179
        return not (self == other)
 
180
 
 
181
    def __hash__(self):
 
182
        raise ValueError('not hashable')
225
183
 
226
184
 
227
185
 
233
191
        self.parent_id = None
234
192
        self.name = ''
235
193
 
236
 
    def __cmp__(self, other):
237
 
        if self is other:
238
 
            return 0
 
194
    def __eq__(self, other):
239
195
        if not isinstance(other, RootEntry):
240
196
            return NotImplemented
241
 
        return cmp(self.file_id, other.file_id) \
242
 
               or cmp(self.children, other.children)
243
 
 
244
 
 
245
 
 
246
 
class Inventory(XMLMixin):
 
197
        
 
198
        return (self.file_id == other.file_id) \
 
199
               and (self.children == other.children)
 
200
 
 
201
 
 
202
 
 
203
class Inventory(object):
247
204
    """Inventory of versioned files in a tree.
248
205
 
249
206
    This describes which file_id is present at each point in the tree,
261
218
    inserted, other than through the Inventory API.
262
219
 
263
220
    >>> inv = Inventory()
264
 
    >>> inv.write_xml(sys.stdout)
265
 
    <inventory>
266
 
    </inventory>
267
221
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
222
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
268
223
    >>> inv['123-123'].name
269
224
    'hello.c'
270
225
 
279
234
 
280
235
    >>> [x[0] for x in inv.iter_entries()]
281
236
    ['hello.c']
282
 
    
283
 
    >>> inv.write_xml(sys.stdout)
284
 
    <inventory>
285
 
    <entry file_id="123-123" kind="file" name="hello.c" />
286
 
    </inventory>
287
 
 
 
237
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
238
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
239
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
288
240
    """
289
 
    def __init__(self):
 
241
    def __init__(self, root_id=ROOT_ID):
290
242
        """Create or read an inventory.
291
243
 
292
244
        If a working directory is specified, the inventory is read
296
248
        The inventory is created with a default root directory, with
297
249
        an id of None.
298
250
        """
299
 
        self.root = RootEntry(ROOT_ID)
 
251
        # We are letting Branch(init=True) create a unique inventory
 
252
        # root id. Rather than generating a random one here.
 
253
        #if root_id is None:
 
254
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
255
        self.root = RootEntry(root_id)
300
256
        self._byid = {self.root.file_id: self.root}
301
257
 
302
258
 
324
280
            if ie.kind == 'directory':
325
281
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
326
282
                    yield os.path.join(name, cn), cie
327
 
                    
 
283
 
 
284
 
 
285
    def entries(self):
 
286
        """Return list of (path, ie) for all entries except the root.
 
287
 
 
288
        This may be faster than iter_entries.
 
289
        """
 
290
        accum = []
 
291
        def descend(dir_ie, dir_path):
 
292
            kids = dir_ie.children.items()
 
293
            kids.sort()
 
294
            for name, ie in kids:
 
295
                child_path = os.path.join(dir_path, name)
 
296
                accum.append((child_path, ie))
 
297
                if ie.kind == 'directory':
 
298
                    descend(ie, child_path)
 
299
 
 
300
        descend(self.root, '')
 
301
        return accum
328
302
 
329
303
 
330
304
    def directories(self):
331
 
        """Return (path, entry) pairs for all directories.
 
305
        """Return (path, entry) pairs for all directories, including the root.
332
306
        """
333
 
        def descend(parent_ie):
334
 
            parent_name = parent_ie.name
335
 
            yield parent_name, parent_ie
336
 
 
337
 
            # directory children in sorted order
338
 
            dn = []
339
 
            for ie in parent_ie.children.itervalues():
340
 
                if ie.kind == 'directory':
341
 
                    dn.append((ie.name, ie))
342
 
            dn.sort()
 
307
        accum = []
 
308
        def descend(parent_ie, parent_path):
 
309
            accum.append((parent_path, parent_ie))
343
310
            
344
 
            for name, child_ie in dn:
345
 
                for sub_name, sub_ie in descend(child_ie):
346
 
                    yield appendpath(parent_name, sub_name), sub_ie
 
311
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
312
            kids.sort()
347
313
 
348
 
        for name, ie in descend(self.root):
349
 
            yield name, ie
 
314
            for name, child_ie in kids:
 
315
                child_path = os.path.join(parent_path, name)
 
316
                descend(child_ie, child_path)
 
317
        descend(self.root, '')
 
318
        return accum
350
319
        
351
320
 
352
321
 
355
324
 
356
325
        >>> inv = Inventory()
357
326
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
327
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
358
328
        >>> '123' in inv
359
329
        True
360
330
        >>> '456' in inv
368
338
 
369
339
        >>> inv = Inventory()
370
340
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
341
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
371
342
        >>> inv['123123'].name
372
343
        'hello.c'
373
344
        """
380
351
                raise BzrError("file_id {%s} not in inventory" % file_id)
381
352
 
382
353
 
 
354
    def get_file_kind(self, file_id):
 
355
        return self._byid[file_id].kind
 
356
 
383
357
    def get_child(self, parent_id, filename):
384
358
        return self[parent_id].children.get(filename)
385
359
 
388
362
        """Add entry to inventory.
389
363
 
390
364
        To add  a file to a branch ready to be committed, use Branch.add,
391
 
        which calls this."""
 
365
        which calls this.
 
366
 
 
367
        Returns the new entry object.
 
368
        """
392
369
        if entry.file_id in self._byid:
393
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
370
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
371
 
 
372
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
373
            entry.parent_id = self.root.file_id
394
374
 
395
375
        try:
396
376
            parent = self._byid[entry.parent_id]
397
377
        except KeyError:
398
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
378
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
399
379
 
400
380
        if parent.children.has_key(entry.name):
401
 
            bailout("%s is already versioned" %
 
381
            raise BzrError("%s is already versioned" %
402
382
                    appendpath(self.id2path(parent.file_id), entry.name))
403
383
 
404
384
        self._byid[entry.file_id] = entry
405
385
        parent.children[entry.name] = entry
 
386
        return entry
406
387
 
407
388
 
408
389
    def add_path(self, relpath, kind, file_id=None):
409
390
        """Add entry from a path.
410
391
 
411
 
        The immediate parent must already be versioned"""
 
392
        The immediate parent must already be versioned.
 
393
 
 
394
        Returns the new entry object."""
 
395
        from bzrlib.branch import gen_file_id
 
396
        
412
397
        parts = bzrlib.osutils.splitpath(relpath)
413
398
        if len(parts) == 0:
414
 
            bailout("cannot re-add root of inventory")
 
399
            raise BzrError("cannot re-add root of inventory")
415
400
 
416
401
        if file_id == None:
417
 
            file_id = bzrlib.branch.gen_file_id(relpath)
418
 
 
419
 
        parent_id = self.path2id(parts[:-1])
420
 
        assert parent_id != None
 
402
            file_id = gen_file_id(relpath)
 
403
 
 
404
        parent_path = parts[:-1]
 
405
        parent_id = self.path2id(parent_path)
 
406
        if parent_id == None:
 
407
            raise NotVersionedError(parent_path)
 
408
 
421
409
        ie = InventoryEntry(file_id, parts[-1],
422
410
                            kind=kind, parent_id=parent_id)
423
411
        return self.add(ie)
428
416
 
429
417
        >>> inv = Inventory()
430
418
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
419
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
431
420
        >>> '123' in inv
432
421
        True
433
422
        >>> del inv['123']
449
438
        del self[ie.parent_id].children[ie.name]
450
439
 
451
440
 
452
 
    def id_set(self):
453
 
        return Set(self._byid)
454
 
 
455
 
 
456
 
    def to_element(self):
457
 
        """Convert to XML Element"""
458
 
        e = Element('inventory')
459
 
        e.text = '\n'
460
 
        for path, ie in self.iter_entries():
461
 
            e.append(ie.to_element())
462
 
        return e
463
 
    
464
 
 
465
 
    def from_element(cls, elt):
466
 
        """Construct from XML Element
467
 
 
468
 
        >>> inv = Inventory()
469
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
470
 
        >>> elt = inv.to_element()
471
 
        >>> inv2 = Inventory.from_element(elt)
472
 
        >>> inv2 == inv
473
 
        True
474
 
        """
475
 
        assert elt.tag == 'inventory'
476
 
        o = cls()
477
 
        for e in elt:
478
 
            o.add(InventoryEntry.from_element(e))
479
 
        return o
480
 
        
481
 
    from_element = classmethod(from_element)
482
 
 
483
 
 
484
 
    def __cmp__(self, other):
 
441
    def __eq__(self, other):
485
442
        """Compare two sets by comparing their contents.
486
443
 
487
444
        >>> i1 = Inventory()
489
446
        >>> i1 == i2
490
447
        True
491
448
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
449
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
492
450
        >>> i1 == i2
493
451
        False
494
452
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
453
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
495
454
        >>> i1 == i2
496
455
        True
497
456
        """
498
 
        if self is other:
499
 
            return 0
500
 
        
501
457
        if not isinstance(other, Inventory):
502
458
            return NotImplemented
503
459
 
504
 
        if self.id_set() ^ other.id_set():
505
 
            return 1
506
 
 
507
 
        for file_id in self._byid:
508
 
            c = cmp(self[file_id], other[file_id])
509
 
            if c: return c
510
 
 
511
 
        return 0
 
460
        if len(self._byid) != len(other._byid):
 
461
            # shortcut: obviously not the same
 
462
            return False
 
463
 
 
464
        return self._byid == other._byid
 
465
 
 
466
 
 
467
    def __ne__(self, other):
 
468
        return not (self == other)
 
469
 
 
470
 
 
471
    def __hash__(self):
 
472
        raise ValueError('not hashable')
512
473
 
513
474
 
514
475
    def get_idpath(self, file_id):
524
485
            try:
525
486
                ie = self._byid[file_id]
526
487
            except KeyError:
527
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
488
                raise BzrError("file_id {%s} not found in inventory" % file_id)
528
489
            p.insert(0, ie.file_id)
529
490
            file_id = ie.parent_id
530
491
        return p
534
495
        """Return as a list the path to file_id."""
535
496
 
536
497
        # get all names, skipping root
537
 
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
 
498
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
538
499
        return os.sep.join(p)
539
500
            
540
501
 
584
545
 
585
546
        This does not move the working file."""
586
547
        if not is_valid_name(new_name):
587
 
            bailout("not an acceptable filename: %r" % new_name)
 
548
            raise BzrError("not an acceptable filename: %r" % new_name)
588
549
 
589
550
        new_parent = self._byid[new_parent_id]
590
551
        if new_name in new_parent.children:
591
 
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
552
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
592
553
 
593
554
        new_parent_idpath = self.get_idpath(new_parent_id)
594
555
        if file_id in new_parent_idpath:
595
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
556
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
596
557
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
597
558
 
598
559
        file_ie = self._byid[file_id]
609
570
 
610
571
 
611
572
 
612
 
_NAME_RE = re.compile(r'^[^/\\]+$')
 
573
_NAME_RE = None
613
574
 
614
575
def is_valid_name(name):
 
576
    global _NAME_RE
 
577
    if _NAME_RE == None:
 
578
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
579
        
615
580
    return bool(_NAME_RE.match(name))