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
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)
200
226
class Inventory(XMLMixin):
201
227
"""Inventory of versioned files in a tree.
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
273
## TODO: Make sure only canonical filenames are stored.
256
275
## TODO: Do something sensible about the possible collisions on
257
276
## case-losing filesystems. Perhaps we should just always forbid
258
277
## such collisions.
260
## _tree should probably just be stored as
261
## 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
263
284
def __init__(self):
264
285
"""Create or read an inventory.
266
287
If a working directory is specified, the inventory is read
267
288
from there. If the file is specified, read from that. If not,
268
289
the inventory is created empty.
291
The inventory is created with a default root directory, with
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: {}}
294
self.root = RootEntry(None)
295
self._byid = {None: self.root}
277
298
def __iter__(self):
283
304
return len(self._byid)
286
def iter_entries(self, parent_id=None):
307
def iter_entries(self, from_dir=None):
287
308
"""Return (path, entry) pairs, in order by name."""
288
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()
290
317
for name, ie in kids:
292
319
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):
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):
298
326
"""Return (path, entry) pairs for all directories.
302
for path, entry in self.iter_entries():
303
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):
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
348
def __contains__(self, file_id):
318
349
"""True if this entry contains a file with given id.
338
369
return self._byid[file_id]
372
def get_child(self, parent_id, filename):
373
return self[parent_id].children.get(filename)
341
376
def add(self, entry):
342
377
"""Add entry to inventory.
344
379
To add a file to a branch ready to be committed, use Branch.add,
345
380
which calls this."""
346
if entry.file_id in self:
381
if entry.file_id in self._byid:
347
382
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))
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))
358
393
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] = {}
394
parent.children[entry.name] = entry
365
397
def add_path(self, relpath, kind, file_id=None):
393
425
ie = self[file_id]
395
assert self._tree[ie.parent_id][ie.name] == ie
427
assert self[ie.parent_id].children[ie.name] == ie
397
429
# TODO: Test deleting all children; maybe hoist to a separate
398
430
# deltree method?
399
431
if ie.kind == 'directory':
400
for cie in self._tree[file_id].values():
432
for cie in ie.children.values():
401
433
del self[cie.file_id]
402
del self._tree[file_id]
404
436
del self._byid[file_id]
405
del self._tree[ie.parent_id][ie.name]
437
del self[ie.parent_id].children[ie.name]
408
440
def id_set(self):
490
522
if isinstance(name, types.StringTypes):
491
523
name = splitpath(name)
496
cie = self._tree[parent_id][f]
528
cie = parent.children[f]
497
529
assert cie.name == f
498
parent_id = cie.file_id
500
532
# or raise an error?
506
def get_child(self, parent_id, child_name):
507
return self._tree[parent_id].get(child_name)
535
return parent.file_id
510
538
def has_filename(self, names):
514
542
def has_id(self, file_id):
515
assert isinstance(file_id, str)
516
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))