~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-05-12 02:14:35 UTC
  • Revision ID: mbp@sourcefrog.net-20050512021435-87fa19f051842647
- new helper function kind_marker()

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
24
33
 
25
34
import bzrlib
26
 
from bzrlib.errors import BzrError, BzrCheckError
27
 
 
28
35
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
29
36
from bzrlib.trace import mutter
30
 
from bzrlib.errors import NotVersionedError
31
 
        
32
37
 
33
 
class InventoryEntry(object):
 
38
class InventoryEntry(XMLMixin):
34
39
    """Description of a versioned file.
35
40
 
36
41
    An InventoryEntry has the following fields, which are also
55
60
    >>> i.path2id('')
56
61
    'TREE_ROOT'
57
62
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
58
 
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
59
63
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
60
 
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
61
64
    >>> for j in i.iter_entries():
62
65
    ...   print j
63
66
    ... 
66
69
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
67
70
    Traceback (most recent call last):
68
71
    ...
69
 
    BzrError: inventory already contains entry with id {2323}
 
72
    BzrError: ('inventory already contains entry with id {2323}', [])
70
73
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
71
 
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
72
74
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
 
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
74
75
    >>> i.path2id('src/wibble')
75
76
    '2325'
76
77
    >>> '2325' in i
77
78
    True
78
79
    >>> 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
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
103
 
                 'text_id', 'parent_id', 'children',
104
 
                 'text_version', 'entry_version', ]
105
 
 
106
 
 
 
102
    text_sha1 = None
 
103
    text_size = None
 
104
    
107
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
108
106
        """Create an InventoryEntry
109
107
        
119
117
        Traceback (most recent call last):
120
118
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
121
119
        """
122
 
        assert isinstance(name, basestring), name
123
120
        if '/' in name or '\\' in name:
124
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
125
122
        
126
 
        self.text_version = None
127
 
        self.entry_version = None
128
 
        self.text_sha1 = None
129
 
        self.text_size = None
130
123
        self.file_id = file_id
131
124
        self.name = name
132
125
        self.kind = kind
152
145
                               self.parent_id, text_id=self.text_id)
153
146
        other.text_sha1 = self.text_sha1
154
147
        other.text_size = self.text_size
155
 
        # note that children are *not* copied; they're pulled across when
156
 
        # others are added
157
148
        return other
158
149
 
159
150
 
166
157
                   self.parent_id))
167
158
 
168
159
    
169
 
    def __eq__(self, other):
 
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
170
215
        if not isinstance(other, InventoryEntry):
171
216
            return NotImplemented
172
217
 
173
 
        return (self.file_id == other.file_id) \
174
 
               and (self.name == other.name) \
175
 
               and (self.text_sha1 == other.text_sha1) \
176
 
               and (self.text_size == other.text_size) \
177
 
               and (self.text_id == other.text_id) \
178
 
               and (self.parent_id == other.parent_id) \
179
 
               and (self.kind == other.kind) \
180
 
               and (self.text_version == other.text_version) \
181
 
               and (self.entry_version == other.entry_version)
182
 
 
183
 
 
184
 
    def __ne__(self, other):
185
 
        return not (self == other)
186
 
 
187
 
    def __hash__(self):
188
 
        raise ValueError('not hashable')
 
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)
189
225
 
190
226
 
191
227
 
197
233
        self.parent_id = None
198
234
        self.name = ''
199
235
 
200
 
    def __eq__(self, other):
 
236
    def __cmp__(self, other):
 
237
        if self is other:
 
238
            return 0
201
239
        if not isinstance(other, RootEntry):
202
240
            return NotImplemented
203
 
        
204
 
        return (self.file_id == other.file_id) \
205
 
               and (self.children == other.children)
206
 
 
207
 
 
208
 
 
209
 
class Inventory(object):
 
241
        return cmp(self.file_id, other.file_id) \
 
242
               or cmp(self.children, other.children)
 
243
 
 
244
 
 
245
 
 
246
class Inventory(XMLMixin):
210
247
    """Inventory of versioned files in a tree.
211
248
 
212
249
    This describes which file_id is present at each point in the tree,
224
261
    inserted, other than through the Inventory API.
225
262
 
226
263
    >>> inv = Inventory()
 
264
    >>> inv.write_xml(sys.stdout)
 
265
    <inventory>
 
266
    </inventory>
227
267
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
228
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
229
268
    >>> inv['123-123'].name
230
269
    'hello.c'
231
270
 
240
279
 
241
280
    >>> [x[0] for x in inv.iter_entries()]
242
281
    ['hello.c']
243
 
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
244
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
245
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
282
    
 
283
    >>> inv.write_xml(sys.stdout)
 
284
    <inventory>
 
285
    <entry file_id="123-123" kind="file" name="hello.c" />
 
286
    </inventory>
 
287
 
246
288
    """
247
 
    def __init__(self, root_id=ROOT_ID):
 
289
    def __init__(self):
248
290
        """Create or read an inventory.
249
291
 
250
292
        If a working directory is specified, the inventory is read
254
296
        The inventory is created with a default root directory, with
255
297
        an id of None.
256
298
        """
257
 
        # We are letting Branch(init=True) create a unique inventory
258
 
        # root id. Rather than generating a random one here.
259
 
        #if root_id is None:
260
 
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
261
 
        self.root = RootEntry(root_id)
 
299
        self.root = RootEntry(ROOT_ID)
262
300
        self._byid = {self.root.file_id: self.root}
263
301
 
264
302
 
286
324
            if ie.kind == 'directory':
287
325
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
288
326
                    yield os.path.join(name, cn), cie
289
 
 
290
 
 
291
 
    def entries(self):
292
 
        """Return list of (path, ie) for all entries except the root.
293
 
 
294
 
        This may be faster than iter_entries.
 
327
                    
 
328
 
 
329
 
 
330
    def directories(self):
 
331
        """Return (path, entry) pairs for all directories.
295
332
        """
296
 
        accum = []
297
 
        def descend(dir_ie, dir_path):
298
 
            kids = dir_ie.children.items()
299
 
            kids.sort()
300
 
            for name, ie in kids:
301
 
                child_path = os.path.join(dir_path, name)
302
 
                accum.append((child_path, ie))
 
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():
303
340
                if ie.kind == 'directory':
304
 
                    descend(ie, child_path)
305
 
 
306
 
        descend(self.root, '')
307
 
        return accum
308
 
 
309
 
 
310
 
    def directories(self):
311
 
        """Return (path, entry) pairs for all directories, including the root.
312
 
        """
313
 
        accum = []
314
 
        def descend(parent_ie, parent_path):
315
 
            accum.append((parent_path, parent_ie))
 
341
                    dn.append((ie.name, ie))
 
342
            dn.sort()
316
343
            
317
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
318
 
            kids.sort()
 
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
319
347
 
320
 
            for name, child_ie in kids:
321
 
                child_path = os.path.join(parent_path, name)
322
 
                descend(child_ie, child_path)
323
 
        descend(self.root, '')
324
 
        return accum
 
348
        for name, ie in descend(self.root):
 
349
            yield name, ie
325
350
        
326
351
 
327
352
 
330
355
 
331
356
        >>> inv = Inventory()
332
357
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
333
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
334
358
        >>> '123' in inv
335
359
        True
336
360
        >>> '456' in inv
344
368
 
345
369
        >>> inv = Inventory()
346
370
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
347
 
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
348
371
        >>> inv['123123'].name
349
372
        'hello.c'
350
373
        """
368
391
        """Add entry to inventory.
369
392
 
370
393
        To add  a file to a branch ready to be committed, use Branch.add,
371
 
        which calls this.
372
 
 
373
 
        Returns the new entry object.
374
 
        """
 
394
        which calls this."""
375
395
        if entry.file_id in self._byid:
376
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
377
 
 
378
 
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
379
 
            entry.parent_id = self.root.file_id
 
396
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
380
397
 
381
398
        try:
382
399
            parent = self._byid[entry.parent_id]
383
400
        except KeyError:
384
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
401
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
385
402
 
386
403
        if parent.children.has_key(entry.name):
387
 
            raise BzrError("%s is already versioned" %
 
404
            bailout("%s is already versioned" %
388
405
                    appendpath(self.id2path(parent.file_id), entry.name))
389
406
 
390
407
        self._byid[entry.file_id] = entry
391
408
        parent.children[entry.name] = entry
392
 
        return entry
393
409
 
394
410
 
395
411
    def add_path(self, relpath, kind, file_id=None):
396
412
        """Add entry from a path.
397
413
 
398
 
        The immediate parent must already be versioned.
399
 
 
400
 
        Returns the new entry object."""
401
 
        from bzrlib.branch import gen_file_id
402
 
        
 
414
        The immediate parent must already be versioned"""
403
415
        parts = bzrlib.osutils.splitpath(relpath)
404
416
        if len(parts) == 0:
405
 
            raise BzrError("cannot re-add root of inventory")
 
417
            bailout("cannot re-add root of inventory")
406
418
 
407
419
        if file_id == None:
408
 
            file_id = gen_file_id(relpath)
409
 
 
410
 
        parent_path = parts[:-1]
411
 
        parent_id = self.path2id(parent_path)
412
 
        if parent_id == None:
413
 
            raise NotVersionedError(parent_path)
414
 
 
 
420
            file_id = bzrlib.branch.gen_file_id(relpath)
 
421
 
 
422
        parent_id = self.path2id(parts[:-1])
 
423
        assert parent_id != None
415
424
        ie = InventoryEntry(file_id, parts[-1],
416
425
                            kind=kind, parent_id=parent_id)
417
426
        return self.add(ie)
422
431
 
423
432
        >>> inv = Inventory()
424
433
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
425
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
426
434
        >>> '123' in inv
427
435
        True
428
436
        >>> del inv['123']
444
452
        del self[ie.parent_id].children[ie.name]
445
453
 
446
454
 
447
 
    def __eq__(self, other):
 
455
    def id_set(self):
 
456
        return Set(self._byid)
 
457
 
 
458
 
 
459
    def to_element(self):
 
460
        """Convert to XML Element"""
 
461
        e = Element('inventory')
 
462
        e.text = '\n'
 
463
        for path, ie in self.iter_entries():
 
464
            e.append(ie.to_element())
 
465
        return e
 
466
    
 
467
 
 
468
    def from_element(cls, elt):
 
469
        """Construct from XML Element
 
470
 
 
471
        >>> inv = Inventory()
 
472
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
473
        >>> elt = inv.to_element()
 
474
        >>> inv2 = Inventory.from_element(elt)
 
475
        >>> inv2 == inv
 
476
        True
 
477
        """
 
478
        assert elt.tag == 'inventory'
 
479
        o = cls()
 
480
        for e in elt:
 
481
            o.add(InventoryEntry.from_element(e))
 
482
        return o
 
483
        
 
484
    from_element = classmethod(from_element)
 
485
 
 
486
 
 
487
    def __cmp__(self, other):
448
488
        """Compare two sets by comparing their contents.
449
489
 
450
490
        >>> i1 = Inventory()
452
492
        >>> i1 == i2
453
493
        True
454
494
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
455
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
456
495
        >>> i1 == i2
457
496
        False
458
497
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
459
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
460
498
        >>> i1 == i2
461
499
        True
462
500
        """
 
501
        if self is other:
 
502
            return 0
 
503
        
463
504
        if not isinstance(other, Inventory):
464
505
            return NotImplemented
465
506
 
466
 
        if len(self._byid) != len(other._byid):
467
 
            # shortcut: obviously not the same
468
 
            return False
469
 
 
470
 
        return self._byid == other._byid
471
 
 
472
 
 
473
 
    def __ne__(self, other):
474
 
        return not (self == other)
475
 
 
476
 
 
477
 
    def __hash__(self):
478
 
        raise ValueError('not hashable')
 
507
        if self.id_set() ^ other.id_set():
 
508
            return 1
 
509
 
 
510
        for file_id in self._byid:
 
511
            c = cmp(self[file_id], other[file_id])
 
512
            if c: return c
 
513
 
 
514
        return 0
479
515
 
480
516
 
481
517
    def get_idpath(self, file_id):
491
527
            try:
492
528
                ie = self._byid[file_id]
493
529
            except KeyError:
494
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
530
                bailout("file_id {%s} not found in inventory" % file_id)
495
531
            p.insert(0, ie.file_id)
496
532
            file_id = ie.parent_id
497
533
        return p
501
537
        """Return as a list the path to file_id."""
502
538
 
503
539
        # get all names, skipping root
504
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
540
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
505
541
        return os.sep.join(p)
506
542
            
507
543
 
551
587
 
552
588
        This does not move the working file."""
553
589
        if not is_valid_name(new_name):
554
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
590
            bailout("not an acceptable filename: %r" % new_name)
555
591
 
556
592
        new_parent = self._byid[new_parent_id]
557
593
        if new_name in new_parent.children:
558
 
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
594
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
559
595
 
560
596
        new_parent_idpath = self.get_idpath(new_parent_id)
561
597
        if file_id in new_parent_idpath:
562
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
598
            bailout("cannot move directory %r into a subdirectory of itself, %r"
563
599
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
564
600
 
565
601
        file_ie = self._byid[file_id]
576
612
 
577
613
 
578
614
 
579
 
_NAME_RE = None
 
615
_NAME_RE = re.compile(r'^[^/\\]+$')
580
616
 
581
617
def is_valid_name(name):
582
 
    global _NAME_RE
583
 
    if _NAME_RE == None:
584
 
        _NAME_RE = re.compile(r'^[^/\\]+$')
585
 
        
586
618
    return bool(_NAME_RE.match(name))