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>"
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
37
from bzrlib.errors import BzrError, BzrCheckError
39
from bzrlib.osutils import quotefn, splitpath, joinpath, appendpath
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
40
37
from bzrlib.trace import mutter
41
from bzrlib.errors import NotVersionedError
44
class InventoryEntry(object):
39
class InventoryEntry(XMLMixin):
45
40
"""Description of a versioned file.
47
42
An InventoryEntry has the following fields, which are also
48
43
present in the XML inventory-entry element:
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.)
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.
76
60
>>> i = Inventory()
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')
62
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
83
64
>>> for j in i.iter_entries():
86
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
87
68
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
88
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
89
70
Traceback (most recent call last):
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')
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'))
96
75
>>> i.path2id('src/wibble')
100
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
101
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
79
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
103
81
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
104
>>> for path, entry in i.iter_entries():
105
... print path.replace('\\\\', '/') # for win32 os.sep
106
... assert i.path2id(path)
82
>>> for j in i.iter_entries():
84
... assert i.path2id(j[0])
112
90
src/wibble/wibble.c
113
>>> i.id2path('2326').replace('\\\\', '/')
114
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.
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):
98
def __init__(self, file_id, name, kind='file', text_id=None,
123
100
"""Create an InventoryEntry
125
102
The filename must be a single component, relative to the
126
103
parent directory; it cannot be a whole path or relative name.
128
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
105
>>> e = InventoryEntry('123', 'hello.c')
133
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
110
>>> e = InventoryEntry('123', 'src/hello.c')
134
111
Traceback (most recent call last):
135
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
112
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
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
115
if len(splitpath(name)) != 1:
116
bailout('InventoryEntry name is not a simple filename: %r'
145
119
self.file_id = file_id
121
assert kind in ['file', 'directory']
148
123
self.text_id = text_id
149
124
self.parent_id = parent_id
125
self.text_sha1 = None
126
self.text_size = None
150
127
if kind == 'directory':
151
128
self.children = {}
155
raise BzrError("unhandled entry kind %r" % kind)
159
131
def sorted_children(self):
187
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):
188
195
if not isinstance(other, InventoryEntry):
189
196
return NotImplemented
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')
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)
315
319
if ie.kind == 'directory':
316
320
for cn, cie in self.iter_entries(from_dir=ie.file_id):
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.
321
yield '/'.join((name, cn)), cie
325
def directories(self, from_dir=None):
326
"""Return (path, entry) pairs for all directories.
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))
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():
332
335
if ie.kind == 'directory':
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))
336
dn.append((ie.name, ie))
346
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
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, '')
343
for name, ie in descend(self.root):
397
377
"""Add entry to inventory.
399
379
To add a file to a branch ready to be committed, use Branch.add,
402
Returns the new entry object.
404
381
if entry.file_id in self._byid:
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
382
bailout("inventory already contains entry with id {%s}" % entry.file_id)
411
385
parent = self._byid[entry.parent_id]
413
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
387
bailout("parent_id %r not in inventory" % entry.parent_id)
415
389
if parent.children.has_key(entry.name):
416
raise BzrError("%s is already versioned" %
390
bailout("%s is already versioned" %
417
391
appendpath(self.id2path(parent.file_id), entry.name))
419
393
self._byid[entry.file_id] = entry
420
394
parent.children[entry.name] = entry
424
397
def add_path(self, relpath, kind, file_id=None):
425
398
"""Add entry from a path.
427
The immediate parent must already be versioned.
429
Returns the new entry object."""
430
from bzrlib.branch import gen_file_id
400
The immediate parent must already be versioned"""
432
401
parts = bzrlib.osutils.splitpath(relpath)
433
402
if len(parts) == 0:
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)
403
bailout("cannot re-add root of inventory")
406
file_id = bzrlib.branch.gen_file_id(relpath)
408
parent_id = self.path2id(parts[:-1])
444
409
ie = InventoryEntry(file_id, parts[-1],
445
410
kind=kind, parent_id=parent_id)
446
411
return self.add(ie)
473
437
del self[ie.parent_id].children[ie.name]
476
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):
477
473
"""Compare two sets by comparing their contents.
479
475
>>> i1 = Inventory()
480
476
>>> i2 = Inventory()
483
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
484
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
479
>>> i1.add(InventoryEntry('123', 'foo'))
487
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
488
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
482
>>> i2.add(InventoryEntry('123', 'foo'))
492
489
if not isinstance(other, Inventory):
493
490
return NotImplemented
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.
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."""
519
505
while file_id != None:
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)
506
ie = self._byid[file_id]
525
508
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)