17
18
"""Inventories map files to their name in a revision."""
19
# TODO: Maybe store inventory_id in the file? Not really needed.
21
21
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
22
22
__author__ = "Martin Pool <mbp@canonical.com>"
24
import sys, os.path, types, re
24
import sys, os.path, types
25
25
from sets import Set
28
from cElementTree import Element, ElementTree, SubElement
30
from elementtree.ElementTree import Element, ElementTree, SubElement
32
27
from xml import XMLMixin
28
from ElementTree import ElementTree, Element
33
29
from errors import bailout
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
from bzrlib.trace import mutter
30
from osutils import uuid, quotefn, splitpath, joinpath, appendpath
31
from trace import mutter
39
33
class InventoryEntry(XMLMixin):
40
34
"""Description of a versioned file.
208
class RootEntry(InventoryEntry):
209
def __init__(self, file_id):
210
self.file_id = file_id
212
self.kind = 'root_directory'
213
self.parent_id = None
216
def __cmp__(self, other):
219
if not isinstance(other, RootEntry):
220
return NotImplemented
221
return cmp(self.file_id, other.file_id) \
222
or cmp(self.children, other.children)
226
194
class Inventory(XMLMixin):
227
195
"""Inventory of versioned files in a tree.
244
## TODO: Clear up handling of files in subdirectories; we probably
245
## do want to be able to just look them up by name but this
246
## probably means gradually walking down the path, looking up as we go.
273
248
## TODO: Make sure only canonical filenames are stored.
275
250
## TODO: Do something sensible about the possible collisions on
276
251
## case-losing filesystems. Perhaps we should just always forbid
277
252
## such collisions.
279
## TODO: No special cases for root, rather just give it a file id
280
## like everything else.
282
## TODO: Probably change XML serialization to use nesting
254
## _tree should probably just be stored as
255
## InventoryEntry._children on each directory.
284
257
def __init__(self):
285
258
"""Create or read an inventory.
287
260
If a working directory is specified, the inventory is read
288
261
from there. If the file is specified, read from that. If not,
289
262
the inventory is created empty.
291
The inventory is created with a default root directory, with
294
self.root = RootEntry(None)
295
self._byid = {None: self.root}
266
# _tree is indexed by parent_id; at each level a map from name
267
# to ie. The None entry is the root.
268
self._tree = {None: {}}
298
271
def __iter__(self):
304
277
return len(self._byid)
307
def iter_entries(self, from_dir=None):
280
def iter_entries(self, parent_id=None):
308
281
"""Return (path, entry) pairs, in order by name."""
312
elif isinstance(from_dir, basestring):
313
from_dir = self._byid[from_dir]
315
kids = from_dir.children.items()
282
kids = self._tree[parent_id].items()
317
284
for name, ie in kids:
319
286
if ie.kind == 'directory':
320
for cn, cie in self.iter_entries(from_dir=ie.file_id):
321
yield '/'.join((name, cn)), cie
287
for cn, cie in self.iter_entries(parent_id=ie.file_id):
288
yield joinpath([name, cn]), cie
291
def directories(self, include_root=True):
292
"""Return (path, entry) pairs for all directories.
296
for path, entry in self.iter_entries():
297
if entry.kind == 'directory':
302
def children(self, parent_id):
303
"""Return entries that are direct children of parent_id."""
304
return self._tree[parent_id]
325
def directories(self, from_dir=None):
326
"""Return (path, entry) pairs for all directories.
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():
335
if ie.kind == 'directory':
336
dn.append((ie.name, 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
343
for name, ie in descend(self.root):
308
# TODO: return all paths and entries
348
311
def __contains__(self, file_id):
369
332
return self._byid[file_id]
372
def get_child(self, parent_id, filename):
373
return self[parent_id].children.get(filename)
376
335
def add(self, entry):
377
336
"""Add entry to inventory.
379
338
To add a file to a branch ready to be committed, use Branch.add,
380
339
which calls this."""
381
if entry.file_id in self._byid:
340
if entry.file_id in self:
382
341
bailout("inventory already contains entry with id {%s}" % entry.file_id)
385
parent = self._byid[entry.parent_id]
387
bailout("parent_id %r not in inventory" % entry.parent_id)
389
if parent.children.has_key(entry.name):
390
bailout("%s is already versioned" %
391
appendpath(self.id2path(parent.file_id), entry.name))
343
if entry.parent_id != None:
344
if entry.parent_id not in self:
345
bailout("parent_id %s of new entry not found in inventory"
348
if self._tree[entry.parent_id].has_key(entry.name):
349
bailout("%s is already versioned"
350
% appendpath(self.id2path(entry.parent_id), entry.name))
393
352
self._byid[entry.file_id] = entry
394
parent.children[entry.name] = entry
397
def add_path(self, relpath, kind, file_id=None):
398
"""Add entry from a path.
400
The immediate parent must already be versioned"""
401
parts = bzrlib.osutils.splitpath(relpath)
403
bailout("cannot re-add root of inventory")
406
file_id = bzrlib.branch.gen_file_id(relpath)
408
parent_id = self.path2id(parts[:-1])
409
ie = InventoryEntry(file_id, parts[-1],
410
kind=kind, parent_id=parent_id)
353
self._tree[entry.parent_id][entry.name] = entry
355
if entry.kind == 'directory':
356
self._tree[entry.file_id] = {}
414
359
def __delitem__(self, file_id):
425
370
ie = self[file_id]
427
assert self[ie.parent_id].children[ie.name] == ie
372
assert self._tree[ie.parent_id][ie.name] == ie
429
374
# TODO: Test deleting all children; maybe hoist to a separate
430
375
# deltree method?
431
376
if ie.kind == 'directory':
432
for cie in ie.children.values():
377
for cie in self._tree[file_id].values():
433
378
del self[cie.file_id]
379
del self._tree[file_id]
436
381
del self._byid[file_id]
437
del self[ie.parent_id].children[ie.name]
382
del self._tree[ie.parent_id][ie.name]
440
385
def id_set(self):
519
464
This returns the entry of the last component in the path,
520
465
which may be either a file or a directory.
522
if isinstance(name, types.StringTypes):
523
name = splitpath(name)
467
assert isinstance(name, types.StringTypes)
470
for f in splitpath(name):
528
cie = parent.children[f]
472
cie = self._tree[parent_id][f]
529
473
assert cie.name == f
474
parent_id = cie.file_id
532
476
# or raise an error?
535
return parent.file_id
482
def get_child(self, parent_id, child_name):
483
return self._tree[parent_id].get(child_name)
538
486
def has_filename(self, names):
542
490
def has_id(self, file_id):
491
assert isinstance(file_id, str)
543
492
return self._byid.has_key(file_id)
546
def rename(self, file_id, new_parent_id, new_name):
547
"""Move a file within the inventory.
549
This can change either the name, or the parent, or both.
551
This does not move the working file."""
552
if not is_valid_name(new_name):
553
bailout("not an acceptable filename: %r" % new_name)
555
new_parent = self._byid[new_parent_id]
556
if new_name in new_parent.children:
557
bailout("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
559
file_ie = self._byid[file_id]
560
old_parent = self._byid[file_ie.parent_id]
562
# TODO: Don't leave things messed up if this fails
564
del old_parent.children[file_ie.name]
565
new_parent.children[new_name] = file_ie
567
file_ie.name = new_name
568
file_ie.parent_id = new_parent_id
573
_NAME_RE = re.compile(r'^[^/\\]+$')
575
def is_valid_name(name):
576
return bool(_NAME_RE.match(name))
580
496
if __name__ == '__main__':
581
497
import doctest, inventory