2
# -*- coding: UTF-8 -*-
 
 
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.
 
 
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.
 
 
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
 
 
18
"""Inventories map files to their name in a revision."""
 
 
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
 
 
22
__author__ = "Martin Pool <mbp@canonical.com>"
 
 
24
import sys, os.path, types
 
 
28
    from cElementTree import Element, ElementTree, SubElement
 
 
30
    from elementtree.ElementTree import Element, ElementTree, SubElement
 
 
32
from xml import XMLMixin
 
 
33
from errors import bailout
 
 
34
from osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
 
35
from trace import mutter
 
 
37
class InventoryEntry(XMLMixin):
 
 
38
    """Description of a versioned file.
 
 
40
    An InventoryEntry has the following fields, which are also
 
 
41
    present in the XML inventory-entry element:
 
 
44
    * *name*: (only the basename within the directory, must not
 
 
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
 
 
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.
 
 
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():
 
 
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):
 
 
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')
 
 
77
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
 
79
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
80
    >>> for j in i.iter_entries():
 
 
82
    ...     assert i.path2id(j[0])
 
 
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.
 
 
96
    def __init__(self, file_id, name, kind='file', text_id=None,
 
 
98
        """Create an InventoryEntry
 
 
100
        The filename must be a single component, relative to the
 
 
101
        parent directory; it cannot be a whole path or relative name.
 
 
103
        >>> e = InventoryEntry('123', 'hello.c')
 
 
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'", [])
 
 
113
        if len(splitpath(name)) != 1:
 
 
114
            bailout('InventoryEntry name is not a simple filename: %r'
 
 
117
        self.file_id = file_id
 
 
119
        assert kind in ['file', 'directory']
 
 
121
        self.text_id = text_id
 
 
122
        self.parent_id = parent_id
 
 
123
        self.text_sha1 = None
 
 
124
        self.text_size = None
 
 
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
 
 
136
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
 
137
                % (self.__class__.__name__,
 
 
144
    def to_element(self):
 
 
145
        """Convert to XML element"""
 
 
148
        e.set('name', self.name)
 
 
149
        e.set('file_id', self.file_id)
 
 
150
        e.set('kind', self.kind)
 
 
152
        if self.text_size is not None:
 
 
153
            e.set('text_size', '%d' % self.text_size)
 
 
155
        for f in ['text_id', 'text_sha1', 'parent_id']:
 
 
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')
 
 
172
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
 
174
        v = elt.get('text_size')
 
 
175
        self.text_size = v and int(v)
 
 
180
    from_element = classmethod(from_element)
 
 
182
    def __cmp__(self, other):
 
 
185
        if not isinstance(other, InventoryEntry):
 
 
186
            return NotImplemented
 
 
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)
 
 
198
class Inventory(XMLMixin):
 
 
199
    """Inventory of versioned files in a tree.
 
 
201
    An Inventory acts like a set of InventoryEntry items.  You can
 
 
202
    also look files up by their file_id or name.
 
 
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.
 
 
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
 
 
215
    InventoryEntry objects must not be modified after they are
 
 
218
    >>> inv = Inventory()
 
 
219
    >>> inv.write_xml(sys.stdout)
 
 
222
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
 
223
    >>> inv['123-123'].name
 
 
225
    >>> for file_id in inv: print file_id
 
 
229
    May be treated as an iterator or set to look up file ids:
 
 
231
    >>> bool(inv.path2id('hello.c'))
 
 
236
    May also look up by name:
 
 
238
    >>> [x[0] for x in inv.iter_entries()]
 
 
241
    >>> inv.write_xml(sys.stdout)
 
 
243
    <entry file_id="123-123" kind="file" name="hello.c" />
 
 
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.
 
 
252
    ## TODO: Make sure only canonical filenames are stored.
 
 
254
    ## TODO: Do something sensible about the possible collisions on
 
 
255
    ## case-losing filesystems.  Perhaps we should just always forbid
 
 
258
    ## _tree should probably just be stored as
 
 
259
    ## InventoryEntry._children on each directory.
 
 
262
        """Create or read an inventory.
 
 
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.
 
 
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: {}}
 
 
276
        return iter(self._byid)
 
 
280
        """Returns number of entries."""
 
 
281
        return len(self._byid)
 
 
284
    def iter_entries(self, parent_id=None):
 
 
285
        """Return (path, entry) pairs, in order by name."""
 
 
286
        kids = self._tree[parent_id].items()
 
 
288
        for name, ie in kids:
 
 
290
            if ie.kind == 'directory':
 
 
291
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
 
 
292
                    yield joinpath([name, cn]), cie
 
 
295
    def directories(self, include_root=True):
 
 
296
        """Return (path, entry) pairs for all directories.
 
 
300
        for path, entry in self.iter_entries():
 
 
301
            if entry.kind == 'directory':
 
 
306
    def children(self, parent_id):
 
 
307
        """Return entries that are direct children of parent_id."""
 
 
308
        return self._tree[parent_id]
 
 
312
    # TODO: return all paths and entries
 
 
315
    def __contains__(self, file_id):
 
 
316
        """True if this entry contains a file with given id.
 
 
318
        >>> inv = Inventory()
 
 
319
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
 
325
        return file_id in self._byid
 
 
328
    def __getitem__(self, file_id):
 
 
329
        """Return the entry for given file_id.
 
 
331
        >>> inv = Inventory()
 
 
332
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
 
333
        >>> inv['123123'].name
 
 
336
        return self._byid[file_id]
 
 
339
    def add(self, entry):
 
 
340
        """Add entry to inventory.
 
 
342
        To add  a file to a branch ready to be committed, use Branch.add,
 
 
344
        if entry.file_id in self:
 
 
345
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
 
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"
 
 
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))
 
 
356
        self._byid[entry.file_id] = entry
 
 
357
        self._tree[entry.parent_id][entry.name] = entry
 
 
359
        if entry.kind == 'directory':
 
 
360
            self._tree[entry.file_id] = {}
 
 
363
    def __delitem__(self, file_id):
 
 
364
        """Remove entry by id.
 
 
366
        >>> inv = Inventory()
 
 
367
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
 
376
        assert self._tree[ie.parent_id][ie.name] == ie
 
 
378
        # TODO: Test deleting all children; maybe hoist to a separate
 
 
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]
 
 
385
        del self._byid[file_id]
 
 
386
        del self._tree[ie.parent_id][ie.name]
 
 
390
        return Set(self._byid)
 
 
393
    def to_element(self):
 
 
394
        """Convert to XML Element"""
 
 
395
        e = Element('inventory')
 
 
397
        for path, ie in self.iter_entries():
 
 
398
            e.append(ie.to_element())
 
 
402
    def from_element(cls, elt):
 
 
403
        """Construct from XML Element
 
 
405
        >>> inv = Inventory()
 
 
406
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
 
 
407
        >>> elt = inv.to_element()
 
 
408
        >>> inv2 = Inventory.from_element(elt)
 
 
412
        assert elt.tag == 'inventory'
 
 
415
            o.add(InventoryEntry.from_element(e))
 
 
418
    from_element = classmethod(from_element)
 
 
421
    def __cmp__(self, other):
 
 
422
        """Compare two sets by comparing their contents.
 
 
428
        >>> i1.add(InventoryEntry('123', 'foo'))
 
 
431
        >>> i2.add(InventoryEntry('123', 'foo'))
 
 
438
        if not isinstance(other, Inventory):
 
 
439
            return NotImplemented
 
 
441
        if self.id_set() ^ other.id_set():
 
 
444
        for file_id in self._byid:
 
 
445
            c = cmp(self[file_id], other[file_id])
 
 
451
    def id2path(self, file_id):
 
 
452
        """Return as a list the path to file_id."""
 
 
454
        while file_id != None:
 
 
457
            file_id = ie.parent_id
 
 
462
    def path2id(self, name):
 
 
463
        """Walk down through directories to return entry of last component.
 
 
465
        names may be either a list of path components, or a single
 
 
466
        string, in which case it is automatically split.
 
 
468
        This returns the entry of the last component in the path,
 
 
469
        which may be either a file or a directory.
 
 
471
        assert isinstance(name, types.StringTypes)
 
 
474
        for f in splitpath(name):
 
 
476
                cie = self._tree[parent_id][f]
 
 
478
                parent_id = cie.file_id
 
 
486
    def get_child(self, parent_id, child_name):
 
 
487
        return self._tree[parent_id].get(child_name)
 
 
490
    def has_filename(self, names):
 
 
491
        return bool(self.path2id(names))
 
 
494
    def has_id(self, file_id):
 
 
495
        assert isinstance(file_id, str)
 
 
496
        return self._byid.has_key(file_id)
 
 
500
if __name__ == '__main__':
 
 
501
    import doctest, inventory
 
 
502
    doctest.testmod(inventory)