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
41
class InventoryEntry(XMLMixin):
31
class InventoryEntry(object):
42
32
"""Description of a versioned file.
44
34
An InventoryEntry has the following fields, which are also
72
62
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
73
63
Traceback (most recent call last):
75
BzrError: ('inventory already contains entry with id {2323}', [])
65
BzrError: inventory already contains entry with id {2323}
76
66
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
77
67
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
78
68
>>> i.path2id('src/wibble')
102
92
# TODO: split InventoryEntry into subclasses for files,
103
93
# directories, etc etc.
105
98
def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
99
"""Create an InventoryEntry
116
109
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
110
Traceback (most recent call last):
118
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
111
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
121
if len(splitpath(name)) != 1:
122
bailout('InventoryEntry name is not a simple filename: %r'
113
if '/' in name or '\\' in name:
114
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
125
116
self.file_id = file_id
128
119
self.text_id = text_id
129
120
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
132
121
if kind == 'directory':
133
122
self.children = {}
134
123
elif kind == 'file':
149
138
self.parent_id, text_id=self.text_id)
150
139
other.text_sha1 = self.text_sha1
151
140
other.text_size = self.text_size
141
# note that children are *not* copied; they're pulled across when
214
207
from_element = classmethod(from_element)
216
def __cmp__(self, other):
209
def __eq__(self, other):
219
210
if not isinstance(other, InventoryEntry):
220
211
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)
213
return (self.file_id == other.file_id) \
214
and (self.name == other.name) \
215
and (self.text_sha1 == other.text_sha1) \
216
and (self.text_size == other.text_size) \
217
and (self.text_id == other.text_id) \
218
and (self.parent_id == other.parent_id) \
219
and (self.kind == other.kind)
222
def __ne__(self, other):
223
return not (self == other)
226
raise ValueError('not hashable')
237
235
self.parent_id = None
240
def __cmp__(self, other):
238
def __eq__(self, other):
243
239
if not isinstance(other, RootEntry):
244
240
return NotImplemented
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
242
return (self.file_id == other.file_id) \
243
and (self.children == other.children)
247
class Inventory(object):
251
248
"""Inventory of versioned files in a tree.
253
250
This describes which file_id is present at each point in the tree,
265
262
inserted, other than through the Inventory API.
267
264
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
271
265
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
272
266
>>> inv['123-123'].name
284
278
>>> [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" />
280
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
281
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', 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?
283
def __init__(self, root_id=ROOT_ID):
310
284
"""Create or read an inventory.
312
286
If a working directory is specified, the inventory is read
316
290
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
293
# We are letting Branch(init=True) create a unique inventory
294
# root id. Rather than generating a random one here.
296
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
297
self.root = RootEntry(root_id)
320
298
self._byid = {self.root.file_id: self.root}
344
322
if ie.kind == 'directory':
345
323
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
324
yield os.path.join(name, cn), cie
328
"""Return list of (path, ie) for all entries except the root.
330
This may be faster than iter_entries.
333
def descend(dir_ie, dir_path):
334
kids = dir_ie.children.items()
336
for name, ie in kids:
337
child_path = os.path.join(dir_path, name)
338
accum.append((child_path, ie))
339
if ie.kind == 'directory':
340
descend(ie, child_path)
342
descend(self.root, '')
350
346
def directories(self):
351
"""Return (path, entry) pairs for all directories.
347
"""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))
350
def descend(parent_ie, parent_path):
351
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
353
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
356
for name, child_ie in kids:
357
child_path = os.path.join(parent_path, name)
358
descend(child_ie, child_path)
359
descend(self.root, '')
391
382
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
386
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
389
raise BzrError("can't look up file_id None")
391
raise BzrError("file_id {%s} not in inventory" % file_id)
394
def get_file_kind(self, file_id):
395
return self._byid[file_id].kind
403
397
def get_child(self, parent_id, filename):
404
398
return self[parent_id].children.get(filename)
410
404
To add a file to a branch ready to be committed, use Branch.add,
411
405
which calls this."""
412
406
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
407
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
409
if entry.parent_id == ROOT_ID or entry.parent_id is None:
410
entry.parent_id = self.root.file_id
416
413
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
415
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
417
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
418
raise BzrError("%s is already versioned" %
422
419
appendpath(self.id2path(parent.file_id), entry.name))
424
421
self._byid[entry.file_id] = entry
429
426
"""Add entry from a path.
431
428
The immediate parent must already be versioned"""
429
from bzrlib.errors import NotVersionedError
432
431
parts = bzrlib.osutils.splitpath(relpath)
433
432
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
433
raise BzrError("cannot re-add root of inventory")
436
435
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
436
from bzrlib.branch import gen_file_id
437
file_id = gen_file_id(relpath)
439
parent_path = parts[:-1]
440
parent_id = self.path2id(parent_path)
441
if parent_id == None:
442
raise NotVersionedError(parent_path)
441
444
ie = InventoryEntry(file_id, parts[-1],
442
445
kind=kind, parent_id=parent_id)
443
446
return self.add(ie)
469
472
del self[ie.parent_id].children[ie.name]
473
return Set(self._byid)
476
475
def to_element(self):
477
476
"""Convert to XML Element"""
477
from bzrlib.xml import Element
478
479
e = Element('inventory')
481
if self.root.file_id not in (None, ROOT_ID):
482
e.set('file_id', self.root.file_id)
480
483
for path, ie in self.iter_entries():
481
484
e.append(ie.to_element())
485
488
def from_element(cls, elt):
486
489
"""Construct from XML Element
488
491
>>> inv = Inventory()
489
492
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
493
>>> elt = inv.to_element()
498
# XXXX: doctest doesn't run this properly under python2.3
495
499
assert elt.tag == 'inventory'
500
root_id = elt.get('file_id') or ROOT_ID
498
o.add(InventoryEntry.from_element(e))
503
ie = InventoryEntry.from_element(e)
504
if ie.parent_id == ROOT_ID:
505
ie.parent_id = root_id
501
509
from_element = classmethod(from_element)
504
def __cmp__(self, other):
512
def __eq__(self, other):
505
513
"""Compare two sets by comparing their contents.
507
515
>>> i1 = Inventory()
521
526
if not isinstance(other, Inventory):
522
527
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])
529
if len(self._byid) != len(other._byid):
530
# shortcut: obviously not the same
533
return self._byid == other._byid
536
def __ne__(self, other):
537
return not (self == other)
541
raise ValueError('not hashable')
534
545
def get_idpath(self, file_id):
605
616
This does not move the working file."""
606
617
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
618
raise BzrError("not an acceptable filename: %r" % new_name)
609
620
new_parent = self._byid[new_parent_id]
610
621
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
622
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
624
new_parent_idpath = self.get_idpath(new_parent_id)
614
625
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
626
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
627
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
629
file_ie = self._byid[file_id]