~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-09 06:21:44 UTC
  • Revision ID: mbp@sourcefrog.net-20050409062144-e47a4b64106e4c21af99beaf
debugĀ output

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# (C) 2005 Canonical Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
19
18
 
20
19
# TODO: Maybe store inventory_id in the file?  Not really needed.
21
20
 
22
 
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
23
21
__author__ = "Martin Pool <mbp@canonical.com>"
24
22
 
25
 
import sys, os.path, types
 
23
 
 
24
# This should really be an id randomly assigned when the tree is
 
25
# created, but it's not for now.
 
26
ROOT_ID = "TREE_ROOT"
 
27
 
 
28
 
 
29
import sys, os.path, types, re
26
30
from sets import Set
27
31
 
28
32
try:
31
35
    from elementtree.ElementTree import Element, ElementTree, SubElement
32
36
 
33
37
from xml import XMLMixin
34
 
from errors import bailout
 
38
from errors import bailout, BzrError
35
39
 
36
40
import bzrlib
37
41
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
60
64
 
61
65
    >>> i = Inventory()
62
66
    >>> i.path2id('')
63
 
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
64
 
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
 
67
    'TREE_ROOT'
 
68
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
69
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
65
70
    >>> for j in i.iter_entries():
66
71
    ...   print j
67
72
    ... 
68
 
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
 
73
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
69
74
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
70
 
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
 
75
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
71
76
    Traceback (most recent call last):
72
77
    ...
73
78
    BzrError: ('inventory already contains entry with id {2323}', [])
74
 
    >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
75
 
    >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
 
79
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
80
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
76
81
    >>> i.path2id('src/wibble')
77
82
    '2325'
78
83
    >>> '2325' in i
79
84
    True
80
 
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
85
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
81
86
    >>> i['2326']
82
87
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
83
88
    >>> for j in i.iter_entries():
96
101
           But those depend on its position within a particular inventory, and
97
102
           it would be nice not to need to hold the backpointer here.
98
103
    """
99
 
    def __init__(self, file_id, name, kind='file', text_id=None,
100
 
                 parent_id=None):
 
104
 
 
105
    # TODO: split InventoryEntry into subclasses for files,
 
106
    # directories, etc etc.
 
107
    
 
108
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
101
109
        """Create an InventoryEntry
102
110
        
103
111
        The filename must be a single component, relative to the
104
112
        parent directory; it cannot be a whole path or relative name.
105
113
 
106
 
        >>> e = InventoryEntry('123', 'hello.c')
 
114
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
107
115
        >>> e.name
108
116
        'hello.c'
109
117
        >>> e.file_id
110
118
        '123'
111
 
        >>> e = InventoryEntry('123', 'src/hello.c')
 
119
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
112
120
        Traceback (most recent call last):
113
121
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
114
122
        """
125
133
        self.parent_id = parent_id
126
134
        self.text_sha1 = None
127
135
        self.text_size = None
 
136
        if kind == 'directory':
 
137
            self.children = {}
 
138
        else:
 
139
            assert kind == 'file'
 
140
 
 
141
 
 
142
    def sorted_children(self):
 
143
        l = self.children.items()
 
144
        l.sort()
 
145
        return l
128
146
 
129
147
 
130
148
    def copy(self):
131
149
        other = InventoryEntry(self.file_id, self.name, self.kind,
132
 
                               self.text_id, self.parent_id)
 
150
                               self.parent_id, text_id=self.text_id)
133
151
        other.text_sha1 = self.text_sha1
134
152
        other.text_size = self.text_size
135
153
        return other
152
170
        e.set('file_id', self.file_id)
153
171
        e.set('kind', self.kind)
154
172
 
155
 
        if self.text_size is not None:
 
173
        if self.text_size != None:
156
174
            e.set('text_size', '%d' % self.text_size)
157
175
            
158
 
        for f in ['text_id', 'text_sha1', 'parent_id']:
 
176
        for f in ['text_id', 'text_sha1']:
159
177
            v = getattr(self, f)
160
 
            if v is not None:
 
178
            if v != None:
161
179
                e.set(f, v)
162
180
 
 
181
        # to be conservative, we don't externalize the root pointers
 
182
        # for now, leaving them as null in the xml form.  in a future
 
183
        # version it will be implied by nested elements.
 
184
        if self.parent_id != ROOT_ID:
 
185
            assert isinstance(self.parent_id, basestring)
 
186
            e.set('parent_id', self.parent_id)
 
187
 
163
188
        e.tail = '\n'
164
189
            
165
190
        return e
167
192
 
168
193
    def from_element(cls, elt):
169
194
        assert elt.tag == 'entry'
170
 
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
 
195
 
 
196
        ## original format inventories don't have a parent_id for
 
197
        ## nodes in the root directory, but it's cleaner to use one
 
198
        ## internally.
 
199
        parent_id = elt.get('parent_id')
 
200
        if parent_id == None:
 
201
            parent_id = ROOT_ID
 
202
 
 
203
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
171
204
        self.text_id = elt.get('text_id')
172
205
        self.text_sha1 = elt.get('text_sha1')
173
 
        self.parent_id = elt.get('parent_id')
174
206
        
175
207
        ## mutter("read inventoryentry: %r" % (elt.attrib))
176
208
 
198
230
 
199
231
 
200
232
 
 
233
class RootEntry(InventoryEntry):
 
234
    def __init__(self, file_id):
 
235
        self.file_id = file_id
 
236
        self.children = {}
 
237
        self.kind = 'root_directory'
 
238
        self.parent_id = None
 
239
        self.name = ''
 
240
 
 
241
    def __cmp__(self, other):
 
242
        if self is other:
 
243
            return 0
 
244
        if not isinstance(other, RootEntry):
 
245
            return NotImplemented
 
246
        return cmp(self.file_id, other.file_id) \
 
247
               or cmp(self.children, other.children)
 
248
 
 
249
 
 
250
 
201
251
class Inventory(XMLMixin):
202
252
    """Inventory of versioned files in a tree.
203
253
 
216
266
    returned quickly.
217
267
 
218
268
    InventoryEntry objects must not be modified after they are
219
 
    inserted.
 
269
    inserted, other than through the Inventory API.
220
270
 
221
271
    >>> inv = Inventory()
222
272
    >>> inv.write_xml(sys.stdout)
223
273
    <inventory>
224
274
    </inventory>
225
 
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
275
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
226
276
    >>> inv['123-123'].name
227
277
    'hello.c'
228
 
    >>> for file_id in inv: print file_id
229
 
    ...
230
 
    123-123
231
278
 
232
279
    May be treated as an iterator or set to look up file ids:
233
280
    
248
295
 
249
296
    """
250
297
 
251
 
    ## TODO: Clear up handling of files in subdirectories; we probably
252
 
    ## do want to be able to just look them up by name but this
253
 
    ## probably means gradually walking down the path, looking up as we go.
254
 
 
255
298
    ## TODO: Make sure only canonical filenames are stored.
256
299
 
257
300
    ## TODO: Do something sensible about the possible collisions on
258
301
    ## case-losing filesystems.  Perhaps we should just always forbid
259
302
    ## such collisions.
260
303
 
261
 
    ## _tree should probably just be stored as
262
 
    ## InventoryEntry._children on each directory.
 
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
263
308
 
264
309
    def __init__(self):
265
310
        """Create or read an inventory.
267
312
        If a working directory is specified, the inventory is read
268
313
        from there.  If the file is specified, read from that. If not,
269
314
        the inventory is created empty.
 
315
 
 
316
        The inventory is created with a default root directory, with
 
317
        an id of None.
270
318
        """
271
 
        self._byid = dict()
272
 
 
273
 
        # _tree is indexed by parent_id; at each level a map from name
274
 
        # to ie.  The None entry is the root.
275
 
        self._tree = {None: {}}
 
319
        self.root = RootEntry(ROOT_ID)
 
320
        self._byid = {self.root.file_id: self.root}
276
321
 
277
322
 
278
323
    def __iter__(self):
284
329
        return len(self._byid)
285
330
 
286
331
 
287
 
    def iter_entries(self, parent_id=None):
 
332
    def iter_entries(self, from_dir=None):
288
333
        """Return (path, entry) pairs, in order by name."""
289
 
        kids = self._tree[parent_id].items()
 
334
        if from_dir == None:
 
335
            assert self.root
 
336
            from_dir = self.root
 
337
        elif isinstance(from_dir, basestring):
 
338
            from_dir = self._byid[from_dir]
 
339
            
 
340
        kids = from_dir.children.items()
290
341
        kids.sort()
291
342
        for name, ie in kids:
292
343
            yield name, ie
293
344
            if ie.kind == 'directory':
294
 
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
295
 
                    yield joinpath([name, cn]), cie
296
 
 
297
 
 
298
 
    def directories(self, include_root=True):
 
345
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
346
                    yield '/'.join((name, cn)), cie
 
347
                    
 
348
 
 
349
 
 
350
    def directories(self):
299
351
        """Return (path, entry) pairs for all directories.
300
352
        """
301
 
        if include_root:
302
 
            yield '', None
303
 
        for path, entry in self.iter_entries():
304
 
            if entry.kind == 'directory':
305
 
                yield path, entry
 
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()
 
363
            
 
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
 
367
 
 
368
        for name, ie in descend(self.root):
 
369
            yield name, ie
306
370
        
307
371
 
308
372
 
309
 
    def children(self, parent_id):
310
 
        """Return entries that are direct children of parent_id."""
311
 
        return self._tree[parent_id]
312
 
                    
313
 
 
314
 
 
315
 
    # TODO: return all paths and entries
316
 
 
317
 
 
318
373
    def __contains__(self, file_id):
319
374
        """True if this entry contains a file with given id.
320
375
 
321
376
        >>> inv = Inventory()
322
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
377
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
323
378
        >>> '123' in inv
324
379
        True
325
380
        >>> '456' in inv
332
387
        """Return the entry for given file_id.
333
388
 
334
389
        >>> inv = Inventory()
335
 
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
390
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
336
391
        >>> inv['123123'].name
337
392
        'hello.c'
338
393
        """
339
 
        return self._byid[file_id]
 
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)
 
401
 
 
402
 
 
403
    def get_child(self, parent_id, filename):
 
404
        return self[parent_id].children.get(filename)
340
405
 
341
406
 
342
407
    def add(self, entry):
344
409
 
345
410
        To add  a file to a branch ready to be committed, use Branch.add,
346
411
        which calls this."""
347
 
        if entry.file_id in self:
 
412
        if entry.file_id in self._byid:
348
413
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
349
414
 
350
 
        if entry.parent_id != None:
351
 
            if entry.parent_id not in self:
352
 
                bailout("parent_id %s of new entry not found in inventory"
353
 
                        % entry.parent_id)
354
 
            
355
 
        if self._tree[entry.parent_id].has_key(entry.name):
356
 
            bailout("%s is already versioned"
357
 
                    % appendpath(self.id2path(entry.parent_id), entry.name))
 
415
        try:
 
416
            parent = self._byid[entry.parent_id]
 
417
        except KeyError:
 
418
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
419
 
 
420
        if parent.children.has_key(entry.name):
 
421
            bailout("%s is already versioned" %
 
422
                    appendpath(self.id2path(parent.file_id), entry.name))
358
423
 
359
424
        self._byid[entry.file_id] = entry
360
 
        self._tree[entry.parent_id][entry.name] = entry
361
 
 
362
 
        if entry.kind == 'directory':
363
 
            self._tree[entry.file_id] = {}
 
425
        parent.children[entry.name] = entry
364
426
 
365
427
 
366
428
    def add_path(self, relpath, kind, file_id=None):
371
433
        if len(parts) == 0:
372
434
            bailout("cannot re-add root of inventory")
373
435
 
374
 
        if file_id is None:
 
436
        if file_id == None:
375
437
            file_id = bzrlib.branch.gen_file_id(relpath)
376
438
 
377
439
        parent_id = self.path2id(parts[:-1])
 
440
        assert parent_id != None
378
441
        ie = InventoryEntry(file_id, parts[-1],
379
442
                            kind=kind, parent_id=parent_id)
380
443
        return self.add(ie)
384
447
        """Remove entry by id.
385
448
 
386
449
        >>> inv = Inventory()
387
 
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
450
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
388
451
        >>> '123' in inv
389
452
        True
390
453
        >>> del inv['123']
393
456
        """
394
457
        ie = self[file_id]
395
458
 
396
 
        assert self._tree[ie.parent_id][ie.name] == ie
 
459
        assert self[ie.parent_id].children[ie.name] == ie
397
460
        
398
461
        # TODO: Test deleting all children; maybe hoist to a separate
399
462
        # deltree method?
400
463
        if ie.kind == 'directory':
401
 
            for cie in self._tree[file_id].values():
 
464
            for cie in ie.children.values():
402
465
                del self[cie.file_id]
403
 
            del self._tree[file_id]
 
466
            del ie.children
404
467
 
405
468
        del self._byid[file_id]
406
 
        del self._tree[ie.parent_id][ie.name]
 
469
        del self[ie.parent_id].children[ie.name]
407
470
 
408
471
 
409
472
    def id_set(self):
423
486
        """Construct from XML Element
424
487
 
425
488
        >>> inv = Inventory()
426
 
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
 
489
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
427
490
        >>> elt = inv.to_element()
428
491
        >>> inv2 = Inventory.from_element(elt)
429
492
        >>> inv2 == inv
445
508
        >>> i2 = Inventory()
446
509
        >>> i1 == i2
447
510
        True
448
 
        >>> i1.add(InventoryEntry('123', 'foo'))
 
511
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
449
512
        >>> i1 == i2
450
513
        False
451
 
        >>> i2.add(InventoryEntry('123', 'foo'))
 
514
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
452
515
        >>> i1 == i2
453
516
        True
454
517
        """
468
531
        return 0
469
532
 
470
533
 
471
 
    def id2path(self, file_id):
472
 
        """Return as a list the path to file_id."""
 
534
    def get_idpath(self, file_id):
 
535
        """Return a list of file_ids for the path to an entry.
 
536
 
 
537
        The list contains one element for each directory followed by
 
538
        the id of the file itself.  So the length of the returned list
 
539
        is equal to the depth of the file in the tree, counting the
 
540
        root directory as depth 1.
 
541
        """
473
542
        p = []
474
543
        while file_id != None:
475
 
            ie = self[file_id]
476
 
            p = [ie.name] + p
 
544
            try:
 
545
                ie = self._byid[file_id]
 
546
            except KeyError:
 
547
                bailout("file_id {%s} not found in inventory" % file_id)
 
548
            p.insert(0, ie.file_id)
477
549
            file_id = ie.parent_id
478
 
        return joinpath(p)
 
550
        return p
 
551
 
 
552
 
 
553
    def id2path(self, file_id):
 
554
        """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 '/'.join(p)
479
559
            
480
560
 
481
561
 
487
567
 
488
568
        This returns the entry of the last component in the path,
489
569
        which may be either a file or a directory.
 
570
 
 
571
        Returns None iff the path is not found.
490
572
        """
491
573
        if isinstance(name, types.StringTypes):
492
574
            name = splitpath(name)
493
575
 
494
 
        parent_id = None
 
576
        mutter("lookup path %r" % name)
 
577
 
 
578
        parent = self.root
495
579
        for f in name:
496
580
            try:
497
 
                cie = self._tree[parent_id][f]
 
581
                cie = parent.children[f]
498
582
                assert cie.name == f
499
 
                parent_id = cie.file_id
 
583
                assert cie.parent_id == parent.file_id
 
584
                parent = cie
500
585
            except KeyError:
501
586
                # or raise an error?
502
587
                return None
503
588
 
504
 
        return parent_id
505
 
 
506
 
 
507
 
    def get_child(self, parent_id, child_name):
508
 
        return self._tree[parent_id].get(child_name)
 
589
        return parent.file_id
509
590
 
510
591
 
511
592
    def has_filename(self, names):
513
594
 
514
595
 
515
596
    def has_id(self, file_id):
516
 
        assert isinstance(file_id, str)
517
597
        return self._byid.has_key(file_id)
518
598
 
519
599
 
520
 
 
521
 
 
522
 
 
523
 
if __name__ == '__main__':
524
 
    import doctest, inventory
525
 
    doctest.testmod(inventory)
 
600
    def rename(self, file_id, new_parent_id, new_name):
 
601
        """Move a file within the inventory.
 
602
 
 
603
        This can change either the name, or the parent, or both.
 
604
 
 
605
        This does not move the working file."""
 
606
        if not is_valid_name(new_name):
 
607
            bailout("not an acceptable filename: %r" % new_name)
 
608
 
 
609
        new_parent = self._byid[new_parent_id]
 
610
        if new_name in new_parent.children:
 
611
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
612
 
 
613
        new_parent_idpath = self.get_idpath(new_parent_id)
 
614
        if file_id in new_parent_idpath:
 
615
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
616
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
617
 
 
618
        file_ie = self._byid[file_id]
 
619
        old_parent = self._byid[file_ie.parent_id]
 
620
 
 
621
        # TODO: Don't leave things messed up if this fails
 
622
 
 
623
        del old_parent.children[file_ie.name]
 
624
        new_parent.children[new_name] = file_ie
 
625
        
 
626
        file_ie.name = new_name
 
627
        file_ie.parent_id = new_parent_id
 
628
 
 
629
 
 
630
 
 
631
 
 
632
_NAME_RE = re.compile(r'^[^/\\]+$')
 
633
 
 
634
def is_valid_name(name):
 
635
    return bool(_NAME_RE.match(name))