78
87
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
88
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
80
89
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> for j in i.iter_entries():
90
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
91
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
92
>>> for ix, j in enumerate(i.iter_entries()):
93
... print (j[0] == shouldbe[ix], j[1])
85
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
95
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
96
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
97
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
87
98
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
88
99
Traceback (most recent call last):
90
101
BzrError: inventory already contains entry with id {2323}
91
102
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
92
InventoryFile('2324', 'bye.c', parent_id='123')
103
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
93
104
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
94
InventoryDirectory('2325', 'wibble', parent_id='123')
105
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
95
106
>>> i.path2id('src/wibble')
99
110
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
100
InventoryFile('2326', 'wibble.c', parent_id='2325')
111
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
InventoryFile('2326', 'wibble.c', parent_id='2325')
113
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
103
114
>>> for path, entry in i.iter_entries():
104
... print path.replace('\\\\', '/') # for win32 os.sep
105
116
... assert i.path2id(path)
111
123
src/wibble/wibble.c
112
>>> i.id2path('2326').replace('\\\\', '/')
124
>>> i.id2path('2326')
113
125
'src/wibble/wibble.c'
128
# Constants returned by describe_change()
130
# TODO: These should probably move to some kind of FileChangeDescription
131
# class; that's like what's inside a TreeDelta but we want to be able to
132
# generate them just for one file at a time.
134
MODIFIED_AND_RENAMED = 'modified and renamed'
116
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
117
'text_id', 'parent_id', 'children', 'executable',
120
def _add_text_to_weave(self, new_lines, parents, weave_store, transaction):
121
weave_store.add_text(self.file_id, self.revision, new_lines, parents,
124
138
def detect_changes(self, old_entry):
125
139
"""Return a (text_modified, meta_modified) from this to old_entry.
150
164
output_to, reverse=False):
151
165
"""Perform a diff between two entries of the same kind."""
153
def find_previous_heads(self, previous_inventories, entry_weave):
154
"""Return the revisions and entries that directly preceed this.
167
def find_previous_heads(self, previous_inventories,
168
versioned_file_store,
171
"""Return the revisions and entries that directly precede this.
156
173
Returned as a map from revision to inventory entry.
158
175
This is a map containing the file revisions in all parents
159
176
for which the file exists, and its revision is not a parent of
160
177
any other. If the file is new, the set will be empty.
179
:param versioned_file_store: A store where ancestry data on this
180
file id can be queried.
181
:param transaction: The transaction that queries to the versioned
182
file store should be completed under.
183
:param entry_vf: The entry versioned file, if its already available.
162
185
def get_ancestors(weave, entry):
163
return set(map(weave.idx_to_name,
164
weave.inclusions([weave.lookup(entry.revision)])))
186
return set(weave.get_ancestry(entry.revision))
187
# revision:ie mapping for each ie found in previous_inventories.
189
# revision:ie mapping with one revision for each head.
191
# revision: ancestor list for each head
166
192
head_ancestors = {}
193
# identify candidate head revision ids.
167
194
for inv in previous_inventories:
168
195
if self.file_id in inv:
169
196
ie = inv[self.file_id]
170
197
assert ie.file_id == self.file_id
171
if ie.revision in heads:
172
# fixup logic, there was a bug in revision updates.
173
# with x bit support.
198
if ie.revision in candidates:
199
# same revision value in two different inventories:
200
# correct possible inconsistencies:
201
# * there was a bug in revision updates with 'x' bit
175
if heads[ie.revision].executable != ie.executable:
176
heads[ie.revision].executable = False
204
if candidates[ie.revision].executable != ie.executable:
205
candidates[ie.revision].executable = False
177
206
ie.executable = False
178
207
except AttributeError:
180
assert heads[ie.revision] == ie
209
# must now be the same.
210
assert candidates[ie.revision] == ie
182
# may want to add it.
183
# may already be covered:
184
already_present = 0 != len(
185
[head for head in heads
186
if ie.revision in head_ancestors[head]])
188
# an ancestor of a known head.
191
ancestors = get_ancestors(entry_weave, ie)
192
# may knock something else out:
193
check_heads = list(heads.keys())
194
for head in check_heads:
195
if head in ancestors:
196
# this head is not really a head
198
head_ancestors[ie.revision] = ancestors
199
heads[ie.revision] = ie
212
# add this revision as a candidate.
213
candidates[ie.revision] = ie
215
# common case optimisation
216
if len(candidates) == 1:
217
# if there is only one candidate revision found
218
# then we can opening the versioned file to access ancestry:
219
# there cannot be any ancestors to eliminate when there is
220
# only one revision available.
221
heads[ie.revision] = ie
224
# eliminate ancestors amongst the available candidates:
225
# heads are those that are not an ancestor of any other candidate
226
# - this provides convergence at a per-file level.
227
for ie in candidates.values():
228
# may be an ancestor of a known head:
229
already_present = 0 != len(
230
[head for head in heads
231
if ie.revision in head_ancestors[head]])
233
# an ancestor of an analyzed candidate.
235
# not an ancestor of a known head:
236
# load the versioned file for this file id if needed
238
entry_vf = versioned_file_store.get_weave_or_empty(
239
self.file_id, transaction)
240
ancestors = get_ancestors(entry_vf, ie)
241
# may knock something else out:
242
check_heads = list(heads.keys())
243
for head in check_heads:
244
if head in ancestors:
245
# this previously discovered 'head' is not
246
# really a head - its an ancestor of the newly
249
head_ancestors[ie.revision] = ancestors
250
heads[ie.revision] = ie
202
253
def get_tar_item(self, root, dp, now, tree):
203
254
"""Get a tarfile item and a file stream for its content."""
204
item = tarfile.TarInfo(os.path.join(root, dp))
255
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
205
256
# TODO: would be cool to actually set it to the timestamp of the
206
257
# revision it was last changed
267
318
This is a template method - implement _put_on_disk in subclasses.
269
fullpath = appendpath(dest, dp)
320
fullpath = osutils.pathjoin(dest, dp)
270
321
self._put_on_disk(fullpath, tree)
271
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
322
# mutter(" export {%s} kind %s to %s", self.file_id,
323
# self.kind, fullpath)
273
325
def _put_on_disk(self, fullpath, tree):
274
326
"""Put this entry onto disk at fullpath, from tree tree."""
275
327
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
277
329
def sorted_children(self):
278
l = self.children.items()
330
return sorted(self.children.items())
283
333
def versionable_kind(kind):
284
return kind in ('file', 'directory', 'symlink')
334
return (kind in ('file', 'directory', 'symlink'))
286
336
def check(self, checker, rev_id, inv, tree):
287
337
"""Check this inventory entry is intact.
289
339
This is a template method, override _check for kind specific
342
:param checker: Check object providing context for the checks;
343
can be used to find out what parts of the repository have already
345
:param rev_id: Revision id from which this InventoryEntry was loaded.
346
Not necessarily the last-changed revision for this file.
347
:param inv: Inventory from which the entry was loaded.
348
:param tree: RevisionTree for this entry.
292
if self.parent_id != None:
350
if self.parent_id is not None:
293
351
if not inv.has_id(self.parent_id):
294
352
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
295
353
% (self.parent_id, rev_id))
300
358
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
301
359
(self.kind, rev_id))
305
362
"""Clone this inventory entry."""
306
363
raise NotImplementedError
308
def _get_snapshot_change(self, previous_entries):
309
if len(previous_entries) > 1:
311
elif len(previous_entries) == 0:
366
def describe_change(old_entry, new_entry):
367
"""Describe the change between old_entry and this.
369
This smells of being an InterInventoryEntry situation, but as its
370
the first one, we're making it a static method for now.
372
An entry with a different parent, or different name is considered
373
to be renamed. Reparenting is an internal detail.
374
Note that renaming the parent does not trigger a rename for the
377
# TODO: Perhaps return an object rather than just a string
378
if old_entry is new_entry:
379
# also the case of both being None
381
elif old_entry is None:
314
return 'modified/renamed/reparented'
383
elif new_entry is None:
385
text_modified, meta_modified = new_entry.detect_changes(old_entry)
386
if text_modified or meta_modified:
390
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
391
if old_entry.parent_id != new_entry.parent_id:
393
elif old_entry.name != new_entry.name:
397
if renamed and not modified:
398
return InventoryEntry.RENAMED
399
if modified and not renamed:
401
if modified and renamed:
402
return InventoryEntry.MODIFIED_AND_RENAMED
316
405
def __repr__(self):
317
return ("%s(%r, %r, parent_id=%r)"
406
return ("%s(%r, %r, parent_id=%r, revision=%r)"
318
407
% (self.__class__.__name__,
323
413
def snapshot(self, revision, path, previous_entries,
324
work_tree, weave_store, transaction):
414
work_tree, commit_builder):
325
415
"""Make a snapshot of this entry which may or may not have changed.
327
417
This means that all its fields are populated, that it has its
328
418
text stored in the text store or weave.
330
mutter('new parents of %s are %r', path, previous_entries)
420
# mutter('new parents of %s are %r', path, previous_entries)
331
421
self._read_tree_state(path, work_tree)
422
# TODO: Where should we determine whether to reuse a
423
# previous revision id or create a new revision? 20060606
332
424
if len(previous_entries) == 1:
333
425
# cannot be unchanged unless there is only one parent file rev.
334
426
parent_ie = previous_entries.values()[0]
335
427
if self._unchanged(parent_ie):
336
mutter("found unchanged entry")
428
# mutter("found unchanged entry")
337
429
self.revision = parent_ie.revision
338
430
return "unchanged"
339
return self.snapshot_revision(revision, previous_entries,
340
work_tree, weave_store, transaction)
342
def snapshot_revision(self, revision, previous_entries, work_tree,
343
weave_store, transaction):
344
"""Record this revision unconditionally."""
345
mutter('new revision for {%s}', self.file_id)
431
return self._snapshot_into_revision(revision, previous_entries,
432
work_tree, commit_builder)
434
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
436
"""Record this revision unconditionally into a store.
438
The entry's last-changed revision property (`revision`) is updated to
439
that of the new revision.
441
:param revision: id of the new revision that is being recorded.
443
:returns: String description of the commit (e.g. "merged", "modified"), etc.
445
# mutter('new revision {%s} for {%s}', revision, self.file_id)
346
446
self.revision = revision
347
change = self._get_snapshot_change(previous_entries)
348
self._snapshot_text(previous_entries, work_tree, weave_store,
447
self._snapshot_text(previous_entries, work_tree, commit_builder)
352
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
449
def _snapshot_text(self, file_parents, work_tree, commit_builder):
353
450
"""Record the 'text' of this entry, whatever form that takes.
355
452
This default implementation simply adds an empty text.
357
mutter('storing file {%s} in revision {%s}',
358
self.file_id, self.revision)
359
self._add_text_to_weave([], file_parents, weave_store, transaction)
454
raise NotImplementedError(self._snapshot_text)
361
456
def __eq__(self, other):
362
457
if not isinstance(other, InventoryEntry):
405
500
# first requested, or preload them if they're already known
406
501
pass # nothing to do by default
503
def _forget_tree_state(self):
409
507
class RootEntry(InventoryEntry):
509
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
510
'text_id', 'parent_id', 'children', 'executable',
511
'revision', 'symlink_target']
411
513
def _check(self, checker, rev_id, tree):
412
514
"""See InventoryEntry._check"""
414
516
def __init__(self, file_id):
415
517
self.file_id = file_id
416
518
self.children = {}
417
self.kind = 'root_directory'
519
self.kind = 'directory'
418
520
self.parent_id = None
523
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
524
' Please use InventoryDirectory instead.',
525
DeprecationWarning, stacklevel=2)
421
527
def __eq__(self, other):
422
528
if not isinstance(other, RootEntry):
464
574
"""See InventoryEntry._put_on_disk."""
465
575
os.mkdir(fullpath)
577
def _snapshot_text(self, file_parents, work_tree, commit_builder):
578
"""See InventoryEntry._snapshot_text."""
579
commit_builder.modified_directory(self.file_id, file_parents)
468
582
class InventoryFile(InventoryEntry):
469
583
"""A file in an inventory."""
471
def _check(self, checker, rev_id, tree):
585
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
586
'text_id', 'parent_id', 'children', 'executable',
587
'revision', 'symlink_target']
589
def _check(self, checker, tree_revision_id, tree):
472
590
"""See InventoryEntry._check"""
473
revision = self.revision
474
t = (self.file_id, revision)
591
t = (self.file_id, self.revision)
475
592
if t in checker.checked_texts:
476
prev_sha = checker.checked_texts[t]
593
prev_sha = checker.checked_texts[t]
477
594
if prev_sha != self.text_sha1:
478
595
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
479
(self.file_id, rev_id))
596
(self.file_id, tree_revision_id))
481
598
checker.repeated_text_cnt += 1
483
mutter('check version {%s} of {%s}', rev_id, self.file_id)
484
file_lines = tree.get_file_lines(self.file_id)
485
checker.checked_text_cnt += 1
486
if self.text_size != sum(map(len, file_lines)):
487
raise BzrCheckError('text {%s} wrong size' % self.text_id)
488
if self.text_sha1 != sha_strings(file_lines):
489
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
601
if self.file_id not in checker.checked_weaves:
602
mutter('check weave {%s}', self.file_id)
603
w = tree.get_weave(self.file_id)
604
# Not passing a progress bar, because it creates a new
605
# progress, which overwrites the current progress,
606
# and doesn't look nice
608
checker.checked_weaves[self.file_id] = True
610
w = tree.get_weave(self.file_id)
612
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
613
checker.checked_text_cnt += 1
614
# We can't check the length, because Weave doesn't store that
615
# information, and the whole point of looking at the weave's
616
# sha1sum is that we don't have to extract the text.
617
if self.text_sha1 != w.get_sha1(self.revision):
618
raise BzrCheckError('text {%s} version {%s} wrong sha1'
619
% (self.file_id, self.revision))
490
620
checker.checked_texts[t] = self.text_sha1
509
639
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
510
640
output_to, reverse=False):
511
641
"""See InventoryEntry._diff."""
512
from_text = tree.get_file(self.file_id).readlines()
514
to_text = to_tree.get_file(to_entry.file_id).readlines()
518
text_diff(from_label, from_text,
519
to_label, to_text, output_to)
521
text_diff(to_label, to_text,
522
from_label, from_text, output_to)
643
from_text = tree.get_file(self.file_id).readlines()
645
to_text = to_tree.get_file(to_entry.file_id).readlines()
649
text_diff(from_label, from_text,
650
to_label, to_text, output_to)
652
text_diff(to_label, to_text,
653
from_label, from_text, output_to)
654
except errors.BinaryFile:
656
label_pair = (to_label, from_label)
658
label_pair = (from_label, to_label)
659
print >> output_to, "Binary files %s and %s differ" % label_pair
524
661
def has_text(self):
525
662
"""See InventoryEntry.has_text."""
547
684
def _put_on_disk(self, fullpath, tree):
548
685
"""See InventoryEntry._put_on_disk."""
549
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
686
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
550
687
if tree.is_executable(self.file_id):
551
688
os.chmod(fullpath, 0755)
553
690
def _read_tree_state(self, path, work_tree):
554
691
"""See InventoryEntry._read_tree_state."""
555
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
556
self.executable = work_tree.is_executable(self.file_id)
558
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
692
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
693
# FIXME: 20050930 probe for the text size when getting sha1
694
# in _read_tree_state
695
self.executable = work_tree.is_executable(self.file_id, path=path)
698
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
699
% (self.__class__.__name__,
706
def _forget_tree_state(self):
707
self.text_sha1 = None
709
def _snapshot_text(self, file_parents, work_tree, commit_builder):
559
710
"""See InventoryEntry._snapshot_text."""
560
mutter('storing file {%s} in revision {%s}',
561
self.file_id, self.revision)
562
# special case to avoid diffing on renames or
564
if (len(file_parents) == 1
565
and self.text_sha1 == file_parents.values()[0].text_sha1
566
and self.text_size == file_parents.values()[0].text_size):
567
previous_ie = file_parents.values()[0]
568
weave_store.add_identical_text(
569
self.file_id, previous_ie.revision,
570
self.revision, file_parents, transaction)
572
new_lines = work_tree.get_file(self.file_id).readlines()
573
self._add_text_to_weave(new_lines, file_parents, weave_store,
575
self.text_sha1 = sha_strings(new_lines)
576
self.text_size = sum(map(len, new_lines))
711
def get_content_byte_lines():
712
return work_tree.get_file(self.file_id).readlines()
713
self.text_sha1, self.text_size = commit_builder.modified_file_text(
714
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
579
716
def _unchanged(self, previous_ie):
580
717
"""See InventoryEntry._unchanged."""
593
730
class InventoryLink(InventoryEntry):
594
731
"""A file in an inventory."""
596
__slots__ = ['symlink_target']
733
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
734
'text_id', 'parent_id', 'children', 'executable',
735
'revision', 'symlink_target']
598
737
def _check(self, checker, rev_id, tree):
599
738
"""See InventoryEntry._check"""
600
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
739
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
601
740
raise BzrCheckError('symlink {%s} has text in revision {%s}'
602
741
% (self.file_id, rev_id))
603
if self.symlink_target == None:
742
if self.symlink_target is None:
604
743
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
605
744
% (self.file_id, rev_id))
705
852
May also look up by name:
707
854
>>> [x[0] for x in inv.iter_entries()]
709
856
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
710
857
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
711
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
858
Traceback (most recent call last):
859
BzrError: parent_id {TREE_ROOT} not in inventory
860
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
861
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
713
def __init__(self, root_id=ROOT_ID):
863
def __init__(self, root_id=ROOT_ID, revision_id=None):
714
864
"""Create or read an inventory.
716
866
If a working directory is specified, the inventory is read
720
870
The inventory is created with a default root directory, with
723
# We are letting Branch.initialize() create a unique inventory
724
# root id. Rather than generating a random one here.
726
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
727
self.root = RootEntry(root_id)
873
if root_id is not None:
874
self._set_root(InventoryDirectory(root_id, '', None))
878
self.revision_id = revision_id
880
def _set_root(self, ie):
728
882
self._byid = {self.root.file_id: self.root}
732
other = Inventory(self.root.file_id)
885
# TODO: jam 20051218 Should copy also copy the revision_id?
886
entries = self.iter_entries()
887
other = Inventory(entries.next()[1].file_id)
733
888
# copy recursively so we know directories will be added before
734
889
# their children. There are more efficient ways than this...
735
for path, entry in self.iter_entries():
736
if entry == self.root:
890
for path, entry in entries():
738
891
other.add(entry.copy())
742
894
def __iter__(self):
743
895
return iter(self._byid)
746
897
def __len__(self):
747
898
"""Returns number of entries."""
748
899
return len(self._byid)
751
901
def iter_entries(self, from_dir=None):
752
902
"""Return (path, entry) pairs, in order by name."""
756
elif isinstance(from_dir, basestring):
757
from_dir = self._byid[from_dir]
759
kids = from_dir.children.items()
761
for name, ie in kids:
763
if ie.kind == 'directory':
764
for cn, cie in self.iter_entries(from_dir=ie.file_id):
765
yield os.path.join(name, cn), cie
904
if self.root is None:
908
elif isinstance(from_dir, basestring):
909
from_dir = self._byid[from_dir]
911
# unrolling the recursive called changed the time from
912
# 440ms/663ms (inline/total) to 116ms/116ms
913
children = from_dir.children.items()
915
children = collections.deque(children)
916
stack = [(u'', children)]
918
from_dir_relpath, children = stack[-1]
921
name, ie = children.popleft()
923
# we know that from_dir_relpath never ends in a slash
924
# and 'f' doesn't begin with one, we can do a string op, rather
925
# than the checks of pathjoin(), though this means that all paths
927
path = from_dir_relpath + '/' + name
931
if ie.kind != 'directory':
934
# But do this child first
935
new_children = ie.children.items()
937
new_children = collections.deque(new_children)
938
stack.append((path, new_children))
939
# Break out of inner loop, so that we start outer loop with child
942
# if we finished all children, pop it off the stack
945
def iter_entries_by_dir(self, from_dir=None):
946
"""Iterate over the entries in a directory first order.
948
This returns all entries for a directory before returning
949
the entries for children of a directory. This is not
950
lexicographically sorted order, and is a hybrid between
951
depth-first and breadth-first.
953
:return: This yields (path, entry) pairs
955
# TODO? Perhaps this should return the from_dir so that the root is
956
# yielded? or maybe an option?
958
if self.root is None:
962
elif isinstance(from_dir, basestring):
963
from_dir = self._byid[from_dir]
965
stack = [(u'', from_dir)]
967
cur_relpath, cur_dir = stack.pop()
970
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
972
child_relpath = cur_relpath + child_name
974
yield child_relpath, child_ie
976
if child_ie.kind == 'directory':
977
child_dirs.append((child_relpath+'/', child_ie))
978
stack.extend(reversed(child_dirs))
768
980
def entries(self):
769
981
"""Return list of (path, ie) for all entries except the root.
797
1008
for name, child_ie in kids:
798
child_path = os.path.join(parent_path, name)
1009
child_path = osutils.pathjoin(parent_path, name)
799
1010
descend(child_ie, child_path)
800
descend(self.root, '')
1011
descend(self.root, u'')
805
1014
def __contains__(self, file_id):
806
1015
"""True if this entry contains a file with given id.
808
1017
>>> inv = Inventory()
809
1018
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
810
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1019
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
811
1020
>>> '123' in inv
813
1022
>>> '456' in inv
816
return file_id in self._byid
1025
return (file_id in self._byid)
819
1027
def __getitem__(self, file_id):
820
1028
"""Return the entry for given file_id.
822
1030
>>> inv = Inventory()
823
1031
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
824
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1032
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
825
1033
>>> inv['123123'].name
829
1037
return self._byid[file_id]
830
1038
except KeyError:
832
raise BzrError("can't look up file_id None")
834
raise BzrError("file_id {%s} not in inventory" % file_id)
1039
# really we're passing an inventory, not a tree...
1040
raise errors.NoSuchId(self, file_id)
837
1042
def get_file_kind(self, file_id):
838
1043
return self._byid[file_id].kind
852
1056
if entry.file_id in self._byid:
853
1057
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
855
if entry.parent_id == ROOT_ID or entry.parent_id is None:
856
entry.parent_id = self.root.file_id
1059
if entry.parent_id is None:
1060
assert self.root is None and len(self._byid) == 0
1061
self._set_root(entry)
859
1064
parent = self._byid[entry.parent_id]
860
1065
except KeyError:
861
1066
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
863
if parent.children.has_key(entry.name):
1068
if entry.name in parent.children:
864
1069
raise BzrError("%s is already versioned" %
865
appendpath(self.id2path(parent.file_id), entry.name))
1070
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
867
1072
self._byid[entry.file_id] = entry
868
1073
parent.children[entry.name] = entry
872
def add_path(self, relpath, kind, file_id=None):
1076
def add_path(self, relpath, kind, file_id=None, parent_id=None):
873
1077
"""Add entry from a path.
875
1079
The immediate parent must already be versioned.
877
1081
Returns the new entry object."""
878
from bzrlib.branch import gen_file_id
880
parts = bzrlib.osutils.splitpath(relpath)
1083
parts = osutils.splitpath(relpath)
881
1085
if len(parts) == 0:
882
raise BzrError("cannot re-add root of inventory")
885
file_id = gen_file_id(relpath)
887
parent_path = parts[:-1]
888
parent_id = self.path2id(parent_path)
889
if parent_id == None:
890
raise NotVersionedError(path=parent_path)
891
if kind == 'directory':
892
ie = InventoryDirectory(file_id, parts[-1], parent_id)
894
ie = InventoryFile(file_id, parts[-1], parent_id)
895
elif kind == 'symlink':
896
ie = InventoryLink(file_id, parts[-1], parent_id)
1087
file_id = bzrlib.workingtree.gen_root_id()
1088
self.root = InventoryDirectory(file_id, '', None)
1089
self._byid = {self.root.file_id: self.root}
898
raise BzrError("unknown kind %r" % kind)
1092
parent_path = parts[:-1]
1093
parent_id = self.path2id(parent_path)
1094
if parent_id is None:
1095
raise errors.NotVersionedError(path=parent_path)
1096
ie = make_entry(kind, parts[-1], parent_id, file_id)
899
1097
return self.add(ie)
902
1099
def __delitem__(self, file_id):
903
1100
"""Remove entry by id.
905
1102
>>> inv = Inventory()
906
1103
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
907
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1104
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
908
1105
>>> '123' in inv
910
1107
>>> del inv['123']
936
1127
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
937
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1128
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
940
1131
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
941
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1132
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
945
1136
if not isinstance(other, Inventory):
946
1137
return NotImplemented
948
if len(self._byid) != len(other._byid):
949
# shortcut: obviously not the same
952
1139
return self._byid == other._byid
955
1141
def __ne__(self, other):
956
1142
return not self.__eq__(other)
959
1144
def __hash__(self):
960
1145
raise ValueError('not hashable')
1147
def _iter_file_id_parents(self, file_id):
1148
"""Yield the parents of file_id up to the root."""
1149
while file_id is not None:
1151
ie = self._byid[file_id]
1153
raise BzrError("file_id {%s} not found in inventory" % file_id)
1155
file_id = ie.parent_id
963
1157
def get_idpath(self, file_id):
964
1158
"""Return a list of file_ids for the path to an entry.
969
1163
root directory as depth 1.
972
while file_id != None:
974
ie = self._byid[file_id]
976
raise BzrError("file_id {%s} not found in inventory" % file_id)
977
p.insert(0, ie.file_id)
978
file_id = ie.parent_id
1166
for parent in self._iter_file_id_parents(file_id):
1167
p.insert(0, parent.file_id)
982
1170
def id2path(self, file_id):
983
"""Return as a list the path to file_id.
1171
"""Return as a string the path to file_id.
985
1173
>>> i = Inventory()
986
1174
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
987
1175
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
988
>>> print i.id2path('foo-id').replace(os.sep, '/')
1176
>>> print i.id2path('foo-id')
991
1179
# get all names, skipping root
992
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
return os.sep.join(p)
1180
return '/'.join(reversed(
1181
[parent.name for parent in
1182
self._iter_file_id_parents(file_id)][:-1]))
997
1184
def path2id(self, name):
998
1185
"""Walk down through directories to return entry of last component.
1024
1216
return parent.file_id
1027
1218
def has_filename(self, names):
1028
1219
return bool(self.path2id(names))
1031
1221
def has_id(self, file_id):
1032
return self._byid.has_key(file_id)
1222
return (file_id in self._byid)
1224
def remove_recursive_id(self, file_id):
1225
"""Remove file_id, and children, from the inventory.
1227
:param file_id: A file_id to remove.
1229
to_find_delete = [self._byid[file_id]]
1231
while to_find_delete:
1232
ie = to_find_delete.pop()
1233
to_delete.append(ie.file_id)
1234
if ie.kind == 'directory':
1235
to_find_delete.extend(ie.children.values())
1236
for file_id in reversed(to_delete):
1238
del self._byid[file_id]
1239
if ie.parent_id is not None:
1240
del self[ie.parent_id].children[ie.name]
1035
1242
def rename(self, file_id, new_parent_id, new_name):
1036
1243
"""Move a file within the inventory.
1061
1268
file_ie.name = new_name
1062
1269
file_ie.parent_id = new_parent_id
1271
def is_root(self, file_id):
1272
return self.root is not None and file_id == self.root.file_id
1275
def make_entry(kind, name, parent_id, file_id=None):
1276
"""Create an inventory entry.
1278
:param kind: the type of inventory entry to create.
1279
:param name: the basename of the entry.
1280
:param parent_id: the parent_id of the entry.
1281
:param file_id: the file_id to use. if None, one will be created.
1284
file_id = bzrlib.workingtree.gen_file_id(name)
1286
norm_name, can_access = osutils.normalized_filename(name)
1287
if norm_name != name:
1291
# TODO: jam 20060701 This would probably be more useful
1292
# if the error was raised with the full path
1293
raise errors.InvalidNormalization(name)
1295
if kind == 'directory':
1296
return InventoryDirectory(file_id, name, parent_id)
1297
elif kind == 'file':
1298
return InventoryFile(file_id, name, parent_id)
1299
elif kind == 'symlink':
1300
return InventoryLink(file_id, name, parent_id)
1302
raise BzrError("unknown kind %r" % kind)
1067
1305
_NAME_RE = None
1069
1307
def is_valid_name(name):
1070
1308
global _NAME_RE
1071
if _NAME_RE == None:
1309
if _NAME_RE is None:
1072
1310
_NAME_RE = re.compile(r'^[^/\\]+$')
1074
1312
return bool(_NAME_RE.match(name))