14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""Inventories map files to their name in a revision."""
19
# TODO: Maybe store inventory_id in the file? Not really needed.
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
__author__ = "Martin Pool <mbp@canonical.com>"
24
import sys, os.path, types, re
28
from cElementTree import Element, ElementTree, SubElement
30
from elementtree.ElementTree import Element, ElementTree, SubElement
32
from xml import XMLMixin
33
from errors import bailout
18
# TODO: Maybe also keep the full path of the entry, and the children?
19
# But those depend on its position within a particular inventory, and
20
# it would be nice not to need to hold the backpointer here.
22
# TODO: Perhaps split InventoryEntry into subclasses for files,
23
# directories, etc etc.
26
# This should really be an id randomly assigned when the tree is
27
# created, but it's not for now.
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
from bzrlib.errors import BzrError, BzrCheckError
39
from bzrlib.osutils import quotefn, splitpath, joinpath, appendpath
37
40
from bzrlib.trace import mutter
39
class InventoryEntry(XMLMixin):
41
from bzrlib.errors import NotVersionedError
44
class InventoryEntry(object):
40
45
"""Description of a versioned file.
42
47
An InventoryEntry has the following fields, which are also
43
48
present in the XML inventory-entry element:
46
* *name*: (only the basename within the directory, must not
48
* *kind*: "directory" or "file"
49
* *directory_id*: (if absent/null means the branch root directory)
50
* *text_sha1*: only for files
51
* *text_size*: in bytes, only for files
52
* *text_id*: identifier for the text version, only for files
54
InventoryEntries can also exist inside a WorkingTree
55
inventory, in which case they are not yet bound to a
56
particular revision of the file. In that case the text_sha1,
57
text_size and text_id are absent.
53
(within the parent directory)
59
file_id of the parent directory, or ROOT_ID
62
the revision_id in which the name or parent of this file was
66
sha-1 of the text of the file
69
size in bytes of the text of the file
72
the revision_id in which the text of this file was introduced
74
(reading a version 4 tree created a text_id field.)
60
76
>>> i = Inventory()
62
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
79
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
80
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
81
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
82
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
64
83
>>> for j in i.iter_entries():
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
86
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
68
87
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
88
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
89
Traceback (most recent call last):
72
BzrError: ('inventory already contains entry with id {2323}', [])
73
>>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
74
>>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
91
BzrError: inventory already contains entry with id {2323}
92
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
93
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
94
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
95
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
75
96
>>> i.path2id('src/wibble')
79
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
100
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
101
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
81
103
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
>>> for j in i.iter_entries():
84
... assert i.path2id(j[0])
104
>>> for path, entry in i.iter_entries():
105
... print path.replace('\\\\', '/') # for win32 os.sep
106
... assert i.path2id(path)
90
112
src/wibble/wibble.c
113
>>> i.id2path('2326').replace('\\\\', '/')
92
114
'src/wibble/wibble.c'
94
:todo: Maybe also keep the full path of the entry, and the children?
95
But those depend on its position within a particular inventory, and
96
it would be nice not to need to hold the backpointer here.
98
def __init__(self, file_id, name, kind='file', text_id=None,
117
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
118
'text_id', 'parent_id', 'children',
119
'text_version', 'name_version', ]
122
def __init__(self, file_id, name, kind, parent_id, text_id=None):
100
123
"""Create an InventoryEntry
102
125
The filename must be a single component, relative to the
103
126
parent directory; it cannot be a whole path or relative name.
105
>>> e = InventoryEntry('123', 'hello.c')
128
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
110
>>> e = InventoryEntry('123', 'src/hello.c')
133
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
111
134
Traceback (most recent call last):
112
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
135
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
115
if len(splitpath(name)) != 1:
116
bailout('InventoryEntry name is not a simple filename: %r'
137
assert isinstance(name, basestring), name
138
if '/' in name or '\\' in name:
139
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
141
self.text_version = None
142
self.name_version = None
143
self.text_sha1 = None
144
self.text_size = None
119
145
self.file_id = file_id
121
assert kind in ['file', 'directory']
123
148
self.text_id = text_id
124
149
self.parent_id = parent_id
125
self.text_sha1 = None
126
self.text_size = None
127
150
if kind == 'directory':
128
151
self.children = {}
155
raise BzrError("unhandled entry kind %r" % kind)
131
159
def sorted_children(self):
154
def to_element(self):
155
"""Convert to XML element"""
158
e.set('name', self.name)
159
e.set('file_id', self.file_id)
160
e.set('kind', self.kind)
162
if self.text_size is not None:
163
e.set('text_size', '%d' % self.text_size)
165
for f in ['text_id', 'text_sha1', 'parent_id']:
175
def from_element(cls, elt):
176
assert elt.tag == 'entry'
177
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
178
self.text_id = elt.get('text_id')
179
self.text_sha1 = elt.get('text_sha1')
180
self.parent_id = elt.get('parent_id')
182
## mutter("read inventoryentry: %r" % (elt.attrib))
184
v = elt.get('text_size')
185
self.text_size = v and int(v)
190
from_element = classmethod(from_element)
192
def __cmp__(self, other):
187
def __eq__(self, other):
195
188
if not isinstance(other, InventoryEntry):
196
189
return NotImplemented
198
return cmp(self.file_id, other.file_id) \
199
or cmp(self.name, other.name) \
200
or cmp(self.text_sha1, other.text_sha1) \
201
or cmp(self.text_size, other.text_size) \
202
or cmp(self.text_id, other.text_id) \
203
or cmp(self.parent_id, other.parent_id) \
204
or cmp(self.kind, other.kind)
191
return (self.file_id == other.file_id) \
192
and (self.name == other.name) \
193
and (self.text_sha1 == other.text_sha1) \
194
and (self.text_size == other.text_size) \
195
and (self.text_id == other.text_id) \
196
and (self.parent_id == other.parent_id) \
197
and (self.kind == other.kind) \
198
and (self.text_version == other.text_version) \
199
and (self.name_version == other.name_version)
202
def __ne__(self, other):
203
return not (self == other)
206
raise ValueError('not hashable')
319
315
if ie.kind == 'directory':
320
316
for cn, cie in self.iter_entries(from_dir=ie.file_id):
321
yield '/'.join((name, cn)), cie
325
def directories(self, from_dir=None):
326
"""Return (path, entry) pairs for all directories.
317
yield os.path.join(name, cn), cie
321
"""Return list of (path, ie) for all entries except the root.
323
This may be faster than iter_entries.
328
def descend(parent_ie):
329
parent_name = parent_ie.name
330
yield parent_name, parent_ie
332
# directory children in sorted order
334
for ie in parent_ie.children.itervalues():
326
def descend(dir_ie, dir_path):
327
kids = dir_ie.children.items()
329
for name, ie in kids:
330
child_path = os.path.join(dir_path, name)
331
accum.append((child_path, ie))
335
332
if ie.kind == 'directory':
336
dn.append((ie.name, ie))
333
descend(ie, child_path)
335
descend(self.root, '')
339
def directories(self):
340
"""Return (path, entry) pairs for all directories, including the root.
343
def descend(parent_ie, parent_path):
344
accum.append((parent_path, parent_ie))
339
for name, child_ie in dn:
340
for sub_name, sub_ie in descend(child_ie):
341
yield appendpath(parent_name, sub_name), sub_ie
346
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
343
for name, ie in descend(self.root):
349
for name, child_ie in kids:
350
child_path = os.path.join(parent_path, name)
351
descend(child_ie, child_path)
352
descend(self.root, '')
377
397
"""Add entry to inventory.
379
399
To add a file to a branch ready to be committed, use Branch.add,
402
Returns the new entry object.
381
404
if entry.file_id in self._byid:
382
bailout("inventory already contains entry with id {%s}" % entry.file_id)
405
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
407
if entry.parent_id == ROOT_ID or entry.parent_id is None:
408
entry.parent_id = self.root.file_id
385
411
parent = self._byid[entry.parent_id]
387
bailout("parent_id %r not in inventory" % entry.parent_id)
413
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
389
415
if parent.children.has_key(entry.name):
390
bailout("%s is already versioned" %
416
raise BzrError("%s is already versioned" %
391
417
appendpath(self.id2path(parent.file_id), entry.name))
393
419
self._byid[entry.file_id] = entry
394
420
parent.children[entry.name] = entry
397
424
def add_path(self, relpath, kind, file_id=None):
398
425
"""Add entry from a path.
400
The immediate parent must already be versioned"""
427
The immediate parent must already be versioned.
429
Returns the new entry object."""
430
from bzrlib.branch import gen_file_id
401
432
parts = bzrlib.osutils.splitpath(relpath)
402
433
if len(parts) == 0:
403
bailout("cannot re-add root of inventory")
406
file_id = bzrlib.branch.gen_file_id(relpath)
408
parent_id = self.path2id(parts[:-1])
434
raise BzrError("cannot re-add root of inventory")
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)
409
444
ie = InventoryEntry(file_id, parts[-1],
410
445
kind=kind, parent_id=parent_id)
411
446
return self.add(ie)
437
473
del self[ie.parent_id].children[ie.name]
441
return Set(self._byid)
444
def to_element(self):
445
"""Convert to XML Element"""
446
e = Element('inventory')
448
for path, ie in self.iter_entries():
449
e.append(ie.to_element())
453
def from_element(cls, elt):
454
"""Construct from XML Element
456
>>> inv = Inventory()
457
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
458
>>> elt = inv.to_element()
459
>>> inv2 = Inventory.from_element(elt)
463
assert elt.tag == 'inventory'
466
o.add(InventoryEntry.from_element(e))
469
from_element = classmethod(from_element)
472
def __cmp__(self, other):
476
def __eq__(self, other):
473
477
"""Compare two sets by comparing their contents.
475
479
>>> i1 = Inventory()
476
480
>>> i2 = Inventory()
479
>>> i1.add(InventoryEntry('123', 'foo'))
483
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
484
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
482
>>> i2.add(InventoryEntry('123', 'foo'))
487
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
488
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
489
492
if not isinstance(other, Inventory):
490
493
return NotImplemented
492
if self.id_set() ^ other.id_set():
495
for file_id in self._byid:
496
c = cmp(self[file_id], other[file_id])
502
def id2path(self, file_id):
503
"""Return as a list the path to file_id."""
495
if len(self._byid) != len(other._byid):
496
# shortcut: obviously not the same
499
return self._byid == other._byid
502
def __ne__(self, other):
503
return not self.__eq__(other)
507
raise ValueError('not hashable')
510
def get_idpath(self, file_id):
511
"""Return a list of file_ids for the path to an entry.
513
The list contains one element for each directory followed by
514
the id of the file itself. So the length of the returned list
515
is equal to the depth of the file in the tree, counting the
516
root directory as depth 1.
505
519
while file_id != None:
506
ie = self._byid[file_id]
521
ie = self._byid[file_id]
523
raise BzrError("file_id {%s} not found in inventory" % file_id)
524
p.insert(0, ie.file_id)
508
525
file_id = ie.parent_id
529
def id2path(self, file_id):
530
"""Return as a list the path to file_id."""
532
# get all names, skipping root
533
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
534
return os.sep.join(p)