~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-05 09:05:32 UTC
  • Revision ID: mbp@sourcefrog.net-20050405090532-af541f6893fd6b75
- clearer check against attempts to introduce directory loops in   the inventory

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
"""Inventories map files to their name in a revision."""
17
18
 
18
19
# TODO: Maybe store inventory_id in the file?  Not really needed.
19
20
 
20
 
 
21
 
# This should really be an id randomly assigned when the tree is
22
 
# created, but it's not for now.
23
 
ROOT_ID = "TREE_ROOT"
24
 
 
 
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
 
22
__author__ = "Martin Pool <mbp@canonical.com>"
25
23
 
26
24
import sys, os.path, types, re
27
25
from sets import Set
32
30
    from elementtree.ElementTree import Element, ElementTree, SubElement
33
31
 
34
32
from xml import XMLMixin
35
 
from errors import bailout, BzrError
 
33
from errors import bailout
36
34
 
37
35
import bzrlib
38
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
61
59
 
62
60
    >>> i = Inventory()
63
61
    >>> i.path2id('')
64
 
    'TREE_ROOT'
65
 
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
66
 
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
62
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
 
63
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
67
64
    >>> for j in i.iter_entries():
68
65
    ...   print j
69
66
    ... 
70
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
67
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
71
68
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
72
 
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
69
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
73
70
    Traceback (most recent call last):
74
71
    ...
75
72
    BzrError: ('inventory already contains entry with id {2323}', [])
76
 
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
77
 
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
73
    >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
 
74
    >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
78
75
    >>> i.path2id('src/wibble')
79
76
    '2325'
80
77
    >>> '2325' in i
81
78
    True
82
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
79
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
83
80
    >>> i['2326']
84
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
85
82
    >>> for j in i.iter_entries():
94
91
    >>> i.id2path('2326')
95
92
    'src/wibble/wibble.c'
96
93
 
97
 
    TODO: Maybe also keep the full path of the entry, and the children?
 
94
    :todo: Maybe also keep the full path of the entry, and the children?
98
95
           But those depend on its position within a particular inventory, and
99
96
           it would be nice not to need to hold the backpointer here.
100
97
    """
101
 
 
102
 
    # TODO: split InventoryEntry into subclasses for files,
103
 
    # directories, etc etc.
104
 
    
105
 
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
98
    def __init__(self, file_id, name, kind='file', text_id=None,
 
99
                 parent_id=None):
106
100
        """Create an InventoryEntry
107
101
        
108
102
        The filename must be a single component, relative to the
109
103
        parent directory; it cannot be a whole path or relative name.
110
104
 
111
 
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
105
        >>> e = InventoryEntry('123', 'hello.c')
112
106
        >>> e.name
113
107
        'hello.c'
114
108
        >>> e.file_id
115
109
        '123'
116
 
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
110
        >>> e = InventoryEntry('123', 'src/hello.c')
117
111
        Traceback (most recent call last):
118
112
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
119
113
        """
124
118
        
125
119
        self.file_id = file_id
126
120
        self.name = name
 
121
        assert kind in ['file', 'directory']
127
122
        self.kind = kind
128
123
        self.text_id = text_id
129
124
        self.parent_id = parent_id
131
126
        self.text_size = None
132
127
        if kind == 'directory':
133
128
            self.children = {}
134
 
        elif kind == 'file':
135
 
            pass
136
 
        else:
137
 
            raise BzrError("unhandled entry kind %r" % kind)
138
 
 
139
129
 
140
130
 
141
131
    def sorted_children(self):
146
136
 
147
137
    def copy(self):
148
138
        other = InventoryEntry(self.file_id, self.name, self.kind,
149
 
                               self.parent_id, text_id=self.text_id)
 
139
                               self.text_id, self.parent_id)
150
140
        other.text_sha1 = self.text_sha1
151
141
        other.text_size = self.text_size
152
142
        return other
169
159
        e.set('file_id', self.file_id)
170
160
        e.set('kind', self.kind)
171
161
 
172
 
        if self.text_size != None:
 
162
        if self.text_size is not None:
173
163
            e.set('text_size', '%d' % self.text_size)
174
164
            
175
 
        for f in ['text_id', 'text_sha1']:
 
165
        for f in ['text_id', 'text_sha1', 'parent_id']:
176
166
            v = getattr(self, f)
177
 
            if v != None:
 
167
            if v is not None:
178
168
                e.set(f, v)
179
169
 
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
170
        e.tail = '\n'
188
171
            
189
172
        return e
191
174
 
192
175
    def from_element(cls, elt):
193
176
        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)
 
177
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
203
178
        self.text_id = elt.get('text_id')
204
179
        self.text_sha1 = elt.get('text_sha1')
 
180
        self.parent_id = elt.get('parent_id')
205
181
        
206
182
        ## mutter("read inventoryentry: %r" % (elt.attrib))
207
183
 
250
226
class Inventory(XMLMixin):
251
227
    """Inventory of versioned files in a tree.
252
228
 
253
 
    This describes which file_id is present at each point in the tree,
254
 
    and possibly the SHA-1 or other information about the file.
255
 
    Entries can be looked up either by path or by file_id.
 
229
    An Inventory acts like a set of InventoryEntry items.  You can
 
230
    also look files up by their file_id or name.
 
231
    
 
232
    May be read from and written to a metadata file in a tree.  To
 
233
    manipulate the inventory (for example to add a file), it is read
 
234
    in, modified, and then written back out.
256
235
 
257
236
    The inventory represents a typical unix file tree, with
258
237
    directories containing files and subdirectories.  We never store
268
247
    >>> inv.write_xml(sys.stdout)
269
248
    <inventory>
270
249
    </inventory>
271
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
250
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
272
251
    >>> inv['123-123'].name
273
252
    'hello.c'
274
253
 
300
279
    ## TODO: No special cases for root, rather just give it a file id
301
280
    ## like everything else.
302
281
 
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?
 
282
    ## TODO: Probably change XML serialization to use nesting
308
283
 
309
284
    def __init__(self):
310
285
        """Create or read an inventory.
316
291
        The inventory is created with a default root directory, with
317
292
        an id of None.
318
293
        """
319
 
        self.root = RootEntry(ROOT_ID)
320
 
        self._byid = {self.root.file_id: self.root}
 
294
        self.root = RootEntry(None)
 
295
        self._byid = {None: self.root}
321
296
 
322
297
 
323
298
    def __iter__(self):
343
318
            yield name, ie
344
319
            if ie.kind == 'directory':
345
320
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
 
                    yield os.path.join(name, cn), cie
 
321
                    yield '/'.join((name, cn)), cie
347
322
                    
348
323
 
349
324
 
350
 
    def directories(self):
 
325
    def directories(self, from_dir=None):
351
326
        """Return (path, entry) pairs for all directories.
352
327
        """
353
328
        def descend(parent_ie):
374
349
        """True if this entry contains a file with given id.
375
350
 
376
351
        >>> inv = Inventory()
377
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
352
        >>> inv.add(InventoryEntry('123', 'foo.c'))
378
353
        >>> '123' in inv
379
354
        True
380
355
        >>> '456' in inv
387
362
        """Return the entry for given file_id.
388
363
 
389
364
        >>> inv = Inventory()
390
 
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
365
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
391
366
        >>> inv['123123'].name
392
367
        'hello.c'
393
368
        """
394
 
        if file_id == None:
395
 
            raise BzrError("can't look up file_id None")
396
 
            
397
 
        try:
398
 
            return self._byid[file_id]
399
 
        except KeyError:
400
 
            raise BzrError("file_id {%s} not in inventory" % file_id)
 
369
        return self._byid[file_id]
401
370
 
402
371
 
403
372
    def get_child(self, parent_id, filename):
415
384
        try:
416
385
            parent = self._byid[entry.parent_id]
417
386
        except KeyError:
418
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
387
            bailout("parent_id %r not in inventory" % entry.parent_id)
419
388
 
420
389
        if parent.children.has_key(entry.name):
421
390
            bailout("%s is already versioned" %
433
402
        if len(parts) == 0:
434
403
            bailout("cannot re-add root of inventory")
435
404
 
436
 
        if file_id == None:
 
405
        if file_id is None:
437
406
            file_id = bzrlib.branch.gen_file_id(relpath)
438
407
 
439
408
        parent_id = self.path2id(parts[:-1])
440
 
        assert parent_id != None
441
409
        ie = InventoryEntry(file_id, parts[-1],
442
410
                            kind=kind, parent_id=parent_id)
443
411
        return self.add(ie)
447
415
        """Remove entry by id.
448
416
 
449
417
        >>> inv = Inventory()
450
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
418
        >>> inv.add(InventoryEntry('123', 'foo.c'))
451
419
        >>> '123' in inv
452
420
        True
453
421
        >>> del inv['123']
486
454
        """Construct from XML Element
487
455
 
488
456
        >>> inv = Inventory()
489
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
457
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
490
458
        >>> elt = inv.to_element()
491
459
        >>> inv2 = Inventory.from_element(elt)
492
460
        >>> inv2 == inv
508
476
        >>> i2 = Inventory()
509
477
        >>> i1 == i2
510
478
        True
511
 
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
479
        >>> i1.add(InventoryEntry('123', 'foo'))
512
480
        >>> i1 == i2
513
481
        False
514
 
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
482
        >>> i2.add(InventoryEntry('123', 'foo'))
515
483
        >>> i1 == i2
516
484
        True
517
485
        """
537
505
        The list contains one element for each directory followed by
538
506
        the id of the file itself.  So the length of the returned list
539
507
        is equal to the depth of the file in the tree, counting the
540
 
        root directory as depth 1.
 
508
        root directory as depth 0.
541
509
        """
542
510
        p = []
543
511
        while file_id != None:
544
 
            try:
545
 
                ie = self._byid[file_id]
546
 
            except KeyError:
547
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
512
            ie = self._byid[file_id]
548
513
            p.insert(0, ie.file_id)
549
514
            file_id = ie.parent_id
550
515
        return p
552
517
 
553
518
    def id2path(self, file_id):
554
519
        """Return as a list the path to file_id."""
555
 
 
556
 
        # get all names, skipping root
557
 
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
558
 
        return os.sep.join(p)
 
520
        p = []
 
521
        while file_id != None:
 
522
            ie = self._byid[file_id]
 
523
            p.insert(0, ie.name)
 
524
            file_id = ie.parent_id
 
525
        return '/'.join(p)
559
526
            
560
527
 
561
528
 
567
534
 
568
535
        This returns the entry of the last component in the path,
569
536
        which may be either a file or a directory.
570
 
 
571
 
        Returns None iff the path is not found.
572
537
        """
573
538
        if isinstance(name, types.StringTypes):
574
539
            name = splitpath(name)
575
540
 
576
 
        mutter("lookup path %r" % name)
577
 
 
578
 
        parent = self.root
 
541
        parent = self[None]
579
542
        for f in name:
580
543
            try:
581
544
                cie = parent.children[f]
582
545
                assert cie.name == f
583
 
                assert cie.parent_id == parent.file_id
584
546
                parent = cie
585
547
            except KeyError:
586
548
                # or raise an error?
633
595
 
634
596
def is_valid_name(name):
635
597
    return bool(_NAME_RE.match(name))
 
598
 
 
599
 
 
600
 
 
601
if __name__ == '__main__':
 
602
    import doctest, inventory
 
603
    doctest.testmod(inventory)