1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# (C) 2005 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.
22
18
# TODO: Maybe also keep the full path of the entry, and the children?
23
19
# But those depend on its position within a particular inventory, and
89
75
>>> i.add(InventoryDirectory('123', 'src', ROOT_ID))
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
76
InventoryDirectory('123', 'src', parent_id='TREE_ROOT')
91
77
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
94
>>> for ix, j in enumerate(i.iter_entries()):
95
... print (j[0] == shouldbe[ix], j[1])
78
InventoryFile('2323', 'hello.c', parent_id='123')
79
>>> for j in i.iter_entries():
97
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
82
('src', InventoryDirectory('123', 'src', parent_id='TREE_ROOT'))
83
('src/hello.c', InventoryFile('2323', 'hello.c', parent_id='123'))
84
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
85
Traceback (most recent call last):
87
BzrError: inventory already contains entry with id {2323}
100
88
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
101
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
89
InventoryFile('2324', 'bye.c', parent_id='123')
102
90
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
103
InventoryDirectory('2325', 'wibble', parent_id='123', revision=None)
91
InventoryDirectory('2325', 'wibble', parent_id='123')
104
92
>>> i.path2id('src/wibble')
108
96
>>> i.add(InventoryFile('2326', 'wibble.c', '2325'))
109
InventoryFile('2326', 'wibble.c', parent_id='2325', sha1=None, len=None)
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')
112
100
>>> for path, entry in i.iter_entries():
101
... print path.replace('\\\\', '/') # for win32 os.sep
114
102
... assert i.path2id(path)
121
108
src/wibble/wibble.c
122
>>> i.id2path('2326')
109
>>> i.id2path('2326').replace('\\\\', '/')
123
110
'src/wibble/wibble.c'
126
# Constants returned by describe_change()
128
# TODO: These should probably move to some kind of FileChangeDescription
129
# class; that's like what's inside a TreeDelta but we want to be able to
130
# generate them just for one file at a time.
132
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)
136
120
def detect_changes(self, old_entry):
137
121
"""Return a (text_modified, meta_modified) from this to old_entry.
162
146
output_to, reverse=False):
163
147
"""Perform a diff between two entries of the same kind."""
165
def find_previous_heads(self, previous_inventories,
166
versioned_file_store,
169
"""Return the revisions and entries that directly precede this.
149
def find_previous_heads(self, previous_inventories, entry_weave):
150
"""Return the revisions and entries that directly preceed this.
171
152
Returned as a map from revision to inventory entry.
173
154
This is a map containing the file revisions in all parents
174
155
for which the file exists, and its revision is not a parent of
175
156
any other. If the file is new, the set will be empty.
177
:param versioned_file_store: A store where ancestry data on this
178
file id can be queried.
179
:param transaction: The transaction that queries to the versioned
180
file store should be completed under.
181
:param entry_vf: The entry versioned file, if its already available.
183
158
def get_ancestors(weave, entry):
184
return set(weave.get_ancestry(entry.revision))
185
# revision:ie mapping for each ie found in previous_inventories.
187
# revision:ie mapping with one revision for each head.
159
return set(map(weave.idx_to_name,
160
weave.inclusions([weave.lookup(entry.revision)])))
189
# revision: ancestor list for each head
190
162
head_ancestors = {}
191
# identify candidate head revision ids.
192
163
for inv in previous_inventories:
193
164
if self.file_id in inv:
194
165
ie = inv[self.file_id]
195
166
assert ie.file_id == self.file_id
196
if ie.revision in candidates:
197
# same revision value in two different inventories:
198
# correct possible inconsistencies:
199
# * there was a bug in revision updates with 'x' bit
202
if candidates[ie.revision].executable != ie.executable:
203
candidates[ie.revision].executable = False
204
ie.executable = False
205
except AttributeError:
207
# must now be the same.
208
assert candidates[ie.revision] == ie
167
if ie.revision in heads:
168
assert heads[ie.revision] == ie
210
# add this revision as a candidate.
211
candidates[ie.revision] = ie
213
# common case optimisation
214
if len(candidates) == 1:
215
# if there is only one candidate revision found
216
# then we can opening the versioned file to access ancestry:
217
# there cannot be any ancestors to eliminate when there is
218
# only one revision available.
219
heads[ie.revision] = ie
222
# eliminate ancestors amongst the available candidates:
223
# heads are those that are not an ancestor of any other candidate
224
# - this provides convergence at a per-file level.
225
for ie in candidates.values():
226
# may be an ancestor of a known head:
227
already_present = 0 != len(
228
[head for head in heads
229
if ie.revision in head_ancestors[head]])
231
# an ancestor of an analyzed candidate.
233
# not an ancestor of a known head:
234
# load the versioned file for this file id if needed
236
entry_vf = versioned_file_store.get_weave_or_empty(
237
self.file_id, transaction)
238
ancestors = get_ancestors(entry_vf, ie)
239
# may knock something else out:
240
check_heads = list(heads.keys())
241
for head in check_heads:
242
if head in ancestors:
243
# this previously discovered 'head' is not
244
# really a head - its an ancestor of the newly
247
head_ancestors[ie.revision] = ancestors
248
heads[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
251
190
def get_tar_item(self, root, dp, now, tree):
252
191
"""Get a tarfile item and a file stream for its content."""
253
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
192
item = tarfile.TarInfo(os.path.join(root, dp))
254
193
# TODO: would be cool to actually set it to the timestamp of the
255
194
# revision it was last changed
318
256
This is a template method - implement _put_on_disk in subclasses.
320
fullpath = osutils.pathjoin(dest, dp)
258
fullpath = appendpath(dest, dp)
321
259
self._put_on_disk(fullpath, tree)
322
# mutter(" export {%s} kind %s to %s", self.file_id,
323
# self.kind, fullpath)
260
mutter(" export {%s} kind %s to %s" % (self.file_id, self.kind, fullpath))
325
262
def _put_on_disk(self, fullpath, tree):
326
263
"""Put this entry onto disk at fullpath, from tree tree."""
327
264
raise BzrError("don't know how to export {%s} of kind %r" % (self.file_id, self.kind))
329
266
def sorted_children(self):
330
return sorted(self.children.items())
267
l = self.children.items()
333
272
def versionable_kind(kind):
334
return (kind in ('file', 'directory', 'symlink'))
273
return kind in ('file', 'directory', 'symlink')
336
275
def check(self, checker, rev_id, inv, tree):
337
276
"""Check this inventory entry is intact.
339
278
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.
350
if self.parent_id is not None:
281
if self.parent_id != None:
351
282
if not inv.has_id(self.parent_id):
352
283
raise BzrCheckError('missing parent {%s} in inventory for revision {%s}'
353
284
% (self.parent_id, rev_id))
358
289
raise BzrCheckError('unknown entry kind %r in revision {%s}' %
359
290
(self.kind, rev_id))
362
294
"""Clone this inventory entry."""
363
295
raise NotImplementedError
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:
297
def _get_snapshot_change(self, previous_entries):
298
if len(previous_entries) > 1:
300
elif len(previous_entries) == 0:
383
elif new_entry is None:
385
if old_entry.kind != new_entry.kind:
387
text_modified, meta_modified = new_entry.detect_changes(old_entry)
388
if text_modified or meta_modified:
392
# TODO 20060511 (mbp, rbc) factor out 'detect_rename' here.
393
if old_entry.parent_id != new_entry.parent_id:
395
elif old_entry.name != new_entry.name:
399
if renamed and not modified:
400
return InventoryEntry.RENAMED
401
if modified and not renamed:
403
if modified and renamed:
404
return InventoryEntry.MODIFIED_AND_RENAMED
303
return 'modified/renamed/reparented'
407
305
def __repr__(self):
408
return ("%s(%r, %r, parent_id=%r, revision=%r)"
306
return ("%s(%r, %r, parent_id=%r)"
409
307
% (self.__class__.__name__,
415
312
def snapshot(self, revision, path, previous_entries,
416
work_tree, commit_builder):
313
work_tree, weave_store):
417
314
"""Make a snapshot of this entry which may or may not have changed.
419
316
This means that all its fields are populated, that it has its
420
317
text stored in the text store or weave.
422
# mutter('new parents of %s are %r', path, previous_entries)
319
mutter('new parents of %s are %r', path, previous_entries)
423
320
self._read_tree_state(path, work_tree)
424
# TODO: Where should we determine whether to reuse a
425
# previous revision id or create a new revision? 20060606
426
321
if len(previous_entries) == 1:
427
322
# cannot be unchanged unless there is only one parent file rev.
428
323
parent_ie = previous_entries.values()[0]
429
324
if self._unchanged(parent_ie):
430
# mutter("found unchanged entry")
325
mutter("found unchanged entry")
431
326
self.revision = parent_ie.revision
432
327
return "unchanged"
433
return self._snapshot_into_revision(revision, previous_entries,
434
work_tree, commit_builder)
436
def _snapshot_into_revision(self, revision, previous_entries, work_tree,
438
"""Record this revision unconditionally into a store.
440
The entry's last-changed revision property (`revision`) is updated to
441
that of the new revision.
443
:param revision: id of the new revision that is being recorded.
445
:returns: String description of the commit (e.g. "merged", "modified"), etc.
447
# mutter('new revision {%s} for {%s}', revision, self.file_id)
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)
448
335
self.revision = revision
449
self._snapshot_text(previous_entries, work_tree, commit_builder)
336
change = self._get_snapshot_change(previous_entries)
337
self._snapshot_text(previous_entries, work_tree, weave_store)
451
def _snapshot_text(self, file_parents, work_tree, commit_builder):
340
def _snapshot_text(self, file_parents, work_tree, weave_store):
452
341
"""Record the 'text' of this entry, whatever form that takes.
454
343
This default implementation simply adds an empty text.
456
raise NotImplementedError(self._snapshot_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)
458
349
def __eq__(self, other):
459
350
if not isinstance(other, InventoryEntry):
497
388
Note that this should be modified to be a noop on virtual trees
498
389
as all entries created there are prepopulated.
500
# TODO: Rather than running this manually, we should check the
501
# working sha1 and other expensive properties when they're
502
# first requested, or preload them if they're already known
503
pass # nothing to do by default
505
def _forget_tree_state(self):
509
393
class RootEntry(InventoryEntry):
511
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
512
'text_id', 'parent_id', 'children', 'executable',
513
'revision', 'symlink_target']
515
395
def _check(self, checker, rev_id, tree):
516
396
"""See InventoryEntry._check"""
518
398
def __init__(self, file_id):
519
399
self.file_id = file_id
520
400
self.children = {}
521
self.kind = 'directory'
401
self.kind = 'root_directory'
522
402
self.parent_id = None
525
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
526
' Please use InventoryDirectory instead.',
527
DeprecationWarning, stacklevel=2)
529
405
def __eq__(self, other):
530
406
if not isinstance(other, RootEntry):
537
413
class InventoryDirectory(InventoryEntry):
538
414
"""A directory in an inventory."""
540
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
541
'text_id', 'parent_id', 'children', 'executable',
542
'revision', 'symlink_target']
544
416
def _check(self, checker, rev_id, tree):
545
417
"""See InventoryEntry._check"""
546
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
418
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
547
419
raise BzrCheckError('directory {%s} has text in revision {%s}'
548
420
% (self.file_id, rev_id))
576
448
"""See InventoryEntry._put_on_disk."""
577
449
os.mkdir(fullpath)
579
def _snapshot_text(self, file_parents, work_tree, commit_builder):
580
"""See InventoryEntry._snapshot_text."""
581
commit_builder.modified_directory(self.file_id, file_parents)
584
452
class InventoryFile(InventoryEntry):
585
453
"""A file in an inventory."""
587
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
588
'text_id', 'parent_id', 'children', 'executable',
589
'revision', 'symlink_target']
591
def _check(self, checker, tree_revision_id, tree):
455
def _check(self, checker, rev_id, tree):
592
456
"""See InventoryEntry._check"""
593
t = (self.file_id, self.revision)
457
revision = self.revision
458
t = (self.file_id, revision)
594
459
if t in checker.checked_texts:
595
prev_sha = checker.checked_texts[t]
460
prev_sha = checker.checked_texts[t]
596
461
if prev_sha != self.text_sha1:
597
462
raise BzrCheckError('mismatched sha1 on {%s} in {%s}' %
598
(self.file_id, tree_revision_id))
463
(self.file_id, rev_id))
600
465
checker.repeated_text_cnt += 1
603
if self.file_id not in checker.checked_weaves:
604
mutter('check weave {%s}', self.file_id)
605
w = tree.get_weave(self.file_id)
606
# Not passing a progress bar, because it creates a new
607
# progress, which overwrites the current progress,
608
# and doesn't look nice
610
checker.checked_weaves[self.file_id] = True
612
w = tree.get_weave(self.file_id)
614
mutter('check version {%s} of {%s}', tree_revision_id, self.file_id)
615
checker.checked_text_cnt += 1
616
# We can't check the length, because Weave doesn't store that
617
# information, and the whole point of looking at the weave's
618
# sha1sum is that we don't have to extract the text.
619
if self.text_sha1 != w.get_sha1(self.revision):
620
raise BzrCheckError('text {%s} version {%s} wrong sha1'
621
% (self.file_id, self.revision))
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)
622
474
checker.checked_texts[t] = self.text_sha1
641
493
def _diff(self, text_diff, from_label, tree, to_label, to_entry, to_tree,
642
494
output_to, reverse=False):
643
495
"""See InventoryEntry._diff."""
645
from_text = tree.get_file(self.file_id).readlines()
647
to_text = to_tree.get_file(to_entry.file_id).readlines()
651
text_diff(from_label, from_text,
652
to_label, to_text, output_to)
654
text_diff(to_label, to_text,
655
from_label, from_text, output_to)
656
except errors.BinaryFile:
658
label_pair = (to_label, from_label)
660
label_pair = (from_label, to_label)
661
print >> output_to, "Binary files %s and %s differ" % label_pair
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)
663
508
def has_text(self):
664
509
"""See InventoryEntry.has_text."""
686
531
def _put_on_disk(self, fullpath, tree):
687
532
"""See InventoryEntry._put_on_disk."""
688
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
533
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
689
534
if tree.is_executable(self.file_id):
690
535
os.chmod(fullpath, 0755)
692
537
def _read_tree_state(self, path, work_tree):
693
538
"""See InventoryEntry._read_tree_state."""
694
self.text_sha1 = work_tree.get_file_sha1(self.file_id, path=path)
695
# FIXME: 20050930 probe for the text size when getting sha1
696
# in _read_tree_state
697
self.executable = work_tree.is_executable(self.file_id, path=path)
700
return ("%s(%r, %r, parent_id=%r, sha1=%r, len=%s)"
701
% (self.__class__.__name__,
708
def _forget_tree_state(self):
709
self.text_sha1 = None
711
def _snapshot_text(self, file_parents, work_tree, commit_builder):
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):
712
543
"""See InventoryEntry._snapshot_text."""
713
def get_content_byte_lines():
714
return work_tree.get_file(self.file_id).readlines()
715
self.text_sha1, self.text_size = commit_builder.modified_file_text(
716
self.file_id, file_parents, get_content_byte_lines, self.text_sha1, self.text_size)
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))
718
562
def _unchanged(self, previous_ie):
719
563
"""See InventoryEntry._unchanged."""
724
568
# FIXME: 20050930 probe for the text size when getting sha1
725
569
# in _read_tree_state
726
570
self.text_size = previous_ie.text_size
727
if self.executable != previous_ie.executable:
729
571
return compatible
732
574
class InventoryLink(InventoryEntry):
733
575
"""A file in an inventory."""
735
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
736
'text_id', 'parent_id', 'children', 'executable',
737
'revision', 'symlink_target']
577
__slots__ = ['symlink_target']
739
579
def _check(self, checker, rev_id, tree):
740
580
"""See InventoryEntry._check"""
741
if self.text_sha1 is not None or self.text_size is not None or self.text_id is not None:
581
if self.text_sha1 != None or self.text_size != None or self.text_id != None:
742
582
raise BzrCheckError('symlink {%s} has text in revision {%s}'
743
583
% (self.file_id, rev_id))
744
if self.symlink_target is None:
584
if self.symlink_target == None:
745
585
raise BzrCheckError('symlink {%s} has no target in revision {%s}'
746
586
% (self.file_id, rev_id))
854
686
May also look up by name:
856
688
>>> [x[0] for x in inv.iter_entries()]
858
690
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
859
691
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
860
Traceback (most recent call last):
861
BzrError: parent_id {TREE_ROOT} not in inventory
862
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
863
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
692
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678')
865
def __init__(self, root_id=ROOT_ID, revision_id=None):
694
def __init__(self, root_id=ROOT_ID):
866
695
"""Create or read an inventory.
868
697
If a working directory is specified, the inventory is read
872
701
The inventory is created with a default root directory, with
875
if root_id is not None:
876
assert root_id.__class__ == str
877
self._set_root(InventoryDirectory(root_id, u'', None))
881
self.revision_id = revision_id
883
def _set_root(self, ie):
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)
885
709
self._byid = {self.root.file_id: self.root}
888
# TODO: jam 20051218 Should copy also copy the revision_id?
889
entries = self.iter_entries()
890
other = Inventory(entries.next()[1].file_id)
713
other = Inventory(self.root.file_id)
891
714
# copy recursively so we know directories will be added before
892
715
# their children. There are more efficient ways than this...
893
for path, entry in entries():
716
for path, entry in self.iter_entries():
717
if entry == self.root:
894
719
other.add(entry.copy())
897
723
def __iter__(self):
898
724
return iter(self._byid)
900
727
def __len__(self):
901
728
"""Returns number of entries."""
902
729
return len(self._byid)
904
732
def iter_entries(self, from_dir=None):
905
733
"""Return (path, entry) pairs, in order by name."""
907
if self.root is None:
911
elif isinstance(from_dir, basestring):
912
from_dir = self._byid[from_dir]
914
# unrolling the recursive called changed the time from
915
# 440ms/663ms (inline/total) to 116ms/116ms
916
children = from_dir.children.items()
918
children = collections.deque(children)
919
stack = [(u'', children)]
921
from_dir_relpath, children = stack[-1]
924
name, ie = children.popleft()
926
# we know that from_dir_relpath never ends in a slash
927
# and 'f' doesn't begin with one, we can do a string op, rather
928
# than the checks of pathjoin(), though this means that all paths
930
path = from_dir_relpath + '/' + name
934
if ie.kind != 'directory':
937
# But do this child first
938
new_children = ie.children.items()
940
new_children = collections.deque(new_children)
941
stack.append((path, new_children))
942
# Break out of inner loop, so that we start outer loop with child
945
# if we finished all children, pop it off the stack
948
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
949
"""Iterate over the entries in a directory first order.
951
This returns all entries for a directory before returning
952
the entries for children of a directory. This is not
953
lexicographically sorted order, and is a hybrid between
954
depth-first and breadth-first.
956
:return: This yields (path, entry) pairs
958
if specific_file_ids:
959
specific_file_ids = [osutils.safe_file_id(fid)
960
for fid in specific_file_ids]
961
# TODO? Perhaps this should return the from_dir so that the root is
962
# yielded? or maybe an option?
964
if self.root is None:
966
# Optimize a common case
967
if specific_file_ids is not None and len(specific_file_ids) == 1:
968
file_id = list(specific_file_ids)[0]
970
yield self.id2path(file_id), self[file_id]
973
if (specific_file_ids is None or
974
self.root.file_id in specific_file_ids):
976
elif isinstance(from_dir, basestring):
977
from_dir = self._byid[from_dir]
979
if specific_file_ids is not None:
981
def add_ancestors(file_id):
982
if file_id not in self:
984
parent_id = self[file_id].parent_id
985
if parent_id is None:
987
if parent_id not in parents:
988
parents.add(parent_id)
989
add_ancestors(parent_id)
990
for file_id in specific_file_ids:
991
add_ancestors(file_id)
995
stack = [(u'', from_dir)]
997
cur_relpath, cur_dir = stack.pop()
1000
for child_name, child_ie in sorted(cur_dir.children.iteritems()):
1002
child_relpath = cur_relpath + child_name
1004
if (specific_file_ids is None or
1005
child_ie.file_id in specific_file_ids):
1006
yield child_relpath, child_ie
1008
if child_ie.kind == 'directory':
1009
if parents is None or child_ie.file_id in parents:
1010
child_dirs.append((child_relpath+'/', child_ie))
1011
stack.extend(reversed(child_dirs))
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
1013
749
def entries(self):
1014
750
"""Return list of (path, ie) for all entries except the root.
1041
778
for name, child_ie in kids:
1042
child_path = osutils.pathjoin(parent_path, name)
779
child_path = os.path.join(parent_path, name)
1043
780
descend(child_ie, child_path)
1044
descend(self.root, u'')
781
descend(self.root, '')
1047
786
def __contains__(self, file_id):
1048
787
"""True if this entry contains a file with given id.
1050
789
>>> inv = Inventory()
1051
790
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1052
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
791
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1053
792
>>> '123' in inv
1055
794
>>> '456' in inv
1058
file_id = osutils.safe_file_id(file_id)
1059
return (file_id in self._byid)
797
return file_id in self._byid
1061
800
def __getitem__(self, file_id):
1062
801
"""Return the entry for given file_id.
1064
803
>>> inv = Inventory()
1065
804
>>> inv.add(InventoryFile('123123', 'hello.c', ROOT_ID))
1066
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT', sha1=None, len=None)
805
InventoryFile('123123', 'hello.c', parent_id='TREE_ROOT')
1067
806
>>> inv['123123'].name
1070
file_id = osutils.safe_file_id(file_id)
1072
810
return self._byid[file_id]
1073
811
except KeyError:
1074
# really we're passing an inventory, not a tree...
1075
raise errors.NoSuchId(self, file_id)
813
raise BzrError("can't look up file_id None")
815
raise BzrError("file_id {%s} not in inventory" % file_id)
1077
818
def get_file_kind(self, file_id):
1078
file_id = osutils.safe_file_id(file_id)
1079
819
return self._byid[file_id].kind
1081
821
def get_child(self, parent_id, filename):
1082
parent_id = osutils.safe_file_id(parent_id)
1083
822
return self[parent_id].children.get(filename)
1085
825
def add(self, entry):
1086
826
"""Add entry to inventory.
1091
831
Returns the new entry object.
1093
833
if entry.file_id in self._byid:
1094
raise errors.DuplicateFileId(entry.file_id,
1095
self._byid[entry.file_id])
1097
if entry.parent_id is None:
1098
assert self.root is None and len(self._byid) == 0
1099
self._set_root(entry)
834
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
1102
840
parent = self._byid[entry.parent_id]
1103
841
except KeyError:
1104
842
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1106
if entry.name in parent.children:
844
if parent.children.has_key(entry.name):
1107
845
raise BzrError("%s is already versioned" %
1108
osutils.pathjoin(self.id2path(parent.file_id), entry.name))
846
appendpath(self.id2path(parent.file_id), entry.name))
1110
848
self._byid[entry.file_id] = entry
1111
849
parent.children[entry.name] = entry
1114
def add_path(self, relpath, kind, file_id=None, parent_id=None):
853
def add_path(self, relpath, kind, file_id=None):
1115
854
"""Add entry from a path.
1117
856
The immediate parent must already be versioned.
1119
858
Returns the new entry object."""
859
from bzrlib.branch import gen_file_id
1121
parts = osutils.splitpath(relpath)
861
parts = bzrlib.osutils.splitpath(relpath)
1123
862
if len(parts) == 0:
1125
file_id = generate_ids.gen_root_id()
1127
file_id = osutils.safe_file_id(file_id)
1128
self.root = InventoryDirectory(file_id, '', None)
1129
self._byid = {self.root.file_id: self.root}
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)
1132
parent_path = parts[:-1]
1133
parent_id = self.path2id(parent_path)
1134
if parent_id is None:
1135
raise errors.NotVersionedError(path=parent_path)
1136
ie = make_entry(kind, parts[-1], parent_id, file_id)
880
raise BzrError("unknown kind %r" % kind)
1137
881
return self.add(ie)
1139
884
def __delitem__(self, file_id):
1140
885
"""Remove entry by id.
1142
887
>>> inv = Inventory()
1143
888
>>> inv.add(InventoryFile('123', 'foo.c', ROOT_ID))
1144
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT', sha1=None, len=None)
889
InventoryFile('123', 'foo.c', parent_id='TREE_ROOT')
1145
890
>>> '123' in inv
1147
892
>>> del inv['123']
1148
893
>>> '123' in inv
1151
file_id = osutils.safe_file_id(file_id)
1152
896
ie = self[file_id]
1154
assert ie.parent_id is None or \
1155
self[ie.parent_id].children[ie.name] == ie
898
assert self[ie.parent_id].children[ie.name] == ie
900
# TODO: Test deleting all children; maybe hoist to a separate
902
if ie.kind == 'directory':
903
for cie in ie.children.values():
904
del self[cie.file_id]
1157
907
del self._byid[file_id]
1158
if ie.parent_id is not None:
1159
del self[ie.parent_id].children[ie.name]
908
del self[ie.parent_id].children[ie.name]
1161
911
def __eq__(self, other):
1162
912
"""Compare two sets by comparing their contents.
1168
918
>>> i1.add(InventoryFile('123', 'foo', ROOT_ID))
1169
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
919
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1172
922
>>> i2.add(InventoryFile('123', 'foo', ROOT_ID))
1173
InventoryFile('123', 'foo', parent_id='TREE_ROOT', sha1=None, len=None)
923
InventoryFile('123', 'foo', parent_id='TREE_ROOT')
1177
927
if not isinstance(other, Inventory):
1178
928
return NotImplemented
930
if len(self._byid) != len(other._byid):
931
# shortcut: obviously not the same
1180
934
return self._byid == other._byid
1182
937
def __ne__(self, other):
1183
938
return not self.__eq__(other)
1185
941
def __hash__(self):
1186
942
raise ValueError('not hashable')
1188
def _iter_file_id_parents(self, file_id):
1189
"""Yield the parents of file_id up to the root."""
1190
file_id = osutils.safe_file_id(file_id)
1191
while file_id is not None:
1193
ie = self._byid[file_id]
1195
raise BzrError("file_id {%s} not found in inventory" % file_id)
1197
file_id = ie.parent_id
1199
945
def get_idpath(self, file_id):
1200
946
"""Return a list of file_ids for the path to an entry.
1204
950
is equal to the depth of the file in the tree, counting the
1205
951
root directory as depth 1.
1207
file_id = osutils.safe_file_id(file_id)
1209
for parent in self._iter_file_id_parents(file_id):
1210
p.insert(0, parent.file_id)
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
1213
964
def id2path(self, file_id):
1214
"""Return as a string the path to file_id.
965
"""Return as a list the path to file_id.
1216
967
>>> i = Inventory()
1217
968
>>> e = i.add(InventoryDirectory('src-id', 'src', ROOT_ID))
1218
969
>>> e = i.add(InventoryFile('foo-id', 'foo.c', parent_id='src-id'))
1219
>>> print i.id2path('foo-id')
970
>>> print i.id2path('foo-id').replace(os.sep, '/')
1222
file_id = osutils.safe_file_id(file_id)
1223
973
# get all names, skipping root
1224
return '/'.join(reversed(
1225
[parent.name for parent in
1226
self._iter_file_id_parents(file_id)][:-1]))
974
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
975
return os.sep.join(p)
1228
979
def path2id(self, name):
1229
980
"""Walk down through directories to return entry of last component.
1260
1006
return parent.file_id
1262
1009
def has_filename(self, names):
1263
1010
return bool(self.path2id(names))
1265
1013
def has_id(self, file_id):
1266
file_id = osutils.safe_file_id(file_id)
1267
return (file_id in self._byid)
1014
return self._byid.has_key(file_id)
1269
def remove_recursive_id(self, file_id):
1270
"""Remove file_id, and children, from the inventory.
1272
:param file_id: A file_id to remove.
1274
file_id = osutils.safe_file_id(file_id)
1275
to_find_delete = [self._byid[file_id]]
1277
while to_find_delete:
1278
ie = to_find_delete.pop()
1279
to_delete.append(ie.file_id)
1280
if ie.kind == 'directory':
1281
to_find_delete.extend(ie.children.values())
1282
for file_id in reversed(to_delete):
1284
del self._byid[file_id]
1285
if ie.parent_id is not None:
1286
del self[ie.parent_id].children[ie.name]
1288
1017
def rename(self, file_id, new_parent_id, new_name):
1289
1018
"""Move a file within the inventory.
1291
1020
This can change either the name, or the parent, or both.
1293
This does not move the working file.
1295
file_id = osutils.safe_file_id(file_id)
1022
This does not move the working file."""
1296
1023
if not is_valid_name(new_name):
1297
1024
raise BzrError("not an acceptable filename: %r" % new_name)
1316
1043
file_ie.name = new_name
1317
1044
file_ie.parent_id = new_parent_id
1319
def is_root(self, file_id):
1320
file_id = osutils.safe_file_id(file_id)
1321
return self.root is not None and file_id == self.root.file_id
1325
'directory':InventoryDirectory,
1326
'file':InventoryFile,
1327
'symlink':InventoryLink,
1330
def make_entry(kind, name, parent_id, file_id=None):
1331
"""Create an inventory entry.
1333
:param kind: the type of inventory entry to create.
1334
:param name: the basename of the entry.
1335
:param parent_id: the parent_id of the entry.
1336
:param file_id: the file_id to use. if None, one will be created.
1339
file_id = generate_ids.gen_file_id(name)
1341
file_id = osutils.safe_file_id(file_id)
1343
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1344
# keep them synchronised.
1345
# we dont import normalized_filename directly because we want to be
1346
# able to change the implementation at runtime for tests.
1347
norm_name, can_access = osutils.normalized_filename(name)
1348
if norm_name != name:
1352
# TODO: jam 20060701 This would probably be more useful
1353
# if the error was raised with the full path
1354
raise errors.InvalidNormalization(name)
1357
factory = entry_factory[kind]
1359
raise BzrError("unknown kind %r" % kind)
1360
return factory(file_id, name, parent_id)
1363
1049
_NAME_RE = None
1365
1051
def is_valid_name(name):
1366
1052
global _NAME_RE
1367
if _NAME_RE is None:
1053
if _NAME_RE == None:
1368
1054
_NAME_RE = re.compile(r'^[^/\\]+$')
1370
1056
return bool(_NAME_RE.match(name))