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
 
 
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
 
37
from bzrlib.trace import mutter
 
 
39
class InventoryEntry(XMLMixin):
 
 
40
    """Description of a versioned file.
 
 
42
    An InventoryEntry has the following fields, which are also
 
 
43
    present in the XML inventory-entry element:
 
 
46
    * *name*: (only the basename within the directory, must not
 
 
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
 
 
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.
 
 
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():
 
 
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):
 
 
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')
 
 
79
    >>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
 
 
81
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
82
    >>> for j in i.iter_entries():
 
 
84
    ...     assert i.path2id(j[0])
 
 
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.
 
 
98
    def __init__(self, file_id, name, kind='file', text_id=None,
 
 
100
        """Create an InventoryEntry
 
 
102
        The filename must be a single component, relative to the
 
 
103
        parent directory; it cannot be a whole path or relative name.
 
 
105
        >>> e = InventoryEntry('123', 'hello.c')
 
 
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'", [])
 
 
115
        if len(splitpath(name)) != 1:
 
 
116
            bailout('InventoryEntry name is not a simple filename: %r'
 
 
119
        self.file_id = file_id
 
 
121
        assert kind in ['file', 'directory']
 
 
123
        self.text_id = text_id
 
 
124
        self.parent_id = parent_id
 
 
125
        self.text_sha1 = None
 
 
126
        self.text_size = None
 
 
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
 
 
138
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
 
139
                % (self.__class__.__name__,
 
 
146
    def to_element(self):
 
 
147
        """Convert to XML element"""
 
 
150
        e.set('name', self.name)
 
 
151
        e.set('file_id', self.file_id)
 
 
152
        e.set('kind', self.kind)
 
 
154
        if self.text_size is not None:
 
 
155
            e.set('text_size', '%d' % self.text_size)
 
 
157
        for f in ['text_id', 'text_sha1', 'parent_id']:
 
 
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')
 
 
174
        ## mutter("read inventoryentry: %r" % (elt.attrib))
 
 
176
        v = elt.get('text_size')
 
 
177
        self.text_size = v and int(v)
 
 
182
    from_element = classmethod(from_element)
 
 
184
    def __cmp__(self, other):
 
 
187
        if not isinstance(other, InventoryEntry):
 
 
188
            return NotImplemented
 
 
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)
 
 
200
class Inventory(XMLMixin):
 
 
201
    """Inventory of versioned files in a tree.
 
 
203
    An Inventory acts like a set of InventoryEntry items.  You can
 
 
204
    also look files up by their file_id or name.
 
 
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.
 
 
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
 
 
217
    InventoryEntry objects must not be modified after they are
 
 
220
    >>> inv = Inventory()
 
 
221
    >>> inv.write_xml(sys.stdout)
 
 
224
    >>> inv.add(InventoryEntry('123-123', 'hello.c'))
 
 
225
    >>> inv['123-123'].name
 
 
227
    >>> for file_id in inv: print file_id
 
 
231
    May be treated as an iterator or set to look up file ids:
 
 
233
    >>> bool(inv.path2id('hello.c'))
 
 
238
    May also look up by name:
 
 
240
    >>> [x[0] for x in inv.iter_entries()]
 
 
243
    >>> inv.write_xml(sys.stdout)
 
 
245
    <entry file_id="123-123" kind="file" name="hello.c" />
 
 
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.
 
 
254
    ## TODO: Make sure only canonical filenames are stored.
 
 
256
    ## TODO: Do something sensible about the possible collisions on
 
 
257
    ## case-losing filesystems.  Perhaps we should just always forbid
 
 
260
    ## _tree should probably just be stored as
 
 
261
    ## InventoryEntry._children on each directory.
 
 
264
        """Create or read an inventory.
 
 
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.
 
 
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: {}}
 
 
278
        return iter(self._byid)
 
 
282
        """Returns number of entries."""
 
 
283
        return len(self._byid)
 
 
286
    def iter_entries(self, parent_id=None):
 
 
287
        """Return (path, entry) pairs, in order by name."""
 
 
288
        kids = self._tree[parent_id].items()
 
 
290
        for name, ie in kids:
 
 
292
            if ie.kind == 'directory':
 
 
293
                for cn, cie in self.iter_entries(parent_id=ie.file_id):
 
 
294
                    yield joinpath([name, cn]), cie
 
 
297
    def directories(self, include_root=True):
 
 
298
        """Return (path, entry) pairs for all directories.
 
 
302
        for path, entry in self.iter_entries():
 
 
303
            if entry.kind == 'directory':
 
 
308
    def children(self, parent_id):
 
 
309
        """Return entries that are direct children of parent_id."""
 
 
310
        return self._tree[parent_id]
 
 
314
    # TODO: return all paths and entries
 
 
317
    def __contains__(self, file_id):
 
 
318
        """True if this entry contains a file with given id.
 
 
320
        >>> inv = Inventory()
 
 
321
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
 
327
        return file_id in self._byid
 
 
330
    def __getitem__(self, file_id):
 
 
331
        """Return the entry for given file_id.
 
 
333
        >>> inv = Inventory()
 
 
334
        >>> inv.add(InventoryEntry('123123', 'hello.c'))
 
 
335
        >>> inv['123123'].name
 
 
338
        return self._byid[file_id]
 
 
341
    def add(self, entry):
 
 
342
        """Add entry to inventory.
 
 
344
        To add  a file to a branch ready to be committed, use Branch.add,
 
 
346
        if entry.file_id in self:
 
 
347
            bailout("inventory already contains entry with id {%s}" % entry.file_id)
 
 
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"
 
 
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))
 
 
358
        self._byid[entry.file_id] = entry
 
 
359
        self._tree[entry.parent_id][entry.name] = entry
 
 
361
        if entry.kind == 'directory':
 
 
362
            self._tree[entry.file_id] = {}
 
 
365
    def add_path(self, relpath, kind, file_id=None):
 
 
366
        """Add entry from a path.
 
 
368
        The immediate parent must already be versioned"""
 
 
369
        parts = bzrlib.osutils.splitpath(relpath)
 
 
371
            bailout("cannot re-add root of inventory")
 
 
374
            file_id = bzrlib.branch.gen_file_id(relpath)
 
 
376
        parent_id = self.path2id(parts[:-1])
 
 
377
        ie = InventoryEntry(file_id, parts[-1],
 
 
378
                            kind=kind, parent_id=parent_id)
 
 
382
    def __delitem__(self, file_id):
 
 
383
        """Remove entry by id.
 
 
385
        >>> inv = Inventory()
 
 
386
        >>> inv.add(InventoryEntry('123', 'foo.c'))
 
 
395
        assert self._tree[ie.parent_id][ie.name] == ie
 
 
397
        # TODO: Test deleting all children; maybe hoist to a separate
 
 
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]
 
 
404
        del self._byid[file_id]
 
 
405
        del self._tree[ie.parent_id][ie.name]
 
 
409
        return Set(self._byid)
 
 
412
    def to_element(self):
 
 
413
        """Convert to XML Element"""
 
 
414
        e = Element('inventory')
 
 
416
        for path, ie in self.iter_entries():
 
 
417
            e.append(ie.to_element())
 
 
421
    def from_element(cls, elt):
 
 
422
        """Construct from XML Element
 
 
424
        >>> inv = Inventory()
 
 
425
        >>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
 
 
426
        >>> elt = inv.to_element()
 
 
427
        >>> inv2 = Inventory.from_element(elt)
 
 
431
        assert elt.tag == 'inventory'
 
 
434
            o.add(InventoryEntry.from_element(e))
 
 
437
    from_element = classmethod(from_element)
 
 
440
    def __cmp__(self, other):
 
 
441
        """Compare two sets by comparing their contents.
 
 
447
        >>> i1.add(InventoryEntry('123', 'foo'))
 
 
450
        >>> i2.add(InventoryEntry('123', 'foo'))
 
 
457
        if not isinstance(other, Inventory):
 
 
458
            return NotImplemented
 
 
460
        if self.id_set() ^ other.id_set():
 
 
463
        for file_id in self._byid:
 
 
464
            c = cmp(self[file_id], other[file_id])
 
 
470
    def id2path(self, file_id):
 
 
471
        """Return as a list the path to file_id."""
 
 
473
        while file_id != None:
 
 
476
            file_id = ie.parent_id
 
 
481
    def path2id(self, name):
 
 
482
        """Walk down through directories to return entry of last component.
 
 
484
        names may be either a list of path components, or a single
 
 
485
        string, in which case it is automatically split.
 
 
487
        This returns the entry of the last component in the path,
 
 
488
        which may be either a file or a directory.
 
 
490
        if isinstance(name, types.StringTypes):
 
 
491
            name = splitpath(name)
 
 
496
                cie = self._tree[parent_id][f]
 
 
498
                parent_id = cie.file_id
 
 
506
    def get_child(self, parent_id, child_name):
 
 
507
        return self._tree[parent_id].get(child_name)
 
 
510
    def has_filename(self, names):
 
 
511
        return bool(self.path2id(names))
 
 
514
    def has_id(self, file_id):
 
 
515
        assert isinstance(file_id, str)
 
 
516
        return self._byid.has_key(file_id)
 
 
522
if __name__ == '__main__':
 
 
523
    import doctest, inventory
 
 
524
    doctest.testmod(inventory)