15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# TODO: Maybe store inventory_id in the file? Not really needed.
18
21
# This should really be an id randomly assigned when the tree is
19
22
# created, but it's not for now.
20
23
ROOT_ID = "TREE_ROOT"
23
26
import sys, os.path, types, re
30
from cElementTree import Element, ElementTree, SubElement
32
from elementtree.ElementTree import Element, ElementTree, SubElement
34
from xml import XMLMixin
35
from errors import bailout, BzrError
26
from bzrlib.errors import BzrError, BzrCheckError
28
38
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
29
39
from bzrlib.trace import mutter
30
from bzrlib.errors import NotVersionedError
33
class InventoryEntry(object):
41
class InventoryEntry(XMLMixin):
34
42
"""Description of a versioned file.
36
44
An InventoryEntry has the following fields, which are also
57
65
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
58
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
59
66
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
60
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
61
67
>>> for j in i.iter_entries():
66
72
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
67
73
Traceback (most recent call last):
69
BzrError: inventory already contains entry with id {2323}
75
BzrError: ('inventory already contains entry with id {2323}', [])
70
76
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
71
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
72
77
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
74
78
>>> i.path2id('src/wibble')
78
82
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
79
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
81
84
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
85
>>> for j in i.iter_entries():
99
102
# TODO: split InventoryEntry into subclasses for files,
100
103
# directories, etc etc.
102
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
103
'text_id', 'parent_id', 'children',
104
'text_version', 'entry_version', ]
107
105
def __init__(self, file_id, name, kind, parent_id, text_id=None):
108
106
"""Create an InventoryEntry
118
116
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
119
117
Traceback (most recent call last):
120
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
118
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
122
assert isinstance(name, basestring), name
123
if '/' in name or '\\' in name:
124
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
126
self.text_version = None
127
self.entry_version = None
128
self.text_sha1 = None
129
self.text_size = None
121
if len(splitpath(name)) != 1:
122
bailout('InventoryEntry name is not a simple filename: %r'
130
125
self.file_id = file_id
133
128
self.text_id = text_id
134
129
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
135
132
if kind == 'directory':
136
133
self.children = {}
137
134
elif kind == 'file':
169
def __eq__(self, other):
164
def to_element(self):
165
"""Convert to XML element"""
168
e.set('name', self.name)
169
e.set('file_id', self.file_id)
170
e.set('kind', self.kind)
172
if self.text_size != None:
173
e.set('text_size', '%d' % self.text_size)
175
for f in ['text_id', 'text_sha1']:
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
def from_element(cls, elt):
193
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)
203
self.text_id = elt.get('text_id')
204
self.text_sha1 = elt.get('text_sha1')
206
## mutter("read inventoryentry: %r" % (elt.attrib))
208
v = elt.get('text_size')
209
self.text_size = v and int(v)
214
from_element = classmethod(from_element)
216
def __cmp__(self, other):
170
219
if not isinstance(other, InventoryEntry):
171
220
return NotImplemented
173
return (self.file_id == other.file_id) \
174
and (self.name == other.name) \
175
and (self.text_sha1 == other.text_sha1) \
176
and (self.text_size == other.text_size) \
177
and (self.text_id == other.text_id) \
178
and (self.parent_id == other.parent_id) \
179
and (self.kind == other.kind) \
180
and (self.text_version == other.text_version) \
181
and (self.entry_version == other.entry_version)
184
def __ne__(self, other):
185
return not (self == other)
188
raise ValueError('not hashable')
222
return cmp(self.file_id, other.file_id) \
223
or cmp(self.name, other.name) \
224
or cmp(self.text_sha1, other.text_sha1) \
225
or cmp(self.text_size, other.text_size) \
226
or cmp(self.text_id, other.text_id) \
227
or cmp(self.parent_id, other.parent_id) \
228
or cmp(self.kind, other.kind)
197
237
self.parent_id = None
200
def __eq__(self, other):
240
def __cmp__(self, other):
201
243
if not isinstance(other, RootEntry):
202
244
return NotImplemented
204
return (self.file_id == other.file_id) \
205
and (self.children == other.children)
209
class Inventory(object):
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
210
251
"""Inventory of versioned files in a tree.
212
253
This describes which file_id is present at each point in the tree,
224
265
inserted, other than through the Inventory API.
226
267
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
227
271
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
228
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
229
272
>>> inv['123-123'].name
241
284
>>> [x[0] for x in inv.iter_entries()]
243
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
244
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
245
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
287
>>> inv.write_xml(sys.stdout)
289
<entry file_id="123-123" kind="file" name="hello.c" />
247
def __init__(self, root_id=ROOT_ID):
294
## TODO: Make sure only canonical filenames are stored.
296
## TODO: Do something sensible about the possible collisions on
297
## case-losing filesystems. Perhaps we should just always forbid
300
## TODO: No special cases for root, rather just give it a file id
301
## 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?
248
310
"""Create or read an inventory.
250
312
If a working directory is specified, the inventory is read
254
316
The inventory is created with a default root directory, with
257
# We are letting Branch(init=True) create a unique inventory
258
# root id. Rather than generating a random one here.
260
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
261
self.root = RootEntry(root_id)
319
self.root = RootEntry(ROOT_ID)
262
320
self._byid = {self.root.file_id: self.root}
286
344
if ie.kind == 'directory':
287
345
for cn, cie in self.iter_entries(from_dir=ie.file_id):
288
346
yield os.path.join(name, cn), cie
292
"""Return list of (path, ie) for all entries except the root.
294
This may be faster than iter_entries.
350
def directories(self):
351
"""Return (path, entry) pairs for all directories.
297
def descend(dir_ie, dir_path):
298
kids = dir_ie.children.items()
300
for name, ie in kids:
301
child_path = os.path.join(dir_path, name)
302
accum.append((child_path, ie))
353
def descend(parent_ie):
354
parent_name = parent_ie.name
355
yield parent_name, parent_ie
357
# directory children in sorted order
359
for ie in parent_ie.children.itervalues():
303
360
if ie.kind == 'directory':
304
descend(ie, child_path)
306
descend(self.root, '')
310
def directories(self):
311
"""Return (path, entry) pairs for all directories, including the root.
314
def descend(parent_ie, parent_path):
315
accum.append((parent_path, parent_ie))
361
dn.append((ie.name, ie))
317
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
364
for name, child_ie in dn:
365
for sub_name, sub_ie in descend(child_ie):
366
yield appendpath(parent_name, sub_name), sub_ie
320
for name, child_ie in kids:
321
child_path = os.path.join(parent_path, name)
322
descend(child_ie, child_path)
323
descend(self.root, '')
368
for name, ie in descend(self.root):
345
389
>>> inv = Inventory()
346
390
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
347
InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
348
391
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
352
398
return self._byid[file_id]
355
raise BzrError("can't look up file_id None")
357
raise BzrError("file_id {%s} not in inventory" % file_id)
360
def get_file_kind(self, file_id):
361
return self._byid[file_id].kind
400
raise BzrError("file_id {%s} not in inventory" % file_id)
363
403
def get_child(self, parent_id, filename):
364
404
return self[parent_id].children.get(filename)
368
408
"""Add entry to inventory.
370
410
To add a file to a branch ready to be committed, use Branch.add,
373
Returns the new entry object.
375
412
if entry.file_id in self._byid:
376
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
378
if entry.parent_id == ROOT_ID or entry.parent_id is None:
379
entry.parent_id = self.root.file_id
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
382
416
parent = self._byid[entry.parent_id]
384
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
386
420
if parent.children.has_key(entry.name):
387
raise BzrError("%s is already versioned" %
421
bailout("%s is already versioned" %
388
422
appendpath(self.id2path(parent.file_id), entry.name))
390
424
self._byid[entry.file_id] = entry
391
425
parent.children[entry.name] = entry
395
428
def add_path(self, relpath, kind, file_id=None):
396
429
"""Add entry from a path.
398
The immediate parent must already be versioned.
400
Returns the new entry object."""
401
from bzrlib.branch import gen_file_id
431
The immediate parent must already be versioned"""
403
432
parts = bzrlib.osutils.splitpath(relpath)
404
433
if len(parts) == 0:
405
raise BzrError("cannot re-add root of inventory")
434
bailout("cannot re-add root of inventory")
407
436
if file_id == None:
408
file_id = gen_file_id(relpath)
410
parent_path = parts[:-1]
411
parent_id = self.path2id(parent_path)
412
if parent_id == None:
413
raise NotVersionedError(parent_path)
437
file_id = bzrlib.branch.gen_file_id(relpath)
439
parent_id = self.path2id(parts[:-1])
440
assert parent_id != None
415
441
ie = InventoryEntry(file_id, parts[-1],
416
442
kind=kind, parent_id=parent_id)
417
443
return self.add(ie)
444
469
del self[ie.parent_id].children[ie.name]
447
def __eq__(self, other):
473
return Set(self._byid)
476
def to_element(self):
477
"""Convert to XML Element"""
478
e = Element('inventory')
480
for path, ie in self.iter_entries():
481
e.append(ie.to_element())
485
def from_element(cls, elt):
486
"""Construct from XML Element
488
>>> inv = Inventory()
489
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
>>> elt = inv.to_element()
491
>>> inv2 = Inventory.from_element(elt)
495
assert elt.tag == 'inventory'
498
o.add(InventoryEntry.from_element(e))
501
from_element = classmethod(from_element)
504
def __cmp__(self, other):
448
505
"""Compare two sets by comparing their contents.
450
507
>>> i1 = Inventory()
454
511
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
455
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
458
514
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
459
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
463
521
if not isinstance(other, Inventory):
464
522
return NotImplemented
466
if len(self._byid) != len(other._byid):
467
# shortcut: obviously not the same
470
return self._byid == other._byid
473
def __ne__(self, other):
474
return not (self == other)
478
raise ValueError('not hashable')
524
if self.id_set() ^ other.id_set():
527
for file_id in self._byid:
528
c = cmp(self[file_id], other[file_id])
481
534
def get_idpath(self, file_id):
552
605
This does not move the working file."""
553
606
if not is_valid_name(new_name):
554
raise BzrError("not an acceptable filename: %r" % new_name)
607
bailout("not an acceptable filename: %r" % new_name)
556
609
new_parent = self._byid[new_parent_id]
557
610
if new_name in new_parent.children:
558
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
560
613
new_parent_idpath = self.get_idpath(new_parent_id)
561
614
if file_id in new_parent_idpath:
562
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
563
616
% (self.id2path(file_id), self.id2path(new_parent_id)))
565
618
file_ie = self._byid[file_id]