1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# (C) 2005 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
# FIXME: This refactoring of the workingtree code doesn't seem to keep
18
# the WorkingTree's copy of the inventory in sync with the branch. The
19
# branch modifies its working inventory when it does a commit to make
20
# missing files permanently removed.
22
18
# TODO: Maybe also keep the full path of the entry, and the children?
23
19
# But those depend on its position within a particular inventory, and
80
75
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
81
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
82
77
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
83
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
84
>>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
85
>>> for ix, j in enumerate(i.iter_entries()):
86
... print (j[0] == shouldbe[ix], j[1])
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
88
(True, RootEntry('TREE_ROOT', u'', parent_id=None, revision=None))
89
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
90
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
91
84
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
92
85
Traceback (most recent call last):
94
87
BzrError: inventory already contains entry with id {2323}
95
88
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
96
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
89
InventoryFile('2324', 'bye.c', parent_id='123')
97
90
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
98
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
91
InventoryDirectory('2325', 'wibble', parent_id='123')
99
92
>>> i.path2id('src/wibble')
103
96
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
104
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
106
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
107
100
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
109
102
... assert i.path2id(path)
116
108
src/wibble/wibble.c
117
>>> i.id2path('2326')
109
>>> i.id2path('2326').replace('\\\\', '/')
118
110
'src/wibble/wibble.c'
121
# Constants returned by describe_change()
123
# TODO: These should probably move to some kind of FileChangeDescription
124
# class; that's like what's inside a TreeDelta but we want to be able to
125
# generate them just for one file at a time.
127
MODIFIED_AND_RENAMED = 'modified and renamed'
113
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
114
'text_id', 'parent_id', 'children', 'executable',
117
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
118
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
131
121
def detect_changes(self, old_entry):
132
122
"""Return a (text_modified, meta_modified) from this to old_entry.
157
147
output_to, reverse=False):
158
148
"""Perform a diff between two entries of the same kind."""
160
def find_previous_heads(self, previous_inventories,
161
versioned_file_store,
164
"""Return the revisions and entries that directly precede this.
150
def find_previous_heads(self, previous_inventories, entry_weave):
151
"""Return the revisions and entries that directly preceed this.
166
153
Returned as a map from revision to inventory entry.
168
155
This is a map containing the file revisions in all parents
169
156
for which the file exists, and its revision is not a parent of
170
157
any other. If the file is new, the set will be empty.
172
:param versioned_file_store: A store where ancestry data on this
173
file id can be queried.
174
:param transaction: The transaction that queries to the versioned
175
file store should be completed under.
176
:param entry_vf: The entry versioned file, if its already available.
178
159
def get_ancestors(weave, entry):
179
return set(weave.get_ancestry(entry.revision))
180
# revision:ie mapping for each ie found in previous_inventories.
182
# revision:ie mapping with one revision for each head.
160
return set(map(weave.idx_to_name,
161
weave.inclusions([weave.lookup(entry.revision)])))
184
# revision: ancestor list for each head
185
163
head_ancestors = {}
186
# identify candidate head revision ids.
187
164
for inv in previous_inventories:
188
165
if self.file_id in inv:
189
166
ie = inv[self.file_id]
190
167
assert ie.file_id == self.file_id
191
if ie.revision in candidates:
192
# same revision value in two different inventories:
193
# correct possible inconsistencies:
194
# * there was a bug in revision updates with 'x' bit
168
if ie.revision in heads:
169
# fixup logic, there was a bug in revision updates.
170
# with x bit support.
197
if candidates[ie.revision].executable != ie.executable:
198
candidates[ie.revision].executable = False
172
if heads[ie.revision].executable != ie.executable:
173
heads[ie.revision].executable = False
199
174
ie.executable = False
200
175
except AttributeError:
202
# must now be the same.
203
assert candidates[ie.revision] == ie
177
assert heads[ie.revision] == ie
205
# add this revision as a candidate.
206
candidates[ie.revision] = ie
208
# common case optimisation
209
if len(candidates) == 1:
210
# if there is only one candidate revision found
211
# then we can opening the versioned file to access ancestry:
212
# there cannot be any ancestors to eliminate when there is
213
# only one revision available.
214
heads[ie.revision] = ie
217
# eliminate ancestors amongst the available candidates:
218
# heads are those that are not an ancestor of any other candidate
219
# - this provides convergence at a per-file level.
220
for ie in candidates.values():
221
# may be an ancestor of a known head:
222
already_present = 0 != len(
223
[head for head in heads
224
if ie.revision in head_ancestors[head]])
226
# an ancestor of an analyzed candidate.
228
# not an ancestor of a known head:
229
# load the versioned file for this file id if needed
231
entry_vf = versioned_file_store.get_weave_or_empty(
232
self.file_id, transaction)
233
ancestors = get_ancestors(entry_vf, ie)
234
# may knock something else out:
235
check_heads = list(heads.keys())
236
for head in check_heads:
237
if head in ancestors:
238
# this previously discovered 'head' is not
239
# really a head - its an ancestor of the newly
242
head_ancestors[ie.revision] = ancestors
243
heads[ie.revision] = ie
179
# may want to add it.
180
# may already be covered:
181
already_present = 0 != len(
182
[head for head in heads
183
if ie.revision in head_ancestors[head]])
185
# an ancestor of a known head.
188
ancestors = get_ancestors(entry_weave, ie)
189
# may knock something else out:
190
check_heads = list(heads.keys())
191
for head in check_heads:
192
if head in ancestors:
193
# this head is not really a head
195
head_ancestors[ie.revision] = ancestors
196
heads[ie.revision] = ie
246
199
def get_tar_item(self, root, dp, now, tree):
247
200
"""Get a tarfile item and a file stream for its content."""
248
item = tarfile.TarInfo(pathjoin(root, dp))
201
item = tarfile.TarInfo(os.path.join(root, dp))
249
202
# TODO: would be cool to actually set it to the timestamp of the
250
203
# revision it was last changed
311
265
This is a template method - implement _put_on_disk in subclasses.
313
fullpath = pathjoin(dest, dp)
267
fullpath = appendpath(dest, dp)
314
268
self._put_on_disk(fullpath, tree)
315
# mutter(" export {%s} kind %s to %s", self.file_id,
316
# self.kind, fullpath)
269
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
318
271
def _put_on_disk(self, fullpath, tree):
319
272
"""Put this entry onto disk at fullpath, from tree tree."""
320
273
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
322
275
def sorted_children(self):
323
return sorted(self.children.items())
276
l = self.children.items()
326
281
def versionable_kind(kind):
351
298
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
352
299
(self.kind, rev_id))
355
303
"""Clone this inventory entry."""
356
304
raise NotImplementedError
359
def describe_change(old_entry, new_entry):
360
"""Describe the change between old_entry and this.
362
This smells of being an InterInventoryEntry situation, but as its
363
the first one, we're making it a static method for now.
365
An entry with a different parent, or different name is considered
366
to be renamed. Reparenting is an internal detail.
367
Note that renaming the parent does not trigger a rename for the
370
# TODO: Perhaps return an object rather than just a string
371
if old_entry is new_entry:
372
# also the case of both being None
374
elif old_entry is None:
306
def _get_snapshot_change(self, previous_entries):
307
if len(previous_entries) > 1:
309
elif len(previous_entries) == 0:
376
elif new_entry is None:
378
text_modified, meta_modified = new_entry.detect_changes(old_entry)
379
if text_modified or meta_modified:
383
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
384
if old_entry.parent_id != new_entry.parent_id:
386
elif old_entry.name != new_entry.name:
390
if renamed and not modified:
391
return InventoryEntry.RENAMED
392
if modified and not renamed:
394
if modified and renamed:
395
return InventoryEntry.MODIFIED_AND_RENAMED
312
return 'modified/renamed/reparented'
398
314
def __repr__(self):
399
return ("%s(%r, %r, parent_id=%r, revision=%r)"
315
return ("%s(%r, %r, parent_id=%r)"
400
316
% (self.__class__.__name__,
406
321
def snapshot(self, revision, path, previous_entries,
407
work_tree, commit_builder):
322
work_tree, weave_store, transaction):
408
323
"""Make a snapshot of this entry which may or may not have changed.
410
325
This means that all its fields are populated, that it has its
411
326
text stored in the text store or weave.
413
# mutter('new parents of %s are %r', path, previous_entries)
328
mutter('new parents of %s are %r', path, previous_entries)
414
329
self._read_tree_state(path, work_tree)
415
# TODO: Where should we determine whether to reuse a
416
# previous revision id or create a new revision? 20060606
417
330
if len(previous_entries) == 1:
418
331
# cannot be unchanged unless there is only one parent file rev.
419
332
parent_ie = previous_entries.values()[0]
420
333
if self._unchanged(parent_ie):
421
# mutter("found unchanged entry")
334
mutter("found unchanged entry")
422
335
self.revision = parent_ie.revision
423
336
return "unchanged"
424
return self._snapshot_into_revision(revision, previous_entries,
425
work_tree, commit_builder)
427
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
429
"""Record this revision unconditionally into a store.
431
The entry's last-changed revision property (`revision`) is updated to
432
that of the new revision.
434
:param revision: id of the new revision that is being recorded.
436
:returns: String description of the commit (e.g. "merged", "modified"), etc.
438
# mutter('new revision {%s} for {%s}', revision, self.file_id)
337
return self.snapshot_revision(revision, previous_entries,
338
work_tree, weave_store, transaction)
340
def snapshot_revision(self, revision, previous_entries, work_tree,
341
weave_store, transaction):
342
"""Record this revision unconditionally."""
343
mutter('new revision for {%s}', self.file_id)
439
344
self.revision = revision
440
self._snapshot_text(previous_entries, work_tree, commit_builder)
345
change = self._get_snapshot_change(previous_entries)
346
self._snapshot_text(previous_entries, work_tree, weave_store,
442
def _snapshot_text(self, file_parents, work_tree, commit_builder):
350
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
443
351
"""Record the 'text' of this entry, whatever form that takes.
445
353
This default implementation simply adds an empty text.
447
raise NotImplementedError(self._snapshot_text)
355
mutter('storing file {%s} in revision {%s}',
356
self.file_id, self.revision)
357
self._add_text_to_weave([], file_parents, weave_store, transaction)
449
359
def __eq__(self, other):
450
360
if not isinstance(other, InventoryEntry):
488
398
Note that this should be modified to be a noop on virtual trees
489
399
as all entries created there are prepopulated.
491
# TODO: Rather than running this manually, we should check the
492
# working sha1 and other expensive properties when they're
493
# first requested, or preload them if they're already known
494
pass # nothing to do by default
496
def _forget_tree_state(self):
500
403
class RootEntry(InventoryEntry):
502
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
503
'text_id', 'parent_id', 'children', 'executable',
504
'revision', 'symlink_target']
506
405
def _check(self, checker, rev_id, tree):
507
406
"""See InventoryEntry._check"""
564
458
"""See InventoryEntry._put_on_disk."""
565
459
os.mkdir(fullpath)
567
def _snapshot_text(self, file_parents, work_tree, commit_builder):
568
"""See InventoryEntry._snapshot_text."""
569
commit_builder.modified_directory(self.file_id, file_parents)
572
462
class InventoryFile(InventoryEntry):
573
463
"""A file in an inventory."""
575
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
576
'text_id', 'parent_id', 'children', 'executable',
577
'revision', 'symlink_target']
579
def _check(self, checker, tree_revision_id, tree):
465
def _check(self, checker, rev_id, tree):
580
466
"""See InventoryEntry._check"""
581
t = (self.file_id, self.revision)
467
revision = self.revision
468
t = (self.file_id, revision)
582
469
if t in checker.checked_texts:
583
prev_sha = checker.checked_texts[t]
470
prev_sha = checker.checked_texts[t]
584
471
if prev_sha != self.text_sha1:
585
472
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
586
(self.file_id, tree_revision_id))
473
(self.file_id, rev_id))
588
475
checker.repeated_text_cnt += 1
591
if self.file_id not in checker.checked_weaves:
592
mutter('check weave {%s}', self.file_id)
593
w = tree.get_weave(self.file_id)
594
# Not passing a progress bar, because it creates a new
595
# progress, which overwrites the current progress,
596
# and doesn't look nice
598
checker.checked_weaves[self.file_id] = True
600
w = tree.get_weave(self.file_id)
602
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
603
checker.checked_text_cnt += 1
604
# We can't check the length, because Weave doesn't store that
605
# information, and the whole point of looking at the weave's
606
# sha1sum is that we don't have to extract the text.
607
if self.text_sha1 != w.get_sha1(self.revision):
608
raise BzrCheckError('text {%s} version {%s} wrong sha1'
609
% (self.file_id, self.revision))
477
mutter('check version {%s} of {%s}', rev_id, self.file_id)
478
file_lines = tree.get_file_lines(self.file_id)
479
checker.checked_text_cnt += 1
480
if self.text_size != sum(map(len, file_lines)):
481
raise BzrCheckError('text {%s} wrong size' % self.text_id)
482
if self.text_sha1 != sha_strings(file_lines):
483
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
610
484
checker.checked_texts[t] = self.text_sha1
629
503
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
630
504
output_to, reverse=False):
631
505
"""See InventoryEntry._diff."""
633
from_text = tree.get_file(self.file_id).readlines()
635
to_text = to_tree.get_file(to_entry.file_id).readlines()
639
text_diff(from_label, from_text,
640
to_label, to_text, output_to)
642
text_diff(to_label, to_text,
643
from_label, from_text, output_to)
646
label_pair = (to_label, from_label)
648
label_pair = (from_label, to_label)
649
print >> output_to, "Binary files %s and %s differ" % label_pair
506
from_text = tree.get_file(self.file_id).readlines()
508
to_text = to_tree.get_file(to_entry.file_id).readlines()
512
text_diff(from_label, from_text,
513
to_label, to_text, output_to)
515
text_diff(to_label, to_text,
516
from_label, from_text, output_to)
651
518
def has_text(self):
652
519
"""See InventoryEntry.has_text."""
680
547
def _read_tree_state(self, path, work_tree):
681
548
"""See InventoryEntry._read_tree_state."""
682
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
683
# FIXME: 20050930 probe for the text size when getting sha1
684
# in _read_tree_state
685
self.executable = work_tree.is_executable(self.file_id, path=path)
688
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
689
% (self.__class__.__name__,
696
def _forget_tree_state(self):
697
self.text_sha1 = None
699
def _snapshot_text(self, file_parents, work_tree, commit_builder):
549
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
550
self.executable = work_tree.is_executable(self.file_id)
552
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
700
553
"""See InventoryEntry._snapshot_text."""
701
def get_content_byte_lines():
702
return work_tree.get_file(self.file_id).readlines()
703
self.text_sha1, self.text_size = commit_builder.modified_file_text(
704
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
554
mutter('storing file {%s} in revision {%s}',
555
self.file_id, self.revision)
556
# special case to avoid diffing on renames or
558
if (len(file_parents) == 1
559
and self.text_sha1 == file_parents.values()[0].text_sha1
560
and self.text_size == file_parents.values()[0].text_size):
561
previous_ie = file_parents.values()[0]
562
weave_store.add_identical_text(
563
self.file_id, previous_ie.revision,
564
self.revision, file_parents, transaction)
566
new_lines = work_tree.get_file(self.file_id).readlines()
567
self._add_text_to_weave(new_lines, file_parents, weave_store,
569
self.text_sha1 = sha_strings(new_lines)
570
self.text_size = sum(map(len, new_lines))
706
573
def _unchanged(self, previous_ie):
707
574
"""See InventoryEntry._unchanged."""
720
587
class InventoryLink(InventoryEntry):
721
588
"""A file in an inventory."""
723
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
724
'text_id', 'parent_id', 'children', 'executable',
725
'revision', 'symlink_target']
590
__slots__ = ['symlink_target']
727
592
def _check(self, checker, rev_id, tree):
728
593
"""See InventoryEntry._check"""
729
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
594
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
730
595
raise BzrCheckError('symlink {%s} has text in revision {%s}'
731
596
% (self.file_id, rev_id))
732
if self.symlink_target is None:
597
if self.symlink_target == None:
733
598
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
734
599
% (self.file_id, rev_id))
857
714
The inventory is created with a default root directory, with
860
# We are letting Branch.create() create a unique inventory
717
# We are letting Branch.initialize() create a unique inventory
861
718
# root id. Rather than generating a random one here.
862
719
#if root_id is None:
863
720
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
864
721
self.root = RootEntry(root_id)
865
# FIXME: this isn't ever used, changing it to self.revision may break
866
# things. TODO make everything use self.revision_id
867
self.revision_id = revision_id
868
722
self._byid = {self.root.file_id: self.root}
871
# TODO: jam 20051218 Should copy also copy the revision_id?
872
entries = self.iter_entries()
873
other = Inventory(entries.next()[1].file_id)
726
other = Inventory(self.root.file_id)
874
727
# copy recursively so we know directories will be added before
875
728
# their children. There are more efficient ways than this...
876
for path, entry in entries():
729
for path, entry in self.iter_entries():
730
if entry == self.root:
877
732
other.add(entry.copy())
880
736
def __iter__(self):
881
737
return iter(self._byid)
883
740
def __len__(self):
884
741
"""Returns number of entries."""
885
742
return len(self._byid)
887
745
def iter_entries(self, from_dir=None):
888
746
"""Return (path, entry) pairs, in order by name."""
893
elif isinstance(from_dir, basestring):
894
from_dir = self._byid[from_dir]
896
# unrolling the recursive called changed the time from
897
# 440ms/663ms (inline/total) to 116ms/116ms
898
children = from_dir.children.items()
900
children = collections.deque(children)
901
stack = [(u'', children)]
903
from_dir_relpath, children = stack[-1]
906
name, ie = children.popleft()
908
# we know that from_dir_relpath never ends in a slash
909
# and 'f' doesn't begin with one, we can do a string op, rather
910
# than the checks of pathjoin(), though this means that all paths
912
path = from_dir_relpath + '/' + name
916
if ie.kind != 'directory':
919
# But do this child first
920
new_children = ie.children.items()
922
new_children = collections.deque(new_children)
923
stack.append((path, new_children))
924
# Break out of inner loop, so that we start outer loop with child
927
# if we finished all children, pop it off the stack
930
def iter_entries_by_dir(self, from_dir=None):
931
"""Iterate over the entries in a directory first order.
933
This returns all entries for a directory before returning
934
the entries for children of a directory. This is not
935
lexicographically sorted order, and is a hybrid between
936
depth-first and breadth-first.
938
:return: This yields (path, entry) pairs
940
# TODO? Perhaps this should return the from_dir so that the root is
941
# yielded? or maybe an option?
946
elif isinstance(from_dir, basestring):
947
from_dir = self._byid[from_dir]
949
stack = [(u'', from_dir)]
951
cur_relpath, cur_dir = stack.pop()
954
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
956
child_relpath = cur_relpath + child_name
958
yield child_relpath, child_ie
960
if child_ie.kind == 'directory':
961
child_dirs.append((child_relpath+'/', child_ie))
962
stack.extend(reversed(child_dirs))
750
elif isinstance(from_dir, basestring):
751
from_dir = self._byid[from_dir]
753
kids = from_dir.children.items()
755
for name, ie in kids:
757
if ie.kind == 'directory':
758
for cn, cie in self.iter_entries(from_dir=ie.file_id):
759
yield os.path.join(name, cn), cie
964
762
def entries(self):
965
763
"""Return list of (path, ie) for all entries except the root.
1009
810
return file_id in self._byid
1011
813
def __getitem__(self, file_id):
1012
814
"""Return the entry for given file_id.
1014
816
>>> inv = Inventory()
1015
817
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1016
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
818
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1017
819
>>> inv['123123'].name
1021
823
return self._byid[file_id]
1022
824
except KeyError:
1024
826
raise BzrError("can't look up file_id None")
1026
828
raise BzrError("file_id {%s} not in inventory" % file_id)
1028
831
def get_file_kind(self, file_id):
1029
832
return self._byid[file_id].kind
1031
834
def get_child(self, parent_id, filename):
1032
835
return self[parent_id].children.get(filename)
1034
838
def add(self, entry):
1035
839
"""Add entry to inventory.
1050
854
except KeyError:
1051
855
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1053
if entry.name in parent.children:
857
if parent.children.has_key(entry.name):
1054
858
raise BzrError("%s is already versioned" %
1055
pathjoin(self.id2path(parent.file_id), entry.name))
859
appendpath(self.id2path(parent.file_id), entry.name))
1057
861
self._byid[entry.file_id] = entry
1058
862
parent.children[entry.name] = entry
1061
def add_path(self, relpath, kind, file_id=None, parent_id=None):
866
def add_path(self, relpath, kind, file_id=None):
1062
867
"""Add entry from a path.
1064
869
The immediate parent must already be versioned.
1066
871
Returns the new entry object."""
872
from bzrlib.branch import gen_file_id
1068
parts = osutils.splitpath(relpath)
874
parts = bzrlib.osutils.splitpath(relpath)
1070
875
if len(parts) == 0:
1072
file_id = bzrlib.workingtree.gen_root_id()
1073
self.root = RootEntry(file_id)
1074
self._byid = {self.root.file_id: self.root}
876
raise BzrError("cannot re-add root of inventory")
879
file_id = gen_file_id(relpath)
881
parent_path = parts[:-1]
882
parent_id = self.path2id(parent_path)
883
if parent_id == None:
884
raise NotVersionedError(parent_path)
886
if kind == 'directory':
887
ie = InventoryDirectory(file_id, parts[-1], parent_id)
889
ie = InventoryFile(file_id, parts[-1], parent_id)
890
elif kind == 'symlink':
891
ie = InventoryLink(file_id, parts[-1], parent_id)
1077
parent_path = parts[:-1]
1078
parent_id = self.path2id(parent_path)
1079
if parent_id is None:
1080
raise NotVersionedError(path=parent_path)
1081
ie = make_entry(kind, parts[-1], parent_id, file_id)
893
raise BzrError("unknown kind %r" % kind)
1082
894
return self.add(ie)
1084
897
def __delitem__(self, file_id):
1085
898
"""Remove entry by id.
1087
900
>>> inv = Inventory()
1088
901
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1089
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
902
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1090
903
>>> '123' in inv
1092
905
>>> del inv['123']
1112
931
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1113
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
932
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1116
935
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1117
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
936
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1121
940
if not isinstance(other, Inventory):
1122
941
return NotImplemented
943
if len(self._byid) != len(other._byid):
944
# shortcut: obviously not the same
1124
947
return self._byid == other._byid
1126
950
def __ne__(self, other):
1127
951
return not self.__eq__(other)
1129
954
def __hash__(self):
1130
955
raise ValueError('not hashable')
1132
def _iter_file_id_parents(self, file_id):
1133
"""Yield the parents of file_id up to the root."""
1134
while file_id is not None:
1136
ie = self._byid[file_id]
1138
raise BzrError("file_id {%s} not found in inventory" % file_id)
1140
file_id = ie.parent_id
1142
958
def get_idpath(self, file_id):
1143
959
"""Return a list of file_ids for the path to an entry.
1148
964
root directory as depth 1.
1151
for parent in self._iter_file_id_parents(file_id):
1152
p.insert(0, parent.file_id)
967
while file_id != None:
969
ie = self._byid[file_id]
971
raise BzrError("file_id {%s} not found in inventory" % file_id)
972
p.insert(0, ie.file_id)
973
file_id = ie.parent_id
1155
977
def id2path(self, file_id):
1156
"""Return as a string the path to file_id.
978
"""Return as a list the path to file_id.
1158
980
>>> i = Inventory()
1159
981
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1160
982
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1161
>>> print i.id2path('foo-id')
983
>>> print i.id2path('foo-id').replace(os.sep, '/')
1164
986
# get all names, skipping root
1165
return '/'.join(reversed(
1166
[parent.name for parent in
1167
self._iter_file_id_parents(file_id)][:-1]))
987
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
988
return os.sep.join(p)
1169
992
def path2id(self, name):
1170
993
"""Walk down through directories to return entry of last component.
1231
1057
file_ie.parent_id = new_parent_id
1234
def make_entry(kind, name, parent_id, file_id=None):
1235
"""Create an inventory entry.
1237
:param kind: the type of inventory entry to create.
1238
:param name: the basename of the entry.
1239
:param parent_id: the parent_id of the entry.
1240
:param file_id: the file_id to use. if None, one will be created.
1243
file_id = bzrlib.workingtree.gen_file_id(name)
1245
norm_name, can_access = osutils.normalized_filename(name)
1246
if norm_name != name:
1250
# TODO: jam 20060701 This would probably be more useful
1251
# if the error was raised with the full path
1252
raise errors.InvalidNormalization(name)
1254
if kind == 'directory':
1255
return InventoryDirectory(file_id, name, parent_id)
1256
elif kind == 'file':
1257
return InventoryFile(file_id, name, parent_id)
1258
elif kind == 'symlink':
1259
return InventoryLink(file_id, name, parent_id)
1261
raise BzrError("unknown kind %r" % kind)
1264
1062
_NAME_RE = None
1266
1064
def is_valid_name(name):
1267
1065
global _NAME_RE
1268
if _NAME_RE is None:
1066
if _NAME_RE == None:
1269
1067
_NAME_RE = re.compile(r'^[^/\\]+$')
1271
1069
return bool(_NAME_RE.match(name))