/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: Robert Collins
  • Date: 2005-09-28 04:58:18 UTC
  • mto: (1092.2.19)
  • mto: This revision was merged to the branch mainline in revision 1391.
  • Revision ID: robertc@robertcollins.net-20050928045818-c5ce6c7cc796f6fc
patch from Rob Weir to correct bzr-man.py

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