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" />
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?
309
281
def __init__(self):
310
282
"""Create or read an inventory.
344
316
if ie.kind == 'directory':
345
317
for cn, cie in self.iter_entries(from_dir=ie.file_id):
346
318
yield os.path.join(name, cn), cie
322
"""Return list of (path, ie) for all entries except the root.
324
This may be faster than iter_entries.
327
def descend(dir_ie, dir_path):
328
kids = dir_ie.children.items()
330
for name, ie in kids:
331
child_path = os.path.join(dir_path, name)
332
accum.append((child_path, ie))
333
if ie.kind == 'directory':
334
descend(ie, child_path)
336
descend(self.root, '')
350
340
def directories(self):
351
"""Return (path, entry) pairs for all directories.
341
"""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))
344
def descend(parent_ie, parent_path):
345
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
347
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
368
for name, ie in descend(self.root):
350
for name, child_ie in kids:
351
child_path = os.path.join(parent_path, name)
352
descend(child_ie, child_path)
353
descend(self.root, '')
391
376
>>> inv['123123'].name
395
raise BzrError("can't look up file_id None")
398
380
return self._byid[file_id]
400
raise BzrError("file_id {%s} not in inventory" % file_id)
383
raise BzrError("can't look up file_id None")
385
raise BzrError("file_id {%s} not in inventory" % file_id)
388
def get_file_kind(self, file_id):
389
return self._byid[file_id].kind
403
391
def get_child(self, parent_id, filename):
404
392
return self[parent_id].children.get(filename)
410
398
To add a file to a branch ready to be committed, use Branch.add,
411
399
which calls this."""
412
400
if entry.file_id in self._byid:
413
bailout("inventory already contains entry with id {%s}" % entry.file_id)
401
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
416
404
parent = self._byid[entry.parent_id]
418
bailout("parent_id {%s} not in inventory" % entry.parent_id)
406
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
420
408
if parent.children.has_key(entry.name):
421
bailout("%s is already versioned" %
409
raise BzrError("%s is already versioned" %
422
410
appendpath(self.id2path(parent.file_id), entry.name))
424
412
self._byid[entry.file_id] = entry
429
417
"""Add entry from a path.
431
419
The immediate parent must already be versioned"""
420
from bzrlib.errors import NotVersionedError
432
422
parts = bzrlib.osutils.splitpath(relpath)
433
423
if len(parts) == 0:
434
bailout("cannot re-add root of inventory")
424
raise BzrError("cannot re-add root of inventory")
436
426
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
427
from bzrlib.branch import gen_file_id
428
file_id = gen_file_id(relpath)
430
parent_path = parts[:-1]
431
parent_id = self.path2id(parent_path)
432
if parent_id == None:
433
raise NotVersionedError(parent_path)
441
435
ie = InventoryEntry(file_id, parts[-1],
442
436
kind=kind, parent_id=parent_id)
443
437
return self.add(ie)
469
463
del self[ie.parent_id].children[ie.name]
473
return Set(self._byid)
476
466
def to_element(self):
477
467
"""Convert to XML Element"""
468
from bzrlib.xml import Element
478
470
e = Element('inventory')
480
472
for path, ie in self.iter_entries():
485
477
def from_element(cls, elt):
486
478
"""Construct from XML Element
488
480
>>> inv = Inventory()
489
481
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c', 'file', ROOT_ID))
490
482
>>> elt = inv.to_element()
521
511
if not isinstance(other, Inventory):
522
512
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])
514
if len(self._byid) != len(other._byid):
515
# shortcut: obviously not the same
518
return self._byid == other._byid
521
def __ne__(self, other):
522
return not (self == other)
526
raise ValueError('not hashable')
534
530
def get_idpath(self, file_id):
605
601
This does not move the working file."""
606
602
if not is_valid_name(new_name):
607
bailout("not an acceptable filename: %r" % new_name)
603
raise BzrError("not an acceptable filename: %r" % new_name)
609
605
new_parent = self._byid[new_parent_id]
610
606
if new_name in new_parent.children:
611
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
607
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
613
609
new_parent_idpath = self.get_idpath(new_parent_id)
614
610
if file_id in new_parent_idpath:
615
bailout("cannot move directory %r into a subdirectory of itself, %r"
611
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
616
612
% (self.id2path(file_id), self.id2path(new_parent_id)))
618
614
file_ie = self._byid[file_id]