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
# This should really be an id randomly assigned when the tree is
 
 
19
# created, but it's not for now.
 
 
29
from bzrlib.errors import BzrError, BzrCheckError
 
 
31
from bzrlib.osutils import quotefn, splitpath, joinpath, appendpath
 
 
32
from bzrlib.trace import mutter
 
 
33
from bzrlib.errors import NotVersionedError
 
 
36
class InventoryEntry(object):
 
 
37
    """Description of a versioned file.
 
 
39
    An InventoryEntry has the following fields, which are also
 
 
40
    present in the XML inventory-entry element:
 
 
43
    * *name*: (only the basename within the directory, must not
 
 
45
    * *kind*: "directory" or "file"
 
 
46
    * *directory_id*: (if absent/null means the branch root directory)
 
 
47
    * *text_sha1*: only for files
 
 
48
    * *text_size*: in bytes, only for files 
 
 
49
    * *text_id*: identifier for the text version, only for files
 
 
51
    InventoryEntries can also exist inside a WorkingTree
 
 
52
    inventory, in which case they are not yet bound to a
 
 
53
    particular revision of the file.  In that case the text_sha1,
 
 
54
    text_size and text_id are absent.
 
 
60
    >>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
 
 
61
    InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
 
 
62
    >>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
 
 
63
    InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
 
 
64
    >>> for j in i.iter_entries():
 
 
67
    ('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
 
 
68
    ('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
 
 
69
    >>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
 
 
70
    Traceback (most recent call last):
 
 
72
    BzrError: inventory already contains entry with id {2323}
 
 
73
    >>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
 
 
74
    InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
 
 
75
    >>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
 
 
76
    InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
 
 
77
    >>> i.path2id('src/wibble')
 
 
81
    >>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
 
 
82
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
84
    InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
 
 
85
    >>> for path, entry in i.iter_entries():
 
 
86
    ...     print path.replace('\\\\', '/')     # for win32 os.sep
 
 
87
    ...     assert i.path2id(path)
 
 
94
    >>> i.id2path('2326').replace('\\\\', '/')
 
 
97
    TODO: Maybe also keep the full path of the entry, and the children?
 
 
98
           But those depend on its position within a particular inventory, and
 
 
99
           it would be nice not to need to hold the backpointer here.
 
 
102
    # TODO: split InventoryEntry into subclasses for files,
 
 
103
    # directories, etc etc.
 
 
105
    __slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
 
 
106
                 'text_id', 'parent_id', 'children',
 
 
107
                 'text_version', 'entry_version', ]
 
 
110
    def __init__(self, file_id, name, kind, parent_id, text_id=None):
 
 
111
        """Create an InventoryEntry
 
 
113
        The filename must be a single component, relative to the
 
 
114
        parent directory; it cannot be a whole path or relative name.
 
 
116
        >>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
 
 
121
        >>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
 
 
122
        Traceback (most recent call last):
 
 
123
        BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
 
 
125
        assert isinstance(name, basestring), name
 
 
126
        if '/' in name or '\\' in name:
 
 
127
            raise BzrCheckError('InventoryEntry name %r is invalid' % name)
 
 
129
        self.text_version = None
 
 
130
        self.entry_version = None
 
 
131
        self.text_sha1 = None
 
 
132
        self.text_size = None
 
 
133
        self.file_id = file_id
 
 
136
        self.text_id = text_id
 
 
137
        self.parent_id = parent_id
 
 
138
        if kind == 'directory':
 
 
143
            raise BzrError("unhandled entry kind %r" % kind)
 
 
147
    def sorted_children(self):
 
 
148
        l = self.children.items()
 
 
154
        other = InventoryEntry(self.file_id, self.name, self.kind,
 
 
155
                               self.parent_id, text_id=self.text_id)
 
 
156
        other.text_sha1 = self.text_sha1
 
 
157
        other.text_size = self.text_size
 
 
158
        # note that children are *not* copied; they're pulled across when
 
 
164
        return ("%s(%r, %r, kind=%r, parent_id=%r)"
 
 
165
                % (self.__class__.__name__,
 
 
172
    def __eq__(self, other):
 
 
173
        if not isinstance(other, InventoryEntry):
 
 
174
            return NotImplemented
 
 
176
        return (self.file_id == other.file_id) \
 
 
177
               and (self.name == other.name) \
 
 
178
               and (self.text_sha1 == other.text_sha1) \
 
 
179
               and (self.text_size == other.text_size) \
 
 
180
               and (self.text_id == other.text_id) \
 
 
181
               and (self.parent_id == other.parent_id) \
 
 
182
               and (self.kind == other.kind) \
 
 
183
               and (self.text_version == other.text_version) \
 
 
184
               and (self.entry_version == other.entry_version)
 
 
187
    def __ne__(self, other):
 
 
188
        return not (self == other)
 
 
191
        raise ValueError('not hashable')
 
 
195
class RootEntry(InventoryEntry):
 
 
196
    def __init__(self, file_id):
 
 
197
        self.file_id = file_id
 
 
199
        self.kind = 'root_directory'
 
 
200
        self.parent_id = None
 
 
203
    def __eq__(self, other):
 
 
204
        if not isinstance(other, RootEntry):
 
 
205
            return NotImplemented
 
 
207
        return (self.file_id == other.file_id) \
 
 
208
               and (self.children == other.children)
 
 
212
class Inventory(object):
 
 
213
    """Inventory of versioned files in a tree.
 
 
215
    This describes which file_id is present at each point in the tree,
 
 
216
    and possibly the SHA-1 or other information about the file.
 
 
217
    Entries can be looked up either by path or by file_id.
 
 
219
    The inventory represents a typical unix file tree, with
 
 
220
    directories containing files and subdirectories.  We never store
 
 
221
    the full path to a file, because renaming a directory implicitly
 
 
222
    moves all of its contents.  This class internally maintains a
 
 
223
    lookup tree that allows the children under a directory to be
 
 
226
    InventoryEntry objects must not be modified after they are
 
 
227
    inserted, other than through the Inventory API.
 
 
229
    >>> inv = Inventory()
 
 
230
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
 
231
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
 
232
    >>> inv['123-123'].name
 
 
235
    May be treated as an iterator or set to look up file ids:
 
 
237
    >>> bool(inv.path2id('hello.c'))
 
 
242
    May also look up by name:
 
 
244
    >>> [x[0] for x in inv.iter_entries()]
 
 
246
    >>> inv = Inventory('TREE_ROOT-12345678-12345678')
 
 
247
    >>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
 
 
248
    InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
 
 
250
    def __init__(self, root_id=ROOT_ID):
 
 
251
        """Create or read an inventory.
 
 
253
        If a working directory is specified, the inventory is read
 
 
254
        from there.  If the file is specified, read from that. If not,
 
 
255
        the inventory is created empty.
 
 
257
        The inventory is created with a default root directory, with
 
 
260
        # We are letting Branch.initialize() create a unique inventory
 
 
261
        # root id. Rather than generating a random one here.
 
 
263
        #    root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
 
 
264
        self.root = RootEntry(root_id)
 
 
265
        self._byid = {self.root.file_id: self.root}
 
 
269
        return iter(self._byid)
 
 
273
        """Returns number of entries."""
 
 
274
        return len(self._byid)
 
 
277
    def iter_entries(self, from_dir=None):
 
 
278
        """Return (path, entry) pairs, in order by name."""
 
 
282
        elif isinstance(from_dir, basestring):
 
 
283
            from_dir = self._byid[from_dir]
 
 
285
        kids = from_dir.children.items()
 
 
287
        for name, ie in kids:
 
 
289
            if ie.kind == 'directory':
 
 
290
                for cn, cie in self.iter_entries(from_dir=ie.file_id):
 
 
291
                    yield os.path.join(name, cn), cie
 
 
295
        """Return list of (path, ie) for all entries except the root.
 
 
297
        This may be faster than iter_entries.
 
 
300
        def descend(dir_ie, dir_path):
 
 
301
            kids = dir_ie.children.items()
 
 
303
            for name, ie in kids:
 
 
304
                child_path = os.path.join(dir_path, name)
 
 
305
                accum.append((child_path, ie))
 
 
306
                if ie.kind == 'directory':
 
 
307
                    descend(ie, child_path)
 
 
309
        descend(self.root, '')
 
 
313
    def directories(self):
 
 
314
        """Return (path, entry) pairs for all directories, including the root.
 
 
317
        def descend(parent_ie, parent_path):
 
 
318
            accum.append((parent_path, parent_ie))
 
 
320
            kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
 
 
323
            for name, child_ie in kids:
 
 
324
                child_path = os.path.join(parent_path, name)
 
 
325
                descend(child_ie, child_path)
 
 
326
        descend(self.root, '')
 
 
331
    def __contains__(self, file_id):
 
 
332
        """True if this entry contains a file with given id.
 
 
334
        >>> inv = Inventory()
 
 
335
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
 
336
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
 
342
        return file_id in self._byid
 
 
345
    def __getitem__(self, file_id):
 
 
346
        """Return the entry for given file_id.
 
 
348
        >>> inv = Inventory()
 
 
349
        >>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
 
 
350
        InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
 
 
351
        >>> inv['123123'].name
 
 
355
            return self._byid[file_id]
 
 
358
                raise BzrError("can't look up file_id None")
 
 
360
                raise BzrError("file_id {%s} not in inventory" % file_id)
 
 
363
    def get_file_kind(self, file_id):
 
 
364
        return self._byid[file_id].kind
 
 
366
    def get_child(self, parent_id, filename):
 
 
367
        return self[parent_id].children.get(filename)
 
 
370
    def add(self, entry):
 
 
371
        """Add entry to inventory.
 
 
373
        To add  a file to a branch ready to be committed, use Branch.add,
 
 
376
        Returns the new entry object.
 
 
378
        if entry.file_id in self._byid:
 
 
379
            raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
 
 
381
        if entry.parent_id == ROOT_ID or entry.parent_id is None:
 
 
382
            entry.parent_id = self.root.file_id
 
 
385
            parent = self._byid[entry.parent_id]
 
 
387
            raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
 
 
389
        if parent.children.has_key(entry.name):
 
 
390
            raise BzrError("%s is already versioned" %
 
 
391
                    appendpath(self.id2path(parent.file_id), entry.name))
 
 
393
        self._byid[entry.file_id] = entry
 
 
394
        parent.children[entry.name] = entry
 
 
398
    def add_path(self, relpath, kind, file_id=None):
 
 
399
        """Add entry from a path.
 
 
401
        The immediate parent must already be versioned.
 
 
403
        Returns the new entry object."""
 
 
404
        from bzrlib.branch import gen_file_id
 
 
406
        parts = bzrlib.osutils.splitpath(relpath)
 
 
408
            raise BzrError("cannot re-add root of inventory")
 
 
411
            file_id = gen_file_id(relpath)
 
 
413
        parent_path = parts[:-1]
 
 
414
        parent_id = self.path2id(parent_path)
 
 
415
        if parent_id == None:
 
 
416
            raise NotVersionedError(parent_path)
 
 
418
        ie = InventoryEntry(file_id, parts[-1],
 
 
419
                            kind=kind, parent_id=parent_id)
 
 
423
    def __delitem__(self, file_id):
 
 
424
        """Remove entry by id.
 
 
426
        >>> inv = Inventory()
 
 
427
        >>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
 
 
428
        InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
 
 
437
        assert self[ie.parent_id].children[ie.name] == ie
 
 
439
        # TODO: Test deleting all children; maybe hoist to a separate
 
 
441
        if ie.kind == 'directory':
 
 
442
            for cie in ie.children.values():
 
 
443
                del self[cie.file_id]
 
 
446
        del self._byid[file_id]
 
 
447
        del self[ie.parent_id].children[ie.name]
 
 
450
    def __eq__(self, other):
 
 
451
        """Compare two sets by comparing their contents.
 
 
457
        >>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
 
458
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
 
461
        >>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
 
 
462
        InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
 
 
466
        if not isinstance(other, Inventory):
 
 
467
            return NotImplemented
 
 
469
        if len(self._byid) != len(other._byid):
 
 
470
            # shortcut: obviously not the same
 
 
473
        return self._byid == other._byid
 
 
476
    def __ne__(self, other):
 
 
477
        return not (self == other)
 
 
481
        raise ValueError('not hashable')
 
 
484
    def get_idpath(self, file_id):
 
 
485
        """Return a list of file_ids for the path to an entry.
 
 
487
        The list contains one element for each directory followed by
 
 
488
        the id of the file itself.  So the length of the returned list
 
 
489
        is equal to the depth of the file in the tree, counting the
 
 
490
        root directory as depth 1.
 
 
493
        while file_id != None:
 
 
495
                ie = self._byid[file_id]
 
 
497
                raise BzrError("file_id {%s} not found in inventory" % file_id)
 
 
498
            p.insert(0, ie.file_id)
 
 
499
            file_id = ie.parent_id
 
 
503
    def id2path(self, file_id):
 
 
504
        """Return as a list the path to file_id."""
 
 
506
        # get all names, skipping root
 
 
507
        p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
 
 
508
        return os.sep.join(p)
 
 
512
    def path2id(self, name):
 
 
513
        """Walk down through directories to return entry of last component.
 
 
515
        names may be either a list of path components, or a single
 
 
516
        string, in which case it is automatically split.
 
 
518
        This returns the entry of the last component in the path,
 
 
519
        which may be either a file or a directory.
 
 
521
        Returns None iff the path is not found.
 
 
523
        if isinstance(name, types.StringTypes):
 
 
524
            name = splitpath(name)
 
 
526
        mutter("lookup path %r" % name)
 
 
531
                cie = parent.children[f]
 
 
533
                assert cie.parent_id == parent.file_id
 
 
539
        return parent.file_id
 
 
542
    def has_filename(self, names):
 
 
543
        return bool(self.path2id(names))
 
 
546
    def has_id(self, file_id):
 
 
547
        return self._byid.has_key(file_id)
 
 
550
    def rename(self, file_id, new_parent_id, new_name):
 
 
551
        """Move a file within the inventory.
 
 
553
        This can change either the name, or the parent, or both.
 
 
555
        This does not move the working file."""
 
 
556
        if not is_valid_name(new_name):
 
 
557
            raise BzrError("not an acceptable filename: %r" % new_name)
 
 
559
        new_parent = self._byid[new_parent_id]
 
 
560
        if new_name in new_parent.children:
 
 
561
            raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
 
 
563
        new_parent_idpath = self.get_idpath(new_parent_id)
 
 
564
        if file_id in new_parent_idpath:
 
 
565
            raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
 
 
566
                    % (self.id2path(file_id), self.id2path(new_parent_id)))
 
 
568
        file_ie = self._byid[file_id]
 
 
569
        old_parent = self._byid[file_ie.parent_id]
 
 
571
        # TODO: Don't leave things messed up if this fails
 
 
573
        del old_parent.children[file_ie.name]
 
 
574
        new_parent.children[new_name] = file_ie
 
 
576
        file_ie.name = new_name
 
 
577
        file_ie.parent_id = new_parent_id
 
 
584
def is_valid_name(name):
 
 
587
        _NAME_RE = re.compile(r'^[^/\\]+$')
 
 
589
    return bool(_NAME_RE.match(name))