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
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.
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>"
31
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
34
from bzrlib.errors import BzrError, BzrCheckError
36
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
37
from bzrlib.trace import mutter
38
from bzrlib.errors import NotVersionedError
41
class InventoryEntry(object):
39
class InventoryEntry(XMLMixin):
42
40
"""Description of a versioned file.
44
42
An InventoryEntry has the following fields, which are also
45
43
present in the XML inventory-entry element:
50
(within the parent directory)
56
file_id of the parent directory, or ROOT_ID
59
the revision_id in which the name or parent of this file was
63
sha-1 of the text of the file
66
size in bytes of the text of the file
69
the revision_id in which the text of this file was introduced
71
(reading a version 4 tree created a text_id field.)
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.
73
60
>>> i = Inventory()
76
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
77
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
78
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
79
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
62
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
80
64
>>> for j in i.iter_entries():
83
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
84
68
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
85
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
86
70
Traceback (most recent call last):
88
BzrError: inventory already contains entry with id {2323}
89
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
90
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
91
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
92
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
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'))
93
75
>>> i.path2id('src/wibble')
97
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
98
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
79
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
100
81
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
101
82
>>> for j in i.iter_entries():
109
90
src/wibble/wibble.c
110
91
>>> i.id2path('2326')
111
92
'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.
114
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
115
'text_id', 'parent_id', 'children',
116
'text_version', 'entry_version', ]
119
def __init__(self, file_id, name, kind, parent_id, text_id=None):
98
def __init__(self, file_id, name, kind='file', text_id=None,
120
100
"""Create an InventoryEntry
122
102
The filename must be a single component, relative to the
123
103
parent directory; it cannot be a whole path or relative name.
125
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
105
>>> e = InventoryEntry('123', 'hello.c')
130
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
110
>>> e = InventoryEntry('123', 'src/hello.c')
131
111
Traceback (most recent call last):
132
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
112
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
134
assert isinstance(name, basestring), name
135
if '/' in name or '\\' in name:
136
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
138
self.text_version = None
139
self.entry_version = None
140
self.text_sha1 = None
141
self.text_size = None
115
if len(splitpath(name)) != 1:
116
bailout('InventoryEntry name is not a simple filename: %r'
142
119
self.file_id = file_id
121
assert kind in ['file', 'directory']
145
123
self.text_id = text_id
146
124
self.parent_id = parent_id
125
self.text_sha1 = None
126
self.text_size = None
147
127
if kind == 'directory':
148
128
self.children = {}
152
raise BzrError("unhandled entry kind %r" % kind)
156
131
def sorted_children(self):
184
def __eq__(self, other):
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):
185
195
if not isinstance(other, InventoryEntry):
186
196
return NotImplemented
188
return (self.file_id == other.file_id) \
189
and (self.name == other.name) \
190
and (self.text_sha1 == other.text_sha1) \
191
and (self.text_size == other.text_size) \
192
and (self.text_id == other.text_id) \
193
and (self.parent_id == other.parent_id) \
194
and (self.kind == other.kind) \
195
and (self.text_version == other.text_version) \
196
and (self.entry_version == other.entry_version)
199
def __ne__(self, other):
200
return not (self == other)
203
raise ValueError('not hashable')
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)
212
213
self.parent_id = None
215
def __eq__(self, other):
216
def __cmp__(self, other):
216
219
if not isinstance(other, RootEntry):
217
220
return NotImplemented
219
return (self.file_id == other.file_id) \
220
and (self.children == other.children)
224
class Inventory(object):
221
return cmp(self.file_id, other.file_id) \
222
or cmp(self.children, other.children)
226
class Inventory(XMLMixin):
225
227
"""Inventory of versioned files in a tree.
227
This describes which file_id is present at each point in the tree,
228
and possibly the SHA-1 or other information about the file.
229
Entries can be looked up either by path or by file_id.
229
An Inventory acts like a set of InventoryEntry items. You can
230
also look files up by their file_id or name.
232
May be read from and written to a metadata file in a tree. To
233
manipulate the inventory (for example to add a file), it is read
234
in, modified, and then written back out.
231
236
The inventory represents a typical unix file tree, with
232
237
directories containing files and subdirectories. We never store
312
319
if ie.kind == 'directory':
313
320
for cn, cie in self.iter_entries(from_dir=ie.file_id):
314
yield os.path.join(name, cn), cie
318
"""Return list of (path, ie) for all entries except the root.
320
This may be faster than iter_entries.
321
yield '/'.join((name, cn)), cie
325
def directories(self, from_dir=None):
326
"""Return (path, entry) pairs for all directories.
323
def descend(dir_ie, dir_path):
324
kids = dir_ie.children.items()
326
for name, ie in kids:
327
child_path = os.path.join(dir_path, name)
328
accum.append((child_path, ie))
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():
329
335
if ie.kind == 'directory':
330
descend(ie, child_path)
332
descend(self.root, '')
336
def directories(self):
337
"""Return (path, entry) pairs for all directories, including the root.
340
def descend(parent_ie, parent_path):
341
accum.append((parent_path, parent_ie))
336
dn.append((ie.name, ie))
343
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
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
for name, child_ie in kids:
347
child_path = os.path.join(parent_path, name)
348
descend(child_ie, child_path)
349
descend(self.root, '')
343
for name, ie in descend(self.root):
394
377
"""Add entry to inventory.
396
379
To add a file to a branch ready to be committed, use Branch.add,
399
Returns the new entry object.
401
381
if entry.file_id in self._byid:
402
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
404
if entry.parent_id == ROOT_ID or entry.parent_id is None:
405
entry.parent_id = self.root.file_id
382
bailout("inventory already contains entry with id {%s}" % entry.file_id)
408
385
parent = self._byid[entry.parent_id]
410
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
387
bailout("parent_id %r not in inventory" % entry.parent_id)
412
389
if parent.children.has_key(entry.name):
413
raise BzrError("%s is already versioned" %
390
bailout("%s is already versioned" %
414
391
appendpath(self.id2path(parent.file_id), entry.name))
416
393
self._byid[entry.file_id] = entry
417
394
parent.children[entry.name] = entry
421
397
def add_path(self, relpath, kind, file_id=None):
422
398
"""Add entry from a path.
424
The immediate parent must already be versioned.
426
Returns the new entry object."""
427
from bzrlib.branch import gen_file_id
400
The immediate parent must already be versioned"""
429
401
parts = bzrlib.osutils.splitpath(relpath)
430
402
if len(parts) == 0:
431
raise BzrError("cannot re-add root of inventory")
434
file_id = gen_file_id(relpath)
436
parent_path = parts[:-1]
437
parent_id = self.path2id(parent_path)
438
if parent_id == None:
439
raise NotVersionedError(parent_path)
403
bailout("cannot re-add root of inventory")
406
file_id = bzrlib.branch.gen_file_id(relpath)
408
parent_id = self.path2id(parts[:-1])
441
409
ie = InventoryEntry(file_id, parts[-1],
442
410
kind=kind, parent_id=parent_id)
443
411
return self.add(ie)
470
437
del self[ie.parent_id].children[ie.name]
473
def __eq__(self, other):
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):
474
473
"""Compare two sets by comparing their contents.
476
475
>>> i1 = Inventory()
477
476
>>> i2 = Inventory()
480
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
481
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
479
>>> i1.add(InventoryEntry('123', 'foo'))
484
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
485
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
482
>>> i2.add(InventoryEntry('123', 'foo'))
489
489
if not isinstance(other, Inventory):
490
490
return NotImplemented
492
if len(self._byid) != len(other._byid):
493
# shortcut: obviously not the same
496
return self._byid == other._byid
499
def __ne__(self, other):
500
return not self.__eq__(other)
504
raise ValueError('not hashable')
492
if self.id_set() ^ other.id_set():
495
for file_id in self._byid:
496
c = cmp(self[file_id], other[file_id])
507
502
def get_idpath(self, file_id):