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.
95
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
96
'text_id', 'parent_id', 'children', ]
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)
116
self.text_sha1 = None
117
self.text_size = None
125
119
self.file_id = file_id
128
122
self.text_id = text_id
129
123
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
132
124
if kind == 'directory':
133
125
self.children = {}
134
126
elif kind == 'file':
149
141
self.parent_id, text_id=self.text_id)
150
142
other.text_sha1 = self.text_sha1
151
143
other.text_size = self.text_size
144
# note that children are *not* copied; they're pulled across when
214
210
from_element = classmethod(from_element)
216
def __cmp__(self, other):
212
def __eq__(self, other):
219
213
if not isinstance(other, InventoryEntry):
220
214
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)
216
return (self.file_id == other.file_id) \
217
and (self.name == other.name) \
218
and (self.text_sha1 == other.text_sha1) \
219
and (self.text_size == other.text_size) \
220
and (self.text_id == other.text_id) \
221
and (self.parent_id == other.parent_id) \
222
and (self.kind == other.kind)
225
def __ne__(self, other):
226
return not (self == other)
229
raise ValueError('not hashable')
237
238
self.parent_id = None
240
def __cmp__(self, other):
241
def __eq__(self, other):
243
242
if not isinstance(other, RootEntry):
244
243
return NotImplemented
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
245
return (self.file_id == other.file_id) \
246
and (self.children == other.children)
250
class Inventory(object):
251
251
"""Inventory of versioned files in a tree.
253
253
This describes which file_id is present at each point in the tree,
265
265
inserted, other than through the Inventory API.
267
267
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
271
268
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
272
269
>>> inv['123-123'].name
284
281
>>> [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" />
283
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
284
>>> 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?
286
def __init__(self, root_id=ROOT_ID):
310
287
"""Create or read an inventory.
312
289
If a working directory is specified, the inventory is read
316
293
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
296
# We are letting Branch(init=True) create a unique inventory
297
# root id. Rather than generating a random one here.
299
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
300
self.root = RootEntry(root_id)
320
301
self._byid = {self.root.file_id: self.root}
344
325
if ie.kind == 'directory':
345
326
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
327
yield os.path.join(name, cn), cie
331
"""Return list of (path, ie) for all entries except the root.
333
This may be faster than iter_entries.
336
def descend(dir_ie, dir_path):
337
kids = dir_ie.children.items()
339
for name, ie in kids:
340
child_path = os.path.join(dir_path, name)
341
accum.append((child_path, ie))
342
if ie.kind == 'directory':
343
descend(ie, child_path)
345
descend(self.root, '')
350
349
def directories(self):
351
"""Return (path, entry) pairs for all directories.
350
"""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))
353
def descend(parent_ie, parent_path):
354
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
356
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
359
for name, child_ie in kids:
360
child_path = os.path.join(parent_path, name)
361
descend(child_ie, child_path)
362
descend(self.root, '')
391
385
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
389
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
392
raise BzrError("can't look up file_id None")
394
raise BzrError("file_id {%s} not in inventory" % file_id)
397
def get_file_kind(self, file_id):
398
return self._byid[file_id].kind
403
400
def get_child(self, parent_id, filename):
404
401
return self[parent_id].children.get(filename)
410
407
To add a file to a branch ready to be committed, use Branch.add,
411
408
which calls this."""
412
409
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
410
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
412
if entry.parent_id == ROOT_ID or entry.parent_id is None:
413
entry.parent_id = self.root.file_id
416
416
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
418
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
420
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
421
raise BzrError("%s is already versioned" %
422
422
appendpath(self.id2path(parent.file_id), entry.name))
424
424
self._byid[entry.file_id] = entry
429
429
"""Add entry from a path.
431
431
The immediate parent must already be versioned"""
432
from bzrlib.errors import NotVersionedError
432
434
parts = bzrlib.osutils.splitpath(relpath)
433
435
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
436
raise BzrError("cannot re-add root of inventory")
436
438
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
439
from bzrlib.branch import gen_file_id
440
file_id = gen_file_id(relpath)
442
parent_path = parts[:-1]
443
parent_id = self.path2id(parent_path)
444
if parent_id == None:
445
raise NotVersionedError(parent_path)
441
447
ie = InventoryEntry(file_id, parts[-1],
442
448
kind=kind, parent_id=parent_id)
443
449
return self.add(ie)
469
475
del self[ie.parent_id].children[ie.name]
473
return Set(self._byid)
476
478
def to_element(self):
477
479
"""Convert to XML Element"""
480
from bzrlib.xml import Element
478
482
e = Element('inventory')
484
if self.root.file_id not in (None, ROOT_ID):
485
e.set('file_id', self.root.file_id)
480
486
for path, ie in self.iter_entries():
481
487
e.append(ie.to_element())
485
491
def from_element(cls, elt):
486
492
"""Construct from XML Element
488
494
>>> inv = Inventory()
489
495
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
496
>>> elt = inv.to_element()
501
# XXXX: doctest doesn't run this properly under python2.3
495
502
assert elt.tag == 'inventory'
503
root_id = elt.get('file_id') or ROOT_ID
498
o.add(InventoryEntry.from_element(e))
506
ie = InventoryEntry.from_element(e)
507
if ie.parent_id == ROOT_ID:
508
ie.parent_id = root_id
501
512
from_element = classmethod(from_element)
504
def __cmp__(self, other):
515
def __eq__(self, other):
505
516
"""Compare two sets by comparing their contents.
507
518
>>> i1 = Inventory()
521
529
if not isinstance(other, Inventory):
522
530
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])
532
if len(self._byid) != len(other._byid):
533
# shortcut: obviously not the same
536
return self._byid == other._byid
539
def __ne__(self, other):
540
return not (self == other)
544
raise ValueError('not hashable')
534
548
def get_idpath(self, file_id):
554
568
"""Return as a list the path to file_id."""
556
570
# get all names, skipping root
557
p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
571
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
558
572
return os.sep.join(p)
605
619
This does not move the working file."""
606
620
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
621
raise BzrError("not an acceptable filename: %r" % new_name)
609
623
new_parent = self._byid[new_parent_id]
610
624
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
625
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
627
new_parent_idpath = self.get_idpath(new_parent_id)
614
628
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
629
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
630
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
632
file_ie = self._byid[file_id]