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
72
64
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
73
65
Traceback (most recent call last):
75
BzrError: ('inventory already contains entry with id {2323}', [])
67
BzrError: inventory already contains entry with id {2323}
76
68
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
77
69
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
78
70
>>> i.path2id('src/wibble')
102
94
# TODO: split InventoryEntry into subclasses for files,
103
95
# directories, etc etc.
97
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
98
'text_id', 'parent_id', 'children', ]
105
100
def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
101
"""Create an InventoryEntry
116
111
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
112
Traceback (most recent call last):
118
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
113
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
121
if len(splitpath(name)) != 1:
122
bailout('InventoryEntry name is not a simple filename: %r'
115
if '/' in name or '\\' in name:
116
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
118
self.text_sha1 = None
119
self.text_size = None
125
121
self.file_id = file_id
128
124
self.text_id = text_id
129
125
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
132
126
if kind == 'directory':
133
127
self.children = {}
134
128
elif kind == 'file':
149
143
self.parent_id, text_id=self.text_id)
150
144
other.text_sha1 = self.text_sha1
151
145
other.text_size = self.text_size
146
# note that children are *not* copied; they're pulled across when
214
212
from_element = classmethod(from_element)
216
def __cmp__(self, other):
214
def __eq__(self, other):
219
215
if not isinstance(other, InventoryEntry):
220
216
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)
218
return (self.file_id == other.file_id) \
219
and (self.name == other.name) \
220
and (self.text_sha1 == other.text_sha1) \
221
and (self.text_size == other.text_size) \
222
and (self.text_id == other.text_id) \
223
and (self.parent_id == other.parent_id) \
224
and (self.kind == other.kind)
227
def __ne__(self, other):
228
return not (self == other)
231
raise ValueError('not hashable')
237
240
self.parent_id = None
240
def __cmp__(self, other):
243
def __eq__(self, other):
243
244
if not isinstance(other, RootEntry):
244
245
return NotImplemented
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
247
return (self.file_id == other.file_id) \
248
and (self.children == other.children)
252
class Inventory(object):
251
253
"""Inventory of versioned files in a tree.
253
255
This describes which file_id is present at each point in the tree,
265
267
inserted, other than through the Inventory API.
267
269
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
271
270
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
272
271
>>> inv['123-123'].name
284
283
>>> [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" />
285
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
286
>>> 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?
288
def __init__(self, root_id=ROOT_ID):
310
289
"""Create or read an inventory.
312
291
If a working directory is specified, the inventory is read
316
295
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
298
# We are letting Branch(init=True) create a unique inventory
299
# root id. Rather than generating a random one here.
301
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
302
self.root = RootEntry(root_id)
320
303
self._byid = {self.root.file_id: self.root}
344
327
if ie.kind == 'directory':
345
328
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
329
yield os.path.join(name, cn), cie
333
"""Return list of (path, ie) for all entries except the root.
335
This may be faster than iter_entries.
338
def descend(dir_ie, dir_path):
339
kids = dir_ie.children.items()
341
for name, ie in kids:
342
child_path = os.path.join(dir_path, name)
343
accum.append((child_path, ie))
344
if ie.kind == 'directory':
345
descend(ie, child_path)
347
descend(self.root, '')
350
351
def directories(self):
351
"""Return (path, entry) pairs for all directories.
352
"""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))
355
def descend(parent_ie, parent_path):
356
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
358
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
361
for name, child_ie in kids:
362
child_path = os.path.join(parent_path, name)
363
descend(child_ie, child_path)
364
descend(self.root, '')
391
387
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
391
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
394
raise BzrError("can't look up file_id None")
396
raise BzrError("file_id {%s} not in inventory" % file_id)
399
def get_file_kind(self, file_id):
400
return self._byid[file_id].kind
403
402
def get_child(self, parent_id, filename):
404
403
return self[parent_id].children.get(filename)
410
409
To add a file to a branch ready to be committed, use Branch.add,
411
410
which calls this."""
412
411
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
412
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
414
if entry.parent_id == ROOT_ID or entry.parent_id is None:
415
entry.parent_id = self.root.file_id
416
418
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
420
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
422
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
423
raise BzrError("%s is already versioned" %
422
424
appendpath(self.id2path(parent.file_id), entry.name))
424
426
self._byid[entry.file_id] = entry
429
431
"""Add entry from a path.
431
433
The immediate parent must already be versioned"""
434
from bzrlib.branch import gen_file_id
432
436
parts = bzrlib.osutils.splitpath(relpath)
433
437
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
438
raise BzrError("cannot re-add root of inventory")
436
440
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
441
file_id = gen_file_id(relpath)
443
parent_path = parts[:-1]
444
parent_id = self.path2id(parent_path)
445
if parent_id == None:
446
raise NotVersionedError(parent_path)
441
448
ie = InventoryEntry(file_id, parts[-1],
442
449
kind=kind, parent_id=parent_id)
443
450
return self.add(ie)
469
476
del self[ie.parent_id].children[ie.name]
473
return Set(self._byid)
476
479
def to_element(self):
477
480
"""Convert to XML Element"""
481
from bzrlib.xml import Element
478
483
e = Element('inventory')
485
if self.root.file_id not in (None, ROOT_ID):
486
e.set('file_id', self.root.file_id)
480
487
for path, ie in self.iter_entries():
481
488
e.append(ie.to_element())
485
492
def from_element(cls, elt):
486
493
"""Construct from XML Element
488
495
>>> inv = Inventory()
489
496
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
497
>>> elt = inv.to_element()
502
# XXXX: doctest doesn't run this properly under python2.3
495
503
assert elt.tag == 'inventory'
504
root_id = elt.get('file_id') or ROOT_ID
498
o.add(InventoryEntry.from_element(e))
507
ie = InventoryEntry.from_element(e)
508
if ie.parent_id == ROOT_ID:
509
ie.parent_id = root_id
501
513
from_element = classmethod(from_element)
504
def __cmp__(self, other):
516
def __eq__(self, other):
505
517
"""Compare two sets by comparing their contents.
507
519
>>> i1 = Inventory()
521
530
if not isinstance(other, Inventory):
522
531
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])
533
if len(self._byid) != len(other._byid):
534
# shortcut: obviously not the same
537
return self._byid == other._byid
540
def __ne__(self, other):
541
return not (self == other)
545
raise ValueError('not hashable')
534
549
def get_idpath(self, file_id):
554
569
"""Return as a list the path to file_id."""
556
571
# get all names, skipping root
557
p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
572
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
558
573
return os.sep.join(p)
605
620
This does not move the working file."""
606
621
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
622
raise BzrError("not an acceptable filename: %r" % new_name)
609
624
new_parent = self._byid[new_parent_id]
610
625
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
626
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
628
new_parent_idpath = self.get_idpath(new_parent_id)
614
629
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
630
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
631
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
633
file_ie = self._byid[file_id]