1
# (C) 2005 Canonical Ltd
 
 
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.
 
 
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.
 
 
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
 
 
18
# TODO: Maybe also keep the full path of the entry, and the children?
 
 
19
# But those depend on its position within a particular inventory, and
 
 
20
# it would be nice not to need to hold the backpointer here.
 
 
22
# TODO: Perhaps split InventoryEntry into subclasses for files,
 
 
23
# directories, etc etc.
 
 
26
# This should really be an id randomly assigned when the tree is
 
 
27
# created, but it's not for now.
 
 
31
import sys, os.path, types, re
 
 
34
from bzrlib.errors import BzrError, BzrCheckError
 
 
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
 
 
37
from bzrlib.trace import mutter
 
 
38
from bzrlib.errors import NotVersionedError
 
 
41
class InventoryEntry(object):
 
 
42
    """Description of a versioned file.
 
 
44
    An InventoryEntry has the following fields, which are also
 
 
45
    present in the XML inventory-entry element:
 
 
50
        (within the parent directory)
 
 
56
        file_id of the parent directory, or ROOT_ID
 
 
59
        the revision_id in which the name or parent of this file was
 
 
63
        sha-1 of the text of the file
 
 
66
        size in bytes of the text of the file
 
 
69
        the revision_id in which the text of this file was introduced
 
 
71
    (reading a version 4 tree created a text_id field.)
 
 
76
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
 
77
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
 
 
78
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
 
79
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
 
 
80
    >>> for j in i.iter_entries():
 
 
83
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
 
84
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
 
85
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
 
86
    Traceback (most recent call last):
 
 
88
    BzrError: inventory already contains entry with id {2323}
 
 
89
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
 
90
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
 
91
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
 
92
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
 
 
93
    >>> i.path2id('src/wibble')
 
 
97
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
 
98
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
100
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
101
    >>> for j in i.iter_entries():
 
 
103
    ...     assert i.path2id(j[0])
 
 
110
    >>> i.id2path('2326')
 
 
111
    'src/wibble/wibble.c'
 
 
114
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
 
115
                 'text_id', 'parent_id', 'children',
 
 
116
                 'text_version', 'name_version', ]
 
 
119
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
 
120
        """Create an InventoryEntry
 
 
122
        The filename must be a single component, relative to the
 
 
123
        parent directory; it cannot be a whole path or relative name.
 
 
125
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
 
130
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
 
131
        Traceback (most recent call last):
 
 
132
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
 
134
        assert isinstance(name, basestring), name
 
 
135
        if '/' in name or '\\' in name:
 
 
136
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
 
138
        self.text_version = None
 
 
139
        self.name_version = None
 
 
140
        self.text_sha1 = None
 
 
141
        self.text_size = None
 
 
142
        self.file_id = file_id
 
 
145
        self.text_id = text_id
 
 
146
        self.parent_id = parent_id
 
 
147
        if kind == 'directory':
 
 
152
            raise BzrError("unhandled entry kind %r" % kind)
 
 
156
    def sorted_children(self):
 
 
157
        l = self.children.items()
 
 
163
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
 
165
        other.text_id = self.text_id
 
 
166
        other.text_sha1 = self.text_sha1
 
 
167
        other.text_size = self.text_size
 
 
168
        other.text_version = self.text_version
 
 
169
        other.name_version = self.name_version
 
 
170
        # note that children are *not* copied; they're pulled across when
 
 
176
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
 
177
                % (self.__class__.__name__,
 
 
184
    def __eq__(self, other):
 
 
185
        if not isinstance(other, InventoryEntry):
 
 
186
            return NotImplemented
 
 
188
        return (self.file_id == other.file_id) \
 
 
189
               and (self.name == other.name) \
 
 
190
               and (self.text_sha1 == other.text_sha1) \
 
 
191
               and (self.text_size == other.text_size) \
 
 
192
               and (self.text_id == other.text_id) \
 
 
193
               and (self.parent_id == other.parent_id) \
 
 
194
               and (self.kind == other.kind) \
 
 
195
               and (self.text_version == other.text_version) \
 
 
196
               and (self.name_version == other.name_version)
 
 
199
    def __ne__(self, other):
 
 
200
        return not (self == other)
 
 
203
        raise ValueError('not hashable')
 
 
207
class RootEntry(InventoryEntry):
 
 
208
    def __init__(self, file_id):
 
 
209
        self.file_id = file_id
 
 
211
        self.kind = 'root_directory'
 
 
212
        self.parent_id = None
 
 
215
    def __eq__(self, other):
 
 
216
        if not isinstance(other, RootEntry):
 
 
217
            return NotImplemented
 
 
219
        return (self.file_id == other.file_id) \
 
 
220
               and (self.children == other.children)
 
 
224
class Inventory(object):
 
 
225
    """Inventory of versioned files in a tree.
 
 
227
    This describes which file_id is present at each point in the tree,
 
 
228
    and possibly the SHA-1 or other information about the file.
 
 
229
    Entries can be looked up either by path or by file_id.
 
 
231
    The inventory represents a typical unix file tree, with
 
 
232
    directories containing files and subdirectories.  We never store
 
 
233
    the full path to a file, because renaming a directory implicitly
 
 
234
    moves all of its contents.  This class internally maintains a
 
 
235
    lookup tree that allows the children under a directory to be
 
 
238
    InventoryEntry objects must not be modified after they are
 
 
239
    inserted, other than through the Inventory API.
 
 
241
    >>> inv = Inventory()
 
 
242
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
 
243
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
 
244
    >>> inv['123-123'].name
 
 
247
    May be treated as an iterator or set to look up file ids:
 
 
249
    >>> bool(inv.path2id('hello.c'))
 
 
254
    May also look up by name:
 
 
256
    >>> [x[0] for x in inv.iter_entries()]
 
 
258
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
 
259
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
 
260
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
 
262
    def __init__(self, root_id=ROOT_ID):
 
 
263
        """Create or read an inventory.
 
 
265
        If a working directory is specified, the inventory is read
 
 
266
        from there.  If the file is specified, read from that. If not,
 
 
267
        the inventory is created empty.
 
 
269
        The inventory is created with a default root directory, with
 
 
272
        # We are letting Branch(init=True) create a unique inventory
 
 
273
        # root id. Rather than generating a random one here.
 
 
275
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
 
276
        self.root = RootEntry(root_id)
 
 
277
        self._byid = {self.root.file_id: self.root}
 
 
281
        other = Inventory(self.root.file_id)
 
 
282
        # copy recursively so we know directories will be added before
 
 
283
        # their children.  There are more efficient ways than this...
 
 
284
        for path, entry in self.iter_entries():
 
 
285
            if entry == self.root:
 
 
287
            other.add(entry.copy())
 
 
292
        return iter(self._byid)
 
 
296
        """Returns number of entries."""
 
 
297
        return len(self._byid)
 
 
300
    def iter_entries(self, from_dir=None):
 
 
301
        """Return (path, entry) pairs, in order by name."""
 
 
305
        elif isinstance(from_dir, basestring):
 
 
306
            from_dir = self._byid[from_dir]
 
 
308
        kids = from_dir.children.items()
 
 
310
        for name, ie in kids:
 
 
312
            if ie.kind == 'directory':
 
 
313
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
 
314
                    yield os.path.join(name, cn), cie
 
 
318
        """Return list of (path, ie) for all entries except the root.
 
 
320
        This may be faster than iter_entries.
 
 
323
        def descend(dir_ie, dir_path):
 
 
324
            kids = dir_ie.children.items()
 
 
326
            for name, ie in kids:
 
 
327
                child_path = os.path.join(dir_path, name)
 
 
328
                accum.append((child_path, ie))
 
 
329
                if ie.kind == 'directory':
 
 
330
                    descend(ie, child_path)
 
 
332
        descend(self.root, '')
 
 
336
    def directories(self):
 
 
337
        """Return (path, entry) pairs for all directories, including the root.
 
 
340
        def descend(parent_ie, parent_path):
 
 
341
            accum.append((parent_path, parent_ie))
 
 
343
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
 
346
            for name, child_ie in kids:
 
 
347
                child_path = os.path.join(parent_path, name)
 
 
348
                descend(child_ie, child_path)
 
 
349
        descend(self.root, '')
 
 
354
    def __contains__(self, file_id):
 
 
355
        """True if this entry contains a file with given id.
 
 
357
        >>> inv = Inventory()
 
 
358
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
 
359
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
 
365
        return file_id in self._byid
 
 
368
    def __getitem__(self, file_id):
 
 
369
        """Return the entry for given file_id.
 
 
371
        >>> inv = Inventory()
 
 
372
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
 
373
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
 
374
        >>> inv['123123'].name
 
 
378
            return self._byid[file_id]
 
 
381
                raise BzrError("can't look up file_id None")
 
 
383
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
 
386
    def get_file_kind(self, file_id):
 
 
387
        return self._byid[file_id].kind
 
 
389
    def get_child(self, parent_id, filename):
 
 
390
        return self[parent_id].children.get(filename)
 
 
393
    def add(self, entry):
 
 
394
        """Add entry to inventory.
 
 
396
        To add  a file to a branch ready to be committed, use Branch.add,
 
 
399
        Returns the new entry object.
 
 
401
        if entry.file_id in self._byid:
 
 
402
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
 
404
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
 
405
            entry.parent_id = self.root.file_id
 
 
408
            parent = self._byid[entry.parent_id]
 
 
410
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
 
412
        if parent.children.has_key(entry.name):
 
 
413
            raise BzrError("%s is already versioned" %
 
 
414
                    appendpath(self.id2path(parent.file_id), entry.name))
 
 
416
        self._byid[entry.file_id] = entry
 
 
417
        parent.children[entry.name] = entry
 
 
421
    def add_path(self, relpath, kind, file_id=None):
 
 
422
        """Add entry from a path.
 
 
424
        The immediate parent must already be versioned.
 
 
426
        Returns the new entry object."""
 
 
427
        from bzrlib.branch import gen_file_id
 
 
429
        parts = bzrlib.osutils.splitpath(relpath)
 
 
431
            raise BzrError("cannot re-add root of inventory")
 
 
434
            file_id = gen_file_id(relpath)
 
 
436
        parent_path = parts[:-1]
 
 
437
        parent_id = self.path2id(parent_path)
 
 
438
        if parent_id == None:
 
 
439
            raise NotVersionedError(parent_path)
 
 
441
        ie = InventoryEntry(file_id, parts[-1],
 
 
442
                            kind=kind, parent_id=parent_id)
 
 
446
    def __delitem__(self, file_id):
 
 
447
        """Remove entry by id.
 
 
449
        >>> inv = Inventory()
 
 
450
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
 
451
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
 
460
        assert self[ie.parent_id].children[ie.name] == ie
 
 
462
        # TODO: Test deleting all children; maybe hoist to a separate
 
 
464
        if ie.kind == 'directory':
 
 
465
            for cie in ie.children.values():
 
 
466
                del self[cie.file_id]
 
 
469
        del self._byid[file_id]
 
 
470
        del self[ie.parent_id].children[ie.name]
 
 
473
    def __eq__(self, other):
 
 
474
        """Compare two sets by comparing their contents.
 
 
480
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
 
481
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
 
484
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
 
485
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
 
489
        if not isinstance(other, Inventory):
 
 
490
            return NotImplemented
 
 
492
        if len(self._byid) != len(other._byid):
 
 
493
            # shortcut: obviously not the same
 
 
496
        return self._byid == other._byid
 
 
499
    def __ne__(self, other):
 
 
500
        return not self.__eq__(other)
 
 
504
        raise ValueError('not hashable')
 
 
507
    def get_idpath(self, file_id):
 
 
508
        """Return a list of file_ids for the path to an entry.
 
 
510
        The list contains one element for each directory followed by
 
 
511
        the id of the file itself.  So the length of the returned list
 
 
512
        is equal to the depth of the file in the tree, counting the
 
 
513
        root directory as depth 1.
 
 
516
        while file_id != None:
 
 
518
                ie = self._byid[file_id]
 
 
520
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
 
521
            p.insert(0, ie.file_id)
 
 
522
            file_id = ie.parent_id
 
 
526
    def id2path(self, file_id):
 
 
527
        """Return as a list the path to file_id."""
 
 
529
        # get all names, skipping root
 
 
530
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
 
531
        return os.sep.join(p)
 
 
535
    def path2id(self, name):
 
 
536
        """Walk down through directories to return entry of last component.
 
 
538
        names may be either a list of path components, or a single
 
 
539
        string, in which case it is automatically split.
 
 
541
        This returns the entry of the last component in the path,
 
 
542
        which may be either a file or a directory.
 
 
544
        Returns None iff the path is not found.
 
 
546
        if isinstance(name, types.StringTypes):
 
 
547
            name = splitpath(name)
 
 
549
        mutter("lookup path %r" % name)
 
 
554
                cie = parent.children[f]
 
 
556
                assert cie.parent_id == parent.file_id
 
 
562
        return parent.file_id
 
 
565
    def has_filename(self, names):
 
 
566
        return bool(self.path2id(names))
 
 
569
    def has_id(self, file_id):
 
 
570
        return self._byid.has_key(file_id)
 
 
573
    def rename(self, file_id, new_parent_id, new_name):
 
 
574
        """Move a file within the inventory.
 
 
576
        This can change either the name, or the parent, or both.
 
 
578
        This does not move the working file."""
 
 
579
        if not is_valid_name(new_name):
 
 
580
            raise BzrError("not an acceptable filename: %r" % new_name)
 
 
582
        new_parent = self._byid[new_parent_id]
 
 
583
        if new_name in new_parent.children:
 
 
584
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
 
586
        new_parent_idpath = self.get_idpath(new_parent_id)
 
 
587
        if file_id in new_parent_idpath:
 
 
588
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
 
589
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
 
591
        file_ie = self._byid[file_id]
 
 
592
        old_parent = self._byid[file_ie.parent_id]
 
 
594
        # TODO: Don't leave things messed up if this fails
 
 
596
        del old_parent.children[file_ie.name]
 
 
597
        new_parent.children[new_name] = file_ie
 
 
599
        file_ie.name = new_name
 
 
600
        file_ie.parent_id = new_parent_id
 
 
607
def is_valid_name(name):
 
 
610
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
 
612
    return bool(_NAME_RE.match(name))