~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-05-17 07:01:47 UTC
  • Revision ID: mbp@sourcefrog.net-20050517070147-c38da17418ea6711
- Add patch to give symlink support

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."""
18
 
 
19
 
# TODO: Maybe store inventory_id in the file?  Not really needed.
20
 
 
21
 
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
 
__author__ = "Martin Pool <mbp@canonical.com>"
 
17
 
 
18
# This should really be an id randomly assigned when the tree is
 
19
# created, but it's not for now.
 
20
ROOT_ID = "TREE_ROOT"
 
21
 
23
22
 
24
23
import sys, os.path, types, re
25
24
from sets import Set
30
29
    from elementtree.ElementTree import Element, ElementTree, SubElement
31
30
 
32
31
from xml import XMLMixin
33
 
from errors import bailout
 
32
from errors import bailout, BzrError, BzrCheckError
34
33
 
35
34
import bzrlib
36
35
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
59
58
 
60
59
    >>> i = Inventory()
61
60
    >>> i.path2id('')
62
 
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
 
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
 
61
    'TREE_ROOT'
 
62
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
63
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
64
64
    >>> for j in i.iter_entries():
65
65
    ...   print j
66
66
    ... 
67
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
 
67
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
68
68
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
 
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
 
69
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
70
    Traceback (most recent call last):
71
71
    ...
72
72
    BzrError: ('inventory already contains entry with id {2323}', [])
73
 
    >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
74
 
    >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
 
73
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
74
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
75
75
    >>> i.path2id('src/wibble')
76
76
    '2325'
77
77
    >>> '2325' in i
78
78
    True
79
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
79
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
80
80
    >>> i['2326']
81
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
82
    >>> for j in i.iter_entries():
91
91
    >>> i.id2path('2326')
92
92
    'src/wibble/wibble.c'
93
93
 
94
 
    :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?
95
95
           But those depend on its position within a particular inventory, and
96
96
           it would be nice not to need to hold the backpointer here.
97
97
    """
98
 
    def __init__(self, file_id, name, kind='file', text_id=None,
99
 
                 parent_id=None):
 
98
 
 
99
    # TODO: split InventoryEntry into subclasses for files,
 
100
    # directories, etc etc.
 
101
 
 
102
    text_sha1 = None
 
103
    text_size = None
 
104
    
 
105
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
100
106
        """Create an InventoryEntry
101
107
        
102
108
        The filename must be a single component, relative to the
103
109
        parent directory; it cannot be a whole path or relative name.
104
110
 
105
 
        >>> e = InventoryEntry('123', 'hello.c')
 
111
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
106
112
        >>> e.name
107
113
        'hello.c'
108
114
        >>> e.file_id
109
115
        '123'
110
 
        >>> e = InventoryEntry('123', 'src/hello.c')
 
116
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
111
117
        Traceback (most recent call last):
112
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
118
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
113
119
        """
114
 
        
115
 
        if len(splitpath(name)) != 1:
116
 
            bailout('InventoryEntry name is not a simple filename: %r'
117
 
                    % name)
 
120
        if '/' in name or '\\' in name:
 
121
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
118
122
        
119
123
        self.file_id = file_id
120
124
        self.name = name
121
 
        assert kind in ['file', 'directory']
122
125
        self.kind = kind
123
126
        self.text_id = text_id
124
127
        self.parent_id = parent_id
125
 
        self.text_sha1 = None
126
 
        self.text_size = None
127
128
        if kind == 'directory':
128
129
            self.children = {}
 
130
        elif kind == 'file':
 
131
            pass
 
132
        else:
 
133
            raise BzrError("unhandled entry kind %r" % kind)
 
134
 
129
135
 
130
136
 
131
137
    def sorted_children(self):
136
142
 
137
143
    def copy(self):
138
144
        other = InventoryEntry(self.file_id, self.name, self.kind,
139
 
                               self.text_id, self.parent_id)
 
145
                               self.parent_id, text_id=self.text_id)
140
146
        other.text_sha1 = self.text_sha1
141
147
        other.text_size = self.text_size
 
148
        # note that children are *not* copied; they're pulled across when
 
149
        # others are added
142
150
        return other
143
151
 
144
152
 
159
167
        e.set('file_id', self.file_id)
160
168
        e.set('kind', self.kind)
161
169
 
162
 
        if self.text_size is not None:
 
170
        if self.text_size != None:
163
171
            e.set('text_size', '%d' % self.text_size)
164
172
            
165
 
        for f in ['text_id', 'text_sha1', 'parent_id']:
 
173
        for f in ['text_id', 'text_sha1']:
166
174
            v = getattr(self, f)
167
 
            if v is not None:
 
175
            if v != None:
168
176
                e.set(f, v)
169
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
 
170
185
        e.tail = '\n'
171
186
            
172
187
        return e
174
189
 
175
190
    def from_element(cls, elt):
176
191
        assert elt.tag == 'entry'
177
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
 
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)
178
201
        self.text_id = elt.get('text_id')
179
202
        self.text_sha1 = elt.get('text_sha1')
180
 
        self.parent_id = elt.get('parent_id')
181
203
        
182
204
        ## mutter("read inventoryentry: %r" % (elt.attrib))
183
205
 
226
248
class Inventory(XMLMixin):
227
249
    """Inventory of versioned files in a tree.
228
250
 
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.
 
251
    This describes which file_id is present at each point in the tree,
 
252
    and possibly the SHA-1 or other information about the file.
 
253
    Entries can be looked up either by path or by file_id.
235
254
 
236
255
    The inventory represents a typical unix file tree, with
237
256
    directories containing files and subdirectories.  We never store
247
266
    >>> inv.write_xml(sys.stdout)
248
267
    <inventory>
249
268
    </inventory>
250
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
269
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
251
270
    >>> inv['123-123'].name
252
271
    'hello.c'
253
272
 
269
288
    </inventory>
270
289
 
271
290
    """
272
 
 
273
 
    ## TODO: Make sure only canonical filenames are stored.
274
 
 
275
 
    ## TODO: Do something sensible about the possible collisions on
276
 
    ## case-losing filesystems.  Perhaps we should just always forbid
277
 
    ## such collisions.
278
 
 
279
 
    ## TODO: No special cases for root, rather just give it a file id
280
 
    ## like everything else.
281
 
 
282
 
    ## TODO: Probably change XML serialization to use nesting
283
 
 
284
291
    def __init__(self):
285
292
        """Create or read an inventory.
286
293
 
291
298
        The inventory is created with a default root directory, with
292
299
        an id of None.
293
300
        """
294
 
        self.root = RootEntry(None)
295
 
        self._byid = {None: self.root}
 
301
        self.root = RootEntry(ROOT_ID)
 
302
        self._byid = {self.root.file_id: self.root}
296
303
 
297
304
 
298
305
    def __iter__(self):
318
325
            yield name, ie
319
326
            if ie.kind == 'directory':
320
327
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
321
 
                    yield '/'.join((name, cn)), cie
 
328
                    yield os.path.join(name, cn), cie
322
329
                    
323
330
 
324
331
 
325
 
    def directories(self, from_dir=None):
 
332
    def directories(self):
326
333
        """Return (path, entry) pairs for all directories.
327
334
        """
328
335
        def descend(parent_ie):
349
356
        """True if this entry contains a file with given id.
350
357
 
351
358
        >>> inv = Inventory()
352
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
359
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
353
360
        >>> '123' in inv
354
361
        True
355
362
        >>> '456' in inv
362
369
        """Return the entry for given file_id.
363
370
 
364
371
        >>> inv = Inventory()
365
 
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
372
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
366
373
        >>> inv['123123'].name
367
374
        'hello.c'
368
375
        """
369
 
        return self._byid[file_id]
370
 
 
 
376
        try:
 
377
            return self._byid[file_id]
 
378
        except KeyError:
 
379
            if file_id == None:
 
380
                raise BzrError("can't look up file_id None")
 
381
            else:
 
382
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
383
 
 
384
 
 
385
    def get_file_kind(self, file_id):
 
386
        return self._byid[file_id].kind
371
387
 
372
388
    def get_child(self, parent_id, filename):
373
389
        return self[parent_id].children.get(filename)
384
400
        try:
385
401
            parent = self._byid[entry.parent_id]
386
402
        except KeyError:
387
 
            bailout("parent_id %r not in inventory" % entry.parent_id)
 
403
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
388
404
 
389
405
        if parent.children.has_key(entry.name):
390
406
            bailout("%s is already versioned" %
402
418
        if len(parts) == 0:
403
419
            bailout("cannot re-add root of inventory")
404
420
 
405
 
        if file_id is None:
 
421
        if file_id == None:
406
422
            file_id = bzrlib.branch.gen_file_id(relpath)
407
423
 
408
424
        parent_id = self.path2id(parts[:-1])
 
425
        assert parent_id != None
409
426
        ie = InventoryEntry(file_id, parts[-1],
410
427
                            kind=kind, parent_id=parent_id)
411
428
        return self.add(ie)
415
432
        """Remove entry by id.
416
433
 
417
434
        >>> inv = Inventory()
418
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
435
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
419
436
        >>> '123' in inv
420
437
        True
421
438
        >>> del inv['123']
454
471
        """Construct from XML Element
455
472
 
456
473
        >>> inv = Inventory()
457
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
 
474
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
458
475
        >>> elt = inv.to_element()
459
476
        >>> inv2 = Inventory.from_element(elt)
460
477
        >>> inv2 == inv
476
493
        >>> i2 = Inventory()
477
494
        >>> i1 == i2
478
495
        True
479
 
        >>> i1.add(InventoryEntry('123', 'foo'))
 
496
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
480
497
        >>> i1 == i2
481
498
        False
482
 
        >>> i2.add(InventoryEntry('123', 'foo'))
 
499
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
483
500
        >>> i1 == i2
484
501
        True
485
502
        """
505
522
        The list contains one element for each directory followed by
506
523
        the id of the file itself.  So the length of the returned list
507
524
        is equal to the depth of the file in the tree, counting the
508
 
        root directory as depth 0.
 
525
        root directory as depth 1.
509
526
        """
510
527
        p = []
511
528
        while file_id != None:
512
 
            ie = self._byid[file_id]
 
529
            try:
 
530
                ie = self._byid[file_id]
 
531
            except KeyError:
 
532
                bailout("file_id {%s} not found in inventory" % file_id)
513
533
            p.insert(0, ie.file_id)
514
534
            file_id = ie.parent_id
515
535
        return p
517
537
 
518
538
    def id2path(self, file_id):
519
539
        """Return as a list the path to file_id."""
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)
 
540
 
 
541
        # get all names, skipping root
 
542
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
 
543
        return os.sep.join(p)
526
544
            
527
545
 
528
546
 
534
552
 
535
553
        This returns the entry of the last component in the path,
536
554
        which may be either a file or a directory.
 
555
 
 
556
        Returns None iff the path is not found.
537
557
        """
538
558
        if isinstance(name, types.StringTypes):
539
559
            name = splitpath(name)
540
560
 
541
 
        parent = self[None]
 
561
        mutter("lookup path %r" % name)
 
562
 
 
563
        parent = self.root
542
564
        for f in name:
543
565
            try:
544
566
                cie = parent.children[f]
545
567
                assert cie.name == f
 
568
                assert cie.parent_id == parent.file_id
546
569
                parent = cie
547
570
            except KeyError:
548
571
                # or raise an error?
595
618
 
596
619
def is_valid_name(name):
597
620
    return bool(_NAME_RE.match(name))
598
 
 
599
 
 
600
 
 
601
 
if __name__ == '__main__':
602
 
    import doctest, inventory
603
 
    doctest.testmod(inventory)