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', ]
105
105
def __init__(self, file_id, name, kind, parent_id, text_id=None):
106
106
"""Create an InventoryEntry
116
116
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
117
117
Traceback (most recent call last):
118
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
118
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
121
if len(splitpath(name)) != 1:
122
bailout('InventoryEntry name is not a simple filename: %r'
120
if '/' in name or '\\' in name:
121
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
123
self.text_sha1 = None
124
self.text_size = None
125
126
self.file_id = file_id
128
129
self.text_id = text_id
129
130
self.parent_id = parent_id
130
self.text_sha1 = None
131
self.text_size = None
132
131
if kind == 'directory':
133
132
self.children = {}
134
133
elif kind == 'file':
149
148
self.parent_id, text_id=self.text_id)
150
149
other.text_sha1 = self.text_sha1
151
150
other.text_size = self.text_size
151
# note that children are *not* copied; they're pulled across when
214
217
from_element = classmethod(from_element)
216
def __cmp__(self, other):
219
def __eq__(self, other):
219
220
if not isinstance(other, InventoryEntry):
220
221
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)
223
return (self.file_id == other.file_id) \
224
and (self.name == other.name) \
225
and (self.text_sha1 == other.text_sha1) \
226
and (self.text_size == other.text_size) \
227
and (self.text_id == other.text_id) \
228
and (self.parent_id == other.parent_id) \
229
and (self.kind == other.kind)
232
def __ne__(self, other):
233
return not (self == other)
236
raise ValueError('not hashable')
237
245
self.parent_id = None
240
def __cmp__(self, other):
248
def __eq__(self, other):
243
249
if not isinstance(other, RootEntry):
244
250
return NotImplemented
245
return cmp(self.file_id, other.file_id) \
246
or cmp(self.children, other.children)
250
class Inventory(XMLMixin):
252
return (self.file_id == other.file_id) \
253
and (self.children == other.children)
257
class Inventory(object):
251
258
"""Inventory of versioned files in a tree.
253
260
This describes which file_id is present at each point in the tree,
265
272
inserted, other than through the Inventory API.
267
274
>>> inv = Inventory()
268
>>> inv.write_xml(sys.stdout)
271
275
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
276
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
272
277
>>> inv['123-123'].name
284
289
>>> [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" />
291
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
292
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
293
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?
295
def __init__(self, root_id=ROOT_ID):
310
296
"""Create or read an inventory.
312
298
If a working directory is specified, the inventory is read
316
302
The inventory is created with a default root directory, with
319
self.root = RootEntry(ROOT_ID)
305
# We are letting Branch(init=True) create a unique inventory
306
# root id. Rather than generating a random one here.
308
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
309
self.root = RootEntry(root_id)
320
310
self._byid = {self.root.file_id: self.root}
344
334
if ie.kind == 'directory':
345
335
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
336
yield os.path.join(name, cn), cie
340
"""Return list of (path, ie) for all entries except the root.
342
This may be faster than iter_entries.
345
def descend(dir_ie, dir_path):
346
kids = dir_ie.children.items()
348
for name, ie in kids:
349
child_path = os.path.join(dir_path, name)
350
accum.append((child_path, ie))
351
if ie.kind == 'directory':
352
descend(ie, child_path)
354
descend(self.root, '')
350
358
def directories(self):
351
"""Return (path, entry) pairs for all directories.
359
"""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))
362
def descend(parent_ie, parent_path):
363
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
365
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
368
for name, child_ie in kids:
369
child_path = os.path.join(parent_path, name)
370
descend(child_ie, child_path)
371
descend(self.root, '')
376
379
>>> inv = Inventory()
377
380
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
381
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
389
393
>>> inv = Inventory()
390
394
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
395
InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
391
396
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
400
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
403
raise BzrError("can't look up file_id None")
405
raise BzrError("file_id {%s} not in inventory" % file_id)
408
def get_file_kind(self, file_id):
409
return self._byid[file_id].kind
403
411
def get_child(self, parent_id, filename):
404
412
return self[parent_id].children.get(filename)
410
418
To add a file to a branch ready to be committed, use Branch.add,
411
419
which calls this."""
412
420
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
421
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
423
if entry.parent_id == ROOT_ID or entry.parent_id is None:
424
entry.parent_id = self.root.file_id
416
427
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
429
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
431
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
432
raise BzrError("%s is already versioned" %
422
433
appendpath(self.id2path(parent.file_id), entry.name))
424
435
self._byid[entry.file_id] = entry
425
436
parent.children[entry.name] = entry
428
440
def add_path(self, relpath, kind, file_id=None):
429
441
"""Add entry from a path.
431
443
The immediate parent must already be versioned"""
444
from bzrlib.branch import gen_file_id
432
446
parts = bzrlib.osutils.splitpath(relpath)
433
447
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
448
raise BzrError("cannot re-add root of inventory")
436
450
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
451
file_id = gen_file_id(relpath)
453
parent_path = parts[:-1]
454
parent_id = self.path2id(parent_path)
455
if parent_id == None:
456
raise NotVersionedError(parent_path)
441
458
ie = InventoryEntry(file_id, parts[-1],
442
459
kind=kind, parent_id=parent_id)
443
460
return self.add(ie)
449
466
>>> inv = Inventory()
450
467
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
468
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
453
471
>>> del inv['123']
469
487
del self[ie.parent_id].children[ie.name]
473
return Set(self._byid)
476
490
def to_element(self):
477
491
"""Convert to XML Element"""
492
from bzrlib.xml import Element
478
494
e = Element('inventory')
496
if self.root.file_id not in (None, ROOT_ID):
497
e.set('file_id', self.root.file_id)
480
498
for path, ie in self.iter_entries():
481
499
e.append(ie.to_element())
485
503
def from_element(cls, elt):
486
504
"""Construct from XML Element
488
506
>>> inv = Inventory()
489
507
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
508
InventoryEntry('foo.c-123981239', 'foo.c', kind='file', parent_id='TREE_ROOT')
490
509
>>> elt = inv.to_element()
491
510
>>> inv2 = Inventory.from_element(elt)
514
# XXXX: doctest doesn't run this properly under python2.3
495
515
assert elt.tag == 'inventory'
516
root_id = elt.get('file_id') or ROOT_ID
498
o.add(InventoryEntry.from_element(e))
519
ie = InventoryEntry.from_element(e)
520
if ie.parent_id == ROOT_ID:
521
ie.parent_id = root_id
501
525
from_element = classmethod(from_element)
504
def __cmp__(self, other):
528
def __eq__(self, other):
505
529
"""Compare two sets by comparing their contents.
507
531
>>> i1 = Inventory()
511
535
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
536
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
514
539
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
540
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
521
544
if not isinstance(other, Inventory):
522
545
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])
547
if len(self._byid) != len(other._byid):
548
# shortcut: obviously not the same
551
return self._byid == other._byid
554
def __ne__(self, other):
555
return not (self == other)
559
raise ValueError('not hashable')
534
563
def get_idpath(self, file_id):
554
583
"""Return as a list the path to file_id."""
556
585
# get all names, skipping root
557
p = [self[fid].name for fid in self.get_idpath(file_id)[1:]]
586
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
558
587
return os.sep.join(p)
605
634
This does not move the working file."""
606
635
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
636
raise BzrError("not an acceptable filename: %r" % new_name)
609
638
new_parent = self._byid[new_parent_id]
610
639
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
640
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
642
new_parent_idpath = self.get_idpath(new_parent_id)
614
643
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
644
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
645
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
647
file_ie = self._byid[file_id]