14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Inventories map files to their name in a revision."""
18
19
# TODO: Maybe store inventory_id in the file? Not really needed.
21
# This should really be an id randomly assigned when the tree is
22
# created, but it's not for now.
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
__author__ = "Martin Pool <mbp@canonical.com>"
26
24
import sys, os.path, types, re
27
25
from sets import Set
32
30
from elementtree.ElementTree import Element, ElementTree, SubElement
34
32
from xml import XMLMixin
35
from errors import bailout, BzrError
33
from errors import bailout
38
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
62
60
>>> i = Inventory()
65
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
66
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
62
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
67
64
>>> for j in i.iter_entries():
70
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
71
68
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
72
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
73
70
Traceback (most recent call last):
75
72
BzrError: ('inventory already contains entry with id {2323}', [])
76
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
77
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
>>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
74
>>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
78
75
>>> i.path2id('src/wibble')
82
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
79
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
84
81
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
85
82
>>> for j in i.iter_entries():
94
91
>>> i.id2path('2326')
95
92
'src/wibble/wibble.c'
97
TODO: Maybe also keep the full path of the entry, and the children?
94
:todo: Maybe also keep the full path of the entry, and the children?
98
95
But those depend on its position within a particular inventory, and
99
96
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
def __init__(self, file_id, name, kind, parent_id, text_id=None):
98
def __init__(self, file_id, name, kind='file', text_id=None,
106
100
"""Create an InventoryEntry
108
102
The filename must be a single component, relative to the
109
103
parent directory; it cannot be a whole path or relative name.
111
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
105
>>> e = InventoryEntry('123', 'hello.c')
116
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
110
>>> e = InventoryEntry('123', 'src/hello.c')
117
111
Traceback (most recent call last):
118
112
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
148
138
other = InventoryEntry(self.file_id, self.name, self.kind,
149
self.parent_id, text_id=self.text_id)
139
self.text_id, self.parent_id)
150
140
other.text_sha1 = self.text_sha1
151
141
other.text_size = self.text_size
169
159
e.set('file_id', self.file_id)
170
160
e.set('kind', self.kind)
172
if self.text_size != None:
162
if self.text_size is not None:
173
163
e.set('text_size', '%d' % self.text_size)
175
for f in ['text_id', 'text_sha1']:
165
for f in ['text_id', 'text_sha1', 'parent_id']:
176
166
v = getattr(self, f)
180
# to be conservative, we don't externalize the root pointers
181
# for now, leaving them as null in the xml form. in a future
182
# version it will be implied by nested elements.
183
if self.parent_id != ROOT_ID:
184
assert isinstance(self.parent_id, basestring)
185
e.set('parent_id', self.parent_id)
192
175
def from_element(cls, elt):
193
176
assert elt.tag == 'entry'
195
## original format inventories don't have a parent_id for
196
## nodes in the root directory, but it's cleaner to use one
198
parent_id = elt.get('parent_id')
199
if parent_id == None:
202
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
177
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
203
178
self.text_id = elt.get('text_id')
204
179
self.text_sha1 = elt.get('text_sha1')
180
self.parent_id = elt.get('parent_id')
206
182
## mutter("read inventoryentry: %r" % (elt.attrib))
250
226
class Inventory(XMLMixin):
251
227
"""Inventory of versioned files in a tree.
253
This describes which file_id is present at each point in the tree,
254
and possibly the SHA-1 or other information about the file.
255
Entries can be looked up either by path or by file_id.
229
An Inventory acts like a set of InventoryEntry items. You can
230
also look files up by their file_id or name.
232
May be read from and written to a metadata file in a tree. To
233
manipulate the inventory (for example to add a file), it is read
234
in, modified, and then written back out.
257
236
The inventory represents a typical unix file tree, with
258
237
directories containing files and subdirectories. We never store
300
279
## TODO: No special cases for root, rather just give it a file id
301
280
## like everything else.
303
## TODO: Probably change XML serialization to use nesting rather
304
## than parent_id pointers.
306
## TODO: Perhaps hold the ElementTree in memory and work directly
307
## on that rather than converting into Python objects every time?
282
## TODO: Probably change XML serialization to use nesting
309
284
def __init__(self):
310
285
"""Create or read an inventory.
316
291
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
320
self._byid = {self.root.file_id: self.root}
294
self.root = RootEntry(None)
295
self._byid = {None: self.root}
323
298
def __iter__(self):
344
319
if ie.kind == 'directory':
345
320
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
yield os.path.join(name, cn), cie
321
yield '/'.join((name, cn)), cie
350
def directories(self):
325
def directories(self, from_dir=None):
351
326
"""Return (path, entry) pairs for all directories.
353
328
def descend(parent_ie):
387
362
"""Return the entry for given file_id.
389
364
>>> inv = Inventory()
390
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
365
>>> inv.add(InventoryEntry('123123', 'hello.c'))
391
366
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
369
return self._byid[file_id]
403
372
def get_child(self, parent_id, filename):
416
385
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
387
bailout("parent_id %r not in inventory" % entry.parent_id)
420
389
if parent.children.has_key(entry.name):
421
390
bailout("%s is already versioned" %
433
402
if len(parts) == 0:
434
403
bailout("cannot re-add root of inventory")
437
406
file_id = bzrlib.branch.gen_file_id(relpath)
439
408
parent_id = self.path2id(parts[:-1])
440
assert parent_id != None
441
409
ie = InventoryEntry(file_id, parts[-1],
442
410
kind=kind, parent_id=parent_id)
443
411
return self.add(ie)
486
454
"""Construct from XML Element
488
456
>>> inv = Inventory()
489
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
457
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
490
458
>>> elt = inv.to_element()
491
459
>>> inv2 = Inventory.from_element(elt)
508
476
>>> i2 = Inventory()
511
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
479
>>> i1.add(InventoryEntry('123', 'foo'))
514
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
482
>>> i2.add(InventoryEntry('123', 'foo'))
537
505
The list contains one element for each directory followed by
538
506
the id of the file itself. So the length of the returned list
539
507
is equal to the depth of the file in the tree, counting the
540
root directory as depth 1.
508
root directory as depth 0.
543
511
while file_id != None:
545
ie = self._byid[file_id]
547
bailout("file_id {%s} not found in inventory" % file_id)
512
ie = self._byid[file_id]
548
513
p.insert(0, ie.file_id)
549
514
file_id = ie.parent_id
568
535
This returns the entry of the last component in the path,
569
536
which may be either a file or a directory.
571
Returns None iff the path is not found.
573
538
if isinstance(name, types.StringTypes):
574
539
name = splitpath(name)
576
mutter("lookup path %r" % name)
581
544
cie = parent.children[f]
582
545
assert cie.name == f
583
assert cie.parent_id == parent.file_id
586
548
# or raise an error?