~bzr-pqm/bzr/bzr.dev

1 by mbp at sourcefrog
import from baz patch-364
1
#! /usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
"""Inventories map files to their name in a revision."""
19
20
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
__author__ = "Martin Pool <mbp@canonical.com>"
23
24
import sys, os.path, types
25
from sets import Set
26
7 by mbp at sourcefrog
depend only on regular ElementTree installation
27
try:
28
    from cElementTree import Element, ElementTree, SubElement
29
except ImportError:
30
    from elementtree import Element, ElementTree, SubElement
31
1 by mbp at sourcefrog
import from baz patch-364
32
from xml import XMLMixin
33
from errors import bailout
34
from osutils import uuid, quotefn, splitpath, joinpath, appendpath
35
from trace import mutter
36
37
class InventoryEntry(XMLMixin):
38
    """Description of a versioned file.
39
40
    An InventoryEntry has the following fields, which are also
41
    present in the XML inventory-entry element:
42
43
    * *file_id*
44
    * *name*: (only the basename within the directory, must not
45
      contain slashes)
46
    * *kind*: "directory" or "file"
47
    * *directory_id*: (if absent/null means the branch root directory)
48
    * *text_sha1*: only for files
49
    * *text_size*: in bytes, only for files 
50
    * *text_id*: identifier for the text version, only for files
51
52
    InventoryEntries can also exist inside a WorkingTree
53
    inventory, in which case they are not yet bound to a
54
    particular revision of the file.  In that case the text_sha1,
55
    text_size and text_id are absent.
56
57
58
    >>> i = Inventory()
59
    >>> i.path2id('')
60
    >>> i.add(InventoryEntry('123', 'src', kind='directory'))
61
    >>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
62
    >>> for j in i.iter_entries():
63
    ...   print j
64
    ... 
65
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
66
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
67
    >>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
68
    Traceback (most recent call last):
69
    ...
70
    BzrError: ('inventory already contains entry with id {2323}', [])
71
    >>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
72
    >>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
73
    >>> i.path2id('src/wibble')
74
    '2325'
75
    >>> '2325' in i
76
    True
77
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
78
    >>> i['2326']
79
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
    >>> for j in i.iter_entries():
81
    ...     print j[0]
82
    ...     assert i.path2id(j[0])
83
    ... 
84
    src
85
    src/bye.c
86
    src/hello.c
87
    src/wibble
88
    src/wibble/wibble.c
89
    >>> i.id2path('2326')
90
    'src/wibble/wibble.c'
91
92
    :todo: Maybe also keep the full path of the entry, and the children?
93
           But those depend on its position within a particular inventory, and
94
           it would be nice not to need to hold the backpointer here.
95
    """
96
    def __init__(self, file_id, name, kind='file', text_id=None,
97
                 parent_id=None):
98
        """Create an InventoryEntry
99
        
100
        The filename must be a single component, relative to the
101
        parent directory; it cannot be a whole path or relative name.
102
103
        >>> e = InventoryEntry('123', 'hello.c')
104
        >>> e.name
105
        'hello.c'
106
        >>> e.file_id
107
        '123'
108
        >>> e = InventoryEntry('123', 'src/hello.c')
109
        Traceback (most recent call last):
110
        BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
111
        """
112
        
113
        if len(splitpath(name)) != 1:
114
            bailout('InventoryEntry name is not a simple filename: %r'
115
                    % name)
116
        
117
        self.file_id = file_id
118
        self.name = name
119
        assert kind in ['file', 'directory']
120
        self.kind = kind
121
        self.text_id = text_id
122
        self.parent_id = parent_id
123
        self.text_sha1 = None
124
        self.text_size = None
125
126
127
    def copy(self):
128
        other = InventoryEntry(self.file_id, self.name, self.kind,
129
                               self.text_id, self.parent_id)
130
        other.text_sha1 = self.text_sha1
131
        other.text_size = self.text_size
132
        return other
133
134
135
    def __repr__(self):
136
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
137
                % (self.__class__.__name__,
138
                   self.file_id,
139
                   self.name,
140
                   self.kind,
141
                   self.parent_id))
142
143
    
144
    def to_element(self):
145
        """Convert to XML element"""
146
        e = Element('entry')
147
148
        e.set('name', self.name)
149
        e.set('file_id', self.file_id)
150
        e.set('kind', self.kind)
151
152
        if self.text_size is not None:
153
            e.set('text_size', '%d' % self.text_size)
154
            
155
        for f in ['text_id', 'text_sha1', 'parent_id']:
156
            v = getattr(self, f)
157
            if v is not None:
158
                e.set(f, v)
159
160
        e.tail = '\n'
161
            
162
        return e
163
164
165
    def from_element(cls, elt):
166
        assert elt.tag == 'entry'
167
        self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
168
        self.text_id = elt.get('text_id')
169
        self.text_sha1 = elt.get('text_sha1')
170
        self.parent_id = elt.get('parent_id')
171
        
172
        ## mutter("read inventoryentry: %r" % (elt.attrib))
173
174
        v = elt.get('text_size')
175
        self.text_size = v and int(v)
176
177
        return self
178
            
179
180
    from_element = classmethod(from_element)
181
182
    def __cmp__(self, other):
183
        if self is other:
184
            return 0
185
        if not isinstance(other, InventoryEntry):
186
            return NotImplemented
187
188
        return cmp(self.file_id, other.file_id) \
189
               or cmp(self.name, other.name) \
190
               or cmp(self.text_sha1, other.text_sha1) \
191
               or cmp(self.text_size, other.text_size) \
192
               or cmp(self.text_id, other.text_id) \
193
               or cmp(self.parent_id, other.parent_id) \
194
               or cmp(self.kind, other.kind)
195
196
197
198
class Inventory(XMLMixin):
199
    """Inventory of versioned files in a tree.
200
201
    An Inventory acts like a set of InventoryEntry items.  You can
202
    also look files up by their file_id or name.
203
    
204
    May be read from and written to a metadata file in a tree.  To
205
    manipulate the inventory (for example to add a file), it is read
206
    in, modified, and then written back out.
207
208
    The inventory represents a typical unix file tree, with
209
    directories containing files and subdirectories.  We never store
210
    the full path to a file, because renaming a directory implicitly
211
    moves all of its contents.  This class internally maintains a
212
    lookup tree that allows the children under a directory to be
213
    returned quickly.
214
215
    InventoryEntry objects must not be modified after they are
216
    inserted.
217
218
    >>> inv = Inventory()
219
    >>> inv.write_xml(sys.stdout)
220
    <inventory>
221
    </inventory>
222
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
223
    >>> inv['123-123'].name
224
    'hello.c'
225
    >>> for file_id in inv: print file_id
226
    ...
227
    123-123
228
229
    May be treated as an iterator or set to look up file ids:
230
    
231
    >>> bool(inv.path2id('hello.c'))
232
    True
233
    >>> '123-123' in inv
234
    True
235
236
    May also look up by name:
237
238
    >>> [x[0] for x in inv.iter_entries()]
239
    ['hello.c']
240
    
241
    >>> inv.write_xml(sys.stdout)
242
    <inventory>
243
    <entry file_id="123-123" kind="file" name="hello.c" />
244
    </inventory>
245
246
    """
247
248
    ## TODO: Clear up handling of files in subdirectories; we probably
249
    ## do want to be able to just look them up by name but this
250
    ## probably means gradually walking down the path, looking up as we go.
251
252
    ## TODO: Make sure only canonical filenames are stored.
253
254
    ## TODO: Do something sensible about the possible collisions on
255
    ## case-losing filesystems.  Perhaps we should just always forbid
256
    ## such collisions.
257
258
    ## _tree should probably just be stored as
259
    ## InventoryEntry._children on each directory.
260
261
    def __init__(self):
262
        """Create or read an inventory.
263
264
        If a working directory is specified, the inventory is read
265
        from there.  If the file is specified, read from that. If not,
266
        the inventory is created empty.
267
        """
268
        self._byid = dict()
269
270
        # _tree is indexed by parent_id; at each level a map from name
271
        # to ie.  The None entry is the root.
272
        self._tree = {None: {}}
273
274
275
    def __iter__(self):
276
        return iter(self._byid)
277
278
279
    def __len__(self):
280
        """Returns number of entries."""
281
        return len(self._byid)
282
283
284
    def iter_entries(self, parent_id=None):
285
        """Return (path, entry) pairs, in order by name."""
286
        kids = self._tree[parent_id].items()
287
        kids.sort()
288
        for name, ie in kids:
289
            yield name, ie
290
            if ie.kind == 'directory':
291
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
292
                    yield joinpath([name, cn]), cie
293
294
295
    def directories(self, include_root=True):
296
        """Return (path, entry) pairs for all directories.
297
        """
298
        if include_root:
299
            yield '', None
300
        for path, entry in self.iter_entries():
301
            if entry.kind == 'directory':
302
                yield path, entry
303
        
304
305
306
    def children(self, parent_id):
307
        """Return entries that are direct children of parent_id."""
308
        return self._tree[parent_id]
309
                    
310
311
312
    # TODO: return all paths and entries
313
314
315
    def __contains__(self, file_id):
316
        """True if this entry contains a file with given id.
317
318
        >>> inv = Inventory()
319
        >>> inv.add(InventoryEntry('123', 'foo.c'))
320
        >>> '123' in inv
321
        True
322
        >>> '456' in inv
323
        False
324
        """
325
        return file_id in self._byid
326
327
328
    def __getitem__(self, file_id):
329
        """Return the entry for given file_id.
330
331
        >>> inv = Inventory()
332
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
333
        >>> inv['123123'].name
334
        'hello.c'
335
        """
336
        return self._byid[file_id]
337
338
339
    def add(self, entry):
340
        """Add entry to inventory.
341
342
        To add  a file to a branch ready to be committed, use Branch.add,
343
        which calls this."""
344
        if entry.file_id in self:
345
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
346
347
        if entry.parent_id != None:
348
            if entry.parent_id not in self:
349
                bailout("parent_id %s of new entry not found in inventory"
350
                        % entry.parent_id)
351
            
352
        if self._tree[entry.parent_id].has_key(entry.name):
353
            bailout("%s is already versioned"
354
                    % appendpath(self.id2path(entry.parent_id), entry.name))
355
356
        self._byid[entry.file_id] = entry
357
        self._tree[entry.parent_id][entry.name] = entry
358
359
        if entry.kind == 'directory':
360
            self._tree[entry.file_id] = {}
361
362
363
    def __delitem__(self, file_id):
364
        """Remove entry by id.
365
366
        >>> inv = Inventory()
367
        >>> inv.add(InventoryEntry('123', 'foo.c'))
368
        >>> '123' in inv
369
        True
370
        >>> del inv['123']
371
        >>> '123' in inv
372
        False
373
        """
374
        ie = self[file_id]
375
376
        assert self._tree[ie.parent_id][ie.name] == ie
377
        
378
        # TODO: Test deleting all children; maybe hoist to a separate
379
        # deltree method?
380
        if ie.kind == 'directory':
381
            for cie in self._tree[file_id].values():
382
                del self[cie.file_id]
383
            del self._tree[file_id]
384
385
        del self._byid[file_id]
386
        del self._tree[ie.parent_id][ie.name]
387
388
389
    def id_set(self):
390
        return Set(self._byid)
391
392
393
    def to_element(self):
394
        """Convert to XML Element"""
395
        e = Element('inventory')
396
        e.text = '\n'
397
        for path, ie in self.iter_entries():
398
            e.append(ie.to_element())
399
        return e
400
    
401
402
    def from_element(cls, elt):
403
        """Construct from XML Element
404
405
        >>> inv = Inventory()
406
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
407
        >>> elt = inv.to_element()
408
        >>> inv2 = Inventory.from_element(elt)
409
        >>> inv2 == inv
410
        True
411
        """
412
        assert elt.tag == 'inventory'
413
        o = cls()
414
        for e in elt:
415
            o.add(InventoryEntry.from_element(e))
416
        return o
417
        
418
    from_element = classmethod(from_element)
419
420
421
    def __cmp__(self, other):
422
        """Compare two sets by comparing their contents.
423
424
        >>> i1 = Inventory()
425
        >>> i2 = Inventory()
426
        >>> i1 == i2
427
        True
428
        >>> i1.add(InventoryEntry('123', 'foo'))
429
        >>> i1 == i2
430
        False
431
        >>> i2.add(InventoryEntry('123', 'foo'))
432
        >>> i1 == i2
433
        True
434
        """
435
        if self is other:
436
            return 0
437
        
438
        if not isinstance(other, Inventory):
439
            return NotImplemented
440
441
        if self.id_set() ^ other.id_set():
442
            return 1
443
444
        for file_id in self._byid:
445
            c = cmp(self[file_id], other[file_id])
446
            if c: return c
447
448
        return 0
449
450
451
    def id2path(self, file_id):
452
        """Return as a list the path to file_id."""
453
        p = []
454
        while file_id != None:
455
            ie = self[file_id]
456
            p = [ie.name] + p
457
            file_id = ie.parent_id
458
        return joinpath(p)
459
            
460
461
462
    def path2id(self, name):
463
        """Walk down through directories to return entry of last component.
464
465
        names may be either a list of path components, or a single
466
        string, in which case it is automatically split.
467
468
        This returns the entry of the last component in the path,
469
        which may be either a file or a directory.
470
        """
471
        assert isinstance(name, types.StringTypes)
472
473
        parent_id = None
474
        for f in splitpath(name):
475
            try:
476
                cie = self._tree[parent_id][f]
477
                assert cie.name == f
478
                parent_id = cie.file_id
479
            except KeyError:
480
                # or raise an error?
481
                return None
482
483
        return parent_id
484
485
486
    def get_child(self, parent_id, child_name):
487
        return self._tree[parent_id].get(child_name)
488
489
490
    def has_filename(self, names):
491
        return bool(self.path2id(names))
492
493
494
    def has_id(self, file_id):
495
        assert isinstance(file_id, str)
496
        return self._byid.has_key(file_id)
497
498
499
500
if __name__ == '__main__':
501
    import doctest, inventory
502
    doctest.testmod(inventory)