/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/inventory.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-11 02:53:57 UTC
  • Revision ID: mbp@sourcefrog.net-20050411025357-af577721308648ae
- remove profiler temporary file when done

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# (C) 2005 Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
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
 
 
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
 
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
 
39
 
 
40
import bzrlib
 
41
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
42
from bzrlib.trace import mutter
 
43
 
 
44
class InventoryEntry(XMLMixin):
 
45
    """Description of a versioned file.
 
46
 
 
47
    An InventoryEntry has the following fields, which are also
 
48
    present in the XML inventory-entry element:
 
49
 
 
50
    * *file_id*
 
51
    * *name*: (only the basename within the directory, must not
 
52
      contain slashes)
 
53
    * *kind*: "directory" or "file"
 
54
    * *directory_id*: (if absent/null means the branch root directory)
 
55
    * *text_sha1*: only for files
 
56
    * *text_size*: in bytes, only for files 
 
57
    * *text_id*: identifier for the text version, only for files
 
58
 
 
59
    InventoryEntries can also exist inside a WorkingTree
 
60
    inventory, in which case they are not yet bound to a
 
61
    particular revision of the file.  In that case the text_sha1,
 
62
    text_size and text_id are absent.
 
63
 
 
64
 
 
65
    >>> i = Inventory()
 
66
    >>> i.path2id('')
 
67
    'TREE_ROOT'
 
68
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
69
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
70
    >>> for j in i.iter_entries():
 
71
    ...   print j
 
72
    ... 
 
73
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
74
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
75
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
76
    Traceback (most recent call last):
 
77
    ...
 
78
    BzrError: ('inventory already contains entry with id {2323}', [])
 
79
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
80
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
81
    >>> i.path2id('src/wibble')
 
82
    '2325'
 
83
    >>> '2325' in i
 
84
    True
 
85
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
86
    >>> i['2326']
 
87
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
88
    >>> for j in i.iter_entries():
 
89
    ...     print j[0]
 
90
    ...     assert i.path2id(j[0])
 
91
    ... 
 
92
    src
 
93
    src/bye.c
 
94
    src/hello.c
 
95
    src/wibble
 
96
    src/wibble/wibble.c
 
97
    >>> i.id2path('2326')
 
98
    'src/wibble/wibble.c'
 
99
 
 
100
    :todo: Maybe also keep the full path of the entry, and the children?
 
101
           But those depend on its position within a particular inventory, and
 
102
           it would be nice not to need to hold the backpointer here.
 
103
    """
 
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):
 
109
        """Create an InventoryEntry
 
110
        
 
111
        The filename must be a single component, relative to the
 
112
        parent directory; it cannot be a whole path or relative name.
 
113
 
 
114
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
115
        >>> e.name
 
116
        'hello.c'
 
117
        >>> e.file_id
 
118
        '123'
 
119
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
120
        Traceback (most recent call last):
 
121
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
 
122
        """
 
123
        
 
124
        if len(splitpath(name)) != 1:
 
125
            bailout('InventoryEntry name is not a simple filename: %r'
 
126
                    % name)
 
127
        
 
128
        self.file_id = file_id
 
129
        self.name = name
 
130
        self.kind = kind
 
131
        self.text_id = text_id
 
132
        self.parent_id = parent_id
 
133
        self.text_sha1 = None
 
134
        self.text_size = None
 
135
        if kind == 'directory':
 
136
            self.children = {}
 
137
        elif kind == 'file':
 
138
            pass
 
139
        else:
 
140
            raise BzrError("unhandled entry kind %r" % kind)
 
141
 
 
142
 
 
143
 
 
144
    def sorted_children(self):
 
145
        l = self.children.items()
 
146
        l.sort()
 
147
        return l
 
148
 
 
149
 
 
150
    def copy(self):
 
151
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
152
                               self.parent_id, text_id=self.text_id)
 
153
        other.text_sha1 = self.text_sha1
 
154
        other.text_size = self.text_size
 
155
        return other
 
156
 
 
157
 
 
158
    def __repr__(self):
 
159
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
160
                % (self.__class__.__name__,
 
161
                   self.file_id,
 
162
                   self.name,
 
163
                   self.kind,
 
164
                   self.parent_id))
 
165
 
 
166
    
 
167
    def to_element(self):
 
168
        """Convert to XML element"""
 
169
        e = Element('entry')
 
170
 
 
171
        e.set('name', self.name)
 
172
        e.set('file_id', self.file_id)
 
173
        e.set('kind', self.kind)
 
174
 
 
175
        if self.text_size != None:
 
176
            e.set('text_size', '%d' % self.text_size)
 
177
            
 
178
        for f in ['text_id', 'text_sha1']:
 
179
            v = getattr(self, f)
 
180
            if v != None:
 
181
                e.set(f, v)
 
182
 
 
183
        # to be conservative, we don't externalize the root pointers
 
184
        # for now, leaving them as null in the xml form.  in a future
 
185
        # version it will be implied by nested elements.
 
186
        if self.parent_id != ROOT_ID:
 
187
            assert isinstance(self.parent_id, basestring)
 
188
            e.set('parent_id', self.parent_id)
 
189
 
 
190
        e.tail = '\n'
 
191
            
 
192
        return e
 
193
 
 
194
 
 
195
    def from_element(cls, elt):
 
196
        assert elt.tag == 'entry'
 
197
 
 
198
        ## original format inventories don't have a parent_id for
 
199
        ## nodes in the root directory, but it's cleaner to use one
 
200
        ## internally.
 
201
        parent_id = elt.get('parent_id')
 
202
        if parent_id == None:
 
203
            parent_id = ROOT_ID
 
204
 
 
205
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
 
206
        self.text_id = elt.get('text_id')
 
207
        self.text_sha1 = elt.get('text_sha1')
 
208
        
 
209
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
210
 
 
211
        v = elt.get('text_size')
 
212
        self.text_size = v and int(v)
 
213
 
 
214
        return self
 
215
            
 
216
 
 
217
    from_element = classmethod(from_element)
 
218
 
 
219
    def __cmp__(self, other):
 
220
        if self is other:
 
221
            return 0
 
222
        if not isinstance(other, InventoryEntry):
 
223
            return NotImplemented
 
224
 
 
225
        return cmp(self.file_id, other.file_id) \
 
226
               or cmp(self.name, other.name) \
 
227
               or cmp(self.text_sha1, other.text_sha1) \
 
228
               or cmp(self.text_size, other.text_size) \
 
229
               or cmp(self.text_id, other.text_id) \
 
230
               or cmp(self.parent_id, other.parent_id) \
 
231
               or cmp(self.kind, other.kind)
 
232
 
 
233
 
 
234
 
 
235
class RootEntry(InventoryEntry):
 
236
    def __init__(self, file_id):
 
237
        self.file_id = file_id
 
238
        self.children = {}
 
239
        self.kind = 'root_directory'
 
240
        self.parent_id = None
 
241
        self.name = ''
 
242
 
 
243
    def __cmp__(self, other):
 
244
        if self is other:
 
245
            return 0
 
246
        if not isinstance(other, RootEntry):
 
247
            return NotImplemented
 
248
        return cmp(self.file_id, other.file_id) \
 
249
               or cmp(self.children, other.children)
 
250
 
 
251
 
 
252
 
 
253
class Inventory(XMLMixin):
 
254
    """Inventory of versioned files in a tree.
 
255
 
 
256
    An Inventory acts like a set of InventoryEntry items.  You can
 
257
    also look files up by their file_id or name.
 
258
    
 
259
    May be read from and written to a metadata file in a tree.  To
 
260
    manipulate the inventory (for example to add a file), it is read
 
261
    in, modified, and then written back out.
 
262
 
 
263
    The inventory represents a typical unix file tree, with
 
264
    directories containing files and subdirectories.  We never store
 
265
    the full path to a file, because renaming a directory implicitly
 
266
    moves all of its contents.  This class internally maintains a
 
267
    lookup tree that allows the children under a directory to be
 
268
    returned quickly.
 
269
 
 
270
    InventoryEntry objects must not be modified after they are
 
271
    inserted, other than through the Inventory API.
 
272
 
 
273
    >>> inv = Inventory()
 
274
    >>> inv.write_xml(sys.stdout)
 
275
    <inventory>
 
276
    </inventory>
 
277
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
278
    >>> inv['123-123'].name
 
279
    'hello.c'
 
280
 
 
281
    May be treated as an iterator or set to look up file ids:
 
282
    
 
283
    >>> bool(inv.path2id('hello.c'))
 
284
    True
 
285
    >>> '123-123' in inv
 
286
    True
 
287
 
 
288
    May also look up by name:
 
289
 
 
290
    >>> [x[0] for x in inv.iter_entries()]
 
291
    ['hello.c']
 
292
    
 
293
    >>> inv.write_xml(sys.stdout)
 
294
    <inventory>
 
295
    <entry file_id="123-123" kind="file" name="hello.c" />
 
296
    </inventory>
 
297
 
 
298
    """
 
299
 
 
300
    ## TODO: Make sure only canonical filenames are stored.
 
301
 
 
302
    ## TODO: Do something sensible about the possible collisions on
 
303
    ## case-losing filesystems.  Perhaps we should just always forbid
 
304
    ## such collisions.
 
305
 
 
306
    ## TODO: No special cases for root, rather just give it a file id
 
307
    ## like everything else.
 
308
 
 
309
    ## TODO: Probably change XML serialization to use nesting
 
310
 
 
311
    def __init__(self):
 
312
        """Create or read an inventory.
 
313
 
 
314
        If a working directory is specified, the inventory is read
 
315
        from there.  If the file is specified, read from that. If not,
 
316
        the inventory is created empty.
 
317
 
 
318
        The inventory is created with a default root directory, with
 
319
        an id of None.
 
320
        """
 
321
        self.root = RootEntry(ROOT_ID)
 
322
        self._byid = {self.root.file_id: self.root}
 
323
 
 
324
 
 
325
    def __iter__(self):
 
326
        return iter(self._byid)
 
327
 
 
328
 
 
329
    def __len__(self):
 
330
        """Returns number of entries."""
 
331
        return len(self._byid)
 
332
 
 
333
 
 
334
    def iter_entries(self, from_dir=None):
 
335
        """Return (path, entry) pairs, in order by name."""
 
336
        if from_dir == None:
 
337
            assert self.root
 
338
            from_dir = self.root
 
339
        elif isinstance(from_dir, basestring):
 
340
            from_dir = self._byid[from_dir]
 
341
            
 
342
        kids = from_dir.children.items()
 
343
        kids.sort()
 
344
        for name, ie in kids:
 
345
            yield name, ie
 
346
            if ie.kind == 'directory':
 
347
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
348
                    yield '/'.join((name, cn)), cie
 
349
                    
 
350
 
 
351
 
 
352
    def directories(self):
 
353
        """Return (path, entry) pairs for all directories.
 
354
        """
 
355
        def descend(parent_ie):
 
356
            parent_name = parent_ie.name
 
357
            yield parent_name, parent_ie
 
358
 
 
359
            # directory children in sorted order
 
360
            dn = []
 
361
            for ie in parent_ie.children.itervalues():
 
362
                if ie.kind == 'directory':
 
363
                    dn.append((ie.name, ie))
 
364
            dn.sort()
 
365
            
 
366
            for name, child_ie in dn:
 
367
                for sub_name, sub_ie in descend(child_ie):
 
368
                    yield appendpath(parent_name, sub_name), sub_ie
 
369
 
 
370
        for name, ie in descend(self.root):
 
371
            yield name, ie
 
372
        
 
373
 
 
374
 
 
375
    def __contains__(self, file_id):
 
376
        """True if this entry contains a file with given id.
 
377
 
 
378
        >>> inv = Inventory()
 
379
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
380
        >>> '123' in inv
 
381
        True
 
382
        >>> '456' in inv
 
383
        False
 
384
        """
 
385
        return file_id in self._byid
 
386
 
 
387
 
 
388
    def __getitem__(self, file_id):
 
389
        """Return the entry for given file_id.
 
390
 
 
391
        >>> inv = Inventory()
 
392
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
393
        >>> inv['123123'].name
 
394
        'hello.c'
 
395
        """
 
396
        if file_id == None:
 
397
            raise BzrError("can't look up file_id None")
 
398
            
 
399
        try:
 
400
            return self._byid[file_id]
 
401
        except KeyError:
 
402
            raise BzrError("file_id {%s} not in inventory" % file_id)
 
403
 
 
404
 
 
405
    def get_child(self, parent_id, filename):
 
406
        return self[parent_id].children.get(filename)
 
407
 
 
408
 
 
409
    def add(self, entry):
 
410
        """Add entry to inventory.
 
411
 
 
412
        To add  a file to a branch ready to be committed, use Branch.add,
 
413
        which calls this."""
 
414
        if entry.file_id in self._byid:
 
415
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
416
 
 
417
        try:
 
418
            parent = self._byid[entry.parent_id]
 
419
        except KeyError:
 
420
            bailout("parent_id {%s} not in inventory" % entry.parent_id)
 
421
 
 
422
        if parent.children.has_key(entry.name):
 
423
            bailout("%s is already versioned" %
 
424
                    appendpath(self.id2path(parent.file_id), entry.name))
 
425
 
 
426
        self._byid[entry.file_id] = entry
 
427
        parent.children[entry.name] = entry
 
428
 
 
429
 
 
430
    def add_path(self, relpath, kind, file_id=None):
 
431
        """Add entry from a path.
 
432
 
 
433
        The immediate parent must already be versioned"""
 
434
        parts = bzrlib.osutils.splitpath(relpath)
 
435
        if len(parts) == 0:
 
436
            bailout("cannot re-add root of inventory")
 
437
 
 
438
        if file_id == None:
 
439
            file_id = bzrlib.branch.gen_file_id(relpath)
 
440
 
 
441
        parent_id = self.path2id(parts[:-1])
 
442
        assert parent_id != None
 
443
        ie = InventoryEntry(file_id, parts[-1],
 
444
                            kind=kind, parent_id=parent_id)
 
445
        return self.add(ie)
 
446
 
 
447
 
 
448
    def __delitem__(self, file_id):
 
449
        """Remove entry by id.
 
450
 
 
451
        >>> inv = Inventory()
 
452
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
453
        >>> '123' in inv
 
454
        True
 
455
        >>> del inv['123']
 
456
        >>> '123' in inv
 
457
        False
 
458
        """
 
459
        ie = self[file_id]
 
460
 
 
461
        assert self[ie.parent_id].children[ie.name] == ie
 
462
        
 
463
        # TODO: Test deleting all children; maybe hoist to a separate
 
464
        # deltree method?
 
465
        if ie.kind == 'directory':
 
466
            for cie in ie.children.values():
 
467
                del self[cie.file_id]
 
468
            del ie.children
 
469
 
 
470
        del self._byid[file_id]
 
471
        del self[ie.parent_id].children[ie.name]
 
472
 
 
473
 
 
474
    def id_set(self):
 
475
        return Set(self._byid)
 
476
 
 
477
 
 
478
    def to_element(self):
 
479
        """Convert to XML Element"""
 
480
        e = Element('inventory')
 
481
        e.text = '\n'
 
482
        for path, ie in self.iter_entries():
 
483
            e.append(ie.to_element())
 
484
        return e
 
485
    
 
486
 
 
487
    def from_element(cls, elt):
 
488
        """Construct from XML Element
 
489
 
 
490
        >>> inv = Inventory()
 
491
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
 
492
        >>> elt = inv.to_element()
 
493
        >>> inv2 = Inventory.from_element(elt)
 
494
        >>> inv2 == inv
 
495
        True
 
496
        """
 
497
        assert elt.tag == 'inventory'
 
498
        o = cls()
 
499
        for e in elt:
 
500
            o.add(InventoryEntry.from_element(e))
 
501
        return o
 
502
        
 
503
    from_element = classmethod(from_element)
 
504
 
 
505
 
 
506
    def __cmp__(self, other):
 
507
        """Compare two sets by comparing their contents.
 
508
 
 
509
        >>> i1 = Inventory()
 
510
        >>> i2 = Inventory()
 
511
        >>> i1 == i2
 
512
        True
 
513
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
514
        >>> i1 == i2
 
515
        False
 
516
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
517
        >>> i1 == i2
 
518
        True
 
519
        """
 
520
        if self is other:
 
521
            return 0
 
522
        
 
523
        if not isinstance(other, Inventory):
 
524
            return NotImplemented
 
525
 
 
526
        if self.id_set() ^ other.id_set():
 
527
            return 1
 
528
 
 
529
        for file_id in self._byid:
 
530
            c = cmp(self[file_id], other[file_id])
 
531
            if c: return c
 
532
 
 
533
        return 0
 
534
 
 
535
 
 
536
    def get_idpath(self, file_id):
 
537
        """Return a list of file_ids for the path to an entry.
 
538
 
 
539
        The list contains one element for each directory followed by
 
540
        the id of the file itself.  So the length of the returned list
 
541
        is equal to the depth of the file in the tree, counting the
 
542
        root directory as depth 1.
 
543
        """
 
544
        p = []
 
545
        while file_id != None:
 
546
            try:
 
547
                ie = self._byid[file_id]
 
548
            except KeyError:
 
549
                bailout("file_id {%s} not found in inventory" % file_id)
 
550
            p.insert(0, ie.file_id)
 
551
            file_id = ie.parent_id
 
552
        return p
 
553
 
 
554
 
 
555
    def id2path(self, file_id):
 
556
        """Return as a list the path to file_id."""
 
557
 
 
558
        # get all names, skipping root
 
559
        p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
 
560
        return '/'.join(p)
 
561
            
 
562
 
 
563
 
 
564
    def path2id(self, name):
 
565
        """Walk down through directories to return entry of last component.
 
566
 
 
567
        names may be either a list of path components, or a single
 
568
        string, in which case it is automatically split.
 
569
 
 
570
        This returns the entry of the last component in the path,
 
571
        which may be either a file or a directory.
 
572
 
 
573
        Returns None iff the path is not found.
 
574
        """
 
575
        if isinstance(name, types.StringTypes):
 
576
            name = splitpath(name)
 
577
 
 
578
        mutter("lookup path %r" % name)
 
579
 
 
580
        parent = self.root
 
581
        for f in name:
 
582
            try:
 
583
                cie = parent.children[f]
 
584
                assert cie.name == f
 
585
                assert cie.parent_id == parent.file_id
 
586
                parent = cie
 
587
            except KeyError:
 
588
                # or raise an error?
 
589
                return None
 
590
 
 
591
        return parent.file_id
 
592
 
 
593
 
 
594
    def has_filename(self, names):
 
595
        return bool(self.path2id(names))
 
596
 
 
597
 
 
598
    def has_id(self, file_id):
 
599
        return self._byid.has_key(file_id)
 
600
 
 
601
 
 
602
    def rename(self, file_id, new_parent_id, new_name):
 
603
        """Move a file within the inventory.
 
604
 
 
605
        This can change either the name, or the parent, or both.
 
606
 
 
607
        This does not move the working file."""
 
608
        if not is_valid_name(new_name):
 
609
            bailout("not an acceptable filename: %r" % new_name)
 
610
 
 
611
        new_parent = self._byid[new_parent_id]
 
612
        if new_name in new_parent.children:
 
613
            bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
614
 
 
615
        new_parent_idpath = self.get_idpath(new_parent_id)
 
616
        if file_id in new_parent_idpath:
 
617
            bailout("cannot move directory %r into a subdirectory of itself, %r"
 
618
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
619
 
 
620
        file_ie = self._byid[file_id]
 
621
        old_parent = self._byid[file_ie.parent_id]
 
622
 
 
623
        # TODO: Don't leave things messed up if this fails
 
624
 
 
625
        del old_parent.children[file_ie.name]
 
626
        new_parent.children[new_name] = file_ie
 
627
        
 
628
        file_ie.name = new_name
 
629
        file_ie.parent_id = new_parent_id
 
630
 
 
631
 
 
632
 
 
633
 
 
634
_NAME_RE = re.compile(r'^[^/\\]+$')
 
635
 
 
636
def is_valid_name(name):
 
637
    return bool(_NAME_RE.match(name))