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