/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-05 08:00:35 UTC
  • Revision ID: mbp@sourcefrog.net-20050905080035-e0439293f8b6b9f9
- start splitting code for xml (de)serialization away from objects
  preparatory to supporting multiple formats by a single library

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