~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: Martin Pool
  • Date: 2005-06-27 01:26:11 UTC
  • Revision ID: mbp@sourcefrog.net-20050627012611-4effb7007553fde1
- tweak rsync upload script

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
 
 
23
17
 
24
18
# This should really be an id randomly assigned when the tree is
25
19
# created, but it's not for now.
27
21
 
28
22
 
29
23
import sys, os.path, types, re
30
 
from sets import Set
31
24
 
32
25
try:
33
26
    from cElementTree import Element, ElementTree, SubElement
34
27
except ImportError:
35
28
    from elementtree.ElementTree import Element, ElementTree, SubElement
36
29
 
37
 
from xml import XMLMixin
38
 
from errors import bailout, BzrError
 
30
from bzrlib.xml import XMLMixin
 
31
from bzrlib.errors import BzrError, BzrCheckError
39
32
 
40
33
import bzrlib
41
34
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
75
68
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
76
69
    Traceback (most recent call last):
77
70
    ...
78
 
    BzrError: ('inventory already contains entry with id {2323}', [])
 
71
    BzrError: inventory already contains entry with id {2323}
79
72
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
80
73
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
81
74
    >>> i.path2id('src/wibble')
97
90
    >>> i.id2path('2326')
98
91
    'src/wibble/wibble.c'
99
92
 
100
 
    :todo: Maybe also keep the full path of the entry, and the children?
 
93
    TODO: Maybe also keep the full path of the entry, and the children?
101
94
           But those depend on its position within a particular inventory, and
102
95
           it would be nice not to need to hold the backpointer here.
103
96
    """
104
97
 
105
98
    # TODO: split InventoryEntry into subclasses for files,
106
99
    # directories, etc etc.
 
100
 
 
101
    text_sha1 = None
 
102
    text_size = None
107
103
    
108
104
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
109
105
        """Create an InventoryEntry
118
114
        '123'
119
115
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
120
116
        Traceback (most recent call last):
121
 
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
117
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
122
118
        """
123
 
        
124
 
        if len(splitpath(name)) != 1:
125
 
            bailout('InventoryEntry name is not a simple filename: %r'
126
 
                    % name)
 
119
        if '/' in name or '\\' in name:
 
120
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
127
121
        
128
122
        self.file_id = file_id
129
123
        self.name = name
130
 
        assert kind in ['file', 'directory']
131
124
        self.kind = kind
132
125
        self.text_id = text_id
133
126
        self.parent_id = parent_id
134
 
        self.text_sha1 = None
135
 
        self.text_size = None
136
127
        if kind == 'directory':
137
128
            self.children = {}
 
129
        elif kind == 'file':
 
130
            pass
138
131
        else:
139
 
            assert kind == 'file'
 
132
            raise BzrError("unhandled entry kind %r" % kind)
 
133
 
140
134
 
141
135
 
142
136
    def sorted_children(self):
150
144
                               self.parent_id, text_id=self.text_id)
151
145
        other.text_sha1 = self.text_sha1
152
146
        other.text_size = self.text_size
 
147
        # note that children are *not* copied; they're pulled across when
 
148
        # others are added
153
149
        return other
154
150
 
155
151
 
214
210
 
215
211
    from_element = classmethod(from_element)
216
212
 
217
 
    def __cmp__(self, other):
218
 
        if self is other:
219
 
            return 0
 
213
    def __eq__(self, other):
220
214
        if not isinstance(other, InventoryEntry):
221
215
            return NotImplemented
222
216
 
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)
 
217
        return (self.file_id == other.file_id) \
 
218
               and (self.name == other.name) \
 
219
               and (self.text_sha1 == other.text_sha1) \
 
220
               and (self.text_size == other.text_size) \
 
221
               and (self.text_id == other.text_id) \
 
222
               and (self.parent_id == other.parent_id) \
 
223
               and (self.kind == other.kind)
 
224
 
 
225
 
 
226
    def __ne__(self, other):
 
227
        return not (self == other)
 
228
 
 
229
    def __hash__(self):
 
230
        raise ValueError('not hashable')
230
231
 
231
232
 
232
233
 
238
239
        self.parent_id = None
239
240
        self.name = ''
240
241
 
241
 
    def __cmp__(self, other):
242
 
        if self is other:
243
 
            return 0
 
242
    def __eq__(self, other):
244
243
        if not isinstance(other, RootEntry):
245
244
            return NotImplemented
246
 
        return cmp(self.file_id, other.file_id) \
247
 
               or cmp(self.children, other.children)
 
245
        
 
246
        return (self.file_id == other.file_id) \
 
247
               and (self.children == other.children)
248
248
 
249
249
 
250
250
 
251
251
class Inventory(XMLMixin):
252
252
    """Inventory of versioned files in a tree.
253
253
 
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.
 
254
    This describes which file_id is present at each point in the tree,
 
255
    and possibly the SHA-1 or other information about the file.
 
256
    Entries can be looked up either by path or by file_id.
260
257
 
261
258
    The inventory represents a typical unix file tree, with
262
259
    directories containing files and subdirectories.  We never store
294
291
    </inventory>
295
292
 
296
293
    """
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
 
 
309
294
    def __init__(self):
310
295
        """Create or read an inventory.
311
296
 
343
328
            yield name, ie
344
329
            if ie.kind == 'directory':
345
330
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
 
                    yield '/'.join((name, cn)), cie
347
 
                    
 
331
                    yield os.path.join(name, cn), cie
 
332
 
 
333
 
 
334
    def entries(self):
 
335
        """Return list of (path, ie) for all entries except the root.
 
336
 
 
337
        This may be faster than iter_entries.
 
338
        """
 
339
        accum = []
 
340
        def descend(dir_ie, dir_path):
 
341
            kids = dir_ie.children.items()
 
342
            kids.sort()
 
343
            for name, ie in kids:
 
344
                child_path = os.path.join(dir_path, name)
 
345
                accum.append((child_path, ie))
 
346
                if ie.kind == 'directory':
 
347
                    descend(ie, child_path)
 
348
 
 
349
        descend(self.root, '')
 
350
        return accum
348
351
 
349
352
 
350
353
    def directories(self):
351
 
        """Return (path, entry) pairs for all directories.
 
354
        """Return (path, entry) pairs for all directories, including the root.
352
355
        """
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():
360
 
                if ie.kind == 'directory':
361
 
                    dn.append((ie.name, ie))
362
 
            dn.sort()
 
356
        accum = []
 
357
        def descend(parent_ie, parent_path):
 
358
            accum.append((parent_path, parent_ie))
363
359
            
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
 
360
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
361
            kids.sort()
367
362
 
368
 
        for name, ie in descend(self.root):
369
 
            yield name, ie
 
363
            for name, child_ie in kids:
 
364
                child_path = os.path.join(parent_path, name)
 
365
                descend(child_ie, child_path)
 
366
        descend(self.root, '')
 
367
        return accum
370
368
        
371
369
 
372
370
 
391
389
        >>> inv['123123'].name
392
390
        'hello.c'
393
391
        """
394
 
        if file_id == None:
395
 
            raise BzrError("can't look up file_id None")
396
 
            
397
392
        try:
398
393
            return self._byid[file_id]
399
394
        except KeyError:
400
 
            raise BzrError("file_id {%s} not in inventory" % file_id)
401
 
 
 
395
            if file_id == None:
 
396
                raise BzrError("can't look up file_id None")
 
397
            else:
 
398
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
399
 
 
400
 
 
401
    def get_file_kind(self, file_id):
 
402
        return self._byid[file_id].kind
402
403
 
403
404
    def get_child(self, parent_id, filename):
404
405
        return self[parent_id].children.get(filename)
410
411
        To add  a file to a branch ready to be committed, use Branch.add,
411
412
        which calls this."""
412
413
        if entry.file_id in self._byid:
413
 
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
414
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
414
415
 
415
416
        try:
416
417
            parent = self._byid[entry.parent_id]
417
418
        except KeyError:
418
 
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
419
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
419
420
 
420
421
        if parent.children.has_key(entry.name):
421
 
            bailout("%s is already versioned" %
 
422
            raise BzrError("%s is already versioned" %
422
423
                    appendpath(self.id2path(parent.file_id), entry.name))
423
424
 
424
425
        self._byid[entry.file_id] = entry
429
430
        """Add entry from a path.
430
431
 
431
432
        The immediate parent must already be versioned"""
 
433
        from bzrlib.errors import NotVersionedError
 
434
        
432
435
        parts = bzrlib.osutils.splitpath(relpath)
433
436
        if len(parts) == 0:
434
 
            bailout("cannot re-add root of inventory")
 
437
            raise BzrError("cannot re-add root of inventory")
435
438
 
436
439
        if file_id == None:
437
440
            file_id = bzrlib.branch.gen_file_id(relpath)
438
441
 
439
 
        parent_id = self.path2id(parts[:-1])
440
 
        assert parent_id != None
 
442
        parent_path = parts[:-1]
 
443
        parent_id = self.path2id(parent_path)
 
444
        if parent_id == None:
 
445
            raise NotVersionedError(parent_path)
 
446
 
441
447
        ie = InventoryEntry(file_id, parts[-1],
442
448
                            kind=kind, parent_id=parent_id)
443
449
        return self.add(ie)
469
475
        del self[ie.parent_id].children[ie.name]
470
476
 
471
477
 
472
 
    def id_set(self):
473
 
        return Set(self._byid)
474
 
 
475
 
 
476
478
    def to_element(self):
477
479
        """Convert to XML Element"""
478
480
        e = Element('inventory')
501
503
    from_element = classmethod(from_element)
502
504
 
503
505
 
504
 
    def __cmp__(self, other):
 
506
    def __eq__(self, other):
505
507
        """Compare two sets by comparing their contents.
506
508
 
507
509
        >>> i1 = Inventory()
515
517
        >>> i1 == i2
516
518
        True
517
519
        """
518
 
        if self is other:
519
 
            return 0
520
 
        
521
520
        if not isinstance(other, Inventory):
522
521
            return NotImplemented
523
522
 
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
 
523
        if len(self._byid) != len(other._byid):
 
524
            # shortcut: obviously not the same
 
525
            return False
 
526
 
 
527
        return self._byid == other._byid
 
528
 
 
529
 
 
530
    def __ne__(self, other):
 
531
        return not (self == other)
 
532
 
 
533
 
 
534
    def __hash__(self):
 
535
        raise ValueError('not hashable')
 
536
 
532
537
 
533
538
 
534
539
    def get_idpath(self, file_id):
544
549
            try:
545
550
                ie = self._byid[file_id]
546
551
            except KeyError:
547
 
                bailout("file_id {%s} not found in inventory" % file_id)
 
552
                raise BzrError("file_id {%s} not found in inventory" % file_id)
548
553
            p.insert(0, ie.file_id)
549
554
            file_id = ie.parent_id
550
555
        return p
555
560
 
556
561
        # get all names, skipping root
557
562
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
558
 
        return '/'.join(p)
 
563
        return os.sep.join(p)
559
564
            
560
565
 
561
566
 
604
609
 
605
610
        This does not move the working file."""
606
611
        if not is_valid_name(new_name):
607
 
            bailout("not an acceptable filename: %r" % new_name)
 
612
            raise BzrError("not an acceptable filename: %r" % new_name)
608
613
 
609
614
        new_parent = self._byid[new_parent_id]
610
615
        if new_name in new_parent.children:
611
 
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
616
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
612
617
 
613
618
        new_parent_idpath = self.get_idpath(new_parent_id)
614
619
        if file_id in new_parent_idpath:
615
 
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
620
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
621
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
617
622
 
618
623
        file_ie = self._byid[file_id]