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
80
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
81
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
77
82
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
83
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
84
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
85
>>> for ix, j in enumerate(i.iter_entries()):
86
... 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'))
88
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
89
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
84
90
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
91
Traceback (most recent call last):
87
93
BzrError: inventory already contains entry with id {2323}
88
94
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
89
InventoryFile('2324', 'bye.c', parent_id='123')
95
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
90
96
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
91
InventoryDirectory('2325', 'wibble', parent_id='123')
97
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
92
98
>>> i.path2id('src/wibble')
96
102
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
103
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
105
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
100
106
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
102
108
... assert i.path2id(path)
108
114
src/wibble/wibble.c
109
>>> i.id2path('2326').replace('\\\\', '/')
115
>>> i.id2path('2326')
110
116
'src/wibble/wibble.c'
119
# Constants returned by describe_change()
121
# TODO: These should probably move to some kind of FileChangeDescription
122
# class; that's like what's inside a TreeDelta but we want to be able to
123
# generate them just for one file at a time.
125
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
129
def detect_changes(self, old_entry):
121
130
"""Return a (text_modified, meta_modified) from this to old_entry.
146
155
output_to, reverse=False):
147
156
"""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.
158
def find_previous_heads(self, previous_inventories,
159
versioned_file_store,
162
"""Return the revisions and entries that directly precede this.
152
164
Returned as a map from revision to inventory entry.
154
166
This is a map containing the file revisions in all parents
155
167
for which the file exists, and its revision is not a parent of
156
168
any other. If the file is new, the set will be empty.
170
:param versioned_file_store: A store where ancestry data on this
171
file id can be queried.
172
:param transaction: The transaction that queries to the versioned
173
file store should be completed under.
174
:param entry_vf: The entry versioned file, if its already available.
158
176
def get_ancestors(weave, entry):
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
177
return set(weave.get_ancestry(entry.revision))
178
# revision:ie mapping for each ie found in previous_inventories.
180
# revision:ie mapping with one revision for each head.
182
# revision: ancestor list for each head
162
183
head_ancestors = {}
184
# identify candidate head revision ids.
163
185
for inv in previous_inventories:
164
186
if self.file_id in inv:
165
187
ie = inv[self.file_id]
166
188
assert ie.file_id == self.file_id
167
if ie.revision in heads:
168
assert heads[ie.revision] == ie
189
if ie.revision in candidates:
190
# same revision value in two different inventories:
191
# correct possible inconsistencies:
192
# * there was a bug in revision updates with 'x' bit
195
if candidates[ie.revision].executable != ie.executable:
196
candidates[ie.revision].executable = False
197
ie.executable = False
198
except AttributeError:
200
# must now be the same.
201
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
203
# add this revision as a candidate.
204
candidates[ie.revision] = ie
206
# common case optimisation
207
if len(candidates) == 1:
208
# if there is only one candidate revision found
209
# then we can opening the versioned file to access ancestry:
210
# there cannot be any ancestors to eliminate when there is
211
# only one revision available.
212
heads[ie.revision] = ie
215
# eliminate ancestors amongst the available candidates:
216
# heads are those that are not an ancestor of any other candidate
217
# - this provides convergence at a per-file level.
218
for ie in candidates.values():
219
# may be an ancestor of a known head:
220
already_present = 0 != len(
221
[head for head in heads
222
if ie.revision in head_ancestors[head]])
224
# an ancestor of an analyzed candidate.
226
# not an ancestor of a known head:
227
# load the versioned file for this file id if needed
229
entry_vf = versioned_file_store.get_weave_or_empty(
230
self.file_id, transaction)
231
ancestors = get_ancestors(entry_vf, ie)
232
# may knock something else out:
233
check_heads = list(heads.keys())
234
for head in check_heads:
235
if head in ancestors:
236
# this previously discovered 'head' is not
237
# really a head - its an ancestor of the newly
240
head_ancestors[ie.revision] = ancestors
241
heads[ie.revision] = ie
190
244
def get_tar_item(self, root, dp, now, tree):
191
245
"""Get a tarfile item and a file stream for its content."""
192
item = tarfile.TarInfo(os.path.join(root, dp))
246
item = tarfile.TarInfo(pathjoin(root, dp))
193
247
# TODO: would be cool to actually set it to the timestamp of the
194
248
# revision it was last changed
256
309
This is a template method - implement _put_on_disk in subclasses.
258
fullpath = appendpath(dest, dp)
311
fullpath = pathjoin(dest, dp)
259
312
self._put_on_disk(fullpath, tree)
260
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
313
mutter(" export {%s} kind %s to %s", self.file_id,
262
316
def _put_on_disk(self, fullpath, tree):
263
317
"""Put this entry onto disk at fullpath, from tree tree."""
264
318
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
266
320
def sorted_children(self):
267
l = self.children.items()
321
return sorted(self.children.items())
272
324
def versionable_kind(kind):
278
330
This is a template method, override _check for kind specific
333
:param checker: Check object providing context for the checks;
334
can be used to find out what parts of the repository have already
336
:param rev_id: Revision id from which this InventoryEntry was loaded.
337
Not necessarily the last-changed revision for this file.
338
:param inv: Inventory from which the entry was loaded.
339
:param tree: RevisionTree for this entry.
281
if self.parent_id != None:
341
if self.parent_id is not None:
282
342
if not inv.has_id(self.parent_id):
283
343
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
284
344
% (self.parent_id, rev_id))
289
349
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
290
350
(self.kind, rev_id))
294
353
"""Clone this inventory entry."""
295
354
raise NotImplementedError
297
def _get_snapshot_change(self, previous_entries):
298
if len(previous_entries) > 1:
300
elif len(previous_entries) == 0:
357
def describe_change(old_entry, new_entry):
358
"""Describe the change between old_entry and this.
360
This smells of being an InterInventoryEntry situation, but as its
361
the first one, we're making it a static method for now.
363
An entry with a different parent, or different name is considered
364
to be renamed. Reparenting is an internal detail.
365
Note that renaming the parent does not trigger a rename for the
368
# TODO: Perhaps return an object rather than just a string
369
if old_entry is new_entry:
370
# also the case of both being None
372
elif old_entry is None:
303
return 'modified/renamed/reparented'
374
elif new_entry is None:
376
text_modified, meta_modified = new_entry.detect_changes(old_entry)
377
if text_modified or meta_modified:
381
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
382
if old_entry.parent_id != new_entry.parent_id:
384
elif old_entry.name != new_entry.name:
388
if renamed and not modified:
389
return InventoryEntry.RENAMED
390
if modified and not renamed:
392
if modified and renamed:
393
return InventoryEntry.MODIFIED_AND_RENAMED
305
396
def __repr__(self):
306
return ("%s(%r, %r, parent_id=%r)"
397
return ("%s(%r, %r, parent_id=%r, revision=%r)"
307
398
% (self.__class__.__name__,
312
404
def snapshot(self, revision, path, previous_entries,
313
work_tree, weave_store):
405
work_tree, commit_builder):
314
406
"""Make a snapshot of this entry which may or may not have changed.
316
408
This means that all its fields are populated, that it has its
325
419
mutter("found unchanged entry")
326
420
self.revision = parent_ie.revision
327
421
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)
422
return self._snapshot_into_revision(revision, previous_entries,
423
work_tree, commit_builder)
425
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
427
"""Record this revision unconditionally into a store.
429
The entry's last-changed revision property (`revision`) is updated to
430
that of the new revision.
432
:param revision: id of the new revision that is being recorded.
434
:returns: String description of the commit (e.g. "merged", "modified"), etc.
436
mutter('new revision {%s} for {%s}', revision, self.file_id)
335
437
self.revision = revision
336
change = self._get_snapshot_change(previous_entries)
337
self._snapshot_text(previous_entries, work_tree, weave_store)
438
self._snapshot_text(previous_entries, work_tree, commit_builder)
340
def _snapshot_text(self, file_parents, work_tree, weave_store):
440
def _snapshot_text(self, file_parents, work_tree, commit_builder):
341
441
"""Record the 'text' of this entry, whatever form that takes.
343
443
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)
445
raise NotImplementedError(self._snapshot_text)
349
447
def __eq__(self, other):
350
448
if not isinstance(other, InventoryEntry):
388
486
Note that this should be modified to be a noop on virtual trees
389
487
as all entries created there are prepopulated.
489
# TODO: Rather than running this manually, we should check the
490
# working sha1 and other expensive properties when they're
491
# first requested, or preload them if they're already known
492
pass # nothing to do by default
494
def _forget_tree_state(self):
393
498
class RootEntry(InventoryEntry):
500
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
501
'text_id', 'parent_id', 'children', 'executable',
502
'revision', 'symlink_target']
395
504
def _check(self, checker, rev_id, tree):
396
505
"""See InventoryEntry._check"""
413
523
class InventoryDirectory(InventoryEntry):
414
524
"""A directory in an inventory."""
526
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
527
'text_id', 'parent_id', 'children', 'executable',
528
'revision', 'symlink_target']
416
530
def _check(self, checker, rev_id, tree):
417
531
"""See InventoryEntry._check"""
418
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
532
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
419
533
raise BzrCheckError('directory {%s} has text in revision {%s}'
420
534
% (self.file_id, rev_id))
448
562
"""See InventoryEntry._put_on_disk."""
449
563
os.mkdir(fullpath)
565
def _snapshot_text(self, file_parents, work_tree, commit_builder):
566
"""See InventoryEntry._snapshot_text."""
567
commit_builder.modified_directory(self.file_id, file_parents)
452
570
class InventoryFile(InventoryEntry):
453
571
"""A file in an inventory."""
455
def _check(self, checker, rev_id, tree):
573
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
574
'text_id', 'parent_id', 'children', 'executable',
575
'revision', 'symlink_target']
577
def _check(self, checker, tree_revision_id, tree):
456
578
"""See InventoryEntry._check"""
457
revision = self.revision
458
t = (self.file_id, revision)
579
t = (self.file_id, self.revision)
459
580
if t in checker.checked_texts:
460
prev_sha = checker.checked_texts[t]
581
prev_sha = checker.checked_texts[t]
461
582
if prev_sha != self.text_sha1:
462
583
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
463
(self.file_id, rev_id))
584
(self.file_id, tree_revision_id))
465
586
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)
589
if self.file_id not in checker.checked_weaves:
590
mutter('check weave {%s}', self.file_id)
591
w = tree.get_weave(self.file_id)
592
# Not passing a progress bar, because it creates a new
593
# progress, which overwrites the current progress,
594
# and doesn't look nice
596
checker.checked_weaves[self.file_id] = True
598
w = tree.get_weave(self.file_id)
600
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
601
checker.checked_text_cnt += 1
602
# We can't check the length, because Weave doesn't store that
603
# information, and the whole point of looking at the weave's
604
# sha1sum is that we don't have to extract the text.
605
if self.text_sha1 != w.get_sha1(self.revision):
606
raise BzrCheckError('text {%s} version {%s} wrong sha1'
607
% (self.file_id, self.revision))
474
608
checker.checked_texts[t] = self.text_sha1
493
627
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
494
628
output_to, reverse=False):
495
629
"""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)
631
from_text = tree.get_file(self.file_id).readlines()
633
to_text = to_tree.get_file(to_entry.file_id).readlines()
637
text_diff(from_label, from_text,
638
to_label, to_text, output_to)
640
text_diff(to_label, to_text,
641
from_label, from_text, output_to)
644
label_pair = (to_label, from_label)
646
label_pair = (from_label, to_label)
647
print >> output_to, "Binary files %s and %s differ" % label_pair
508
649
def has_text(self):
509
650
"""See InventoryEntry.has_text."""
537
678
def _read_tree_state(self, path, work_tree):
538
679
"""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):
680
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
681
# FIXME: 20050930 probe for the text size when getting sha1
682
# in _read_tree_state
683
self.executable = work_tree.is_executable(self.file_id, path=path)
686
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
687
% (self.__class__.__name__,
694
def _forget_tree_state(self):
695
self.text_sha1 = 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
parts = bzrlib.osutils.splitpath(relpath)
1065
parts = 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)
1242
norm_name, can_access = osutils.normalized_filename(name)
1243
if norm_name != name:
1247
# TODO: jam 20060701 This would probably be more useful
1248
# if the error was raised with the full path
1249
raise errors.InvalidNormalization(name)
1251
if kind == 'directory':
1252
return InventoryDirectory(file_id, name, parent_id)
1253
elif kind == 'file':
1254
return InventoryFile(file_id, name, parent_id)
1255
elif kind == 'symlink':
1256
return InventoryLink(file_id, name, parent_id)
1258
raise BzrError("unknown kind %r" % kind)
1049
1261
_NAME_RE = None
1051
1263
def is_valid_name(name):
1052
1264
global _NAME_RE
1053
if _NAME_RE == None:
1265
if _NAME_RE is None:
1054
1266
_NAME_RE = re.compile(r'^[^/\\]+$')
1056
1268
return bool(_NAME_RE.match(name))