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
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
from bzrlib.trace import mutter
39
class InventoryEntry(XMLMixin):
40
"""Description of a versioned file.
42
An InventoryEntry has the following fields, which are also
43
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.
62
>>> i.add(InventoryEntry('123', 'src', kind='directory'))
63
>>> i.add(InventoryEntry('2323', 'hello.c', parent_id='123'))
64
>>> for j in i.iter_entries():
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id=None))
68
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', parent_id='123'))
70
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'))
75
>>> i.path2id('src/wibble')
79
>>> i.add(InventoryEntry('2326', 'wibble.c', parent_id='2325'))
81
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
82
>>> for j in i.iter_entries():
84
... assert i.path2id(j[0])
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,
100
"""Create an InventoryEntry
102
The filename must be a single component, relative to the
103
parent directory; it cannot be a whole path or relative name.
105
>>> e = InventoryEntry('123', 'hello.c')
110
>>> e = InventoryEntry('123', 'src/hello.c')
111
Traceback (most recent call last):
112
BzrError: ("InventoryEntry name is not a simple filename: 'src/hello.c'", [])
115
if len(splitpath(name)) != 1:
116
bailout('InventoryEntry name is not a simple filename: %r'
119
self.file_id = file_id
121
assert kind in ['file', 'directory']
123
self.text_id = text_id
124
self.parent_id = parent_id
125
self.text_sha1 = None
126
self.text_size = None
130
other = InventoryEntry(self.file_id, self.name, self.kind,
131
self.text_id, self.parent_id)
132
other.text_sha1 = self.text_sha1
133
other.text_size = self.text_size
138
return ("%s(%r, %r, kind=%r, parent_id=%r)"
139
% (self.__class__.__name__,
146
def to_element(self):
147
"""Convert to XML element"""
150
e.set('name', self.name)
151
e.set('file_id', self.file_id)
152
e.set('kind', self.kind)
154
if self.text_size is not None:
155
e.set('text_size', '%d' % self.text_size)
157
for f in ['text_id', 'text_sha1', 'parent_id']:
167
def from_element(cls, elt):
168
assert elt.tag == 'entry'
169
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'))
170
self.text_id = elt.get('text_id')
171
self.text_sha1 = elt.get('text_sha1')
172
self.parent_id = elt.get('parent_id')
174
## mutter("read inventoryentry: %r" % (elt.attrib))
176
v = elt.get('text_size')
177
self.text_size = v and int(v)
182
from_element = classmethod(from_element)
184
def __cmp__(self, other):
187
if not isinstance(other, InventoryEntry):
188
return NotImplemented
190
return cmp(self.file_id, other.file_id) \
191
or cmp(self.name, other.name) \
192
or cmp(self.text_sha1, other.text_sha1) \
193
or cmp(self.text_size, other.text_size) \
194
or cmp(self.text_id, other.text_id) \
195
or cmp(self.parent_id, other.parent_id) \
196
or cmp(self.kind, other.kind)
200
class Inventory(XMLMixin):
201
"""Inventory of versioned files in a tree.
203
An Inventory acts like a set of InventoryEntry items. You can
204
also look files up by their file_id or name.
206
May be read from and written to a metadata file in a tree. To
207
manipulate the inventory (for example to add a file), it is read
208
in, modified, and then written back out.
210
The inventory represents a typical unix file tree, with
211
directories containing files and subdirectories. We never store
212
the full path to a file, because renaming a directory implicitly
213
moves all of its contents. This class internally maintains a
214
lookup tree that allows the children under a directory to be
217
InventoryEntry objects must not be modified after they are
220
>>> inv = Inventory()
221
>>> inv.write_xml(sys.stdout)
224
>>> inv.add(InventoryEntry('123-123', 'hello.c'))
225
>>> inv['123-123'].name
227
>>> for file_id in inv: print file_id
231
May be treated as an iterator or set to look up file ids:
233
>>> bool(inv.path2id('hello.c'))
238
May also look up by name:
240
>>> [x[0] for x in inv.iter_entries()]
243
>>> inv.write_xml(sys.stdout)
245
<entry file_id="123-123" kind="file" name="hello.c" />
250
## TODO: Clear up handling of files in subdirectories; we probably
251
## do want to be able to just look them up by name but this
252
## probably means gradually walking down the path, looking up as we go.
254
## TODO: Make sure only canonical filenames are stored.
256
## TODO: Do something sensible about the possible collisions on
257
## case-losing filesystems. Perhaps we should just always forbid
260
## _tree should probably just be stored as
261
## InventoryEntry._children on each directory.
264
"""Create or read an inventory.
266
If a working directory is specified, the inventory is read
267
from there. If the file is specified, read from that. If not,
268
the inventory is created empty.
272
# _tree is indexed by parent_id; at each level a map from name
273
# to ie. The None entry is the root.
274
self._tree = {None: {}}
278
return iter(self._byid)
282
"""Returns number of entries."""
283
return len(self._byid)
286
def iter_entries(self, parent_id=None):
287
"""Return (path, entry) pairs, in order by name."""
288
kids = self._tree[parent_id].items()
290
for name, ie in kids:
292
if ie.kind == 'directory':
293
for cn, cie in self.iter_entries(parent_id=ie.file_id):
294
yield joinpath([name, cn]), cie
297
def directories(self, include_root=True):
298
"""Return (path, entry) pairs for all directories.
302
for path, entry in self.iter_entries():
303
if entry.kind == 'directory':
308
def children(self, parent_id):
309
"""Return entries that are direct children of parent_id."""
310
return self._tree[parent_id]
314
# TODO: return all paths and entries
317
def __contains__(self, file_id):
318
"""True if this entry contains a file with given id.
320
>>> inv = Inventory()
321
>>> inv.add(InventoryEntry('123', 'foo.c'))
327
return file_id in self._byid
330
def __getitem__(self, file_id):
331
"""Return the entry for given file_id.
333
>>> inv = Inventory()
334
>>> inv.add(InventoryEntry('123123', 'hello.c'))
335
>>> inv['123123'].name
338
return self._byid[file_id]
341
def add(self, entry):
342
"""Add entry to inventory.
344
To add a file to a branch ready to be committed, use Branch.add,
346
if entry.file_id in self:
347
bailout("inventory already contains entry with id {%s}" % entry.file_id)
349
if entry.parent_id != None:
350
if entry.parent_id not in self:
351
bailout("parent_id %s of new entry not found in inventory"
354
if self._tree[entry.parent_id].has_key(entry.name):
355
bailout("%s is already versioned"
356
% appendpath(self.id2path(entry.parent_id), entry.name))
358
self._byid[entry.file_id] = entry
359
self._tree[entry.parent_id][entry.name] = entry
361
if entry.kind == 'directory':
362
self._tree[entry.file_id] = {}
365
def add_path(self, relpath, kind, file_id=None):
366
"""Add entry from a path.
368
The immediate parent must already be versioned"""
369
parts = bzrlib.osutils.splitpath(relpath)
371
bailout("cannot re-add root of inventory")
374
file_id = bzrlib.branch.gen_file_id(relpath)
376
parent_id = self.path2id(parts[:-1])
377
ie = InventoryEntry(file_id, parts[-1],
378
kind=kind, parent_id=parent_id)
382
def __delitem__(self, file_id):
383
"""Remove entry by id.
385
>>> inv = Inventory()
386
>>> inv.add(InventoryEntry('123', 'foo.c'))
395
assert self._tree[ie.parent_id][ie.name] == ie
397
# TODO: Test deleting all children; maybe hoist to a separate
399
if ie.kind == 'directory':
400
for cie in self._tree[file_id].values():
401
del self[cie.file_id]
402
del self._tree[file_id]
404
del self._byid[file_id]
405
del self._tree[ie.parent_id][ie.name]
409
return Set(self._byid)
412
def to_element(self):
413
"""Convert to XML Element"""
414
e = Element('inventory')
416
for path, ie in self.iter_entries():
417
e.append(ie.to_element())
421
def from_element(cls, elt):
422
"""Construct from XML Element
424
>>> inv = Inventory()
425
>>> inv.add(InventoryEntry('foo.c-123981239', 'foo.c'))
426
>>> elt = inv.to_element()
427
>>> inv2 = Inventory.from_element(elt)
431
assert elt.tag == 'inventory'
434
o.add(InventoryEntry.from_element(e))
437
from_element = classmethod(from_element)
440
def __cmp__(self, other):
441
"""Compare two sets by comparing their contents.
447
>>> i1.add(InventoryEntry('123', 'foo'))
450
>>> i2.add(InventoryEntry('123', 'foo'))
457
if not isinstance(other, Inventory):
458
return NotImplemented
460
if self.id_set() ^ other.id_set():
463
for file_id in self._byid:
464
c = cmp(self[file_id], other[file_id])
470
def id2path(self, file_id):
471
"""Return as a list the path to file_id."""
473
while file_id != None:
476
file_id = ie.parent_id
481
def path2id(self, name):
482
"""Walk down through directories to return entry of last component.
484
names may be either a list of path components, or a single
485
string, in which case it is automatically split.
487
This returns the entry of the last component in the path,
488
which may be either a file or a directory.
490
if isinstance(name, types.StringTypes):
491
name = splitpath(name)
496
cie = self._tree[parent_id][f]
498
parent_id = cie.file_id
506
def get_child(self, parent_id, child_name):
507
return self._tree[parent_id].get(child_name)
510
def has_filename(self, names):
511
return bool(self.path2id(names))
514
def has_id(self, file_id):
515
assert isinstance(file_id, str)
516
return self._byid.has_key(file_id)
522
if __name__ == '__main__':
523
import doctest, inventory
524
doctest.testmod(inventory)