18
17
"""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
24
import sys, os.path, types, re
25
25
from sets import Set
32
32
from xml import XMLMixin
33
33
from errors import bailout
34
from osutils import uuid, quotefn, splitpath, joinpath, appendpath
35
from trace import mutter
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
from bzrlib.trace import mutter
37
39
class InventoryEntry(XMLMixin):
38
40
"""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)
198
226
class Inventory(XMLMixin):
199
227
"""Inventory of versioned files in a tree.
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
273
## TODO: Make sure only canonical filenames are stored.
254
275
## TODO: Do something sensible about the possible collisions on
255
276
## case-losing filesystems. Perhaps we should just always forbid
256
277
## such collisions.
258
## _tree should probably just be stored as
259
## InventoryEntry._children on each directory.
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
261
284
def __init__(self):
262
285
"""Create or read an inventory.
264
287
If a working directory is specified, the inventory is read
265
288
from there. If the file is specified, read from that. If not,
266
289
the inventory is created empty.
291
The inventory is created with a default root directory, with
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: {}}
294
self.root = RootEntry(None)
295
self._byid = {None: self.root}
275
298
def __iter__(self):
281
304
return len(self._byid)
284
def iter_entries(self, parent_id=None):
307
def iter_entries(self, from_dir=None):
285
308
"""Return (path, entry) pairs, in order by name."""
286
kids = self._tree[parent_id].items()
312
elif isinstance(from_dir, basestring):
313
from_dir = self._byid[from_dir]
315
kids = from_dir.children.items()
288
317
for name, ie in kids:
290
319
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):
320
for cn, cie in self.iter_entries(from_dir=ie.file_id):
321
yield '/'.join((name, cn)), cie
325
def directories(self, from_dir=None):
296
326
"""Return (path, entry) pairs for all directories.
300
for path, entry in self.iter_entries():
301
if entry.kind == 'directory':
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):
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
348
def __contains__(self, file_id):
316
349
"""True if this entry contains a file with given id.
336
369
return self._byid[file_id]
372
def get_child(self, parent_id, filename):
373
return self[parent_id].children.get(filename)
339
376
def add(self, entry):
340
377
"""Add entry to inventory.
342
379
To add a file to a branch ready to be committed, use Branch.add,
343
380
which calls this."""
344
if entry.file_id in self:
381
if entry.file_id in self._byid:
345
382
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))
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))
356
393
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] = {}
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)
363
414
def __delitem__(self, file_id):
374
425
ie = self[file_id]
376
assert self._tree[ie.parent_id][ie.name] == ie
427
assert self[ie.parent_id].children[ie.name] == ie
378
429
# TODO: Test deleting all children; maybe hoist to a separate
379
430
# deltree method?
380
431
if ie.kind == 'directory':
381
for cie in self._tree[file_id].values():
432
for cie in ie.children.values():
382
433
del self[cie.file_id]
383
del self._tree[file_id]
385
436
del self._byid[file_id]
386
del self._tree[ie.parent_id][ie.name]
437
del self[ie.parent_id].children[ie.name]
389
440
def id_set(self):
468
519
This returns the entry of the last component in the path,
469
520
which may be either a file or a directory.
471
assert isinstance(name, types.StringTypes)
522
if isinstance(name, types.StringTypes):
523
name = splitpath(name)
474
for f in splitpath(name):
476
cie = self._tree[parent_id][f]
528
cie = parent.children[f]
477
529
assert cie.name == f
478
parent_id = cie.file_id
480
532
# or raise an error?
486
def get_child(self, parent_id, child_name):
487
return self._tree[parent_id].get(child_name)
535
return parent.file_id
490
538
def has_filename(self, names):
494
542
def has_id(self, file_id):
495
assert isinstance(file_id, str)
496
543
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))
500
580
if __name__ == '__main__':
501
581
import doctest, inventory