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.
21
18
# This should really be an id randomly assigned when the tree is
22
19
# created, but it's not for now.
23
20
ROOT_ID = "TREE_ROOT"
26
23
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
38
28
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
39
29
from bzrlib.trace import mutter
30
from bzrlib.errors import NotVersionedError
41
class InventoryEntry(XMLMixin):
33
class InventoryEntry(object):
42
34
"""Description of a versioned file.
44
36
An InventoryEntry has the following fields, which are also
65
57
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
58
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
66
59
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
60
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
67
61
>>> for j in i.iter_entries():
72
66
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
73
67
Traceback (most recent call last):
75
BzrError: ('inventory already contains entry with id {2323}', [])
69
BzrError: inventory already contains entry with id {2323}
76
70
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
71
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
77
72
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
73
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
78
74
>>> i.path2id('src/wibble')
82
78
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
79
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
84
81
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
85
82
>>> for j in i.iter_entries():
102
99
# TODO: split InventoryEntry into subclasses for files,
103
100
# directories, etc etc.
102
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
103
'text_id', 'parent_id', 'children',
104
'text_version', 'entry_version', ]
105
107
def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
108
"""Create an InventoryEntry
116
118
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
119
Traceback (most recent call last):
118
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
120
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
121
if len(splitpath(name)) != 1:
122
bailout('InventoryEntry name is not a simple filename: %r'
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
125
130
self.file_id = file_id
128
133
self.text_id = text_id
129
134
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
132
135
if kind == 'directory':
133
136
self.children = {}
134
137
elif kind == 'file':
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):
169
def __eq__(self, other):
219
170
if not isinstance(other, InventoryEntry):
220
171
return NotImplemented
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)
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')
237
197
self.parent_id = None
240
def __cmp__(self, other):
200
def __eq__(self, other):
243
201
if not isinstance(other, RootEntry):
244
202
return NotImplemented
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
204
return (self.file_id == other.file_id) \
205
and (self.children == other.children)
209
class Inventory(object):
251
210
"""Inventory of versioned files in a tree.
253
212
This describes which file_id is present at each point in the tree,
265
224
inserted, other than through the Inventory API.
267
226
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
271
227
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
228
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
272
229
>>> inv['123-123'].name
284
241
>>> [x[0] for x in inv.iter_entries()]
287
>>> inv.write_xml(sys.stdout)
289
<entry file_id="123-123" kind="file" name="hello.c" />
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')
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?
247
def __init__(self, root_id=ROOT_ID):
310
248
"""Create or read an inventory.
312
250
If a working directory is specified, the inventory is read
316
254
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
257
# We are letting Branch.initialize() 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)
320
262
self._byid = {self.root.file_id: self.root}
344
286
if ie.kind == 'directory':
345
287
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
288
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.
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))
303
if ie.kind == 'directory':
304
descend(ie, child_path)
306
descend(self.root, '')
350
310
def directories(self):
351
"""Return (path, entry) pairs for all directories.
311
"""Return (path, entry) pairs for all directories, including the root.
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():
360
if ie.kind == 'directory':
361
dn.append((ie.name, ie))
314
def descend(parent_ie, parent_path):
315
accum.append((parent_path, parent_ie))
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
317
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
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, '')
389
345
>>> inv = Inventory()
390
346
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
347
InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
391
348
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
352
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % 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
403
363
def get_child(self, parent_id, filename):
404
364
return self[parent_id].children.get(filename)
408
368
"""Add entry to inventory.
410
370
To add a file to a branch ready to be committed, use Branch.add,
373
Returns the new entry object.
412
375
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
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
416
382
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
384
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
386
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
387
raise BzrError("%s is already versioned" %
422
388
appendpath(self.id2path(parent.file_id), entry.name))
424
390
self._byid[entry.file_id] = entry
425
391
parent.children[entry.name] = entry
428
395
def add_path(self, relpath, kind, file_id=None):
429
396
"""Add entry from a path.
431
The immediate parent must already be versioned"""
398
The immediate parent must already be versioned.
400
Returns the new entry object."""
401
from bzrlib.branch import gen_file_id
432
403
parts = bzrlib.osutils.splitpath(relpath)
433
404
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
405
raise BzrError("cannot re-add root of inventory")
436
407
if file_id == None:
437
file_id = bzrlib.branch.gen_file_id(relpath)
439
parent_id = self.path2id(parts[:-1])
440
assert parent_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)
441
415
ie = InventoryEntry(file_id, parts[-1],
442
416
kind=kind, parent_id=parent_id)
443
417
return self.add(ie)
469
444
del self[ie.parent_id].children[ie.name]
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):
447
def __eq__(self, other):
505
448
"""Compare two sets by comparing their contents.
507
450
>>> i1 = Inventory()
511
454
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
455
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
514
458
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
459
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
521
463
if not isinstance(other, Inventory):
522
464
return NotImplemented
524
if self.id_set() ^ other.id_set():
527
for file_id in self._byid:
528
c = cmp(self[file_id], other[file_id])
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')
534
481
def get_idpath(self, file_id):
605
552
This does not move the working file."""
606
553
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
554
raise BzrError("not an acceptable filename: %r" % new_name)
609
556
new_parent = self._byid[new_parent_id]
610
557
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
558
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
560
new_parent_idpath = self.get_idpath(new_parent_id)
614
561
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
562
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
563
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
565
file_ie = self._byid[file_id]