/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: Martin Pool
  • Date: 2005-09-22 12:57:22 UTC
  • Revision ID: mbp@sourcefrog.net-20050922125722-b267ac97a33cba4e
- turn off branch weave caches when we're done with checking

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
 
 
18
# TODO: Maybe also keep the full path of the entry, and the children?
 
19
# But those depend on its position within a particular inventory, and
 
20
# it would be nice not to need to hold the backpointer here.
 
21
 
 
22
# TODO: Perhaps split InventoryEntry into subclasses for files,
 
23
# directories, etc etc.
 
24
 
 
25
 
 
26
# This should really be an id randomly assigned when the tree is
 
27
# created, but it's not for now.
 
28
ROOT_ID = "TREE_ROOT"
 
29
 
 
30
 
 
31
import sys, os.path, types, re
 
32
 
 
33
import bzrlib
 
34
from bzrlib.errors import BzrError, BzrCheckError
 
35
 
 
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
37
from bzrlib.trace import mutter
 
38
from bzrlib.errors import NotVersionedError
 
39
        
 
40
 
 
41
class InventoryEntry(object):
 
42
    """Description of a versioned file.
 
43
 
 
44
    An InventoryEntry has the following fields, which are also
 
45
    present in the XML inventory-entry element:
 
46
 
 
47
    file_id
 
48
 
 
49
    name
 
50
        (within the parent directory)
 
51
 
 
52
    kind
 
53
        'directory' or 'file'
 
54
 
 
55
    parent_id
 
56
        file_id of the parent directory, or ROOT_ID
 
57
 
 
58
    name_version
 
59
        the revision_id in which the name or parent of this file was
 
60
        last changed
 
61
 
 
62
    text_sha1
 
63
        sha-1 of the text of the file
 
64
        
 
65
    text_size
 
66
        size in bytes of the text of the file
 
67
        
 
68
    text_version
 
69
        the revision_id in which the text of this file was introduced
 
70
 
 
71
    (reading a version 4 tree created a text_id field.)
 
72
 
 
73
    >>> i = Inventory()
 
74
    >>> i.path2id('')
 
75
    'TREE_ROOT'
 
76
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
77
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
 
78
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
79
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
 
80
    >>> for j in i.iter_entries():
 
81
    ...   print j
 
82
    ... 
 
83
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
84
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
85
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
86
    Traceback (most recent call last):
 
87
    ...
 
88
    BzrError: inventory already contains entry with id {2323}
 
89
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
90
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
91
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
92
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
 
93
    >>> i.path2id('src/wibble')
 
94
    '2325'
 
95
    >>> '2325' in i
 
96
    True
 
97
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
98
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
99
    >>> i['2326']
 
100
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
101
    >>> for j in i.iter_entries():
 
102
    ...     print j[0]
 
103
    ...     assert i.path2id(j[0])
 
104
    ... 
 
105
    src
 
106
    src/bye.c
 
107
    src/hello.c
 
108
    src/wibble
 
109
    src/wibble/wibble.c
 
110
    >>> i.id2path('2326')
 
111
    'src/wibble/wibble.c'
 
112
    """
 
113
    
 
114
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
115
                 'text_id', 'parent_id', 'children',
 
116
                 'text_version', 'name_version', ]
 
117
 
 
118
 
 
119
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
120
        """Create an InventoryEntry
 
121
        
 
122
        The filename must be a single component, relative to the
 
123
        parent directory; it cannot be a whole path or relative name.
 
124
 
 
125
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
126
        >>> e.name
 
127
        'hello.c'
 
128
        >>> e.file_id
 
129
        '123'
 
130
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
131
        Traceback (most recent call last):
 
132
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
133
        """
 
134
        assert isinstance(name, basestring), name
 
135
        if '/' in name or '\\' in name:
 
136
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
137
        
 
138
        self.text_version = None
 
139
        self.name_version = None
 
140
        self.text_sha1 = None
 
141
        self.text_size = None
 
142
        self.file_id = file_id
 
143
        self.name = name
 
144
        self.kind = kind
 
145
        self.text_id = text_id
 
146
        self.parent_id = parent_id
 
147
        if kind == 'directory':
 
148
            self.children = {}
 
149
        elif kind == 'file':
 
150
            pass
 
151
        else:
 
152
            raise BzrError("unhandled entry kind %r" % kind)
 
153
 
 
154
 
 
155
 
 
156
    def sorted_children(self):
 
157
        l = self.children.items()
 
158
        l.sort()
 
159
        return l
 
160
 
 
161
 
 
162
    def copy(self):
 
163
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
164
                               self.parent_id)
 
165
        other.text_id = self.text_id
 
166
        other.text_sha1 = self.text_sha1
 
167
        other.text_size = self.text_size
 
168
        other.text_version = self.text_version
 
169
        other.name_version = self.name_version
 
170
        # note that children are *not* copied; they're pulled across when
 
171
        # others are added
 
172
        return other
 
173
 
 
174
 
 
175
    def __repr__(self):
 
176
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
177
                % (self.__class__.__name__,
 
178
                   self.file_id,
 
179
                   self.name,
 
180
                   self.kind,
 
181
                   self.parent_id))
 
182
 
 
183
    
 
184
    def __eq__(self, other):
 
185
        if not isinstance(other, InventoryEntry):
 
186
            return NotImplemented
 
187
 
 
188
        return (self.file_id == other.file_id) \
 
189
               and (self.name == other.name) \
 
190
               and (self.text_sha1 == other.text_sha1) \
 
191
               and (self.text_size == other.text_size) \
 
192
               and (self.text_id == other.text_id) \
 
193
               and (self.parent_id == other.parent_id) \
 
194
               and (self.kind == other.kind) \
 
195
               and (self.text_version == other.text_version) \
 
196
               and (self.name_version == other.name_version)
 
197
 
 
198
 
 
199
    def __ne__(self, other):
 
200
        return not (self == other)
 
201
 
 
202
    def __hash__(self):
 
203
        raise ValueError('not hashable')
 
204
 
 
205
 
 
206
 
 
207
class RootEntry(InventoryEntry):
 
208
    def __init__(self, file_id):
 
209
        self.file_id = file_id
 
210
        self.children = {}
 
211
        self.kind = 'root_directory'
 
212
        self.parent_id = None
 
213
        self.name = ''
 
214
 
 
215
    def __eq__(self, other):
 
216
        if not isinstance(other, RootEntry):
 
217
            return NotImplemented
 
218
        
 
219
        return (self.file_id == other.file_id) \
 
220
               and (self.children == other.children)
 
221
 
 
222
 
 
223
 
 
224
class Inventory(object):
 
225
    """Inventory of versioned files in a tree.
 
226
 
 
227
    This describes which file_id is present at each point in the tree,
 
228
    and possibly the SHA-1 or other information about the file.
 
229
    Entries can be looked up either by path or by file_id.
 
230
 
 
231
    The inventory represents a typical unix file tree, with
 
232
    directories containing files and subdirectories.  We never store
 
233
    the full path to a file, because renaming a directory implicitly
 
234
    moves all of its contents.  This class internally maintains a
 
235
    lookup tree that allows the children under a directory to be
 
236
    returned quickly.
 
237
 
 
238
    InventoryEntry objects must not be modified after they are
 
239
    inserted, other than through the Inventory API.
 
240
 
 
241
    >>> inv = Inventory()
 
242
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
243
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
244
    >>> inv['123-123'].name
 
245
    'hello.c'
 
246
 
 
247
    May be treated as an iterator or set to look up file ids:
 
248
    
 
249
    >>> bool(inv.path2id('hello.c'))
 
250
    True
 
251
    >>> '123-123' in inv
 
252
    True
 
253
 
 
254
    May also look up by name:
 
255
 
 
256
    >>> [x[0] for x in inv.iter_entries()]
 
257
    ['hello.c']
 
258
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
259
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
260
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
261
    """
 
262
    def __init__(self, root_id=ROOT_ID):
 
263
        """Create or read an inventory.
 
264
 
 
265
        If a working directory is specified, the inventory is read
 
266
        from there.  If the file is specified, read from that. If not,
 
267
        the inventory is created empty.
 
268
 
 
269
        The inventory is created with a default root directory, with
 
270
        an id of None.
 
271
        """
 
272
        # We are letting Branch(init=True) create a unique inventory
 
273
        # root id. Rather than generating a random one here.
 
274
        #if root_id is None:
 
275
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
276
        self.root = RootEntry(root_id)
 
277
        self._byid = {self.root.file_id: self.root}
 
278
 
 
279
 
 
280
    def copy(self):
 
281
        other = Inventory(self.root.file_id)
 
282
        # copy recursively so we know directories will be added before
 
283
        # their children.  There are more efficient ways than this...
 
284
        for path, entry in self.iter_entries():
 
285
            if entry == self.root:
 
286
                continue
 
287
            other.add(entry.copy())
 
288
        return other
 
289
 
 
290
 
 
291
    def __iter__(self):
 
292
        return iter(self._byid)
 
293
 
 
294
 
 
295
    def __len__(self):
 
296
        """Returns number of entries."""
 
297
        return len(self._byid)
 
298
 
 
299
 
 
300
    def iter_entries(self, from_dir=None):
 
301
        """Return (path, entry) pairs, in order by name."""
 
302
        if from_dir == None:
 
303
            assert self.root
 
304
            from_dir = self.root
 
305
        elif isinstance(from_dir, basestring):
 
306
            from_dir = self._byid[from_dir]
 
307
            
 
308
        kids = from_dir.children.items()
 
309
        kids.sort()
 
310
        for name, ie in kids:
 
311
            yield name, ie
 
312
            if ie.kind == 'directory':
 
313
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
314
                    yield os.path.join(name, cn), cie
 
315
 
 
316
 
 
317
    def entries(self):
 
318
        """Return list of (path, ie) for all entries except the root.
 
319
 
 
320
        This may be faster than iter_entries.
 
321
        """
 
322
        accum = []
 
323
        def descend(dir_ie, dir_path):
 
324
            kids = dir_ie.children.items()
 
325
            kids.sort()
 
326
            for name, ie in kids:
 
327
                child_path = os.path.join(dir_path, name)
 
328
                accum.append((child_path, ie))
 
329
                if ie.kind == 'directory':
 
330
                    descend(ie, child_path)
 
331
 
 
332
        descend(self.root, '')
 
333
        return accum
 
334
 
 
335
 
 
336
    def directories(self):
 
337
        """Return (path, entry) pairs for all directories, including the root.
 
338
        """
 
339
        accum = []
 
340
        def descend(parent_ie, parent_path):
 
341
            accum.append((parent_path, parent_ie))
 
342
            
 
343
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
344
            kids.sort()
 
345
 
 
346
            for name, child_ie in kids:
 
347
                child_path = os.path.join(parent_path, name)
 
348
                descend(child_ie, child_path)
 
349
        descend(self.root, '')
 
350
        return accum
 
351
        
 
352
 
 
353
 
 
354
    def __contains__(self, file_id):
 
355
        """True if this entry contains a file with given id.
 
356
 
 
357
        >>> inv = Inventory()
 
358
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
359
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
360
        >>> '123' in inv
 
361
        True
 
362
        >>> '456' in inv
 
363
        False
 
364
        """
 
365
        return file_id in self._byid
 
366
 
 
367
 
 
368
    def __getitem__(self, file_id):
 
369
        """Return the entry for given file_id.
 
370
 
 
371
        >>> inv = Inventory()
 
372
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
373
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
374
        >>> inv['123123'].name
 
375
        'hello.c'
 
376
        """
 
377
        try:
 
378
            return self._byid[file_id]
 
379
        except KeyError:
 
380
            if file_id == None:
 
381
                raise BzrError("can't look up file_id None")
 
382
            else:
 
383
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
384
 
 
385
 
 
386
    def get_file_kind(self, file_id):
 
387
        return self._byid[file_id].kind
 
388
 
 
389
    def get_child(self, parent_id, filename):
 
390
        return self[parent_id].children.get(filename)
 
391
 
 
392
 
 
393
    def add(self, entry):
 
394
        """Add entry to inventory.
 
395
 
 
396
        To add  a file to a branch ready to be committed, use Branch.add,
 
397
        which calls this.
 
398
 
 
399
        Returns the new entry object.
 
400
        """
 
401
        if entry.file_id in self._byid:
 
402
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
403
 
 
404
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
405
            entry.parent_id = self.root.file_id
 
406
 
 
407
        try:
 
408
            parent = self._byid[entry.parent_id]
 
409
        except KeyError:
 
410
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
411
 
 
412
        if parent.children.has_key(entry.name):
 
413
            raise BzrError("%s is already versioned" %
 
414
                    appendpath(self.id2path(parent.file_id), entry.name))
 
415
 
 
416
        self._byid[entry.file_id] = entry
 
417
        parent.children[entry.name] = entry
 
418
        return entry
 
419
 
 
420
 
 
421
    def add_path(self, relpath, kind, file_id=None):
 
422
        """Add entry from a path.
 
423
 
 
424
        The immediate parent must already be versioned.
 
425
 
 
426
        Returns the new entry object."""
 
427
        from bzrlib.branch import gen_file_id
 
428
        
 
429
        parts = bzrlib.osutils.splitpath(relpath)
 
430
        if len(parts) == 0:
 
431
            raise BzrError("cannot re-add root of inventory")
 
432
 
 
433
        if file_id == None:
 
434
            file_id = gen_file_id(relpath)
 
435
 
 
436
        parent_path = parts[:-1]
 
437
        parent_id = self.path2id(parent_path)
 
438
        if parent_id == None:
 
439
            raise NotVersionedError(parent_path)
 
440
 
 
441
        ie = InventoryEntry(file_id, parts[-1],
 
442
                            kind=kind, parent_id=parent_id)
 
443
        return self.add(ie)
 
444
 
 
445
 
 
446
    def __delitem__(self, file_id):
 
447
        """Remove entry by id.
 
448
 
 
449
        >>> inv = Inventory()
 
450
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
451
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
452
        >>> '123' in inv
 
453
        True
 
454
        >>> del inv['123']
 
455
        >>> '123' in inv
 
456
        False
 
457
        """
 
458
        ie = self[file_id]
 
459
 
 
460
        assert self[ie.parent_id].children[ie.name] == ie
 
461
        
 
462
        # TODO: Test deleting all children; maybe hoist to a separate
 
463
        # deltree method?
 
464
        if ie.kind == 'directory':
 
465
            for cie in ie.children.values():
 
466
                del self[cie.file_id]
 
467
            del ie.children
 
468
 
 
469
        del self._byid[file_id]
 
470
        del self[ie.parent_id].children[ie.name]
 
471
 
 
472
 
 
473
    def __eq__(self, other):
 
474
        """Compare two sets by comparing their contents.
 
475
 
 
476
        >>> i1 = Inventory()
 
477
        >>> i2 = Inventory()
 
478
        >>> i1 == i2
 
479
        True
 
480
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
481
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
482
        >>> i1 == i2
 
483
        False
 
484
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
485
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
486
        >>> i1 == i2
 
487
        True
 
488
        """
 
489
        if not isinstance(other, Inventory):
 
490
            return NotImplemented
 
491
 
 
492
        if len(self._byid) != len(other._byid):
 
493
            # shortcut: obviously not the same
 
494
            return False
 
495
 
 
496
        return self._byid == other._byid
 
497
 
 
498
 
 
499
    def __ne__(self, other):
 
500
        return not self.__eq__(other)
 
501
 
 
502
 
 
503
    def __hash__(self):
 
504
        raise ValueError('not hashable')
 
505
 
 
506
 
 
507
    def get_idpath(self, file_id):
 
508
        """Return a list of file_ids for the path to an entry.
 
509
 
 
510
        The list contains one element for each directory followed by
 
511
        the id of the file itself.  So the length of the returned list
 
512
        is equal to the depth of the file in the tree, counting the
 
513
        root directory as depth 1.
 
514
        """
 
515
        p = []
 
516
        while file_id != None:
 
517
            try:
 
518
                ie = self._byid[file_id]
 
519
            except KeyError:
 
520
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
521
            p.insert(0, ie.file_id)
 
522
            file_id = ie.parent_id
 
523
        return p
 
524
 
 
525
 
 
526
    def id2path(self, file_id):
 
527
        """Return as a list the path to file_id."""
 
528
 
 
529
        # get all names, skipping root
 
530
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
531
        return os.sep.join(p)
 
532
            
 
533
 
 
534
 
 
535
    def path2id(self, name):
 
536
        """Walk down through directories to return entry of last component.
 
537
 
 
538
        names may be either a list of path components, or a single
 
539
        string, in which case it is automatically split.
 
540
 
 
541
        This returns the entry of the last component in the path,
 
542
        which may be either a file or a directory.
 
543
 
 
544
        Returns None iff the path is not found.
 
545
        """
 
546
        if isinstance(name, types.StringTypes):
 
547
            name = splitpath(name)
 
548
 
 
549
        mutter("lookup path %r" % name)
 
550
 
 
551
        parent = self.root
 
552
        for f in name:
 
553
            try:
 
554
                cie = parent.children[f]
 
555
                assert cie.name == f
 
556
                assert cie.parent_id == parent.file_id
 
557
                parent = cie
 
558
            except KeyError:
 
559
                # or raise an error?
 
560
                return None
 
561
 
 
562
        return parent.file_id
 
563
 
 
564
 
 
565
    def has_filename(self, names):
 
566
        return bool(self.path2id(names))
 
567
 
 
568
 
 
569
    def has_id(self, file_id):
 
570
        return self._byid.has_key(file_id)
 
571
 
 
572
 
 
573
    def rename(self, file_id, new_parent_id, new_name):
 
574
        """Move a file within the inventory.
 
575
 
 
576
        This can change either the name, or the parent, or both.
 
577
 
 
578
        This does not move the working file."""
 
579
        if not is_valid_name(new_name):
 
580
            raise BzrError("not an acceptable filename: %r" % new_name)
 
581
 
 
582
        new_parent = self._byid[new_parent_id]
 
583
        if new_name in new_parent.children:
 
584
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
585
 
 
586
        new_parent_idpath = self.get_idpath(new_parent_id)
 
587
        if file_id in new_parent_idpath:
 
588
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
589
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
590
 
 
591
        file_ie = self._byid[file_id]
 
592
        old_parent = self._byid[file_ie.parent_id]
 
593
 
 
594
        # TODO: Don't leave things messed up if this fails
 
595
 
 
596
        del old_parent.children[file_ie.name]
 
597
        new_parent.children[new_name] = file_ie
 
598
        
 
599
        file_ie.name = new_name
 
600
        file_ie.parent_id = new_parent_id
 
601
 
 
602
 
 
603
 
 
604
 
 
605
_NAME_RE = None
 
606
 
 
607
def is_valid_name(name):
 
608
    global _NAME_RE
 
609
    if _NAME_RE == None:
 
610
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
611
        
 
612
    return bool(_NAME_RE.match(name))