~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-05-16 02:19:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050516021913-3a933f871079e3fe
- patch from ddaa to create api/ directory 
  before building API docs

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
166
159
                   self.parent_id))
167
160
 
168
161
    
169
 
    def __eq__(self, other):
 
162
    def to_element(self):
 
163
        """Convert to XML element"""
 
164
        e = Element('entry')
 
165
 
 
166
        e.set('name', self.name)
 
167
        e.set('file_id', self.file_id)
 
168
        e.set('kind', self.kind)
 
169
 
 
170
        if self.text_size != None:
 
171
            e.set('text_size', '%d' % self.text_size)
 
172
            
 
173
        for f in ['text_id', 'text_sha1']:
 
174
            v = getattr(self, f)
 
175
            if v != None:
 
176
                e.set(f, v)
 
177
 
 
178
        # to be conservative, we don't externalize the root pointers
 
179
        # for now, leaving them as null in the xml form.  in a future
 
180
        # version it will be implied by nested elements.
 
181
        if self.parent_id != ROOT_ID:
 
182
            assert isinstance(self.parent_id, basestring)
 
183
            e.set('parent_id', self.parent_id)
 
184
 
 
185
        e.tail = '\n'
 
186
            
 
187
        return e
 
188
 
 
189
 
 
190
    def from_element(cls, elt):
 
191
        assert elt.tag == 'entry'
 
192
 
 
193
        ## original format inventories don't have a parent_id for
 
194
        ## nodes in the root directory, but it's cleaner to use one
 
195
        ## internally.
 
196
        parent_id = elt.get('parent_id')
 
197
        if parent_id == None:
 
198
            parent_id = ROOT_ID
 
199
 
 
200
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
 
201
        self.text_id = elt.get('text_id')
 
202
        self.text_sha1 = elt.get('text_sha1')
 
203
        
 
204
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
205
 
 
206
        v = elt.get('text_size')
 
207
        self.text_size = v and int(v)
 
208
 
 
209
        return self
 
210
            
 
211
 
 
212
    from_element = classmethod(from_element)
 
213
 
 
214
    def __cmp__(self, other):
 
215
        if self is other:
 
216
            return 0
170
217
        if not isinstance(other, InventoryEntry):
171
218
            return NotImplemented
172
219
 
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')
 
220
        return cmp(self.file_id, other.file_id) \
 
221
               or cmp(self.name, other.name) \
 
222
               or cmp(self.text_sha1, other.text_sha1) \
 
223
               or cmp(self.text_size, other.text_size) \
 
224
               or cmp(self.text_id, other.text_id) \
 
225
               or cmp(self.parent_id, other.parent_id) \
 
226
               or cmp(self.kind, other.kind)
189
227
 
190
228
 
191
229
 
197
235
        self.parent_id = None
198
236
        self.name = ''
199
237
 
200
 
    def __eq__(self, other):
 
238
    def __cmp__(self, other):
 
239
        if self is other:
 
240
            return 0
201
241
        if not isinstance(other, RootEntry):
202
242
            return NotImplemented
203
 
        
204
 
        return (self.file_id == other.file_id) \
205
 
               and (self.children == other.children)
206
 
 
207
 
 
208
 
 
209
 
class Inventory(object):
 
243
        return cmp(self.file_id, other.file_id) \
 
244
               or cmp(self.children, other.children)
 
245
 
 
246
 
 
247
 
 
248
class Inventory(XMLMixin):
210
249
    """Inventory of versioned files in a tree.
211
250
 
212
251
    This describes which file_id is present at each point in the tree,
224
263
    inserted, other than through the Inventory API.
225
264
 
226
265
    >>> inv = Inventory()
 
266
    >>> inv.write_xml(sys.stdout)
 
267
    <inventory>
 
268
    </inventory>
227
269
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
228
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
229
270
    >>> inv['123-123'].name
230
271
    'hello.c'
231
272
 
240
281
 
241
282
    >>> [x[0] for x in inv.iter_entries()]
242
283
    ['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')
 
284
    
 
285
    >>> inv.write_xml(sys.stdout)
 
286
    <inventory>
 
287
    <entry file_id="123-123" kind="file" name="hello.c" />
 
288
    </inventory>
 
289
 
246
290
    """
247
 
    def __init__(self, root_id=ROOT_ID):
 
291
    def __init__(self):
248
292
        """Create or read an inventory.
249
293
 
250
294
        If a working directory is specified, the inventory is read
254
298
        The inventory is created with a default root directory, with
255
299
        an id of None.
256
300
        """
257
 
        # We are letting Branch.initialize() 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)
 
301
        self.root = RootEntry(ROOT_ID)
262
302
        self._byid = {self.root.file_id: self.root}
263
303
 
264
304
 
286
326
            if ie.kind == 'directory':
287
327
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
288
328
                    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.
 
329
                    
 
330
 
 
331
 
 
332
    def directories(self):
 
333
        """Return (path, entry) pairs for all directories.
295
334
        """
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))
 
335
        def descend(parent_ie):
 
336
            parent_name = parent_ie.name
 
337
            yield parent_name, parent_ie
 
338
 
 
339
            # directory children in sorted order
 
340
            dn = []
 
341
            for ie in parent_ie.children.itervalues():
303
342
                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))
 
343
                    dn.append((ie.name, ie))
 
344
            dn.sort()
316
345
            
317
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
318
 
            kids.sort()
 
346
            for name, child_ie in dn:
 
347
                for sub_name, sub_ie in descend(child_ie):
 
348
                    yield appendpath(parent_name, sub_name), sub_ie
319
349
 
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
 
350
        for name, ie in descend(self.root):
 
351
            yield name, ie
325
352
        
326
353
 
327
354
 
330
357
 
331
358
        >>> inv = Inventory()
332
359
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
333
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
334
360
        >>> '123' in inv
335
361
        True
336
362
        >>> '456' in inv
344
370
 
345
371
        >>> inv = Inventory()
346
372
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
347
 
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
348
373
        >>> inv['123123'].name
349
374
        'hello.c'
350
375
        """
368
393
        """Add entry to inventory.
369
394
 
370
395
        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
 
        """
 
396
        which calls this."""
375
397
        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
 
398
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
380
399
 
381
400
        try:
382
401
            parent = self._byid[entry.parent_id]
383
402
        except KeyError:
384
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
403
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
385
404
 
386
405
        if parent.children.has_key(entry.name):
387
 
            raise BzrError("%s is already versioned" %
 
406
            bailout("%s is already versioned" %
388
407
                    appendpath(self.id2path(parent.file_id), entry.name))
389
408
 
390
409
        self._byid[entry.file_id] = entry
391
410
        parent.children[entry.name] = entry
392
 
        return entry
393
411
 
394
412
 
395
413
    def add_path(self, relpath, kind, file_id=None):
396
414
        """Add entry from a path.
397
415
 
398
 
        The immediate parent must already be versioned.
399
 
 
400
 
        Returns the new entry object."""
401
 
        from bzrlib.branch import gen_file_id
402
 
        
 
416
        The immediate parent must already be versioned"""
403
417
        parts = bzrlib.osutils.splitpath(relpath)
404
418
        if len(parts) == 0:
405
 
            raise BzrError("cannot re-add root of inventory")
 
419
            bailout("cannot re-add root of inventory")
406
420
 
407
421
        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
 
 
 
422
            file_id = bzrlib.branch.gen_file_id(relpath)
 
423
 
 
424
        parent_id = self.path2id(parts[:-1])
 
425
        assert parent_id != None
415
426
        ie = InventoryEntry(file_id, parts[-1],
416
427
                            kind=kind, parent_id=parent_id)
417
428
        return self.add(ie)
422
433
 
423
434
        >>> inv = Inventory()
424
435
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
425
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
426
436
        >>> '123' in inv
427
437
        True
428
438
        >>> del inv['123']
444
454
        del self[ie.parent_id].children[ie.name]
445
455
 
446
456
 
447
 
    def __eq__(self, other):
 
457
    def id_set(self):
 
458
        return Set(self._byid)
 
459
 
 
460
 
 
461
    def to_element(self):
 
462
        """Convert to XML Element"""
 
463
        e = Element('inventory')
 
464
        e.text = '\n'
 
465
        for path, ie in self.iter_entries():
 
466
            e.append(ie.to_element())
 
467
        return e
 
468
    
 
469
 
 
470
    def from_element(cls, elt):
 
471
        """Construct from XML Element
 
472
 
 
473
        >>> inv = Inventory()
 
474
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
475
        >>> elt = inv.to_element()
 
476
        >>> inv2 = Inventory.from_element(elt)
 
477
        >>> inv2 == inv
 
478
        True
 
479
        """
 
480
        assert elt.tag == 'inventory'
 
481
        o = cls()
 
482
        for e in elt:
 
483
            o.add(InventoryEntry.from_element(e))
 
484
        return o
 
485
        
 
486
    from_element = classmethod(from_element)
 
487
 
 
488
 
 
489
    def __cmp__(self, other):
448
490
        """Compare two sets by comparing their contents.
449
491
 
450
492
        >>> i1 = Inventory()
452
494
        >>> i1 == i2
453
495
        True
454
496
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
455
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
456
497
        >>> i1 == i2
457
498
        False
458
499
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
459
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
460
500
        >>> i1 == i2
461
501
        True
462
502
        """
 
503
        if self is other:
 
504
            return 0
 
505
        
463
506
        if not isinstance(other, Inventory):
464
507
            return NotImplemented
465
508
 
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')
 
509
        if self.id_set() ^ other.id_set():
 
510
            return 1
 
511
 
 
512
        for file_id in self._byid:
 
513
            c = cmp(self[file_id], other[file_id])
 
514
            if c: return c
 
515
 
 
516
        return 0
479
517
 
480
518
 
481
519
    def get_idpath(self, file_id):
491
529
            try:
492
530
                ie = self._byid[file_id]
493
531
            except KeyError:
494
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
532
                bailout("file_id {%s} not found in inventory" % file_id)
495
533
            p.insert(0, ie.file_id)
496
534
            file_id = ie.parent_id
497
535
        return p
501
539
        """Return as a list the path to file_id."""
502
540
 
503
541
        # get all names, skipping root
504
 
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
542
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
505
543
        return os.sep.join(p)
506
544
            
507
545
 
551
589
 
552
590
        This does not move the working file."""
553
591
        if not is_valid_name(new_name):
554
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
592
            bailout("not an acceptable filename: %r" % new_name)
555
593
 
556
594
        new_parent = self._byid[new_parent_id]
557
595
        if new_name in new_parent.children:
558
 
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
596
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
559
597
 
560
598
        new_parent_idpath = self.get_idpath(new_parent_id)
561
599
        if file_id in new_parent_idpath:
562
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
600
            bailout("cannot move directory %r into a subdirectory of itself, %r"
563
601
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
564
602
 
565
603
        file_ie = self._byid[file_id]
576
614
 
577
615
 
578
616
 
579
 
_NAME_RE = None
 
617
_NAME_RE = re.compile(r'^[^/\\]+$')
580
618
 
581
619
def is_valid_name(name):
582
 
    global _NAME_RE
583
 
    if _NAME_RE == None:
584
 
        _NAME_RE = re.compile(r'^[^/\\]+$')
585
 
        
586
620
    return bool(_NAME_RE.match(name))