~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-08 22:14:09 UTC
  • Revision ID: mbp@sourcefrog.net-20050408221409-a99bd4796a56f42edbf3f13a
selected-file diff

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
__author__ = "Martin Pool <mbp@canonical.com>"
 
22
 
17
23
 
18
24
# This should really be an id randomly assigned when the tree is
19
25
# created, but it's not for now.
21
27
 
22
28
 
23
29
import sys, os.path, types, re
 
30
from sets import Set
 
31
 
 
32
try:
 
33
    from cElementTree import Element, ElementTree, SubElement
 
34
except ImportError:
 
35
    from elementtree.ElementTree import Element, ElementTree, SubElement
 
36
 
 
37
from xml import XMLMixin
 
38
from errors import bailout, BzrError
24
39
 
25
40
import bzrlib
26
 
from bzrlib.errors import BzrError, BzrCheckError
27
 
 
28
41
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
29
42
from bzrlib.trace import mutter
30
43
 
31
 
class InventoryEntry(object):
 
44
class InventoryEntry(XMLMixin):
32
45
    """Description of a versioned file.
33
46
 
34
47
    An InventoryEntry has the following fields, which are also
62
75
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
63
76
    Traceback (most recent call last):
64
77
    ...
65
 
    BzrError: inventory already contains entry with id {2323}
 
78
    BzrError: ('inventory already contains entry with id {2323}', [])
66
79
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
67
80
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
68
81
    >>> i.path2id('src/wibble')
84
97
    >>> i.id2path('2326')
85
98
    'src/wibble/wibble.c'
86
99
 
87
 
    TODO: Maybe also keep the full path of the entry, and the children?
 
100
    :todo: Maybe also keep the full path of the entry, and the children?
88
101
           But those depend on its position within a particular inventory, and
89
102
           it would be nice not to need to hold the backpointer here.
90
103
    """
91
104
 
92
105
    # TODO: split InventoryEntry into subclasses for files,
93
106
    # directories, etc etc.
94
 
 
95
 
    text_sha1 = None
96
 
    text_size = None
97
107
    
98
108
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
99
109
        """Create an InventoryEntry
108
118
        '123'
109
119
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
110
120
        Traceback (most recent call last):
111
 
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
121
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
112
122
        """
113
 
        if '/' in name or '\\' in name:
114
 
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
123
        
 
124
        if len(splitpath(name)) != 1:
 
125
            bailout('InventoryEntry name is not a simple filename: %r'
 
126
                    % name)
115
127
        
116
128
        self.file_id = file_id
117
129
        self.name = name
 
130
        assert kind in ['file', 'directory']
118
131
        self.kind = kind
119
132
        self.text_id = text_id
120
133
        self.parent_id = parent_id
 
134
        self.text_sha1 = None
 
135
        self.text_size = None
121
136
        if kind == 'directory':
122
137
            self.children = {}
123
 
        elif kind == 'file':
124
 
            pass
125
138
        else:
126
 
            raise BzrError("unhandled entry kind %r" % kind)
127
 
 
 
139
            assert kind == 'file'
128
140
 
129
141
 
130
142
    def sorted_children(self):
138
150
                               self.parent_id, text_id=self.text_id)
139
151
        other.text_sha1 = self.text_sha1
140
152
        other.text_size = self.text_size
141
 
        # note that children are *not* copied; they're pulled across when
142
 
        # others are added
143
153
        return other
144
154
 
145
155
 
154
164
    
155
165
    def to_element(self):
156
166
        """Convert to XML element"""
157
 
        from bzrlib.xml import Element
158
 
        
159
167
        e = Element('entry')
160
168
 
161
169
        e.set('name', self.name)
206
214
 
207
215
    from_element = classmethod(from_element)
208
216
 
209
 
    def __eq__(self, other):
 
217
    def __cmp__(self, other):
 
218
        if self is other:
 
219
            return 0
210
220
        if not isinstance(other, InventoryEntry):
211
221
            return NotImplemented
212
222
 
213
 
        return (self.file_id == other.file_id) \
214
 
               and (self.name == other.name) \
215
 
               and (self.text_sha1 == other.text_sha1) \
216
 
               and (self.text_size == other.text_size) \
217
 
               and (self.text_id == other.text_id) \
218
 
               and (self.parent_id == other.parent_id) \
219
 
               and (self.kind == other.kind)
220
 
 
221
 
 
222
 
    def __ne__(self, other):
223
 
        return not (self == other)
224
 
 
225
 
    def __hash__(self):
226
 
        raise ValueError('not hashable')
 
223
        return cmp(self.file_id, other.file_id) \
 
224
               or cmp(self.name, other.name) \
 
225
               or cmp(self.text_sha1, other.text_sha1) \
 
226
               or cmp(self.text_size, other.text_size) \
 
227
               or cmp(self.text_id, other.text_id) \
 
228
               or cmp(self.parent_id, other.parent_id) \
 
229
               or cmp(self.kind, other.kind)
227
230
 
228
231
 
229
232
 
235
238
        self.parent_id = None
236
239
        self.name = ''
237
240
 
238
 
    def __eq__(self, other):
 
241
    def __cmp__(self, other):
 
242
        if self is other:
 
243
            return 0
239
244
        if not isinstance(other, RootEntry):
240
245
            return NotImplemented
241
 
        
242
 
        return (self.file_id == other.file_id) \
243
 
               and (self.children == other.children)
244
 
 
245
 
 
246
 
 
247
 
class Inventory(object):
 
246
        return cmp(self.file_id, other.file_id) \
 
247
               or cmp(self.children, other.children)
 
248
 
 
249
 
 
250
 
 
251
class Inventory(XMLMixin):
248
252
    """Inventory of versioned files in a tree.
249
253
 
250
 
    This describes which file_id is present at each point in the tree,
251
 
    and possibly the SHA-1 or other information about the file.
252
 
    Entries can be looked up either by path or by file_id.
 
254
    An Inventory acts like a set of InventoryEntry items.  You can
 
255
    also look files up by their file_id or name.
 
256
    
 
257
    May be read from and written to a metadata file in a tree.  To
 
258
    manipulate the inventory (for example to add a file), it is read
 
259
    in, modified, and then written back out.
253
260
 
254
261
    The inventory represents a typical unix file tree, with
255
262
    directories containing files and subdirectories.  We never store
262
269
    inserted, other than through the Inventory API.
263
270
 
264
271
    >>> inv = Inventory()
 
272
    >>> inv.write_xml(sys.stdout)
 
273
    <inventory>
 
274
    </inventory>
265
275
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
266
276
    >>> inv['123-123'].name
267
277
    'hello.c'
277
287
 
278
288
    >>> [x[0] for x in inv.iter_entries()]
279
289
    ['hello.c']
 
290
    
 
291
    >>> inv.write_xml(sys.stdout)
 
292
    <inventory>
 
293
    <entry file_id="123-123" kind="file" name="hello.c" />
 
294
    </inventory>
 
295
 
280
296
    """
 
297
 
 
298
    ## TODO: Make sure only canonical filenames are stored.
 
299
 
 
300
    ## TODO: Do something sensible about the possible collisions on
 
301
    ## case-losing filesystems.  Perhaps we should just always forbid
 
302
    ## such collisions.
 
303
 
 
304
    ## TODO: No special cases for root, rather just give it a file id
 
305
    ## like everything else.
 
306
 
 
307
    ## TODO: Probably change XML serialization to use nesting
 
308
 
281
309
    def __init__(self):
282
310
        """Create or read an inventory.
283
311
 
315
343
            yield name, ie
316
344
            if ie.kind == 'directory':
317
345
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
318
 
                    yield os.path.join(name, cn), cie
319
 
 
320
 
 
321
 
    def entries(self):
322
 
        """Return list of (path, ie) for all entries except the root.
323
 
 
324
 
        This may be faster than iter_entries.
 
346
                    yield '/'.join((name, cn)), cie
 
347
                    
 
348
 
 
349
 
 
350
    def directories(self):
 
351
        """Return (path, entry) pairs for all directories.
325
352
        """
326
 
        accum = []
327
 
        def descend(dir_ie, dir_path):
328
 
            kids = dir_ie.children.items()
329
 
            kids.sort()
330
 
            for name, ie in kids:
331
 
                child_path = os.path.join(dir_path, name)
332
 
                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():
333
360
                if ie.kind == 'directory':
334
 
                    descend(ie, child_path)
335
 
 
336
 
        descend(self.root, '')
337
 
        return accum
338
 
 
339
 
 
340
 
    def directories(self):
341
 
        """Return (path, entry) pairs for all directories, including the root.
342
 
        """
343
 
        accum = []
344
 
        def descend(parent_ie, parent_path):
345
 
            accum.append((parent_path, parent_ie))
 
361
                    dn.append((ie.name, ie))
 
362
            dn.sort()
346
363
            
347
 
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
348
 
            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
349
367
 
350
 
            for name, child_ie in kids:
351
 
                child_path = os.path.join(parent_path, name)
352
 
                descend(child_ie, child_path)
353
 
        descend(self.root, '')
354
 
        return accum
 
368
        for name, ie in descend(self.root):
 
369
            yield name, ie
355
370
        
356
371
 
357
372
 
376
391
        >>> inv['123123'].name
377
392
        'hello.c'
378
393
        """
 
394
        if file_id == None:
 
395
            raise BzrError("can't look up file_id None")
 
396
            
379
397
        try:
380
398
            return self._byid[file_id]
381
399
        except KeyError:
382
 
            if file_id == None:
383
 
                raise BzrError("can't look up file_id None")
384
 
            else:
385
 
                raise BzrError("file_id {%s} not in inventory" % file_id)
386
 
 
387
 
 
388
 
    def get_file_kind(self, file_id):
389
 
        return self._byid[file_id].kind
 
400
            raise BzrError("file_id {%s} not in inventory" % file_id)
 
401
 
390
402
 
391
403
    def get_child(self, parent_id, filename):
392
404
        return self[parent_id].children.get(filename)
398
410
        To add  a file to a branch ready to be committed, use Branch.add,
399
411
        which calls this."""
400
412
        if entry.file_id in self._byid:
401
 
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
413
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
402
414
 
403
415
        try:
404
416
            parent = self._byid[entry.parent_id]
405
417
        except KeyError:
406
 
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
418
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
407
419
 
408
420
        if parent.children.has_key(entry.name):
409
 
            raise BzrError("%s is already versioned" %
 
421
            bailout("%s is already versioned" %
410
422
                    appendpath(self.id2path(parent.file_id), entry.name))
411
423
 
412
424
        self._byid[entry.file_id] = entry
417
429
        """Add entry from a path.
418
430
 
419
431
        The immediate parent must already be versioned"""
420
 
        from bzrlib.errors import NotVersionedError
421
 
        
422
432
        parts = bzrlib.osutils.splitpath(relpath)
423
433
        if len(parts) == 0:
424
 
            raise BzrError("cannot re-add root of inventory")
 
434
            bailout("cannot re-add root of inventory")
425
435
 
426
436
        if file_id == None:
427
 
            from bzrlib.branch import gen_file_id
428
 
            file_id = gen_file_id(relpath)
429
 
 
430
 
        parent_path = parts[:-1]
431
 
        parent_id = self.path2id(parent_path)
432
 
        if parent_id == None:
433
 
            raise NotVersionedError(parent_path)
434
 
 
 
437
            file_id = bzrlib.branch.gen_file_id(relpath)
 
438
 
 
439
        parent_id = self.path2id(parts[:-1])
 
440
        assert parent_id != None
435
441
        ie = InventoryEntry(file_id, parts[-1],
436
442
                            kind=kind, parent_id=parent_id)
437
443
        return self.add(ie)
463
469
        del self[ie.parent_id].children[ie.name]
464
470
 
465
471
 
 
472
    def id_set(self):
 
473
        return Set(self._byid)
 
474
 
 
475
 
466
476
    def to_element(self):
467
477
        """Convert to XML Element"""
468
 
        from bzrlib.xml import Element
469
 
        
470
478
        e = Element('inventory')
471
479
        e.text = '\n'
472
480
        for path, ie in self.iter_entries():
493
501
    from_element = classmethod(from_element)
494
502
 
495
503
 
496
 
    def __eq__(self, other):
 
504
    def __cmp__(self, other):
497
505
        """Compare two sets by comparing their contents.
498
506
 
499
507
        >>> i1 = Inventory()
507
515
        >>> i1 == i2
508
516
        True
509
517
        """
 
518
        if self is other:
 
519
            return 0
 
520
        
510
521
        if not isinstance(other, Inventory):
511
522
            return NotImplemented
512
523
 
513
 
        if len(self._byid) != len(other._byid):
514
 
            # shortcut: obviously not the same
515
 
            return False
516
 
 
517
 
        return self._byid == other._byid
518
 
 
519
 
 
520
 
    def __ne__(self, other):
521
 
        return not (self == other)
522
 
 
523
 
 
524
 
    def __hash__(self):
525
 
        raise ValueError('not hashable')
526
 
 
 
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
527
532
 
528
533
 
529
534
    def get_idpath(self, file_id):
539
544
            try:
540
545
                ie = self._byid[file_id]
541
546
            except KeyError:
542
 
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
547
                bailout("file_id {%s} not found in inventory" % file_id)
543
548
            p.insert(0, ie.file_id)
544
549
            file_id = ie.parent_id
545
550
        return p
550
555
 
551
556
        # get all names, skipping root
552
557
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
553
 
        return os.sep.join(p)
 
558
        return '/'.join(p)
554
559
            
555
560
 
556
561
 
599
604
 
600
605
        This does not move the working file."""
601
606
        if not is_valid_name(new_name):
602
 
            raise BzrError("not an acceptable filename: %r" % new_name)
 
607
            bailout("not an acceptable filename: %r" % new_name)
603
608
 
604
609
        new_parent = self._byid[new_parent_id]
605
610
        if new_name in new_parent.children:
606
 
            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)))
607
612
 
608
613
        new_parent_idpath = self.get_idpath(new_parent_id)
609
614
        if file_id in new_parent_idpath:
610
 
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
615
            bailout("cannot move directory %r into a subdirectory of itself, %r"
611
616
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
612
617
 
613
618
        file_ie = self._byid[file_id]