89
78
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
79
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
80
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
94
>>> for ix, j in enumerate(i.iter_entries()):
95
... print (j[0] == shouldbe[ix], j[1])
81
InventoryFile('2323', 'hello.c', parent_id='123')
82
>>> for j in i.iter_entries():
97
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
85
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
86
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
100
87
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
101
88
Traceback (most recent call last):
103
90
BzrError: inventory already contains entry with id {2323}
104
91
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
105
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
92
InventoryFile('2324', 'bye.c', parent_id='123')
106
93
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
107
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
94
InventoryDirectory('2325', 'wibble', parent_id='123')
108
95
>>> i.path2id('src/wibble')
112
99
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
113
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
100
InventoryFile('2326', 'wibble.c', parent_id='2325')
115
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
102
InventoryFile('2326', 'wibble.c', parent_id='2325')
116
103
>>> for path, entry in i.iter_entries():
104
... print path.replace('\\\\', '/') # for win32 os.sep
118
105
... assert i.path2id(path)
125
111
src/wibble/wibble.c
126
>>> i.id2path('2326')
112
>>> i.id2path('2326').replace('\\\\', '/')
127
113
'src/wibble/wibble.c'
130
# Constants returned by describe_change()
132
# TODO: These should probably move to some kind of FileChangeDescription
133
# class; that's like what's inside a TreeDelta but we want to be able to
134
# generate them just for one file at a time.
136
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,
140
124
def detect_changes(self, old_entry):
141
125
"""Return a (text_modified, meta_modified) from this to old_entry.
166
150
output_to, reverse=False):
167
151
"""Perform a diff between two entries of the same kind."""
169
def find_previous_heads(self, previous_inventories,
170
versioned_file_store,
173
"""Return the revisions and entries that directly precede this.
153
def find_previous_heads(self, previous_inventories, entry_weave):
154
"""Return the revisions and entries that directly preceed this.
175
156
Returned as a map from revision to inventory entry.
177
158
This is a map containing the file revisions in all parents
178
159
for which the file exists, and its revision is not a parent of
179
160
any other. If the file is new, the set will be empty.
181
:param versioned_file_store: A store where ancestry data on this
182
file id can be queried.
183
:param transaction: The transaction that queries to the versioned
184
file store should be completed under.
185
:param entry_vf: The entry versioned file, if its already available.
187
162
def get_ancestors(weave, entry):
188
return set(weave.get_ancestry(entry.revision))
189
# revision:ie mapping for each ie found in previous_inventories.
191
# revision:ie mapping with one revision for each head.
163
return set(map(weave.idx_to_name,
164
weave.inclusions([weave.lookup(entry.revision)])))
193
# revision: ancestor list for each head
194
166
head_ancestors = {}
195
# identify candidate head revision ids.
196
167
for inv in previous_inventories:
197
168
if self.file_id in inv:
198
169
ie = inv[self.file_id]
199
170
assert ie.file_id == self.file_id
200
if ie.revision in candidates:
201
# same revision value in two different inventories:
202
# correct possible inconsistencies:
203
# * there was a bug in revision updates with 'x' bit
171
if ie.revision in heads:
172
# fixup logic, there was a bug in revision updates.
173
# with x bit support.
206
if candidates[ie.revision].executable != ie.executable:
207
candidates[ie.revision].executable = False
175
if heads[ie.revision].executable != ie.executable:
176
heads[ie.revision].executable = False
208
177
ie.executable = False
209
178
except AttributeError:
211
# must now be the same.
212
assert candidates[ie.revision] == ie
180
assert heads[ie.revision] == ie
214
# add this revision as a candidate.
215
candidates[ie.revision] = ie
217
# common case optimisation
218
if len(candidates) == 1:
219
# if there is only one candidate revision found
220
# then we can opening the versioned file to access ancestry:
221
# there cannot be any ancestors to eliminate when there is
222
# only one revision available.
223
heads[ie.revision] = ie
226
# eliminate ancestors amongst the available candidates:
227
# heads are those that are not an ancestor of any other candidate
228
# - this provides convergence at a per-file level.
229
for ie in candidates.values():
230
# may be an ancestor of a known head:
231
already_present = 0 != len(
232
[head for head in heads
233
if ie.revision in head_ancestors[head]])
235
# an ancestor of an analyzed candidate.
237
# not an ancestor of a known head:
238
# load the versioned file for this file id if needed
240
entry_vf = versioned_file_store.get_weave_or_empty(
241
self.file_id, transaction)
242
ancestors = get_ancestors(entry_vf, ie)
243
# may knock something else out:
244
check_heads = list(heads.keys())
245
for head in check_heads:
246
if head in ancestors:
247
# this previously discovered 'head' is not
248
# really a head - its an ancestor of the newly
251
head_ancestors[ie.revision] = ancestors
252
heads[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
255
202
def get_tar_item(self, root, dp, now, tree):
256
203
"""Get a tarfile item and a file stream for its content."""
257
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
204
item = tarfile.TarInfo(os.path.join(root, dp))
258
205
# TODO: would be cool to actually set it to the timestamp of the
259
206
# revision it was last changed
320
267
This is a template method - implement _put_on_disk in subclasses.
322
fullpath = osutils.pathjoin(dest, dp)
269
fullpath = appendpath(dest, dp)
323
270
self._put_on_disk(fullpath, tree)
324
# mutter(" export {%s} kind %s to %s", self.file_id,
325
# self.kind, fullpath)
271
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
327
273
def _put_on_disk(self, fullpath, tree):
328
274
"""Put this entry onto disk at fullpath, from tree tree."""
329
275
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
331
277
def sorted_children(self):
332
return sorted(self.children.items())
278
l = self.children.items()
335
283
def versionable_kind(kind):
336
return (kind in ('file', 'directory', 'symlink'))
284
return kind in ('file', 'directory', 'symlink')
338
286
def check(self, checker, rev_id, inv, tree):
339
287
"""Check this inventory entry is intact.
341
289
This is a template method, override _check for kind specific
344
:param checker: Check object providing context for the checks;
345
can be used to find out what parts of the repository have already
347
:param rev_id: Revision id from which this InventoryEntry was loaded.
348
Not necessarily the last-changed revision for this file.
349
:param inv: Inventory from which the entry was loaded.
350
:param tree: RevisionTree for this entry.
352
if self.parent_id is not None:
292
if self.parent_id != None:
353
293
if not inv.has_id(self.parent_id):
354
294
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
355
295
% (self.parent_id, rev_id))
360
300
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
361
301
(self.kind, rev_id))
364
305
"""Clone this inventory entry."""
365
306
raise NotImplementedError
368
def describe_change(old_entry, new_entry):
369
"""Describe the change between old_entry and this.
371
This smells of being an InterInventoryEntry situation, but as its
372
the first one, we're making it a static method for now.
374
An entry with a different parent, or different name is considered
375
to be renamed. Reparenting is an internal detail.
376
Note that renaming the parent does not trigger a rename for the
379
# TODO: Perhaps return an object rather than just a string
380
if old_entry is new_entry:
381
# also the case of both being None
383
elif old_entry is None:
308
def _get_snapshot_change(self, previous_entries):
309
if len(previous_entries) > 1:
311
elif len(previous_entries) == 0:
385
elif new_entry is None:
387
text_modified, meta_modified = new_entry.detect_changes(old_entry)
388
if text_modified or meta_modified:
392
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
393
if old_entry.parent_id != new_entry.parent_id:
395
elif old_entry.name != new_entry.name:
399
if renamed and not modified:
400
return InventoryEntry.RENAMED
401
if modified and not renamed:
403
if modified and renamed:
404
return InventoryEntry.MODIFIED_AND_RENAMED
314
return 'modified/renamed/reparented'
407
316
def __repr__(self):
408
return ("%s(%r, %r, parent_id=%r, revision=%r)"
317
return ("%s(%r, %r, parent_id=%r)"
409
318
% (self.__class__.__name__,
415
323
def snapshot(self, revision, path, previous_entries,
416
work_tree, commit_builder):
324
work_tree, weave_store, transaction):
417
325
"""Make a snapshot of this entry which may or may not have changed.
419
327
This means that all its fields are populated, that it has its
420
328
text stored in the text store or weave.
422
# mutter('new parents of %s are %r', path, previous_entries)
330
mutter('new parents of %s are %r', path, previous_entries)
423
331
self._read_tree_state(path, work_tree)
424
# TODO: Where should we determine whether to reuse a
425
# previous revision id or create a new revision? 20060606
426
332
if len(previous_entries) == 1:
427
333
# cannot be unchanged unless there is only one parent file rev.
428
334
parent_ie = previous_entries.values()[0]
429
335
if self._unchanged(parent_ie):
430
# mutter("found unchanged entry")
336
mutter("found unchanged entry")
431
337
self.revision = parent_ie.revision
432
338
return "unchanged"
433
return self._snapshot_into_revision(revision, previous_entries,
434
work_tree, commit_builder)
436
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
438
"""Record this revision unconditionally into a store.
440
The entry's last-changed revision property (`revision`) is updated to
441
that of the new revision.
443
:param revision: id of the new revision that is being recorded.
445
:returns: String description of the commit (e.g. "merged", "modified"), etc.
447
# mutter('new revision {%s} for {%s}', revision, self.file_id)
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)
448
346
self.revision = revision
449
self._snapshot_text(previous_entries, work_tree, commit_builder)
347
change = self._get_snapshot_change(previous_entries)
348
self._snapshot_text(previous_entries, work_tree, weave_store,
451
def _snapshot_text(self, file_parents, work_tree, commit_builder):
352
def _snapshot_text(self, file_parents, work_tree, weave_store, transaction):
452
353
"""Record the 'text' of this entry, whatever form that takes.
454
355
This default implementation simply adds an empty text.
456
raise NotImplementedError(self._snapshot_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)
458
361
def __eq__(self, other):
459
362
if not isinstance(other, InventoryEntry):
502
405
# first requested, or preload them if they're already known
503
406
pass # nothing to do by default
505
def _forget_tree_state(self):
509
409
class RootEntry(InventoryEntry):
511
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
512
'text_id', 'parent_id', 'children', 'executable',
513
'revision', 'symlink_target']
515
411
def _check(self, checker, rev_id, tree):
516
412
"""See InventoryEntry._check"""
518
414
def __init__(self, file_id):
519
415
self.file_id = file_id
520
416
self.children = {}
521
self.kind = 'directory'
417
self.kind = 'root_directory'
522
418
self.parent_id = None
525
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
526
' Please use InventoryDirectory instead.',
527
DeprecationWarning, stacklevel=2)
529
421
def __eq__(self, other):
530
422
if not isinstance(other, RootEntry):
576
464
"""See InventoryEntry._put_on_disk."""
577
465
os.mkdir(fullpath)
579
def _snapshot_text(self, file_parents, work_tree, commit_builder):
580
"""See InventoryEntry._snapshot_text."""
581
commit_builder.modified_directory(self.file_id, file_parents)
584
468
class InventoryFile(InventoryEntry):
585
469
"""A file in an inventory."""
587
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
588
'text_id', 'parent_id', 'children', 'executable',
589
'revision', 'symlink_target']
591
def _check(self, checker, tree_revision_id, tree):
471
def _check(self, checker, rev_id, tree):
592
472
"""See InventoryEntry._check"""
593
t = (self.file_id, self.revision)
473
revision = self.revision
474
t = (self.file_id, revision)
594
475
if t in checker.checked_texts:
595
prev_sha = checker.checked_texts[t]
476
prev_sha = checker.checked_texts[t]
596
477
if prev_sha != self.text_sha1:
597
478
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
598
(self.file_id, tree_revision_id))
479
(self.file_id, rev_id))
600
481
checker.repeated_text_cnt += 1
603
if self.file_id not in checker.checked_weaves:
604
mutter('check weave {%s}', self.file_id)
605
w = tree.get_weave(self.file_id)
606
# Not passing a progress bar, because it creates a new
607
# progress, which overwrites the current progress,
608
# and doesn't look nice
610
checker.checked_weaves[self.file_id] = True
612
w = tree.get_weave(self.file_id)
614
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
615
checker.checked_text_cnt += 1
616
# We can't check the length, because Weave doesn't store that
617
# information, and the whole point of looking at the weave's
618
# sha1sum is that we don't have to extract the text.
619
if self.text_sha1 != w.get_sha1(self.revision):
620
raise BzrCheckError('text {%s} version {%s} wrong sha1'
621
% (self.file_id, self.revision))
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)
622
490
checker.checked_texts[t] = self.text_sha1
641
509
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
642
510
output_to, reverse=False):
643
511
"""See InventoryEntry._diff."""
645
from_text = tree.get_file(self.file_id).readlines()
647
to_text = to_tree.get_file(to_entry.file_id).readlines()
651
text_diff(from_label, from_text,
652
to_label, to_text, output_to)
654
text_diff(to_label, to_text,
655
from_label, from_text, output_to)
656
except errors.BinaryFile:
658
label_pair = (to_label, from_label)
660
label_pair = (from_label, to_label)
661
print >> output_to, "Binary files %s and %s differ" % label_pair
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)
663
524
def has_text(self):
664
525
"""See InventoryEntry.has_text."""
686
547
def _put_on_disk(self, fullpath, tree):
687
548
"""See InventoryEntry._put_on_disk."""
688
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
549
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
689
550
if tree.is_executable(self.file_id):
690
551
os.chmod(fullpath, 0755)
692
553
def _read_tree_state(self, path, work_tree):
693
554
"""See InventoryEntry._read_tree_state."""
694
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
695
# FIXME: 20050930 probe for the text size when getting sha1
696
# in _read_tree_state
697
self.executable = work_tree.is_executable(self.file_id, path=path)
700
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
701
% (self.__class__.__name__,
708
def _forget_tree_state(self):
709
self.text_sha1 = None
711
def _snapshot_text(self, file_parents, work_tree, commit_builder):
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):
712
559
"""See InventoryEntry._snapshot_text."""
713
def get_content_byte_lines():
714
return work_tree.get_file(self.file_id).readlines()
715
self.text_sha1, self.text_size = commit_builder.modified_file_text(
716
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
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))
718
579
def _unchanged(self, previous_ie):
719
580
"""See InventoryEntry._unchanged."""
732
593
class InventoryLink(InventoryEntry):
733
594
"""A file in an inventory."""
735
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
736
'text_id', 'parent_id', 'children', 'executable',
737
'revision', 'symlink_target']
596
__slots__ = ['symlink_target']
739
598
def _check(self, checker, rev_id, tree):
740
599
"""See InventoryEntry._check"""
741
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
600
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
742
601
raise BzrCheckError('symlink {%s} has text in revision {%s}'
743
602
% (self.file_id, rev_id))
744
if self.symlink_target is None:
603
if self.symlink_target == None:
745
604
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
746
605
% (self.file_id, rev_id))
854
705
May also look up by name:
856
707
>>> [x[0] for x in inv.iter_entries()]
858
709
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
859
710
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
860
Traceback (most recent call last):
861
BzrError: parent_id {TREE_ROOT} not in inventory
862
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
863
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
711
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
865
def __init__(self, root_id=ROOT_ID, revision_id=None):
713
def __init__(self, root_id=ROOT_ID):
866
714
"""Create or read an inventory.
868
716
If a working directory is specified, the inventory is read
872
720
The inventory is created with a default root directory, with
875
if root_id is not None:
876
self._set_root(InventoryDirectory(root_id, '', None))
880
self.revision_id = revision_id
882
def _set_root(self, ie):
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)
884
728
self._byid = {self.root.file_id: self.root}
887
# TODO: jam 20051218 Should copy also copy the revision_id?
888
entries = self.iter_entries()
889
other = Inventory(entries.next()[1].file_id)
732
other = Inventory(self.root.file_id)
890
733
# copy recursively so we know directories will be added before
891
734
# their children. There are more efficient ways than this...
892
for path, entry in entries():
735
for path, entry in self.iter_entries():
736
if entry == self.root:
893
738
other.add(entry.copy())
896
742
def __iter__(self):
897
743
return iter(self._byid)
899
746
def __len__(self):
900
747
"""Returns number of entries."""
901
748
return len(self._byid)
903
751
def iter_entries(self, from_dir=None):
904
752
"""Return (path, entry) pairs, in order by name."""
906
if self.root is None:
910
elif isinstance(from_dir, basestring):
911
from_dir = self._byid[from_dir]
913
# unrolling the recursive called changed the time from
914
# 440ms/663ms (inline/total) to 116ms/116ms
915
children = from_dir.children.items()
917
children = collections.deque(children)
918
stack = [(u'', children)]
920
from_dir_relpath, children = stack[-1]
923
name, ie = children.popleft()
925
# we know that from_dir_relpath never ends in a slash
926
# and 'f' doesn't begin with one, we can do a string op, rather
927
# than the checks of pathjoin(), though this means that all paths
929
path = from_dir_relpath + '/' + name
933
if ie.kind != 'directory':
936
# But do this child first
937
new_children = ie.children.items()
939
new_children = collections.deque(new_children)
940
stack.append((path, new_children))
941
# Break out of inner loop, so that we start outer loop with child
944
# if we finished all children, pop it off the stack
947
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
948
"""Iterate over the entries in a directory first order.
950
This returns all entries for a directory before returning
951
the entries for children of a directory. This is not
952
lexicographically sorted order, and is a hybrid between
953
depth-first and breadth-first.
955
:return: This yields (path, entry) pairs
957
# TODO? Perhaps this should return the from_dir so that the root is
958
# yielded? or maybe an option?
960
if self.root is None:
962
# Optimize a common case
963
if specific_file_ids is not None and len(specific_file_ids) == 1:
964
file_id = list(specific_file_ids)[0]
966
yield self.id2path(file_id), self[file_id]
969
if (specific_file_ids is None or
970
self.root.file_id in specific_file_ids):
972
elif isinstance(from_dir, basestring):
973
from_dir = self._byid[from_dir]
975
if specific_file_ids is not None:
977
def add_ancestors(file_id):
978
if file_id not in self:
980
parent_id = self[file_id].parent_id
981
if parent_id is None:
983
if parent_id not in parents:
984
parents.add(parent_id)
985
add_ancestors(parent_id)
986
for file_id in specific_file_ids:
987
add_ancestors(file_id)
991
stack = [(u'', from_dir)]
993
cur_relpath, cur_dir = stack.pop()
996
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
998
child_relpath = cur_relpath + child_name
1000
if (specific_file_ids is None or
1001
child_ie.file_id in specific_file_ids):
1002
yield child_relpath, child_ie
1004
if child_ie.kind == 'directory':
1005
if parents is None or child_ie.file_id in parents:
1006
child_dirs.append((child_relpath+'/', child_ie))
1007
stack.extend(reversed(child_dirs))
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
1009
768
def entries(self):
1010
769
"""Return list of (path, ie) for all entries except the root.
1037
797
for name, child_ie in kids:
1038
child_path = osutils.pathjoin(parent_path, name)
798
child_path = os.path.join(parent_path, name)
1039
799
descend(child_ie, child_path)
1040
descend(self.root, u'')
800
descend(self.root, '')
1043
805
def __contains__(self, file_id):
1044
806
"""True if this entry contains a file with given id.
1046
808
>>> inv = Inventory()
1047
809
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1048
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
810
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1049
811
>>> '123' in inv
1051
813
>>> '456' in inv
1054
return (file_id in self._byid)
816
return file_id in self._byid
1056
819
def __getitem__(self, file_id):
1057
820
"""Return the entry for given file_id.
1059
822
>>> inv = Inventory()
1060
823
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1061
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
824
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1062
825
>>> inv['123123'].name
1066
829
return self._byid[file_id]
1067
830
except KeyError:
1068
# really we're passing an inventory, not a tree...
1069
raise errors.NoSuchId(self, file_id)
832
raise BzrError("can't look up file_id None")
834
raise BzrError("file_id {%s} not in inventory" % file_id)
1071
837
def get_file_kind(self, file_id):
1072
838
return self._byid[file_id].kind
1085
852
if entry.file_id in self._byid:
1086
853
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1088
if entry.parent_id is None:
1089
assert self.root is None and len(self._byid) == 0
1090
self._set_root(entry)
855
if entry.parent_id == ROOT_ID or entry.parent_id is None:
856
entry.parent_id = self.root.file_id
1093
859
parent = self._byid[entry.parent_id]
1094
860
except KeyError:
1095
861
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1097
if entry.name in parent.children:
863
if parent.children.has_key(entry.name):
1098
864
raise BzrError("%s is already versioned" %
1099
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
865
appendpath(self.id2path(parent.file_id), entry.name))
1101
867
self._byid[entry.file_id] = entry
1102
868
parent.children[entry.name] = entry
1105
def add_path(self, relpath, kind, file_id=None, parent_id=None):
872
def add_path(self, relpath, kind, file_id=None):
1106
873
"""Add entry from a path.
1108
875
The immediate parent must already be versioned.
1110
877
Returns the new entry object."""
878
from bzrlib.branch import gen_file_id
1112
parts = osutils.splitpath(relpath)
880
parts = bzrlib.osutils.splitpath(relpath)
1114
881
if len(parts) == 0:
1116
file_id = generate_ids.gen_root_id()
1117
self.root = InventoryDirectory(file_id, '', None)
1118
self._byid = {self.root.file_id: self.root}
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)
1121
parent_path = parts[:-1]
1122
parent_id = self.path2id(parent_path)
1123
if parent_id is None:
1124
raise errors.NotVersionedError(path=parent_path)
1125
ie = make_entry(kind, parts[-1], parent_id, file_id)
898
raise BzrError("unknown kind %r" % kind)
1126
899
return self.add(ie)
1128
902
def __delitem__(self, file_id):
1129
903
"""Remove entry by id.
1131
905
>>> inv = Inventory()
1132
906
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1133
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
907
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1134
908
>>> '123' in inv
1136
910
>>> del inv['123']
1156
936
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1157
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
937
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1160
940
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1161
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
941
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1165
945
if not isinstance(other, Inventory):
1166
946
return NotImplemented
948
if len(self._byid) != len(other._byid):
949
# shortcut: obviously not the same
1168
952
return self._byid == other._byid
1170
955
def __ne__(self, other):
1171
956
return not self.__eq__(other)
1173
959
def __hash__(self):
1174
960
raise ValueError('not hashable')
1176
def _iter_file_id_parents(self, file_id):
1177
"""Yield the parents of file_id up to the root."""
1178
while file_id is not None:
1180
ie = self._byid[file_id]
1182
raise BzrError("file_id {%s} not found in inventory" % file_id)
1184
file_id = ie.parent_id
1186
963
def get_idpath(self, file_id):
1187
964
"""Return a list of file_ids for the path to an entry.
1192
969
root directory as depth 1.
1195
for parent in self._iter_file_id_parents(file_id):
1196
p.insert(0, parent.file_id)
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
1199
982
def id2path(self, file_id):
1200
"""Return as a string the path to file_id.
983
"""Return as a list the path to file_id.
1202
985
>>> i = Inventory()
1203
986
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1204
987
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1205
>>> print i.id2path('foo-id')
988
>>> print i.id2path('foo-id').replace(os.sep, '/')
1208
991
# get all names, skipping root
1209
return '/'.join(reversed(
1210
[parent.name for parent in
1211
self._iter_file_id_parents(file_id)][:-1]))
992
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
993
return os.sep.join(p)
1213
997
def path2id(self, name):
1214
998
"""Walk down through directories to return entry of last component.
1245
1024
return parent.file_id
1247
1027
def has_filename(self, names):
1248
1028
return bool(self.path2id(names))
1250
1031
def has_id(self, file_id):
1251
return (file_id in self._byid)
1032
return self._byid.has_key(file_id)
1253
def remove_recursive_id(self, file_id):
1254
"""Remove file_id, and children, from the inventory.
1256
:param file_id: A file_id to remove.
1258
to_find_delete = [self._byid[file_id]]
1260
while to_find_delete:
1261
ie = to_find_delete.pop()
1262
to_delete.append(ie.file_id)
1263
if ie.kind == 'directory':
1264
to_find_delete.extend(ie.children.values())
1265
for file_id in reversed(to_delete):
1267
del self._byid[file_id]
1268
if ie.parent_id is not None:
1269
del self[ie.parent_id].children[ie.name]
1271
1035
def rename(self, file_id, new_parent_id, new_name):
1272
1036
"""Move a file within the inventory.
1297
1061
file_ie.name = new_name
1298
1062
file_ie.parent_id = new_parent_id
1300
def is_root(self, file_id):
1301
return self.root is not None and file_id == self.root.file_id
1304
def make_entry(kind, name, parent_id, file_id=None):
1305
"""Create an inventory entry.
1307
:param kind: the type of inventory entry to create.
1308
:param name: the basename of the entry.
1309
:param parent_id: the parent_id of the entry.
1310
:param file_id: the file_id to use. if None, one will be created.
1313
file_id = generate_ids.gen_file_id(name)
1315
norm_name, can_access = osutils.normalized_filename(name)
1316
if norm_name != name:
1320
# TODO: jam 20060701 This would probably be more useful
1321
# if the error was raised with the full path
1322
raise errors.InvalidNormalization(name)
1324
if kind == 'directory':
1325
return InventoryDirectory(file_id, name, parent_id)
1326
elif kind == 'file':
1327
return InventoryFile(file_id, name, parent_id)
1328
elif kind == 'symlink':
1329
return InventoryLink(file_id, name, parent_id)
1331
raise BzrError("unknown kind %r" % kind)
1334
1067
_NAME_RE = None
1336
1069
def is_valid_name(name):
1337
1070
global _NAME_RE
1338
if _NAME_RE is None:
1071
if _NAME_RE == None:
1339
1072
_NAME_RE = re.compile(r'^[^/\\]+$')
1341
1074
return bool(_NAME_RE.match(name))