~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
 
from bzrlib.errors import NotVersionedError
31
 
        
32
40
 
33
 
class InventoryEntry(object):
 
41
class InventoryEntry(XMLMixin):
34
42
    """Description of a versioned file.
35
43
 
36
44
    An InventoryEntry has the following fields, which are also
55
63
    >>> i.path2id('')
56
64
    'TREE_ROOT'
57
65
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
58
 
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
59
66
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
60
 
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
61
67
    >>> for j in i.iter_entries():
62
68
    ...   print j
63
69
    ... 
66
72
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
67
73
    Traceback (most recent call last):
68
74
    ...
69
 
    BzrError: inventory already contains entry with id {2323}
 
75
    BzrError: ('inventory already contains entry with id {2323}', [])
70
76
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
71
 
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
72
77
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
 
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
74
78
    >>> i.path2id('src/wibble')
75
79
    '2325'
76
80
    >>> '2325' in i
77
81
    True
78
82
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
79
 
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
83
    >>> i['2326']
81
84
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
85
    >>> for j in i.iter_entries():
98
101
 
99
102
    # TODO: split InventoryEntry into subclasses for files,
100
103
    # directories, etc etc.
101
 
 
102
 
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
103
 
                 'text_id', 'parent_id', 'children',
104
 
                 'text_version', 'entry_version', ]
105
 
 
106
 
 
 
104
    
107
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
108
106
        """Create an InventoryEntry
109
107
        
117
115
        '123'
118
116
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
119
117
        Traceback (most recent call last):
120
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
118
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
121
119
        """
122
 
        assert isinstance(name, basestring), name
123
 
        if '/' in name or '\\' in name:
124
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
125
 
        
126
 
        self.text_version = None
127
 
        self.entry_version = None
128
 
        self.text_sha1 = None
129
 
        self.text_size = None
 
120
        
 
121
        if len(splitpath(name)) != 1:
 
122
            bailout('InventoryEntry name is not a simple filename: %r'
 
123
                    % name)
 
124
        
130
125
        self.file_id = file_id
131
126
        self.name = name
132
127
        self.kind = kind
133
128
        self.text_id = text_id
134
129
        self.parent_id = parent_id
 
130
        self.text_sha1 = None
 
131
        self.text_size = None
135
132
        if kind == 'directory':
136
133
            self.children = {}
137
134
        elif kind == 'file':
152
149
                               self.parent_id, text_id=self.text_id)
153
150
        other.text_sha1 = self.text_sha1
154
151
        other.text_size = self.text_size
155
 
        # note that children are *not* copied; they're pulled across when
156
 
        # others are added
157
152
        return other
158
153
 
159
154
 
166
161
                   self.parent_id))
167
162
 
168
163
    
169
 
    def __eq__(self, other):
 
164
    def to_element(self):
 
165
        """Convert to XML element"""
 
166
        e = Element('entry')
 
167
 
 
168
        e.set('name', self.name)
 
169
        e.set('file_id', self.file_id)
 
170
        e.set('kind', self.kind)
 
171
 
 
172
        if self.text_size != None:
 
173
            e.set('text_size', '%d' % self.text_size)
 
174
            
 
175
        for f in ['text_id', 'text_sha1']:
 
176
            v = getattr(self, f)
 
177
            if v != None:
 
178
                e.set(f, v)
 
179
 
 
180
        # to be conservative, we don't externalize the root pointers
 
181
        # for now, leaving them as null in the xml form.  in a future
 
182
        # version it will be implied by nested elements.
 
183
        if self.parent_id != ROOT_ID:
 
184
            assert isinstance(self.parent_id, basestring)
 
185
            e.set('parent_id', self.parent_id)
 
186
 
 
187
        e.tail = '\n'
 
188
            
 
189
        return e
 
190
 
 
191
 
 
192
    def from_element(cls, elt):
 
193
        assert elt.tag == 'entry'
 
194
 
 
195
        ## original format inventories don't have a parent_id for
 
196
        ## nodes in the root directory, but it's cleaner to use one
 
197
        ## internally.
 
198
        parent_id = elt.get('parent_id')
 
199
        if parent_id == None:
 
200
            parent_id = ROOT_ID
 
201
 
 
202
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
 
203
        self.text_id = elt.get('text_id')
 
204
        self.text_sha1 = elt.get('text_sha1')
 
205
        
 
206
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
207
 
 
208
        v = elt.get('text_size')
 
209
        self.text_size = v and int(v)
 
210
 
 
211
        return self
 
212
            
 
213
 
 
214
    from_element = classmethod(from_element)
 
215
 
 
216
    def __cmp__(self, other):
 
217
        if self is other:
 
218
            return 0
170
219
        if not isinstance(other, InventoryEntry):
171
220
            return NotImplemented
172
221
 
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')
 
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)
189
229
 
190
230
 
191
231
 
197
237
        self.parent_id = None
198
238
        self.name = ''
199
239
 
200
 
    def __eq__(self, other):
 
240
    def __cmp__(self, other):
 
241
        if self is other:
 
242
            return 0
201
243
        if not isinstance(other, RootEntry):
202
244
            return NotImplemented
203
 
        
204
 
        return (self.file_id == other.file_id) \
205
 
               and (self.children == other.children)
206
 
 
207
 
 
208
 
 
209
 
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):
210
251
    """Inventory of versioned files in a tree.
211
252
 
212
253
    This describes which file_id is present at each point in the tree,
224
265
    inserted, other than through the Inventory API.
225
266
 
226
267
    >>> inv = Inventory()
 
268
    >>> inv.write_xml(sys.stdout)
 
269
    <inventory>
 
270
    </inventory>
227
271
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
228
 
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
229
272
    >>> inv['123-123'].name
230
273
    'hello.c'
231
274
 
240
283
 
241
284
    >>> [x[0] for x in inv.iter_entries()]
242
285
    ['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')
 
286
    
 
287
    >>> inv.write_xml(sys.stdout)
 
288
    <inventory>
 
289
    <entry file_id="123-123" kind="file" name="hello.c" />
 
290
    </inventory>
 
291
 
246
292
    """
247
 
    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):
248
310
        """Create or read an inventory.
249
311
 
250
312
        If a working directory is specified, the inventory is read
254
316
        The inventory is created with a default root directory, with
255
317
        an id of None.
256
318
        """
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)
 
319
        self.root = RootEntry(ROOT_ID)
262
320
        self._byid = {self.root.file_id: self.root}
263
321
 
264
322
 
286
344
            if ie.kind == 'directory':
287
345
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
288
346
                    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.
 
347
                    
 
348
 
 
349
 
 
350
    def directories(self):
 
351
        """Return (path, entry) pairs for all directories.
295
352
        """
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))
 
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():
303
360
                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))
 
361
                    dn.append((ie.name, ie))
 
362
            dn.sort()
316
363
            
317
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
318
 
            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
319
367
 
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
 
368
        for name, ie in descend(self.root):
 
369
            yield name, ie
325
370
        
326
371
 
327
372
 
330
375
 
331
376
        >>> inv = Inventory()
332
377
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
333
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
334
378
        >>> '123' in inv
335
379
        True
336
380
        >>> '456' in inv
344
388
 
345
389
        >>> inv = Inventory()
346
390
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
347
 
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
348
391
        >>> inv['123123'].name
349
392
        'hello.c'
350
393
        """
 
394
        if file_id == None:
 
395
            raise BzrError("can't look up file_id None")
 
396
            
351
397
        try:
352
398
            return self._byid[file_id]
353
399
        except KeyError:
354
 
            if file_id == None:
355
 
                raise BzrError("can't look up file_id None")
356
 
            else:
357
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
358
 
 
359
 
 
360
 
    def get_file_kind(self, file_id):
361
 
        return self._byid[file_id].kind
 
400
            raise BzrError("file_id {%s} not in inventory" % file_id)
 
401
 
362
402
 
363
403
    def get_child(self, parent_id, filename):
364
404
        return self[parent_id].children.get(filename)
368
408
        """Add entry to inventory.
369
409
 
370
410
        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
 
        """
 
411
        which calls this."""
375
412
        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
 
413
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
380
414
 
381
415
        try:
382
416
            parent = self._byid[entry.parent_id]
383
417
        except KeyError:
384
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
418
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
385
419
 
386
420
        if parent.children.has_key(entry.name):
387
 
            raise BzrError("%s is already versioned" %
 
421
            bailout("%s is already versioned" %
388
422
                    appendpath(self.id2path(parent.file_id), entry.name))
389
423
 
390
424
        self._byid[entry.file_id] = entry
391
425
        parent.children[entry.name] = entry
392
 
        return entry
393
426
 
394
427
 
395
428
    def add_path(self, relpath, kind, file_id=None):
396
429
        """Add entry from a path.
397
430
 
398
 
        The immediate parent must already be versioned.
399
 
 
400
 
        Returns the new entry object."""
401
 
        from bzrlib.branch import gen_file_id
402
 
        
 
431
        The immediate parent must already be versioned"""
403
432
        parts = bzrlib.osutils.splitpath(relpath)
404
433
        if len(parts) == 0:
405
 
            raise BzrError("cannot re-add root of inventory")
 
434
            bailout("cannot re-add root of inventory")
406
435
 
407
436
        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
 
 
 
437
            file_id = bzrlib.branch.gen_file_id(relpath)
 
438
 
 
439
        parent_id = self.path2id(parts[:-1])
 
440
        assert parent_id != None
415
441
        ie = InventoryEntry(file_id, parts[-1],
416
442
                            kind=kind, parent_id=parent_id)
417
443
        return self.add(ie)
422
448
 
423
449
        >>> inv = Inventory()
424
450
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
425
 
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
426
451
        >>> '123' in inv
427
452
        True
428
453
        >>> del inv['123']
444
469
        del self[ie.parent_id].children[ie.name]
445
470
 
446
471
 
447
 
    def __eq__(self, other):
 
472
    def id_set(self):
 
473
        return Set(self._byid)
 
474
 
 
475
 
 
476
    def to_element(self):
 
477
        """Convert to XML Element"""
 
478
        e = Element('inventory')
 
479
        e.text = '\n'
 
480
        for path, ie in self.iter_entries():
 
481
            e.append(ie.to_element())
 
482
        return e
 
483
    
 
484
 
 
485
    def from_element(cls, elt):
 
486
        """Construct from XML Element
 
487
 
 
488
        >>> inv = Inventory()
 
489
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
490
        >>> elt = inv.to_element()
 
491
        >>> inv2 = Inventory.from_element(elt)
 
492
        >>> inv2 == inv
 
493
        True
 
494
        """
 
495
        assert elt.tag == 'inventory'
 
496
        o = cls()
 
497
        for e in elt:
 
498
            o.add(InventoryEntry.from_element(e))
 
499
        return o
 
500
        
 
501
    from_element = classmethod(from_element)
 
502
 
 
503
 
 
504
    def __cmp__(self, other):
448
505
        """Compare two sets by comparing their contents.
449
506
 
450
507
        >>> i1 = Inventory()
452
509
        >>> i1 == i2
453
510
        True
454
511
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
455
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
456
512
        >>> i1 == i2
457
513
        False
458
514
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
459
 
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
460
515
        >>> i1 == i2
461
516
        True
462
517
        """
 
518
        if self is other:
 
519
            return 0
 
520
        
463
521
        if not isinstance(other, Inventory):
464
522
            return NotImplemented
465
523
 
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')
 
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
479
532
 
480
533
 
481
534
    def get_idpath(self, file_id):
491
544
            try:
492
545
                ie = self._byid[file_id]
493
546
            except KeyError:
494
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
547
                bailout("file_id {%s} not found in inventory" % file_id)
495
548
            p.insert(0, ie.file_id)
496
549
            file_id = ie.parent_id
497
550
        return p
501
554
        """Return as a list the path to file_id."""
502
555
 
503
556
        # get all names, skipping root
504
 
        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:]]
505
558
        return os.sep.join(p)
506
559
            
507
560
 
551
604
 
552
605
        This does not move the working file."""
553
606
        if not is_valid_name(new_name):
554
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
607
            bailout("not an acceptable filename: %r" % new_name)
555
608
 
556
609
        new_parent = self._byid[new_parent_id]
557
610
        if new_name in new_parent.children:
558
 
            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)))
559
612
 
560
613
        new_parent_idpath = self.get_idpath(new_parent_id)
561
614
        if file_id in new_parent_idpath:
562
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
615
            bailout("cannot move directory %r into a subdirectory of itself, %r"
563
616
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
564
617
 
565
618
        file_ie = self._byid[file_id]
576
629
 
577
630
 
578
631
 
579
 
_NAME_RE = None
 
632
_NAME_RE = re.compile(r'^[^/\\]+$')
580
633
 
581
634
def is_valid_name(name):
582
 
    global _NAME_RE
583
 
    if _NAME_RE == None:
584
 
        _NAME_RE = re.compile(r'^[^/\\]+$')
585
 
        
586
635
    return bool(_NAME_RE.match(name))