1
# (C) 2005 Canonical Ltd
1
# Copyright (C) 2005, 2006 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.
18
22
# TODO: Maybe also keep the full path of the entry, and the children?
19
23
# But those depend on its position within a particular inventory, and
75
79
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
80
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
77
81
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
82
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
83
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
84
>>> for ix, j in enumerate(i.iter_entries()):
85
... print (j[0] == shouldbe[ix], j[1])
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
87
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
88
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
84
89
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
90
Traceback (most recent call last):
87
92
BzrError: inventory already contains entry with id {2323}
88
93
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
89
InventoryFile('2324', 'bye.c', parent_id='123')
94
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
90
95
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
91
InventoryDirectory('2325', 'wibble', parent_id='123')
96
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
92
97
>>> i.path2id('src/wibble')
96
101
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
102
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
104
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
100
105
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
102
107
... assert i.path2id(path)
108
113
src/wibble/wibble.c
109
>>> i.id2path('2326').replace('\\\\', '/')
114
>>> i.id2path('2326')
110
115
'src/wibble/wibble.c'
118
# Constants returned by describe_change()
120
# TODO: These should probably move to some kind of FileChangeDescription
121
# class; that's like what's inside a TreeDelta but we want to be able to
122
# generate them just for one file at a time.
124
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):
118
weave_store.add_text(self.file_id, self.revision, new_lines, parents)
120
128
def detect_changes(self, old_entry):
121
129
"""Return a (text_modified, meta_modified) from this to old_entry.
146
154
output_to, reverse=False):
147
155
"""Perform a diff between two entries of the same kind."""
149
def find_previous_heads(self, previous_inventories, entry_weave):
150
"""Return the revisions and entries that directly preceed this.
157
def find_previous_heads(self, previous_inventories,
158
versioned_file_store,
161
"""Return the revisions and entries that directly precede this.
152
163
Returned as a map from revision to inventory entry.
154
165
This is a map containing the file revisions in all parents
155
166
for which the file exists, and its revision is not a parent of
156
167
any other. If the file is new, the set will be empty.
169
:param versioned_file_store: A store where ancestry data on this
170
file id can be queried.
171
:param transaction: The transaction that queries to the versioned
172
file store should be completed under.
173
:param entry_vf: The entry versioned file, if its already available.
158
175
def get_ancestors(weave, entry):
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
176
return set(weave.get_ancestry(entry.revision))
177
# revision:ie mapping for each ie found in previous_inventories.
179
# revision:ie mapping with one revision for each head.
181
# revision: ancestor list for each head
162
182
head_ancestors = {}
183
# identify candidate head revision ids.
163
184
for inv in previous_inventories:
164
185
if self.file_id in inv:
165
186
ie = inv[self.file_id]
166
187
assert ie.file_id == self.file_id
167
if ie.revision in heads:
168
assert heads[ie.revision] == ie
188
if ie.revision in candidates:
189
# same revision value in two different inventories:
190
# correct possible inconsistencies:
191
# * there was a bug in revision updates with 'x' bit
194
if candidates[ie.revision].executable != ie.executable:
195
candidates[ie.revision].executable = False
196
ie.executable = False
197
except AttributeError:
199
# must now be the same.
200
assert candidates[ie.revision] == ie
170
# may want to add it.
171
# may already be covered:
172
already_present = 0 != len(
173
[head for head in heads
174
if ie.revision in head_ancestors[head]])
176
# an ancestor of a known head.
179
ancestors = get_ancestors(entry_weave, ie)
180
# may knock something else out:
181
check_heads = list(heads.keys())
182
for head in check_heads:
183
if head in ancestors:
184
# this head is not really a head
186
head_ancestors[ie.revision] = ancestors
187
heads[ie.revision] = ie
202
# add this revision as a candidate.
203
candidates[ie.revision] = ie
205
# common case optimisation
206
if len(candidates) == 1:
207
# if there is only one candidate revision found
208
# then we can opening the versioned file to access ancestry:
209
# there cannot be any ancestors to eliminate when there is
210
# only one revision available.
211
heads[ie.revision] = ie
214
# eliminate ancestors amongst the available candidates:
215
# heads are those that are not an ancestor of any other candidate
216
# - this provides convergence at a per-file level.
217
for ie in candidates.values():
218
# may be an ancestor of a known head:
219
already_present = 0 != len(
220
[head for head in heads
221
if ie.revision in head_ancestors[head]])
223
# an ancestor of an analyzed candidate.
225
# not an ancestor of a known head:
226
# load the versioned file for this file id if needed
228
entry_vf = versioned_file_store.get_weave_or_empty(
229
self.file_id, transaction)
230
ancestors = get_ancestors(entry_vf, ie)
231
# may knock something else out:
232
check_heads = list(heads.keys())
233
for head in check_heads:
234
if head in ancestors:
235
# this previously discovered 'head' is not
236
# really a head - its an ancestor of the newly
239
head_ancestors[ie.revision] = ancestors
240
heads[ie.revision] = ie
190
243
def get_tar_item(self, root, dp, now, tree):
191
244
"""Get a tarfile item and a file stream for its content."""
192
item = tarfile.TarInfo(os.path.join(root, dp))
245
item = tarfile.TarInfo(pathjoin(root, dp))
193
246
# TODO: would be cool to actually set it to the timestamp of the
194
247
# revision it was last changed
256
308
This is a template method - implement _put_on_disk in subclasses.
258
fullpath = appendpath(dest, dp)
310
fullpath = pathjoin(dest, dp)
259
311
self._put_on_disk(fullpath, tree)
260
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
312
mutter(" export {%s} kind %s to %s", self.file_id,
262
315
def _put_on_disk(self, fullpath, tree):
263
316
"""Put this entry onto disk at fullpath, from tree tree."""
264
317
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
266
319
def sorted_children(self):
267
l = self.children.items()
320
return sorted(self.children.items())
272
323
def versionable_kind(kind):
278
329
This is a template method, override _check for kind specific
332
:param checker: Check object providing context for the checks;
333
can be used to find out what parts of the repository have already
335
:param rev_id: Revision id from which this InventoryEntry was loaded.
336
Not necessarily the last-changed revision for this file.
337
:param inv: Inventory from which the entry was loaded.
338
:param tree: RevisionTree for this entry.
281
if self.parent_id != None:
340
if self.parent_id is not None:
282
341
if not inv.has_id(self.parent_id):
283
342
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
284
343
% (self.parent_id, rev_id))
289
348
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
290
349
(self.kind, rev_id))
294
352
"""Clone this inventory entry."""
295
353
raise NotImplementedError
297
def _get_snapshot_change(self, previous_entries):
298
if len(previous_entries) > 1:
300
elif len(previous_entries) == 0:
356
def describe_change(old_entry, new_entry):
357
"""Describe the change between old_entry and this.
359
This smells of being an InterInventoryEntry situation, but as its
360
the first one, we're making it a static method for now.
362
An entry with a different parent, or different name is considered
363
to be renamed. Reparenting is an internal detail.
364
Note that renaming the parent does not trigger a rename for the
367
# TODO: Perhaps return an object rather than just a string
368
if old_entry is new_entry:
369
# also the case of both being None
371
elif old_entry is None:
303
return 'modified/renamed/reparented'
373
elif new_entry is None:
375
text_modified, meta_modified = new_entry.detect_changes(old_entry)
376
if text_modified or meta_modified:
380
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
381
if old_entry.parent_id != new_entry.parent_id:
383
elif old_entry.name != new_entry.name:
387
if renamed and not modified:
388
return InventoryEntry.RENAMED
389
if modified and not renamed:
391
if modified and renamed:
392
return InventoryEntry.MODIFIED_AND_RENAMED
305
395
def __repr__(self):
306
return ("%s(%r, %r, parent_id=%r)"
396
return ("%s(%r, %r, parent_id=%r, revision=%r)"
307
397
% (self.__class__.__name__,
312
403
def snapshot(self, revision, path, previous_entries,
313
work_tree, weave_store):
404
work_tree, commit_builder):
314
405
"""Make a snapshot of this entry which may or may not have changed.
316
407
This means that all its fields are populated, that it has its
325
418
mutter("found unchanged entry")
326
419
self.revision = parent_ie.revision
327
420
return "unchanged"
328
return self.snapshot_revision(revision, previous_entries,
329
work_tree, weave_store)
331
def snapshot_revision(self, revision, previous_entries, work_tree,
333
"""Record this revision unconditionally."""
334
mutter('new revision for {%s}', self.file_id)
421
return self._snapshot_into_revision(revision, previous_entries,
422
work_tree, commit_builder)
424
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
426
"""Record this revision unconditionally into a store.
428
The entry's last-changed revision property (`revision`) is updated to
429
that of the new revision.
431
:param revision: id of the new revision that is being recorded.
433
:returns: String description of the commit (e.g. "merged", "modified"), etc.
435
mutter('new revision {%s} for {%s}', revision, self.file_id)
335
436
self.revision = revision
336
change = self._get_snapshot_change(previous_entries)
337
self._snapshot_text(previous_entries, work_tree, weave_store)
437
self._snapshot_text(previous_entries, work_tree, commit_builder)
340
def _snapshot_text(self, file_parents, work_tree, weave_store):
439
def _snapshot_text(self, file_parents, work_tree, commit_builder):
341
440
"""Record the 'text' of this entry, whatever form that takes.
343
442
This default implementation simply adds an empty text.
345
mutter('storing file {%s} in revision {%s}',
346
self.file_id, self.revision)
347
self._add_text_to_weave([], file_parents, weave_store)
444
raise NotImplementedError(self._snapshot_text)
349
446
def __eq__(self, other):
350
447
if not isinstance(other, InventoryEntry):
388
485
Note that this should be modified to be a noop on virtual trees
389
486
as all entries created there are prepopulated.
488
# TODO: Rather than running this manually, we should check the
489
# working sha1 and other expensive properties when they're
490
# first requested, or preload them if they're already known
491
pass # nothing to do by default
493
def _forget_tree_state(self):
393
497
class RootEntry(InventoryEntry):
499
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
500
'text_id', 'parent_id', 'children', 'executable',
501
'revision', 'symlink_target']
395
503
def _check(self, checker, rev_id, tree):
396
504
"""See InventoryEntry._check"""
413
522
class InventoryDirectory(InventoryEntry):
414
523
"""A directory in an inventory."""
525
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
526
'text_id', 'parent_id', 'children', 'executable',
527
'revision', 'symlink_target']
416
529
def _check(self, checker, rev_id, tree):
417
530
"""See InventoryEntry._check"""
418
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
531
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
419
532
raise BzrCheckError('directory {%s} has text in revision {%s}'
420
533
% (self.file_id, rev_id))
448
561
"""See InventoryEntry._put_on_disk."""
449
562
os.mkdir(fullpath)
564
def _snapshot_text(self, file_parents, work_tree, commit_builder):
565
"""See InventoryEntry._snapshot_text."""
566
commit_builder.modified_directory(self.file_id, file_parents)
452
569
class InventoryFile(InventoryEntry):
453
570
"""A file in an inventory."""
455
def _check(self, checker, rev_id, tree):
572
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
573
'text_id', 'parent_id', 'children', 'executable',
574
'revision', 'symlink_target']
576
def _check(self, checker, tree_revision_id, tree):
456
577
"""See InventoryEntry._check"""
457
revision = self.revision
458
t = (self.file_id, revision)
578
t = (self.file_id, self.revision)
459
579
if t in checker.checked_texts:
460
prev_sha = checker.checked_texts[t]
580
prev_sha = checker.checked_texts[t]
461
581
if prev_sha != self.text_sha1:
462
582
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
463
(self.file_id, rev_id))
583
(self.file_id, tree_revision_id))
465
585
checker.repeated_text_cnt += 1
467
mutter('check version {%s} of {%s}', rev_id, self.file_id)
468
file_lines = tree.get_file_lines(self.file_id)
469
checker.checked_text_cnt += 1
470
if self.text_size != sum(map(len, file_lines)):
471
raise BzrCheckError('text {%s} wrong size' % self.text_id)
472
if self.text_sha1 != sha_strings(file_lines):
473
raise BzrCheckError('text {%s} wrong sha1' % self.text_id)
588
if self.file_id not in checker.checked_weaves:
589
mutter('check weave {%s}', self.file_id)
590
w = tree.get_weave(self.file_id)
591
# Not passing a progress bar, because it creates a new
592
# progress, which overwrites the current progress,
593
# and doesn't look nice
595
checker.checked_weaves[self.file_id] = True
597
w = tree.get_weave(self.file_id)
599
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
600
checker.checked_text_cnt += 1
601
# We can't check the length, because Weave doesn't store that
602
# information, and the whole point of looking at the weave's
603
# sha1sum is that we don't have to extract the text.
604
if self.text_sha1 != w.get_sha1(self.revision):
605
raise BzrCheckError('text {%s} version {%s} wrong sha1'
606
% (self.file_id, self.revision))
474
607
checker.checked_texts[t] = self.text_sha1
493
626
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
494
627
output_to, reverse=False):
495
628
"""See InventoryEntry._diff."""
496
from_text = tree.get_file(self.file_id).readlines()
498
to_text = to_tree.get_file(to_entry.file_id).readlines()
502
text_diff(from_label, from_text,
503
to_label, to_text, output_to)
505
text_diff(to_label, to_text,
506
from_label, from_text, output_to)
630
from_text = tree.get_file(self.file_id).readlines()
632
to_text = to_tree.get_file(to_entry.file_id).readlines()
636
text_diff(from_label, from_text,
637
to_label, to_text, output_to)
639
text_diff(to_label, to_text,
640
from_label, from_text, output_to)
643
label_pair = (to_label, from_label)
645
label_pair = (from_label, to_label)
646
print >> output_to, "Binary files %s and %s differ" % label_pair
508
648
def has_text(self):
509
649
"""See InventoryEntry.has_text."""
537
677
def _read_tree_state(self, path, work_tree):
538
678
"""See InventoryEntry._read_tree_state."""
539
self.text_sha1 = work_tree.get_file_sha1(self.file_id)
540
self.executable = work_tree.is_executable(self.file_id)
542
def _snapshot_text(self, file_parents, work_tree, weave_store):
679
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
680
# FIXME: 20050930 probe for the text size when getting sha1
681
# in _read_tree_state
682
self.executable = work_tree.is_executable(self.file_id, path=path)
685
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
686
% (self.__class__.__name__,
693
def _forget_tree_state(self):
694
self.text_sha1 = None
695
self.executable = None
697
def _snapshot_text(self, file_parents, work_tree, commit_builder):
543
698
"""See InventoryEntry._snapshot_text."""
544
mutter('storing file {%s} in revision {%s}',
545
self.file_id, self.revision)
546
# special case to avoid diffing on renames or
548
if (len(file_parents) == 1
549
and self.text_sha1 == file_parents.values()[0].text_sha1
550
and self.text_size == file_parents.values()[0].text_size):
551
previous_ie = file_parents.values()[0]
552
weave_store.add_identical_text(
553
self.file_id, previous_ie.revision,
554
self.revision, file_parents)
556
new_lines = work_tree.get_file(self.file_id).readlines()
557
self._add_text_to_weave(new_lines, file_parents, weave_store)
558
self.text_sha1 = sha_strings(new_lines)
559
self.text_size = sum(map(len, new_lines))
699
def get_content_byte_lines():
700
return work_tree.get_file(self.file_id).readlines()
701
self.text_sha1, self.text_size = commit_builder.modified_file_text(
702
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
562
704
def _unchanged(self, previous_ie):
563
705
"""See InventoryEntry._unchanged."""
568
710
# FIXME: 20050930 probe for the text size when getting sha1
569
711
# in _read_tree_state
570
712
self.text_size = previous_ie.text_size
713
if self.executable != previous_ie.executable:
571
715
return compatible
574
718
class InventoryLink(InventoryEntry):
575
719
"""A file in an inventory."""
577
__slots__ = ['symlink_target']
721
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
722
'text_id', 'parent_id', 'children', 'executable',
723
'revision', 'symlink_target']
579
725
def _check(self, checker, rev_id, tree):
580
726
"""See InventoryEntry._check"""
581
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
727
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
582
728
raise BzrCheckError('symlink {%s} has text in revision {%s}'
583
729
% (self.file_id, rev_id))
584
if self.symlink_target == None:
730
if self.symlink_target is None:
585
731
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
586
732
% (self.file_id, rev_id))
686
840
May also look up by name:
688
842
>>> [x[0] for x in inv.iter_entries()]
690
844
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
691
845
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
692
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
846
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
694
def __init__(self, root_id=ROOT_ID):
848
def __init__(self, root_id=ROOT_ID, revision_id=None):
695
849
"""Create or read an inventory.
697
851
If a working directory is specified, the inventory is read
701
855
The inventory is created with a default root directory, with
704
# We are letting Branch.initialize() create a unique inventory
858
# We are letting Branch.create() create a unique inventory
705
859
# root id. Rather than generating a random one here.
706
860
#if root_id is None:
707
861
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
708
862
self.root = RootEntry(root_id)
863
# FIXME: this isn't ever used, changing it to self.revision may break
864
# things. TODO make everything use self.revision_id
865
self.revision_id = revision_id
709
866
self._byid = {self.root.file_id: self.root}
869
# TODO: jam 20051218 Should copy also copy the revision_id?
713
870
other = Inventory(self.root.file_id)
714
871
# copy recursively so we know directories will be added before
715
872
# their children. There are more efficient ways than this...
719
876
other.add(entry.copy())
723
879
def __iter__(self):
724
880
return iter(self._byid)
727
882
def __len__(self):
728
883
"""Returns number of entries."""
729
884
return len(self._byid)
732
886
def iter_entries(self, from_dir=None):
733
887
"""Return (path, entry) pairs, in order by name."""
737
elif isinstance(from_dir, basestring):
738
from_dir = self._byid[from_dir]
740
kids = from_dir.children.items()
742
for name, ie in kids:
744
if ie.kind == 'directory':
745
for cn, cie in self.iter_entries(from_dir=ie.file_id):
746
yield os.path.join(name, cn), cie
891
elif isinstance(from_dir, basestring):
892
from_dir = self._byid[from_dir]
894
# unrolling the recursive called changed the time from
895
# 440ms/663ms (inline/total) to 116ms/116ms
896
children = from_dir.children.items()
898
children = collections.deque(children)
899
stack = [(u'', children)]
901
from_dir_relpath, children = stack[-1]
904
name, ie = children.popleft()
906
# we know that from_dir_relpath never ends in a slash
907
# and 'f' doesn't begin with one, we can do a string op, rather
908
# than the checks of pathjoin(), though this means that all paths
910
path = from_dir_relpath + '/' + name
914
if ie.kind != 'directory':
917
# But do this child first
918
new_children = ie.children.items()
920
new_children = collections.deque(new_children)
921
stack.append((path, new_children))
922
# Break out of inner loop, so that we start outer loop with child
925
# if we finished all children, pop it off the stack
928
def iter_entries_by_dir(self, from_dir=None):
929
"""Iterate over the entries in a directory first order.
931
This returns all entries for a directory before returning
932
the entries for children of a directory. This is not
933
lexicographically sorted order, and is a hybrid between
934
depth-first and breadth-first.
936
:return: This yields (path, entry) pairs
938
# TODO? Perhaps this should return the from_dir so that the root is
939
# yielded? or maybe an option?
943
elif isinstance(from_dir, basestring):
944
from_dir = self._byid[from_dir]
946
stack = [(u'', from_dir)]
948
cur_relpath, cur_dir = stack.pop()
951
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
953
child_relpath = cur_relpath + child_name
955
yield child_relpath, child_ie
957
if child_ie.kind == 'directory':
958
child_dirs.append((child_relpath+'/', child_ie))
959
stack.extend(reversed(child_dirs))
749
961
def entries(self):
750
962
"""Return list of (path, ie) for all entries except the root.
778
989
for name, child_ie in kids:
779
child_path = os.path.join(parent_path, name)
990
child_path = pathjoin(parent_path, name)
780
991
descend(child_ie, child_path)
781
descend(self.root, '')
992
descend(self.root, u'')
786
995
def __contains__(self, file_id):
787
996
"""True if this entry contains a file with given id.
789
998
>>> inv = Inventory()
790
999
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
791
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1000
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
792
1001
>>> '123' in inv
794
1003
>>> '456' in inv
797
1006
return file_id in self._byid
800
1008
def __getitem__(self, file_id):
801
1009
"""Return the entry for given file_id.
803
1011
>>> inv = Inventory()
804
1012
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
805
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1013
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
806
1014
>>> inv['123123'].name
810
1018
return self._byid[file_id]
811
1019
except KeyError:
813
1021
raise BzrError("can't look up file_id None")
815
1023
raise BzrError("file_id {%s} not in inventory" % file_id)
818
1025
def get_file_kind(self, file_id):
819
1026
return self._byid[file_id].kind
821
1028
def get_child(self, parent_id, filename):
822
1029
return self[parent_id].children.get(filename)
825
1031
def add(self, entry):
826
1032
"""Add entry to inventory.
841
1047
except KeyError:
842
1048
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
844
if parent.children.has_key(entry.name):
1050
if entry.name in parent.children:
845
1051
raise BzrError("%s is already versioned" %
846
appendpath(self.id2path(parent.file_id), entry.name))
1052
pathjoin(self.id2path(parent.file_id), entry.name))
848
1054
self._byid[entry.file_id] = entry
849
1055
parent.children[entry.name] = entry
853
def add_path(self, relpath, kind, file_id=None):
1058
def add_path(self, relpath, kind, file_id=None, parent_id=None):
854
1059
"""Add entry from a path.
856
1061
The immediate parent must already be versioned.
858
1063
Returns the new entry object."""
859
from bzrlib.branch import gen_file_id
861
1065
parts = bzrlib.osutils.splitpath(relpath)
862
1067
if len(parts) == 0:
863
raise BzrError("cannot re-add root of inventory")
866
file_id = gen_file_id(relpath)
868
parent_path = parts[:-1]
869
parent_id = self.path2id(parent_path)
870
if parent_id == None:
871
raise NotVersionedError(parent_path)
873
if kind == 'directory':
874
ie = InventoryDirectory(file_id, parts[-1], parent_id)
876
ie = InventoryFile(file_id, parts[-1], parent_id)
877
elif kind == 'symlink':
878
ie = InventoryLink(file_id, parts[-1], parent_id)
1069
file_id = bzrlib.workingtree.gen_root_id()
1070
self.root = RootEntry(file_id)
1071
self._byid = {self.root.file_id: self.root}
880
raise BzrError("unknown kind %r" % kind)
1074
parent_path = parts[:-1]
1075
parent_id = self.path2id(parent_path)
1076
if parent_id is None:
1077
raise NotVersionedError(path=parent_path)
1078
ie = make_entry(kind, parts[-1], parent_id, file_id)
881
1079
return self.add(ie)
884
1081
def __delitem__(self, file_id):
885
1082
"""Remove entry by id.
887
1084
>>> inv = Inventory()
888
1085
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
889
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1086
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
890
1087
>>> '123' in inv
892
1089
>>> del inv['123']
918
1109
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
919
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1110
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
922
1113
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
923
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1114
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
927
1118
if not isinstance(other, Inventory):
928
1119
return NotImplemented
930
if len(self._byid) != len(other._byid):
931
# shortcut: obviously not the same
934
1121
return self._byid == other._byid
937
1123
def __ne__(self, other):
938
1124
return not self.__eq__(other)
941
1126
def __hash__(self):
942
1127
raise ValueError('not hashable')
1129
def _iter_file_id_parents(self, file_id):
1130
"""Yield the parents of file_id up to the root."""
1131
while file_id is not None:
1133
ie = self._byid[file_id]
1135
raise BzrError("file_id {%s} not found in inventory" % file_id)
1137
file_id = ie.parent_id
945
1139
def get_idpath(self, file_id):
946
1140
"""Return a list of file_ids for the path to an entry.
951
1145
root directory as depth 1.
954
while file_id != None:
956
ie = self._byid[file_id]
958
raise BzrError("file_id {%s} not found in inventory" % file_id)
959
p.insert(0, ie.file_id)
960
file_id = ie.parent_id
1148
for parent in self._iter_file_id_parents(file_id):
1149
p.insert(0, parent.file_id)
964
1152
def id2path(self, file_id):
965
"""Return as a list the path to file_id.
1153
"""Return as a string the path to file_id.
967
1155
>>> i = Inventory()
968
1156
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
969
1157
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
970
>>> print i.id2path('foo-id').replace(os.sep, '/')
1158
>>> print i.id2path('foo-id')
973
1161
# get all names, skipping root
974
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
975
return os.sep.join(p)
1162
return '/'.join(reversed(
1163
[parent.name for parent in
1164
self._iter_file_id_parents(file_id)][:-1]))
979
1166
def path2id(self, name):
980
1167
"""Walk down through directories to return entry of last component.
1044
1228
file_ie.parent_id = new_parent_id
1231
def make_entry(kind, name, parent_id, file_id=None):
1232
"""Create an inventory entry.
1234
:param kind: the type of inventory entry to create.
1235
:param name: the basename of the entry.
1236
:param parent_id: the parent_id of the entry.
1237
:param file_id: the file_id to use. if None, one will be created.
1240
file_id = bzrlib.workingtree.gen_file_id(name)
1241
if kind == 'directory':
1242
return InventoryDirectory(file_id, name, parent_id)
1243
elif kind == 'file':
1244
return InventoryFile(file_id, name, parent_id)
1245
elif kind == 'symlink':
1246
return InventoryLink(file_id, name, parent_id)
1248
raise BzrError("unknown kind %r" % kind)
1049
1252
_NAME_RE = None
1051
1254
def is_valid_name(name):
1052
1255
global _NAME_RE
1053
if _NAME_RE == None:
1256
if _NAME_RE is None:
1054
1257
_NAME_RE = re.compile(r'^[^/\\]+$')
1056
1259
return bool(_NAME_RE.match(name))