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
87
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
88
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
77
89
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
90
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
91
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
92
>>> for ix, j in enumerate(i.iter_entries()):
93
... print (j[0] == shouldbe[ix], j[1])
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
95
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
96
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
97
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
84
98
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
99
Traceback (most recent call last):
87
101
BzrError: inventory already contains entry with id {2323}
88
102
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
89
InventoryFile('2324', 'bye.c', parent_id='123')
103
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
90
104
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
91
InventoryDirectory('2325', 'wibble', parent_id='123')
105
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
92
106
>>> i.path2id('src/wibble')
96
110
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
97
InventoryFile('2326', 'wibble.c', parent_id='2325')
111
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
99
InventoryFile('2326', 'wibble.c', parent_id='2325')
113
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
100
114
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
102
116
... assert i.path2id(path)
108
123
src/wibble/wibble.c
109
>>> i.id2path('2326').replace('\\\\', '/')
124
>>> i.id2path('2326')
110
125
'src/wibble/wibble.c'
128
# Constants returned by describe_change()
130
# TODO: These should probably move to some kind of FileChangeDescription
131
# class; that's like what's inside a TreeDelta but we want to be able to
132
# generate them just for one file at a time.
134
MODIFIED_AND_RENAMED = 'modified and renamed'
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
138
def detect_changes(self, old_entry):
121
139
"""Return a (text_modified, meta_modified) from this to old_entry.
146
164
output_to, reverse=False):
147
165
"""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.
167
def find_previous_heads(self, previous_inventories,
168
versioned_file_store,
171
"""Return the revisions and entries that directly precede this.
152
173
Returned as a map from revision to inventory entry.
154
175
This is a map containing the file revisions in all parents
155
176
for which the file exists, and its revision is not a parent of
156
177
any other. If the file is new, the set will be empty.
179
:param versioned_file_store: A store where ancestry data on this
180
file id can be queried.
181
:param transaction: The transaction that queries to the versioned
182
file store should be completed under.
183
:param entry_vf: The entry versioned file, if its already available.
158
185
def get_ancestors(weave, entry):
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
186
return set(weave.get_ancestry(entry.revision))
187
# revision:ie mapping for each ie found in previous_inventories.
189
# revision:ie mapping with one revision for each head.
191
# revision: ancestor list for each head
162
192
head_ancestors = {}
193
# identify candidate head revision ids.
163
194
for inv in previous_inventories:
164
195
if self.file_id in inv:
165
196
ie = inv[self.file_id]
166
197
assert ie.file_id == self.file_id
167
if ie.revision in heads:
168
assert heads[ie.revision] == ie
198
if ie.revision in candidates:
199
# same revision value in two different inventories:
200
# correct possible inconsistencies:
201
# * there was a bug in revision updates with 'x' bit
204
if candidates[ie.revision].executable != ie.executable:
205
candidates[ie.revision].executable = False
206
ie.executable = False
207
except AttributeError:
209
# must now be the same.
210
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
212
# add this revision as a candidate.
213
candidates[ie.revision] = ie
215
# common case optimisation
216
if len(candidates) == 1:
217
# if there is only one candidate revision found
218
# then we can opening the versioned file to access ancestry:
219
# there cannot be any ancestors to eliminate when there is
220
# only one revision available.
221
heads[ie.revision] = ie
224
# eliminate ancestors amongst the available candidates:
225
# heads are those that are not an ancestor of any other candidate
226
# - this provides convergence at a per-file level.
227
for ie in candidates.values():
228
# may be an ancestor of a known head:
229
already_present = 0 != len(
230
[head for head in heads
231
if ie.revision in head_ancestors[head]])
233
# an ancestor of an analyzed candidate.
235
# not an ancestor of a known head:
236
# load the versioned file for this file id if needed
238
entry_vf = versioned_file_store.get_weave_or_empty(
239
self.file_id, transaction)
240
ancestors = get_ancestors(entry_vf, ie)
241
# may knock something else out:
242
check_heads = list(heads.keys())
243
for head in check_heads:
244
if head in ancestors:
245
# this previously discovered 'head' is not
246
# really a head - its an ancestor of the newly
249
head_ancestors[ie.revision] = ancestors
250
heads[ie.revision] = ie
190
253
def get_tar_item(self, root, dp, now, tree):
191
254
"""Get a tarfile item and a file stream for its content."""
192
item = tarfile.TarInfo(os.path.join(root, dp))
255
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
193
256
# TODO: would be cool to actually set it to the timestamp of the
194
257
# revision it was last changed
256
318
This is a template method - implement _put_on_disk in subclasses.
258
fullpath = appendpath(dest, dp)
320
fullpath = osutils.pathjoin(dest, dp)
259
321
self._put_on_disk(fullpath, tree)
260
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
322
# mutter(" export {%s} kind %s to %s", self.file_id,
323
# self.kind, fullpath)
262
325
def _put_on_disk(self, fullpath, tree):
263
326
"""Put this entry onto disk at fullpath, from tree tree."""
264
327
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
266
329
def sorted_children(self):
267
l = self.children.items()
330
return sorted(self.children.items())
272
333
def versionable_kind(kind):
273
return kind in ('file', 'directory', 'symlink')
334
return (kind in ('file', 'directory', 'symlink'))
275
336
def check(self, checker, rev_id, inv, tree):
276
337
"""Check this inventory entry is intact.
278
339
This is a template method, override _check for kind specific
342
:param checker: Check object providing context for the checks;
343
can be used to find out what parts of the repository have already
345
:param rev_id: Revision id from which this InventoryEntry was loaded.
346
Not necessarily the last-changed revision for this file.
347
:param inv: Inventory from which the entry was loaded.
348
:param tree: RevisionTree for this entry.
281
if self.parent_id != None:
350
if self.parent_id is not None:
282
351
if not inv.has_id(self.parent_id):
283
352
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
284
353
% (self.parent_id, rev_id))
289
358
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
290
359
(self.kind, rev_id))
294
362
"""Clone this inventory entry."""
295
363
raise NotImplementedError
297
def _get_snapshot_change(self, previous_entries):
298
if len(previous_entries) > 1:
300
elif len(previous_entries) == 0:
366
def describe_change(old_entry, new_entry):
367
"""Describe the change between old_entry and this.
369
This smells of being an InterInventoryEntry situation, but as its
370
the first one, we're making it a static method for now.
372
An entry with a different parent, or different name is considered
373
to be renamed. Reparenting is an internal detail.
374
Note that renaming the parent does not trigger a rename for the
377
# TODO: Perhaps return an object rather than just a string
378
if old_entry is new_entry:
379
# also the case of both being None
381
elif old_entry is None:
303
return 'modified/renamed/reparented'
383
elif new_entry is None:
385
text_modified, meta_modified = new_entry.detect_changes(old_entry)
386
if text_modified or meta_modified:
390
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
391
if old_entry.parent_id != new_entry.parent_id:
393
elif old_entry.name != new_entry.name:
397
if renamed and not modified:
398
return InventoryEntry.RENAMED
399
if modified and not renamed:
401
if modified and renamed:
402
return InventoryEntry.MODIFIED_AND_RENAMED
305
405
def __repr__(self):
306
return ("%s(%r, %r, parent_id=%r)"
406
return ("%s(%r, %r, parent_id=%r, revision=%r)"
307
407
% (self.__class__.__name__,
312
413
def snapshot(self, revision, path, previous_entries,
313
work_tree, weave_store):
414
work_tree, commit_builder):
314
415
"""Make a snapshot of this entry which may or may not have changed.
316
417
This means that all its fields are populated, that it has its
317
418
text stored in the text store or weave.
319
mutter('new parents of %s are %r', path, previous_entries)
420
# mutter('new parents of %s are %r', path, previous_entries)
320
421
self._read_tree_state(path, work_tree)
422
# TODO: Where should we determine whether to reuse a
423
# previous revision id or create a new revision? 20060606
321
424
if len(previous_entries) == 1:
322
425
# cannot be unchanged unless there is only one parent file rev.
323
426
parent_ie = previous_entries.values()[0]
324
427
if self._unchanged(parent_ie):
325
mutter("found unchanged entry")
428
# mutter("found unchanged entry")
326
429
self.revision = parent_ie.revision
327
430
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)
431
return self._snapshot_into_revision(revision, previous_entries,
432
work_tree, commit_builder)
434
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
436
"""Record this revision unconditionally into a store.
438
The entry's last-changed revision property (`revision`) is updated to
439
that of the new revision.
441
:param revision: id of the new revision that is being recorded.
443
:returns: String description of the commit (e.g. "merged", "modified"), etc.
445
# mutter('new revision {%s} for {%s}', revision, self.file_id)
335
446
self.revision = revision
336
change = self._get_snapshot_change(previous_entries)
337
self._snapshot_text(previous_entries, work_tree, weave_store)
447
self._snapshot_text(previous_entries, work_tree, commit_builder)
340
def _snapshot_text(self, file_parents, work_tree, weave_store):
449
def _snapshot_text(self, file_parents, work_tree, commit_builder):
341
450
"""Record the 'text' of this entry, whatever form that takes.
343
452
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)
454
raise NotImplementedError(self._snapshot_text)
349
456
def __eq__(self, other):
350
457
if not isinstance(other, InventoryEntry):
388
495
Note that this should be modified to be a noop on virtual trees
389
496
as all entries created there are prepopulated.
498
# TODO: Rather than running this manually, we should check the
499
# working sha1 and other expensive properties when they're
500
# first requested, or preload them if they're already known
501
pass # nothing to do by default
503
def _forget_tree_state(self):
393
507
class RootEntry(InventoryEntry):
509
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
510
'text_id', 'parent_id', 'children', 'executable',
511
'revision', 'symlink_target']
395
513
def _check(self, checker, rev_id, tree):
396
514
"""See InventoryEntry._check"""
398
516
def __init__(self, file_id):
399
517
self.file_id = file_id
400
518
self.children = {}
401
self.kind = 'root_directory'
519
self.kind = 'directory'
402
520
self.parent_id = None
523
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
524
' Please use InventoryDirectory instead.',
525
DeprecationWarning, stacklevel=2)
405
527
def __eq__(self, other):
406
528
if not isinstance(other, RootEntry):
413
535
class InventoryDirectory(InventoryEntry):
414
536
"""A directory in an inventory."""
538
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
539
'text_id', 'parent_id', 'children', 'executable',
540
'revision', 'symlink_target']
416
542
def _check(self, checker, rev_id, tree):
417
543
"""See InventoryEntry._check"""
418
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
544
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
419
545
raise BzrCheckError('directory {%s} has text in revision {%s}'
420
546
% (self.file_id, rev_id))
448
574
"""See InventoryEntry._put_on_disk."""
449
575
os.mkdir(fullpath)
577
def _snapshot_text(self, file_parents, work_tree, commit_builder):
578
"""See InventoryEntry._snapshot_text."""
579
commit_builder.modified_directory(self.file_id, file_parents)
452
582
class InventoryFile(InventoryEntry):
453
583
"""A file in an inventory."""
455
def _check(self, checker, rev_id, tree):
585
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
586
'text_id', 'parent_id', 'children', 'executable',
587
'revision', 'symlink_target']
589
def _check(self, checker, tree_revision_id, tree):
456
590
"""See InventoryEntry._check"""
457
revision = self.revision
458
t = (self.file_id, revision)
591
t = (self.file_id, self.revision)
459
592
if t in checker.checked_texts:
460
prev_sha = checker.checked_texts[t]
593
prev_sha = checker.checked_texts[t]
461
594
if prev_sha != self.text_sha1:
462
595
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
463
(self.file_id, rev_id))
596
(self.file_id, tree_revision_id))
465
598
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)
601
if self.file_id not in checker.checked_weaves:
602
mutter('check weave {%s}', self.file_id)
603
w = tree.get_weave(self.file_id)
604
# Not passing a progress bar, because it creates a new
605
# progress, which overwrites the current progress,
606
# and doesn't look nice
608
checker.checked_weaves[self.file_id] = True
610
w = tree.get_weave(self.file_id)
612
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
613
checker.checked_text_cnt += 1
614
# We can't check the length, because Weave doesn't store that
615
# information, and the whole point of looking at the weave's
616
# sha1sum is that we don't have to extract the text.
617
if self.text_sha1 != w.get_sha1(self.revision):
618
raise BzrCheckError('text {%s} version {%s} wrong sha1'
619
% (self.file_id, self.revision))
474
620
checker.checked_texts[t] = self.text_sha1
493
639
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
494
640
output_to, reverse=False):
495
641
"""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)
643
from_text = tree.get_file(self.file_id).readlines()
645
to_text = to_tree.get_file(to_entry.file_id).readlines()
649
text_diff(from_label, from_text,
650
to_label, to_text, output_to)
652
text_diff(to_label, to_text,
653
from_label, from_text, output_to)
654
except errors.BinaryFile:
656
label_pair = (to_label, from_label)
658
label_pair = (from_label, to_label)
659
print >> output_to, "Binary files %s and %s differ" % label_pair
508
661
def has_text(self):
509
662
"""See InventoryEntry.has_text."""
531
684
def _put_on_disk(self, fullpath, tree):
532
685
"""See InventoryEntry._put_on_disk."""
533
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
686
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
534
687
if tree.is_executable(self.file_id):
535
688
os.chmod(fullpath, 0755)
537
690
def _read_tree_state(self, path, work_tree):
538
691
"""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):
692
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
693
# FIXME: 20050930 probe for the text size when getting sha1
694
# in _read_tree_state
695
self.executable = work_tree.is_executable(self.file_id, path=path)
698
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
699
% (self.__class__.__name__,
706
def _forget_tree_state(self):
707
self.text_sha1 = None
709
def _snapshot_text(self, file_parents, work_tree, commit_builder):
543
710
"""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))
711
def get_content_byte_lines():
712
return work_tree.get_file(self.file_id).readlines()
713
self.text_sha1, self.text_size = commit_builder.modified_file_text(
714
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
562
716
def _unchanged(self, previous_ie):
563
717
"""See InventoryEntry._unchanged."""
568
722
# FIXME: 20050930 probe for the text size when getting sha1
569
723
# in _read_tree_state
570
724
self.text_size = previous_ie.text_size
725
if self.executable != previous_ie.executable:
571
727
return compatible
574
730
class InventoryLink(InventoryEntry):
575
731
"""A file in an inventory."""
577
__slots__ = ['symlink_target']
733
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
734
'text_id', 'parent_id', 'children', 'executable',
735
'revision', 'symlink_target']
579
737
def _check(self, checker, rev_id, tree):
580
738
"""See InventoryEntry._check"""
581
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
739
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
582
740
raise BzrCheckError('symlink {%s} has text in revision {%s}'
583
741
% (self.file_id, rev_id))
584
if self.symlink_target == None:
742
if self.symlink_target is None:
585
743
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
586
744
% (self.file_id, rev_id))
686
852
May also look up by name:
688
854
>>> [x[0] for x in inv.iter_entries()]
690
856
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
691
857
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
692
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
858
Traceback (most recent call last):
859
BzrError: parent_id {TREE_ROOT} not in inventory
860
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
861
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
694
def __init__(self, root_id=ROOT_ID):
863
def __init__(self, root_id=ROOT_ID, revision_id=None):
695
864
"""Create or read an inventory.
697
866
If a working directory is specified, the inventory is read
701
870
The inventory is created with a default root directory, with
704
# We are letting Branch.initialize() create a unique inventory
705
# root id. Rather than generating a random one here.
707
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
708
self.root = RootEntry(root_id)
873
if root_id is not None:
874
self._set_root(InventoryDirectory(root_id, '', None))
878
self.revision_id = revision_id
880
def _set_root(self, ie):
709
882
self._byid = {self.root.file_id: self.root}
713
other = Inventory(self.root.file_id)
885
# TODO: jam 20051218 Should copy also copy the revision_id?
886
entries = self.iter_entries()
887
other = Inventory(entries.next()[1].file_id)
714
888
# copy recursively so we know directories will be added before
715
889
# their children. There are more efficient ways than this...
716
for path, entry in self.iter_entries():
717
if entry == self.root:
890
for path, entry in entries():
719
891
other.add(entry.copy())
723
894
def __iter__(self):
724
895
return iter(self._byid)
727
897
def __len__(self):
728
898
"""Returns number of entries."""
729
899
return len(self._byid)
732
901
def iter_entries(self, from_dir=None):
733
902
"""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
904
if self.root is None:
908
elif isinstance(from_dir, basestring):
909
from_dir = self._byid[from_dir]
911
# unrolling the recursive called changed the time from
912
# 440ms/663ms (inline/total) to 116ms/116ms
913
children = from_dir.children.items()
915
children = collections.deque(children)
916
stack = [(u'', children)]
918
from_dir_relpath, children = stack[-1]
921
name, ie = children.popleft()
923
# we know that from_dir_relpath never ends in a slash
924
# and 'f' doesn't begin with one, we can do a string op, rather
925
# than the checks of pathjoin(), though this means that all paths
927
path = from_dir_relpath + '/' + name
931
if ie.kind != 'directory':
934
# But do this child first
935
new_children = ie.children.items()
937
new_children = collections.deque(new_children)
938
stack.append((path, new_children))
939
# Break out of inner loop, so that we start outer loop with child
942
# if we finished all children, pop it off the stack
945
def iter_entries_by_dir(self, from_dir=None):
946
"""Iterate over the entries in a directory first order.
948
This returns all entries for a directory before returning
949
the entries for children of a directory. This is not
950
lexicographically sorted order, and is a hybrid between
951
depth-first and breadth-first.
953
:return: This yields (path, entry) pairs
955
# TODO? Perhaps this should return the from_dir so that the root is
956
# yielded? or maybe an option?
958
if self.root is None:
962
elif isinstance(from_dir, basestring):
963
from_dir = self._byid[from_dir]
965
stack = [(u'', from_dir)]
967
cur_relpath, cur_dir = stack.pop()
970
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
972
child_relpath = cur_relpath + child_name
974
yield child_relpath, child_ie
976
if child_ie.kind == 'directory':
977
child_dirs.append((child_relpath+'/', child_ie))
978
stack.extend(reversed(child_dirs))
749
980
def entries(self):
750
981
"""Return list of (path, ie) for all entries except the root.
778
1008
for name, child_ie in kids:
779
child_path = os.path.join(parent_path, name)
1009
child_path = osutils.pathjoin(parent_path, name)
780
1010
descend(child_ie, child_path)
781
descend(self.root, '')
1011
descend(self.root, u'')
786
1014
def __contains__(self, file_id):
787
1015
"""True if this entry contains a file with given id.
789
1017
>>> inv = Inventory()
790
1018
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
791
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1019
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
792
1020
>>> '123' in inv
794
1022
>>> '456' in inv
797
return file_id in self._byid
1025
return (file_id in self._byid)
800
1027
def __getitem__(self, file_id):
801
1028
"""Return the entry for given file_id.
803
1030
>>> inv = Inventory()
804
1031
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
805
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1032
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
806
1033
>>> inv['123123'].name
810
1037
return self._byid[file_id]
811
1038
except KeyError:
813
1040
raise BzrError("can't look up file_id None")
815
1042
raise BzrError("file_id {%s} not in inventory" % file_id)
818
1044
def get_file_kind(self, file_id):
819
1045
return self._byid[file_id].kind
821
1047
def get_child(self, parent_id, filename):
822
1048
return self[parent_id].children.get(filename)
825
1050
def add(self, entry):
826
1051
"""Add entry to inventory.
833
1058
if entry.file_id in self._byid:
834
1059
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
836
if entry.parent_id == ROOT_ID or entry.parent_id is None:
837
entry.parent_id = self.root.file_id
1061
if entry.parent_id is None:
1062
assert self.root is None and len(self._byid) == 0
1063
self._set_root(entry)
840
1066
parent = self._byid[entry.parent_id]
841
1067
except KeyError:
842
1068
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
844
if parent.children.has_key(entry.name):
1070
if entry.name in parent.children:
845
1071
raise BzrError("%s is already versioned" %
846
appendpath(self.id2path(parent.file_id), entry.name))
1072
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
848
1074
self._byid[entry.file_id] = entry
849
1075
parent.children[entry.name] = entry
853
def add_path(self, relpath, kind, file_id=None):
1078
def add_path(self, relpath, kind, file_id=None, parent_id=None):
854
1079
"""Add entry from a path.
856
1081
The immediate parent must already be versioned.
858
1083
Returns the new entry object."""
859
from bzrlib.branch import gen_file_id
861
parts = bzrlib.osutils.splitpath(relpath)
1085
parts = osutils.splitpath(relpath)
862
1087
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)
1089
file_id = bzrlib.workingtree.gen_root_id()
1090
self.root = InventoryDirectory(file_id, '', None)
1091
self._byid = {self.root.file_id: self.root}
880
raise BzrError("unknown kind %r" % kind)
1094
parent_path = parts[:-1]
1095
parent_id = self.path2id(parent_path)
1096
if parent_id is None:
1097
raise errors.NotVersionedError(path=parent_path)
1098
ie = make_entry(kind, parts[-1], parent_id, file_id)
881
1099
return self.add(ie)
884
1101
def __delitem__(self, file_id):
885
1102
"""Remove entry by id.
887
1104
>>> inv = Inventory()
888
1105
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
889
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1106
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
890
1107
>>> '123' in inv
892
1109
>>> del inv['123']
918
1129
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
919
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1130
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
922
1133
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
923
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1134
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
927
1138
if not isinstance(other, Inventory):
928
1139
return NotImplemented
930
if len(self._byid) != len(other._byid):
931
# shortcut: obviously not the same
934
1141
return self._byid == other._byid
937
1143
def __ne__(self, other):
938
1144
return not self.__eq__(other)
941
1146
def __hash__(self):
942
1147
raise ValueError('not hashable')
1149
def _iter_file_id_parents(self, file_id):
1150
"""Yield the parents of file_id up to the root."""
1151
while file_id is not None:
1153
ie = self._byid[file_id]
1155
raise BzrError("file_id {%s} not found in inventory" % file_id)
1157
file_id = ie.parent_id
945
1159
def get_idpath(self, file_id):
946
1160
"""Return a list of file_ids for the path to an entry.
951
1165
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
1168
for parent in self._iter_file_id_parents(file_id):
1169
p.insert(0, parent.file_id)
964
1172
def id2path(self, file_id):
965
"""Return as a list the path to file_id.
1173
"""Return as a string the path to file_id.
967
1175
>>> i = Inventory()
968
1176
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
969
1177
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
970
>>> print i.id2path('foo-id').replace(os.sep, '/')
1178
>>> print i.id2path('foo-id')
973
1181
# 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)
1182
return '/'.join(reversed(
1183
[parent.name for parent in
1184
self._iter_file_id_parents(file_id)][:-1]))
979
1186
def path2id(self, name):
980
1187
"""Walk down through directories to return entry of last component.
1006
1215
return parent.file_id
1009
1217
def has_filename(self, names):
1010
1218
return bool(self.path2id(names))
1013
1220
def has_id(self, file_id):
1014
return self._byid.has_key(file_id)
1221
return (file_id in self._byid)
1223
def remove_recursive_id(self, file_id):
1224
"""Remove file_id, and children, from the inventory.
1226
:param file_id: A file_id to remove.
1228
to_find_delete = [self._byid[file_id]]
1230
while to_find_delete:
1231
ie = to_find_delete.pop()
1232
to_delete.append(ie.file_id)
1233
if ie.kind == 'directory':
1234
to_find_delete.extend(ie.children.values())
1235
for file_id in reversed(to_delete):
1237
del self._byid[file_id]
1238
if ie.parent_id is not None:
1239
del self[ie.parent_id].children[ie.name]
1017
1241
def rename(self, file_id, new_parent_id, new_name):
1018
1242
"""Move a file within the inventory.
1043
1267
file_ie.name = new_name
1044
1268
file_ie.parent_id = new_parent_id
1270
def is_root(self, file_id):
1271
return self.root is not None and file_id == self.root.file_id
1274
def make_entry(kind, name, parent_id, file_id=None):
1275
"""Create an inventory entry.
1277
:param kind: the type of inventory entry to create.
1278
:param name: the basename of the entry.
1279
:param parent_id: the parent_id of the entry.
1280
:param file_id: the file_id to use. if None, one will be created.
1283
file_id = bzrlib.workingtree.gen_file_id(name)
1285
norm_name, can_access = osutils.normalized_filename(name)
1286
if norm_name != name:
1290
# TODO: jam 20060701 This would probably be more useful
1291
# if the error was raised with the full path
1292
raise errors.InvalidNormalization(name)
1294
if kind == 'directory':
1295
return InventoryDirectory(file_id, name, parent_id)
1296
elif kind == 'file':
1297
return InventoryFile(file_id, name, parent_id)
1298
elif kind == 'symlink':
1299
return InventoryLink(file_id, name, parent_id)
1301
raise BzrError("unknown kind %r" % kind)
1049
1304
_NAME_RE = None
1051
1306
def is_valid_name(name):
1052
1307
global _NAME_RE
1053
if _NAME_RE == None:
1308
if _NAME_RE is None:
1054
1309
_NAME_RE = re.compile(r'^[^/\\]+$')
1056
1311
return bool(_NAME_RE.match(name))