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."""
20
# TODO: Maybe store inventory_id in the file? Not really needed.
22
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
23
__author__ = "Martin Pool <mbp@canonical.com>"
25
import sys, os.path, types
29
from cElementTree import Element, ElementTree, SubElement
31
from elementtree.ElementTree import Element, ElementTree, SubElement
33
from xml import XMLMixin
34
from errors import bailout
37
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
38
from bzrlib.trace import mutter
40
class InventoryEntry(XMLMixin):
41
"""Description of a versioned file.
43
An InventoryEntry has the following fields, which are also
44
present in the XML inventory-entry element:
47
* *name*: (only the basename within the directory, must not
49
* *kind*: "directory" or "file"
50
* *directory_id*: (if absent/null means the branch root directory)
51
* *text_sha1*: only for files
52
* *text_size*: in bytes, only for files
53
* *text_id*: identifier for the text version, only for files
55
InventoryEntries can also exist inside a WorkingTree
56
inventory, in which case they are not yet bound to a
57
particular revision of the file. In that case the text_sha1,
58
text_size and text_id are absent.
63
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
64
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
65
>>> for j in i.iter_entries():
68
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
69
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
70
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
71
Traceback (most recent call last):
73
BzrError: ('inventory already contains entry with id {2323}', [])
74
>>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
75
>>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
76
>>> i.path2id('src/wibble')
80
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
82
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
83
>>> for j in i.iter_entries():
85
... assert i.path2id(j[0])
95
:todo: Maybe also keep the full path of the entry, and the children?
96
But those depend on its position within a particular inventory, and
97
it would be nice not to need to hold the backpointer here.
99
def __init__(self, file_id, name, kind='file', text_id=None,
101
"""Create an InventoryEntry
103
The filename must be a single component, relative to the
104
parent directory; it cannot be a whole path or relative name.
106
>>> e = InventoryEntry('123', 'hello.c')
111
>>> e = InventoryEntry('123', 'src/hello.c')
112
Traceback (most recent call last):
113
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
116
if len(splitpath(name)) != 1:
117
bailout('InventoryEntry name is not a simple filename: %r'
120
self.file_id = file_id
122
assert kind in ['file', 'directory']
124
self.text_id = text_id
125
self.parent_id = parent_id
126
self.text_sha1 = None
127
self.text_size = None
131
other = InventoryEntry(self.file_id, self.name, self.kind,
132
self.text_id, self.parent_id)
133
other.text_sha1 = self.text_sha1
134
other.text_size = self.text_size
139
return ("%s(%r, %r, kind=%r, parent_id=%r)"
140
% (self.__class__.__name__,
147
def to_element(self):
148
"""Convert to XML element"""
151
e.set('name', self.name)
152
e.set('file_id', self.file_id)
153
e.set('kind', self.kind)
155
if self.text_size is not None:
156
e.set('text_size', '%d' % self.text_size)
158
for f in ['text_id', 'text_sha1', 'parent_id']:
168
def from_element(cls, elt):
169
assert elt.tag == 'entry'
170
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
171
self.text_id = elt.get('text_id')
172
self.text_sha1 = elt.get('text_sha1')
173
self.parent_id = elt.get('parent_id')
175
## mutter("read inventoryentry: %r" % (elt.attrib))
177
v = elt.get('text_size')
178
self.text_size = v and int(v)
183
from_element = classmethod(from_element)
185
def __cmp__(self, other):
188
if not isinstance(other, InventoryEntry):
189
return NotImplemented
191
return cmp(self.file_id, other.file_id) \
192
or cmp(self.name, other.name) \
193
or cmp(self.text_sha1, other.text_sha1) \
194
or cmp(self.text_size, other.text_size) \
195
or cmp(self.text_id, other.text_id) \
196
or cmp(self.parent_id, other.parent_id) \
197
or cmp(self.kind, other.kind)
201
class Inventory(XMLMixin):
202
"""Inventory of versioned files in a tree.
204
An Inventory acts like a set of InventoryEntry items. You can
205
also look files up by their file_id or name.
207
May be read from and written to a metadata file in a tree. To
208
manipulate the inventory (for example to add a file), it is read
209
in, modified, and then written back out.
211
The inventory represents a typical unix file tree, with
212
directories containing files and subdirectories. We never store
213
the full path to a file, because renaming a directory implicitly
214
moves all of its contents. This class internally maintains a
215
lookup tree that allows the children under a directory to be
218
InventoryEntry objects must not be modified after they are
221
>>> inv = Inventory()
222
>>> inv.write_xml(sys.stdout)
225
>>> inv.add(InventoryEntry('123-123', 'hello.c'))
226
>>> inv['123-123'].name
228
>>> for file_id in inv: print file_id
232
May be treated as an iterator or set to look up file ids:
234
>>> bool(inv.path2id('hello.c'))
239
May also look up by name:
241
>>> [x[0] for x in inv.iter_entries()]
244
>>> inv.write_xml(sys.stdout)
246
<entry file_id="123-123" kind="file" name="hello.c" />
251
## TODO: Clear up handling of files in subdirectories; we probably
252
## do want to be able to just look them up by name but this
253
## probably means gradually walking down the path, looking up as we go.
255
## TODO: Make sure only canonical filenames are stored.
257
## TODO: Do something sensible about the possible collisions on
258
## case-losing filesystems. Perhaps we should just always forbid
261
## _tree should probably just be stored as
262
## InventoryEntry._children on each directory.
265
"""Create or read an inventory.
267
If a working directory is specified, the inventory is read
268
from there. If the file is specified, read from that. If not,
269
the inventory is created empty.
273
# _tree is indexed by parent_id; at each level a map from name
274
# to ie. The None entry is the root.
275
self._tree = {None: {}}
279
return iter(self._byid)
283
"""Returns number of entries."""
284
return len(self._byid)
287
def iter_entries(self, parent_id=None):
288
"""Return (path, entry) pairs, in order by name."""
289
kids = self._tree[parent_id].items()
291
for name, ie in kids:
293
if ie.kind == 'directory':
294
for cn, cie in self.iter_entries(parent_id=ie.file_id):
295
yield joinpath([name, cn]), cie
298
def directories(self, include_root=True):
299
"""Return (path, entry) pairs for all directories.
303
for path, entry in self.iter_entries():
304
if entry.kind == 'directory':
309
def children(self, parent_id):
310
"""Return entries that are direct children of parent_id."""
311
return self._tree[parent_id]
315
# TODO: return all paths and entries
318
def __contains__(self, file_id):
319
"""True if this entry contains a file with given id.
321
>>> inv = Inventory()
322
>>> inv.add(InventoryEntry('123', 'foo.c'))
328
return file_id in self._byid
331
def __getitem__(self, file_id):
332
"""Return the entry for given file_id.
334
>>> inv = Inventory()
335
>>> inv.add(InventoryEntry('123123', 'hello.c'))
336
>>> inv['123123'].name
339
return self._byid[file_id]
342
def add(self, entry):
343
"""Add entry to inventory.
345
To add a file to a branch ready to be committed, use Branch.add,
347
if entry.file_id in self:
348
bailout("inventory already contains entry with id {%s}" % entry.file_id)
350
if entry.parent_id != None:
351
if entry.parent_id not in self:
352
bailout("parent_id %s of new entry not found in inventory"
355
if self._tree[entry.parent_id].has_key(entry.name):
356
bailout("%s is already versioned"
357
% appendpath(self.id2path(entry.parent_id), entry.name))
359
self._byid[entry.file_id] = entry
360
self._tree[entry.parent_id][entry.name] = entry
362
if entry.kind == 'directory':
363
self._tree[entry.file_id] = {}
366
def add_path(self, relpath, kind, file_id=None):
367
"""Add entry from a path.
369
The immediate parent must already be versioned"""
370
parts = bzrlib.osutils.splitpath(relpath)
372
bailout("cannot re-add root of inventory")
375
file_id = bzrlib.branch.gen_file_id(relpath)
377
parent_id = self.path2id(parts[:-1])
378
ie = InventoryEntry(file_id, parts[-1],
379
kind=kind, parent_id=parent_id)
383
def __delitem__(self, file_id):
384
"""Remove entry by id.
386
>>> inv = Inventory()
387
>>> inv.add(InventoryEntry('123', 'foo.c'))
396
assert self._tree[ie.parent_id][ie.name] == ie
398
# TODO: Test deleting all children; maybe hoist to a separate
400
if ie.kind == 'directory':
401
for cie in self._tree[file_id].values():
402
del self[cie.file_id]
403
del self._tree[file_id]
405
del self._byid[file_id]
406
del self._tree[ie.parent_id][ie.name]
410
return Set(self._byid)
413
def to_element(self):
414
"""Convert to XML Element"""
415
e = Element('inventory')
417
for path, ie in self.iter_entries():
418
e.append(ie.to_element())
422
def from_element(cls, elt):
423
"""Construct from XML Element
425
>>> inv = Inventory()
426
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
427
>>> elt = inv.to_element()
428
>>> inv2 = Inventory.from_element(elt)
432
assert elt.tag == 'inventory'
435
o.add(InventoryEntry.from_element(e))
438
from_element = classmethod(from_element)
441
def __cmp__(self, other):
442
"""Compare two sets by comparing their contents.
448
>>> i1.add(InventoryEntry('123', 'foo'))
451
>>> i2.add(InventoryEntry('123', 'foo'))
458
if not isinstance(other, Inventory):
459
return NotImplemented
461
if self.id_set() ^ other.id_set():
464
for file_id in self._byid:
465
c = cmp(self[file_id], other[file_id])
471
def id2path(self, file_id):
472
"""Return as a list the path to file_id."""
474
while file_id != None:
477
file_id = ie.parent_id
482
def path2id(self, name):
483
"""Walk down through directories to return entry of last component.
485
names may be either a list of path components, or a single
486
string, in which case it is automatically split.
488
This returns the entry of the last component in the path,
489
which may be either a file or a directory.
491
if isinstance(name, types.StringTypes):
492
name = splitpath(name)
497
cie = self._tree[parent_id][f]
499
parent_id = cie.file_id
507
def get_child(self, parent_id, child_name):
508
return self._tree[parent_id].get(child_name)
511
def has_filename(self, names):
512
return bool(self.path2id(names))
515
def has_id(self, file_id):
516
assert isinstance(file_id, str)
517
return self._byid.has_key(file_id)
523
if __name__ == '__main__':
524
import doctest, inventory
525
doctest.testmod(inventory)