2
# -*- coding: UTF-8 -*-
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
"""Inventories map files to their name in a revision."""
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
__author__ = "Martin Pool <mbp@canonical.com>"
24
import sys, os.path, types
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 osutils import uuid, quotefn, splitpath, joinpath, appendpath
35
from trace import mutter
37
class InventoryEntry(XMLMixin):
38
"""Description of a versioned file.
40
An InventoryEntry has the following fields, which are also
41
present in the XML inventory-entry element:
44
* *name*: (only the basename within the directory, must not
46
* *kind*: "directory" or "file"
47
* *directory_id*: (if absent/null means the branch root directory)
48
* *text_sha1*: only for files
49
* *text_size*: in bytes, only for files
50
* *text_id*: identifier for the text version, only for files
52
InventoryEntries can also exist inside a WorkingTree
53
inventory, in which case they are not yet bound to a
54
particular revision of the file. In that case the text_sha1,
55
text_size and text_id are absent.
60
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
61
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
62
>>> for j in i.iter_entries():
65
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
66
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
67
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
68
Traceback (most recent call last):
70
BzrError: ('inventory already contains entry with id {2323}', [])
71
>>> i.add(InventoryEntry('2324', 'bye.c', parent_id='123'))
72
>>> i.add(InventoryEntry('2325', 'wibble', parent_id='123', kind='directory'))
73
>>> i.path2id('src/wibble')
77
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
79
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
80
>>> for j in i.iter_entries():
82
... assert i.path2id(j[0])
92
:todo: Maybe also keep the full path of the entry, and the children?
93
But those depend on its position within a particular inventory, and
94
it would be nice not to need to hold the backpointer here.
96
def __init__(self, file_id, name, kind='file', text_id=None,
98
"""Create an InventoryEntry
100
The filename must be a single component, relative to the
101
parent directory; it cannot be a whole path or relative name.
103
>>> e = InventoryEntry('123', 'hello.c')
108
>>> e = InventoryEntry('123', 'src/hello.c')
109
Traceback (most recent call last):
110
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
113
if len(splitpath(name)) != 1:
114
bailout('InventoryEntry name is not a simple filename: %r'
117
self.file_id = file_id
119
assert kind in ['file', 'directory']
121
self.text_id = text_id
122
self.parent_id = parent_id
123
self.text_sha1 = None
124
self.text_size = None
128
other = InventoryEntry(self.file_id, self.name, self.kind,
129
self.text_id, self.parent_id)
130
other.text_sha1 = self.text_sha1
131
other.text_size = self.text_size
136
return ("%s(%r, %r, kind=%r, parent_id=%r)"
137
% (self.__class__.__name__,
144
def to_element(self):
145
"""Convert to XML element"""
148
e.set('name', self.name)
149
e.set('file_id', self.file_id)
150
e.set('kind', self.kind)
152
if self.text_size is not None:
153
e.set('text_size', '%d' % self.text_size)
155
for f in ['text_id', 'text_sha1', 'parent_id']:
165
def from_element(cls, elt):
166
assert elt.tag == 'entry'
167
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
168
self.text_id = elt.get('text_id')
169
self.text_sha1 = elt.get('text_sha1')
170
self.parent_id = elt.get('parent_id')
172
## mutter("read inventoryentry: %r" % (elt.attrib))
174
v = elt.get('text_size')
175
self.text_size = v and int(v)
180
from_element = classmethod(from_element)
182
def __cmp__(self, other):
185
if not isinstance(other, InventoryEntry):
186
return NotImplemented
188
return cmp(self.file_id, other.file_id) \
189
or cmp(self.name, other.name) \
190
or cmp(self.text_sha1, other.text_sha1) \
191
or cmp(self.text_size, other.text_size) \
192
or cmp(self.text_id, other.text_id) \
193
or cmp(self.parent_id, other.parent_id) \
194
or cmp(self.kind, other.kind)
198
class Inventory(XMLMixin):
199
"""Inventory of versioned files in a tree.
201
An Inventory acts like a set of InventoryEntry items. You can
202
also look files up by their file_id or name.
204
May be read from and written to a metadata file in a tree. To
205
manipulate the inventory (for example to add a file), it is read
206
in, modified, and then written back out.
208
The inventory represents a typical unix file tree, with
209
directories containing files and subdirectories. We never store
210
the full path to a file, because renaming a directory implicitly
211
moves all of its contents. This class internally maintains a
212
lookup tree that allows the children under a directory to be
215
InventoryEntry objects must not be modified after they are
218
>>> inv = Inventory()
219
>>> inv.write_xml(sys.stdout)
222
>>> inv.add(InventoryEntry('123-123', 'hello.c'))
223
>>> inv['123-123'].name
225
>>> for file_id in inv: print file_id
229
May be treated as an iterator or set to look up file ids:
231
>>> bool(inv.path2id('hello.c'))
236
May also look up by name:
238
>>> [x[0] for x in inv.iter_entries()]
241
>>> inv.write_xml(sys.stdout)
243
<entry file_id="123-123" kind="file" name="hello.c" />
248
## TODO: Clear up handling of files in subdirectories; we probably
249
## do want to be able to just look them up by name but this
250
## probably means gradually walking down the path, looking up as we go.
252
## TODO: Make sure only canonical filenames are stored.
254
## TODO: Do something sensible about the possible collisions on
255
## case-losing filesystems. Perhaps we should just always forbid
258
## _tree should probably just be stored as
259
## InventoryEntry._children on each directory.
262
"""Create or read an inventory.
264
If a working directory is specified, the inventory is read
265
from there. If the file is specified, read from that. If not,
266
the inventory is created empty.
270
# _tree is indexed by parent_id; at each level a map from name
271
# to ie. The None entry is the root.
272
self._tree = {None: {}}
276
return iter(self._byid)
280
"""Returns number of entries."""
281
return len(self._byid)
284
def iter_entries(self, parent_id=None):
285
"""Return (path, entry) pairs, in order by name."""
286
kids = self._tree[parent_id].items()
288
for name, ie in kids:
290
if ie.kind == 'directory':
291
for cn, cie in self.iter_entries(parent_id=ie.file_id):
292
yield joinpath([name, cn]), cie
295
def directories(self, include_root=True):
296
"""Return (path, entry) pairs for all directories.
300
for path, entry in self.iter_entries():
301
if entry.kind == 'directory':
306
def children(self, parent_id):
307
"""Return entries that are direct children of parent_id."""
308
return self._tree[parent_id]
312
# TODO: return all paths and entries
315
def __contains__(self, file_id):
316
"""True if this entry contains a file with given id.
318
>>> inv = Inventory()
319
>>> inv.add(InventoryEntry('123', 'foo.c'))
325
return file_id in self._byid
328
def __getitem__(self, file_id):
329
"""Return the entry for given file_id.
331
>>> inv = Inventory()
332
>>> inv.add(InventoryEntry('123123', 'hello.c'))
333
>>> inv['123123'].name
336
return self._byid[file_id]
339
def add(self, entry):
340
"""Add entry to inventory.
342
To add a file to a branch ready to be committed, use Branch.add,
344
if entry.file_id in self:
345
bailout("inventory already contains entry with id {%s}" % entry.file_id)
347
if entry.parent_id != None:
348
if entry.parent_id not in self:
349
bailout("parent_id %s of new entry not found in inventory"
352
if self._tree[entry.parent_id].has_key(entry.name):
353
bailout("%s is already versioned"
354
% appendpath(self.id2path(entry.parent_id), entry.name))
356
self._byid[entry.file_id] = entry
357
self._tree[entry.parent_id][entry.name] = entry
359
if entry.kind == 'directory':
360
self._tree[entry.file_id] = {}
363
def __delitem__(self, file_id):
364
"""Remove entry by id.
366
>>> inv = Inventory()
367
>>> inv.add(InventoryEntry('123', 'foo.c'))
376
assert self._tree[ie.parent_id][ie.name] == ie
378
# TODO: Test deleting all children; maybe hoist to a separate
380
if ie.kind == 'directory':
381
for cie in self._tree[file_id].values():
382
del self[cie.file_id]
383
del self._tree[file_id]
385
del self._byid[file_id]
386
del self._tree[ie.parent_id][ie.name]
390
return Set(self._byid)
393
def to_element(self):
394
"""Convert to XML Element"""
395
e = Element('inventory')
397
for path, ie in self.iter_entries():
398
e.append(ie.to_element())
402
def from_element(cls, elt):
403
"""Construct from XML Element
405
>>> inv = Inventory()
406
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
407
>>> elt = inv.to_element()
408
>>> inv2 = Inventory.from_element(elt)
412
assert elt.tag == 'inventory'
415
o.add(InventoryEntry.from_element(e))
418
from_element = classmethod(from_element)
421
def __cmp__(self, other):
422
"""Compare two sets by comparing their contents.
428
>>> i1.add(InventoryEntry('123', 'foo'))
431
>>> i2.add(InventoryEntry('123', 'foo'))
438
if not isinstance(other, Inventory):
439
return NotImplemented
441
if self.id_set() ^ other.id_set():
444
for file_id in self._byid:
445
c = cmp(self[file_id], other[file_id])
451
def id2path(self, file_id):
452
"""Return as a list the path to file_id."""
454
while file_id != None:
457
file_id = ie.parent_id
462
def path2id(self, name):
463
"""Walk down through directories to return entry of last component.
465
names may be either a list of path components, or a single
466
string, in which case it is automatically split.
468
This returns the entry of the last component in the path,
469
which may be either a file or a directory.
471
assert isinstance(name, types.StringTypes)
474
for f in splitpath(name):
476
cie = self._tree[parent_id][f]
478
parent_id = cie.file_id
486
def get_child(self, parent_id, child_name):
487
return self._tree[parent_id].get(child_name)
490
def has_filename(self, names):
491
return bool(self.path2id(names))
494
def has_id(self, file_id):
495
assert isinstance(file_id, str)
496
return self._byid.has_key(file_id)
500
if __name__ == '__main__':
501
import doctest, inventory
502
doctest.testmod(inventory)