1
# (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# TODO: Maybe also keep the full path of the entry, and the children?
19
# But those depend on its position within a particular inventory, and
20
# it would be nice not to need to hold the backpointer here.
22
# This should really be an id randomly assigned when the tree is
23
# created, but it's not for now.
34
from bzrlib.errors import BzrError, BzrCheckError
36
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
37
appendpath, sha_strings)
38
from bzrlib.trace import mutter
39
from bzrlib.errors import NotVersionedError
42
class InventoryEntry(object):
43
"""Description of a versioned file.
45
An InventoryEntry has the following fields, which are also
46
present in the XML inventory-entry element:
51
(within the parent directory)
54
file_id of the parent directory, or ROOT_ID
57
the revision_id in which this variation of this file was
61
Indicates that this file should be executable on systems
65
sha-1 of the text of the file
68
size in bytes of the text of the file
70
(reading a version 4 tree created a text_id field.)
75
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
77
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
84
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
Traceback (most recent call last):
87
BzrError: inventory already contains entry with id {2323}
88
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
89
InventoryFile('2324', 'bye.c', parent_id='123')
90
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
91
InventoryDirectory('2325', 'wibble', parent_id='123')
92
>>> i.path2id('src/wibble')
96
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
100
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
102
... assert i.path2id(path)
109
>>> i.id2path('2326').replace('\\\\', '/')
110
'src/wibble/wibble.c'
113
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
'text_id', 'parent_id', 'children', 'executable',
115
'revision', 'symlink_target']
117
def _add_text_to_weave(self, new_lines, parents, weave_store):
118
weave_store.add_text(self.file_id, self.revision, new_lines, parents)
120
def detect_changes(self, old_entry):
121
"""Return a (text_modified, meta_modified) from this to old_entry.
123
_read_tree_state must have been called on self and old_entry prior to
124
calling detect_changes.
126
if self.kind == 'file':
127
assert self.text_sha1 != None
128
assert old_entry.text_sha1 != None
129
text_modified = (self.text_sha1 != old_entry.text_sha1)
130
meta_modified = (self.executable != old_entry.executable)
131
elif self.kind == 'symlink':
132
# FIXME: which _modified field should we use ? RBC 20051003
133
text_modified = (self.symlink_target != old_entry.symlink_target)
135
mutter(" symlink target changed")
136
meta_modified = False
138
text_modified = False
139
meta_modified = False
140
return text_modified, meta_modified
142
def diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
143
output_to, reverse=False):
144
"""Perform a diff from this to to_entry.
146
text_diff will be used for textual difference calculation.
148
self._read_tree_state(tree.id2path(self.file_id), tree)
150
# cannot diff from one kind to another - you must do a removal
151
# and an addif they do not match.
152
assert self.kind == to_entry.kind
153
to_entry._read_tree_state(to_tree.id2path(to_entry.file_id),
155
if self.kind == 'file':
156
from_text = tree.get_file(self.file_id).readlines()
158
to_text = to_tree.get_file(to_entry.file_id).readlines()
162
text_diff(from_label, from_text,
163
to_label, to_text, output_to)
165
text_diff(to_label, to_text,
166
from_label, from_text, output_to)
167
elif self.kind == 'symlink':
168
from_text = self.symlink_target
169
if to_entry is not None:
170
to_text = to_entry.symlink_target
175
print >>output_to, '=== target changed %r => %r' % (from_text, to_text)
178
print >>output_to, '=== target was %r' % self.symlink_target
180
print >>output_to, '=== target is %r' % self.symlink_target
182
def get_tar_item(self, root, dp, now, tree):
183
"""Get a tarfile item and a file stream for its content."""
184
item = tarfile.TarInfo(os.path.join(root, dp))
185
# TODO: would be cool to actually set it to the timestamp of the
186
# revision it was last changed
188
fileobj = self._put_in_tar(item, tree)
192
"""Return true if the object this entry represents has textual data.
194
Note that textual data includes binary content.
198
def __init__(self, file_id, name, parent_id, text_id=None):
199
"""Create an InventoryEntry
201
The filename must be a single component, relative to the
202
parent directory; it cannot be a whole path or relative name.
204
>>> e = InventoryFile('123', 'hello.c', ROOT_ID)
209
>>> e = InventoryFile('123', 'src/hello.c', ROOT_ID)
210
Traceback (most recent call last):
211
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
213
assert isinstance(name, basestring), name
214
if '/' in name or '\\' in name:
215
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
217
self.executable = False
219
self.text_sha1 = None
220
self.text_size = None
221
self.file_id = file_id
223
self.text_id = text_id
224
self.parent_id = parent_id
225
self.symlink_target = None
227
def kind_character(self):
228
"""Return a short kind indicator useful for appending to names."""
229
if self.kind == 'symlink':
231
raise RuntimeError('unreachable code')
233
known_kinds = ('file', 'directory', 'symlink', 'root_directory')
235
def _put_in_tar(self, item, tree):
236
"""populate item for stashing in a tar, and return the content stream.
238
If no content is available, return None.
240
raise BzrError("don't know how to export {%s} of kind %r" %
241
(self.file_id, self.kind))
243
def put_on_disk(self, dest, dp, tree):
244
"""Create a representation of self on disk in the prefix dest.
246
This is a template method - implement _put_on_disk in subclasses.
248
fullpath = appendpath(dest, dp)
249
self._put_on_disk(fullpath, tree)
250
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
252
def _put_on_disk(self, fullpath, tree):
253
"""Put this entry onto disk at fullpath, from tree tree."""
254
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
256
def sorted_children(self):
257
l = self.children.items()
262
def versionable_kind(kind):
263
return kind in ('file', 'directory', 'symlink')
265
def check(self, checker, rev_id, inv, tree):
266
"""Check this inventory entry is intact.
268
This is a template method, override _check for kind specific
271
if self.parent_id != None:
272
if not inv.has_id(self.parent_id):
273
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
274
% (self.parent_id, rev_id))
275
self._check(checker, rev_id, tree)
277
def _check(self, checker, rev_id, tree):
278
"""Check this inventory entry for kind specific errors."""
279
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
284
"""Clone this inventory entry."""
285
raise NotImplementedError
287
def _get_snapshot_change(self, previous_entries):
288
if len(previous_entries) > 1:
290
elif len(previous_entries) == 0:
293
return 'modified/renamed/reparented'
296
return ("%s(%r, %r, parent_id=%r)"
297
% (self.__class__.__name__,
302
def snapshot(self, revision, path, previous_entries, work_tree,
304
"""Make a snapshot of this entry.
306
This means that all its fields are populated, that it has its
307
text stored in the text store or weave.
309
mutter('new parents of %s are %r', path, previous_entries)
310
self._read_tree_state(path, work_tree)
311
if len(previous_entries) == 1:
312
# cannot be unchanged unless there is only one parent file rev.
313
parent_ie = previous_entries.values()[0]
314
if self._unchanged(path, parent_ie, work_tree):
315
mutter("found unchanged entry")
316
self.revision = parent_ie.revision
318
mutter('new revision for {%s}', self.file_id)
319
self.revision = revision
320
change = self._get_snapshot_change(previous_entries)
321
if self.kind != 'file':
323
self._snapshot_text(previous_entries, work_tree, weave_store)
326
def _snapshot_text(self, file_parents, work_tree, weave_store):
327
mutter('storing file {%s} in revision {%s}',
328
self.file_id, self.revision)
329
# special case to avoid diffing on renames or
331
if (len(file_parents) == 1
332
and self.text_sha1 == file_parents.values()[0].text_sha1
333
and self.text_size == file_parents.values()[0].text_size):
334
previous_ie = file_parents.values()[0]
335
weave_store.add_identical_text(
336
self.file_id, previous_ie.revision,
337
self.revision, file_parents)
339
new_lines = work_tree.get_file(self.file_id).readlines()
340
self._add_text_to_weave(new_lines, file_parents, weave_store)
341
self.text_sha1 = sha_strings(new_lines)
342
self.text_size = sum(map(len, new_lines))
344
def __eq__(self, other):
345
if not isinstance(other, InventoryEntry):
346
return NotImplemented
348
return ((self.file_id == other.file_id)
349
and (self.name == other.name)
350
and (other.symlink_target == self.symlink_target)
351
and (self.text_sha1 == other.text_sha1)
352
and (self.text_size == other.text_size)
353
and (self.text_id == other.text_id)
354
and (self.parent_id == other.parent_id)
355
and (self.kind == other.kind)
356
and (self.revision == other.revision)
357
and (self.executable == other.executable)
360
def __ne__(self, other):
361
return not (self == other)
364
raise ValueError('not hashable')
366
def _unchanged(self, path, previous_ie, work_tree):
368
# different inv parent
369
if previous_ie.parent_id != self.parent_id:
372
elif previous_ie.name != self.name:
374
if self.kind == 'symlink':
375
if self.symlink_target != previous_ie.symlink_target:
377
if self.kind == 'file':
378
if self.text_sha1 != previous_ie.text_sha1:
381
# FIXME: 20050930 probe for the text size when getting sha1
382
# in _read_tree_state
383
self.text_size = previous_ie.text_size
386
def _read_tree_state(self, path, work_tree):
387
if self.kind == 'symlink':
388
self.symlink_target = work_tree.get_symlink_target(self.file_id)
389
if self.kind == 'file':
390
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
391
self.executable = work_tree.is_executable(self.file_id)
394
class RootEntry(InventoryEntry):
396
def _check(self, checker, rev_id, tree):
397
"""See InventoryEntry._check"""
399
def __init__(self, file_id):
400
self.file_id = file_id
402
self.kind = 'root_directory'
403
self.parent_id = None
406
def __eq__(self, other):
407
if not isinstance(other, RootEntry):
408
return NotImplemented
410
return (self.file_id == other.file_id) \
411
and (self.children == other.children)
414
class InventoryDirectory(InventoryEntry):
415
"""A directory in an inventory."""
417
def _check(self, checker, rev_id, tree):
418
"""See InventoryEntry._check"""
419
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
420
raise BzrCheckError('directory {%s} has text in revision {%s}'
421
% (self.file_id, rev_id))
424
other = InventoryDirectory(self.file_id, self.name, self.parent_id)
425
other.revision = self.revision
426
# note that children are *not* copied; they're pulled across when
430
def __init__(self, file_id, name, parent_id):
431
super(InventoryDirectory, self).__init__(file_id, name, parent_id)
433
self.kind = 'directory'
435
def kind_character(self):
436
"""See InventoryEntry.kind_character."""
439
def _put_in_tar(self, item, tree):
440
"""See InventoryEntry._put_in_tar."""
441
item.type = tarfile.DIRTYPE
448
def _put_on_disk(self, fullpath, tree):
449
"""See InventoryEntry._put_on_disk."""
453
class InventoryFile(InventoryEntry):
454
"""A file in an inventory."""
456
def _check(self, checker, rev_id, tree):
457
"""See InventoryEntry._check"""
458
revision = self.revision
459
t = (self.file_id, revision)
460
if t in checker.checked_texts:
461
prev_sha = checker.checked_texts[t]
462
if prev_sha != self.text_sha1:
463
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
464
(self.file_id, rev_id))
466
checker.repeated_text_cnt += 1
468
mutter('check version {%s} of {%s}', rev_id, self.file_id)
469
file_lines = tree.get_file_lines(self.file_id)
470
checker.checked_text_cnt += 1
471
if self.text_size != sum(map(len, file_lines)):
472
raise BzrCheckError('text {%s} wrong size' % self.text_id)
473
if self.text_sha1 != sha_strings(file_lines):
474
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
475
checker.checked_texts[t] = self.text_sha1
478
other = InventoryFile(self.file_id, self.name, self.parent_id)
479
other.executable = self.executable
480
other.text_id = self.text_id
481
other.text_sha1 = self.text_sha1
482
other.text_size = self.text_size
483
other.revision = self.revision
487
"""See InventoryEntry.has_text."""
490
def __init__(self, file_id, name, parent_id):
491
super(InventoryFile, self).__init__(file_id, name, parent_id)
494
def kind_character(self):
495
"""See InventoryEntry.kind_character."""
498
def _put_in_tar(self, item, tree):
499
"""See InventoryEntry._put_in_tar."""
500
item.type = tarfile.REGTYPE
501
fileobj = tree.get_file(self.file_id)
502
item.size = self.text_size
503
if tree.is_executable(self.file_id):
509
def _put_on_disk(self, fullpath, tree):
510
"""See InventoryEntry._put_on_disk."""
511
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
512
if tree.is_executable(self.file_id):
513
os.chmod(fullpath, 0755)
516
class InventoryLink(InventoryEntry):
517
"""A file in an inventory."""
519
def _check(self, checker, rev_id, tree):
520
"""See InventoryEntry._check"""
521
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
522
raise BzrCheckError('symlink {%s} has text in revision {%s}'
523
% (self.file_id, rev_id))
524
if self.symlink_target == None:
525
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
526
% (self.file_id, rev_id))
529
other = InventoryLink(self.file_id, self.name, self.parent_id)
530
other.symlink_target = self.symlink_target
531
other.revision = self.revision
534
def __init__(self, file_id, name, parent_id):
535
super(InventoryLink, self).__init__(file_id, name, parent_id)
536
self.kind = 'symlink'
538
def kind_character(self):
539
"""See InventoryEntry.kind_character."""
542
def _put_in_tar(self, item, tree):
543
"""See InventoryEntry._put_in_tar."""
544
iterm.type = tarfile.SYMTYPE
548
item.linkname = self.symlink_target
551
def _put_on_disk(self, fullpath, tree):
552
"""See InventoryEntry._put_on_disk."""
554
os.symlink(self.symlink_target, fullpath)
556
raise BzrError("Failed to create symlink %r -> %r, error: %s" % (fullpath, self.symlink_target, e))
559
class Inventory(object):
560
"""Inventory of versioned files in a tree.
562
This describes which file_id is present at each point in the tree,
563
and possibly the SHA-1 or other information about the file.
564
Entries can be looked up either by path or by file_id.
566
The inventory represents a typical unix file tree, with
567
directories containing files and subdirectories. We never store
568
the full path to a file, because renaming a directory implicitly
569
moves all of its contents. This class internally maintains a
570
lookup tree that allows the children under a directory to be
573
InventoryEntry objects must not be modified after they are
574
inserted, other than through the Inventory API.
576
>>> inv = Inventory()
577
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
578
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT')
579
>>> inv['123-123'].name
582
May be treated as an iterator or set to look up file ids:
584
>>> bool(inv.path2id('hello.c'))
589
May also look up by name:
591
>>> [x[0] for x in inv.iter_entries()]
593
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
594
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
595
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
597
def __init__(self, root_id=ROOT_ID):
598
"""Create or read an inventory.
600
If a working directory is specified, the inventory is read
601
from there. If the file is specified, read from that. If not,
602
the inventory is created empty.
604
The inventory is created with a default root directory, with
607
# We are letting Branch.initialize() create a unique inventory
608
# root id. Rather than generating a random one here.
610
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
611
self.root = RootEntry(root_id)
612
self._byid = {self.root.file_id: self.root}
616
other = Inventory(self.root.file_id)
617
# copy recursively so we know directories will be added before
618
# their children. There are more efficient ways than this...
619
for path, entry in self.iter_entries():
620
if entry == self.root:
622
other.add(entry.copy())
627
return iter(self._byid)
631
"""Returns number of entries."""
632
return len(self._byid)
635
def iter_entries(self, from_dir=None):
636
"""Return (path, entry) pairs, in order by name."""
640
elif isinstance(from_dir, basestring):
641
from_dir = self._byid[from_dir]
643
kids = from_dir.children.items()
645
for name, ie in kids:
647
if ie.kind == 'directory':
648
for cn, cie in self.iter_entries(from_dir=ie.file_id):
649
yield os.path.join(name, cn), cie
653
"""Return list of (path, ie) for all entries except the root.
655
This may be faster than iter_entries.
658
def descend(dir_ie, dir_path):
659
kids = dir_ie.children.items()
661
for name, ie in kids:
662
child_path = os.path.join(dir_path, name)
663
accum.append((child_path, ie))
664
if ie.kind == 'directory':
665
descend(ie, child_path)
667
descend(self.root, '')
671
def directories(self):
672
"""Return (path, entry) pairs for all directories, including the root.
675
def descend(parent_ie, parent_path):
676
accum.append((parent_path, parent_ie))
678
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
681
for name, child_ie in kids:
682
child_path = os.path.join(parent_path, name)
683
descend(child_ie, child_path)
684
descend(self.root, '')
689
def __contains__(self, file_id):
690
"""True if this entry contains a file with given id.
692
>>> inv = Inventory()
693
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
694
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
700
return file_id in self._byid
703
def __getitem__(self, file_id):
704
"""Return the entry for given file_id.
706
>>> inv = Inventory()
707
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
708
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
709
>>> inv['123123'].name
713
return self._byid[file_id]
716
raise BzrError("can't look up file_id None")
718
raise BzrError("file_id {%s} not in inventory" % file_id)
721
def get_file_kind(self, file_id):
722
return self._byid[file_id].kind
724
def get_child(self, parent_id, filename):
725
return self[parent_id].children.get(filename)
728
def add(self, entry):
729
"""Add entry to inventory.
731
To add a file to a branch ready to be committed, use Branch.add,
734
Returns the new entry object.
736
if entry.file_id in self._byid:
737
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
739
if entry.parent_id == ROOT_ID or entry.parent_id is None:
740
entry.parent_id = self.root.file_id
743
parent = self._byid[entry.parent_id]
745
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
747
if parent.children.has_key(entry.name):
748
raise BzrError("%s is already versioned" %
749
appendpath(self.id2path(parent.file_id), entry.name))
751
self._byid[entry.file_id] = entry
752
parent.children[entry.name] = entry
756
def add_path(self, relpath, kind, file_id=None):
757
"""Add entry from a path.
759
The immediate parent must already be versioned.
761
Returns the new entry object."""
762
from bzrlib.branch import gen_file_id
764
parts = bzrlib.osutils.splitpath(relpath)
766
raise BzrError("cannot re-add root of inventory")
769
file_id = gen_file_id(relpath)
771
parent_path = parts[:-1]
772
parent_id = self.path2id(parent_path)
773
if parent_id == None:
774
raise NotVersionedError(parent_path)
776
if kind == 'directory':
777
ie = InventoryDirectory(file_id, parts[-1], parent_id)
779
ie = InventoryFile(file_id, parts[-1], parent_id)
780
elif kind == 'symlink':
781
ie = InventoryLink(file_id, parts[-1], parent_id)
783
raise BzrError("unknown kind %r" % kind)
787
def __delitem__(self, file_id):
788
"""Remove entry by id.
790
>>> inv = Inventory()
791
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
792
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
801
assert self[ie.parent_id].children[ie.name] == ie
803
# TODO: Test deleting all children; maybe hoist to a separate
805
if ie.kind == 'directory':
806
for cie in ie.children.values():
807
del self[cie.file_id]
810
del self._byid[file_id]
811
del self[ie.parent_id].children[ie.name]
814
def __eq__(self, other):
815
"""Compare two sets by comparing their contents.
821
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
822
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
825
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
826
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
830
if not isinstance(other, Inventory):
831
return NotImplemented
833
if len(self._byid) != len(other._byid):
834
# shortcut: obviously not the same
837
return self._byid == other._byid
840
def __ne__(self, other):
841
return not self.__eq__(other)
845
raise ValueError('not hashable')
848
def get_idpath(self, file_id):
849
"""Return a list of file_ids for the path to an entry.
851
The list contains one element for each directory followed by
852
the id of the file itself. So the length of the returned list
853
is equal to the depth of the file in the tree, counting the
854
root directory as depth 1.
857
while file_id != None:
859
ie = self._byid[file_id]
861
raise BzrError("file_id {%s} not found in inventory" % file_id)
862
p.insert(0, ie.file_id)
863
file_id = ie.parent_id
867
def id2path(self, file_id):
868
"""Return as a list the path to file_id."""
870
# get all names, skipping root
871
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
872
return os.sep.join(p)
876
def path2id(self, name):
877
"""Walk down through directories to return entry of last component.
879
names may be either a list of path components, or a single
880
string, in which case it is automatically split.
882
This returns the entry of the last component in the path,
883
which may be either a file or a directory.
885
Returns None iff the path is not found.
887
if isinstance(name, types.StringTypes):
888
name = splitpath(name)
890
mutter("lookup path %r" % name)
895
cie = parent.children[f]
897
assert cie.parent_id == parent.file_id
903
return parent.file_id
906
def has_filename(self, names):
907
return bool(self.path2id(names))
910
def has_id(self, file_id):
911
return self._byid.has_key(file_id)
914
def rename(self, file_id, new_parent_id, new_name):
915
"""Move a file within the inventory.
917
This can change either the name, or the parent, or both.
919
This does not move the working file."""
920
if not is_valid_name(new_name):
921
raise BzrError("not an acceptable filename: %r" % new_name)
923
new_parent = self._byid[new_parent_id]
924
if new_name in new_parent.children:
925
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
927
new_parent_idpath = self.get_idpath(new_parent_id)
928
if file_id in new_parent_idpath:
929
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
930
% (self.id2path(file_id), self.id2path(new_parent_id)))
932
file_ie = self._byid[file_id]
933
old_parent = self._byid[file_ie.parent_id]
935
# TODO: Don't leave things messed up if this fails
937
del old_parent.children[file_ie.name]
938
new_parent.children[new_name] = file_ie
940
file_ie.name = new_name
941
file_ie.parent_id = new_parent_id
948
def is_valid_name(name):
951
_NAME_RE = re.compile(r'^[^/\\]+$')
953
return bool(_NAME_RE.match(name))