~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 13:46:36 UTC
  • Revision ID: mbp@sourcefrog.net-20050405134635-488e04a5092ce0faec0ff181
- New 'move' command; now separated out from rename

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
 
 
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
 
 
 
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>"
22
23
 
23
24
import sys, os.path, types, re
24
25
from sets import Set
29
30
    from elementtree.ElementTree import Element, ElementTree, SubElement
30
31
 
31
32
from xml import XMLMixin
32
 
from errors import bailout, BzrError, BzrCheckError
 
33
from errors import bailout
33
34
 
34
35
import bzrlib
35
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
58
59
 
59
60
    >>> i = Inventory()
60
61
    >>> i.path2id('')
61
 
    'TREE_ROOT'
62
 
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
63
 
    >>> 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'))
64
64
    >>> for j in i.iter_entries():
65
65
    ...   print j
66
66
    ... 
67
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
67
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
68
68
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
 
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
69
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='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', 'file', '123'))
74
 
    >>> 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'))
75
75
    >>> i.path2id('src/wibble')
76
76
    '2325'
77
77
    >>> '2325' in i
78
78
    True
79
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
79
    >>> i.add(InventoryEntry('2326', 'wibble.c', 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():
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
 
 
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):
 
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
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
112
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
119
113
        """
120
 
        if '/' in name or '\\' in name:
121
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
114
        
 
115
        if len(splitpath(name)) != 1:
 
116
            bailout('InventoryEntry name is not a simple filename: %r'
 
117
                    % name)
122
118
        
123
119
        self.file_id = file_id
124
120
        self.name = name
 
121
        assert kind in ['file', 'directory']
125
122
        self.kind = kind
126
123
        self.text_id = text_id
127
124
        self.parent_id = parent_id
 
125
        self.text_sha1 = None
 
126
        self.text_size = None
128
127
        if kind == 'directory':
129
128
            self.children = {}
130
 
        elif kind == 'file':
131
 
            pass
132
 
        else:
133
 
            raise BzrError("unhandled entry kind %r" % kind)
134
 
 
135
129
 
136
130
 
137
131
    def sorted_children(self):
142
136
 
143
137
    def copy(self):
144
138
        other = InventoryEntry(self.file_id, self.name, self.kind,
145
 
                               self.parent_id, text_id=self.text_id)
 
139
                               self.text_id, self.parent_id)
146
140
        other.text_sha1 = self.text_sha1
147
141
        other.text_size = self.text_size
148
 
        # note that children are *not* copied; they're pulled across when
149
 
        # others are added
150
142
        return other
151
143
 
152
144
 
167
159
        e.set('file_id', self.file_id)
168
160
        e.set('kind', self.kind)
169
161
 
170
 
        if self.text_size != None:
 
162
        if self.text_size is not None:
171
163
            e.set('text_size', '%d' % self.text_size)
172
164
            
173
 
        for f in ['text_id', 'text_sha1']:
 
165
        for f in ['text_id', 'text_sha1', 'parent_id']:
174
166
            v = getattr(self, f)
175
 
            if v != None:
 
167
            if v is not None:
176
168
                e.set(f, v)
177
169
 
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
170
        e.tail = '\n'
186
171
            
187
172
        return e
189
174
 
190
175
    def from_element(cls, elt):
191
176
        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)
 
177
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
201
178
        self.text_id = elt.get('text_id')
202
179
        self.text_sha1 = elt.get('text_sha1')
 
180
        self.parent_id = elt.get('parent_id')
203
181
        
204
182
        ## mutter("read inventoryentry: %r" % (elt.attrib))
205
183
 
248
226
class Inventory(XMLMixin):
249
227
    """Inventory of versioned files in a tree.
250
228
 
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.
 
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.
254
235
 
255
236
    The inventory represents a typical unix file tree, with
256
237
    directories containing files and subdirectories.  We never store
266
247
    >>> inv.write_xml(sys.stdout)
267
248
    <inventory>
268
249
    </inventory>
269
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
250
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
270
251
    >>> inv['123-123'].name
271
252
    'hello.c'
272
253
 
288
269
    </inventory>
289
270
 
290
271
    """
 
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
 
291
284
    def __init__(self):
292
285
        """Create or read an inventory.
293
286
 
298
291
        The inventory is created with a default root directory, with
299
292
        an id of None.
300
293
        """
301
 
        self.root = RootEntry(ROOT_ID)
302
 
        self._byid = {self.root.file_id: self.root}
 
294
        self.root = RootEntry(None)
 
295
        self._byid = {None: self.root}
303
296
 
304
297
 
305
298
    def __iter__(self):
325
318
            yield name, ie
326
319
            if ie.kind == 'directory':
327
320
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
328
 
                    yield os.path.join(name, cn), cie
 
321
                    yield '/'.join((name, cn)), cie
329
322
                    
330
323
 
331
324
 
332
 
    def directories(self):
 
325
    def directories(self, from_dir=None):
333
326
        """Return (path, entry) pairs for all directories.
334
327
        """
335
328
        def descend(parent_ie):
356
349
        """True if this entry contains a file with given id.
357
350
 
358
351
        >>> inv = Inventory()
359
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
352
        >>> inv.add(InventoryEntry('123', 'foo.c'))
360
353
        >>> '123' in inv
361
354
        True
362
355
        >>> '456' in inv
369
362
        """Return the entry for given file_id.
370
363
 
371
364
        >>> inv = Inventory()
372
 
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
365
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
373
366
        >>> inv['123123'].name
374
367
        'hello.c'
375
368
        """
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
 
369
        return self._byid[file_id]
 
370
 
387
371
 
388
372
    def get_child(self, parent_id, filename):
389
373
        return self[parent_id].children.get(filename)
400
384
        try:
401
385
            parent = self._byid[entry.parent_id]
402
386
        except KeyError:
403
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
387
            bailout("parent_id %r not in inventory" % entry.parent_id)
404
388
 
405
389
        if parent.children.has_key(entry.name):
406
390
            bailout("%s is already versioned" %
418
402
        if len(parts) == 0:
419
403
            bailout("cannot re-add root of inventory")
420
404
 
421
 
        if file_id == None:
 
405
        if file_id is None:
422
406
            file_id = bzrlib.branch.gen_file_id(relpath)
423
407
 
424
408
        parent_id = self.path2id(parts[:-1])
425
 
        assert parent_id != None
426
409
        ie = InventoryEntry(file_id, parts[-1],
427
410
                            kind=kind, parent_id=parent_id)
428
411
        return self.add(ie)
432
415
        """Remove entry by id.
433
416
 
434
417
        >>> inv = Inventory()
435
 
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
418
        >>> inv.add(InventoryEntry('123', 'foo.c'))
436
419
        >>> '123' in inv
437
420
        True
438
421
        >>> del inv['123']
471
454
        """Construct from XML Element
472
455
 
473
456
        >>> inv = Inventory()
474
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
457
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
475
458
        >>> elt = inv.to_element()
476
459
        >>> inv2 = Inventory.from_element(elt)
477
460
        >>> inv2 == inv
493
476
        >>> i2 = Inventory()
494
477
        >>> i1 == i2
495
478
        True
496
 
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
479
        >>> i1.add(InventoryEntry('123', 'foo'))
497
480
        >>> i1 == i2
498
481
        False
499
 
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
482
        >>> i2.add(InventoryEntry('123', 'foo'))
500
483
        >>> i1 == i2
501
484
        True
502
485
        """
522
505
        The list contains one element for each directory followed by
523
506
        the id of the file itself.  So the length of the returned list
524
507
        is equal to the depth of the file in the tree, counting the
525
 
        root directory as depth 1.
 
508
        root directory as depth 0.
526
509
        """
527
510
        p = []
528
511
        while file_id != None:
529
 
            try:
530
 
                ie = self._byid[file_id]
531
 
            except KeyError:
532
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
512
            ie = self._byid[file_id]
533
513
            p.insert(0, ie.file_id)
534
514
            file_id = ie.parent_id
535
515
        return p
537
517
 
538
518
    def id2path(self, file_id):
539
519
        """Return as a list the path to file_id."""
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)
 
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)
544
526
            
545
527
 
546
528
 
552
534
 
553
535
        This returns the entry of the last component in the path,
554
536
        which may be either a file or a directory.
555
 
 
556
 
        Returns None iff the path is not found.
557
537
        """
558
538
        if isinstance(name, types.StringTypes):
559
539
            name = splitpath(name)
560
540
 
561
 
        mutter("lookup path %r" % name)
562
 
 
563
 
        parent = self.root
 
541
        parent = self[None]
564
542
        for f in name:
565
543
            try:
566
544
                cie = parent.children[f]
567
545
                assert cie.name == f
568
 
                assert cie.parent_id == parent.file_id
569
546
                parent = cie
570
547
            except KeyError:
571
548
                # or raise an error?
618
595
 
619
596
def is_valid_name(name):
620
597
    return bool(_NAME_RE.match(name))
 
598
 
 
599
 
 
600
 
 
601
if __name__ == '__main__':
 
602
    import doctest, inventory
 
603
    doctest.testmod(inventory)