Merge lp:~jelmer/brz/mainline-ghosts into lp:brz
- mainline-ghosts
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~jelmer/brz/mainline-ghosts | ||||
Merge into: | lp:brz | ||||
Diff against target: |
493 lines (+142/-115) 13 files modified
breezy/annotate.py (+4/-4) breezy/bzr/groupcompress_repo.py (+1/-1) breezy/bzr/remote.py (+13/-23) breezy/bzr/vf_repository.py (+21/-36) breezy/check.py (+1/-1) breezy/log.py (+47/-23) breezy/plugins/stats/cmds.py (+2/-2) breezy/plugins/weave_fmt/repository.py (+0/-5) breezy/repository.py (+21/-3) breezy/status.py (+2/-14) breezy/tests/blackbox/test_log.py (+5/-3) breezy/tests/per_repository/test_repository.py (+19/-0) doc/en/release-notes/brz-3.0.txt (+6/-0) |
||||
To merge this branch: | bzr merge lp:~jelmer/brz/mainline-ghosts | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman | Pending | ||
Review via email: mp+326120@code.launchpad.net |
This proposal has been superseded by a proposal from 2017-06-22.
Commit message
Don't apply matcher logic unless --match is specified in ``bzr log``.
Description of the change
Quick performance fix for ``bzr log`` - don't run regexes over all logs that are printed *unless* one of the --match options is specified.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/annotate.py' |
2 | --- breezy/annotate.py 2017-06-04 18:09:30 +0000 |
3 | +++ breezy/annotate.py 2017-06-22 01:54:53 +0000 |
4 | @@ -188,10 +188,10 @@ |
5 | revision_id_to_revno[CURRENT_REVISION] = ( |
6 | "%d?" % (branch.revno() + 1),) |
7 | revisions[CURRENT_REVISION] = current_rev |
8 | - revision_ids = [o for o in revision_ids if |
9 | - repository.has_revision(o)] |
10 | - revisions.update((r.revision_id, r) for r in |
11 | - repository.get_revisions(revision_ids)) |
12 | + revisions.update( |
13 | + entry for entry in |
14 | + repository.iter_revisions(revision_ids) |
15 | + if entry[1] is not None) |
16 | for origin, text in annotations: |
17 | text = text.rstrip('\r\n') |
18 | if origin == last_origin: |
19 | |
20 | === modified file 'breezy/bzr/groupcompress_repo.py' |
21 | --- breezy/bzr/groupcompress_repo.py 2017-06-22 00:21:13 +0000 |
22 | +++ breezy/bzr/groupcompress_repo.py 2017-06-22 01:54:53 +0000 |
23 | @@ -1136,7 +1136,7 @@ |
24 | raise AssertionError() |
25 | vf = self.revisions |
26 | if revisions_iterator is None: |
27 | - revisions_iterator = self._iter_revisions(self.all_revision_ids()) |
28 | + revisions_iterator = self.iter_revisions(self.all_revision_ids()) |
29 | for revid, revision in revisions_iterator: |
30 | if revision is None: |
31 | pass |
32 | |
33 | === modified file 'breezy/bzr/remote.py' |
34 | --- breezy/bzr/remote.py 2017-06-22 00:23:56 +0000 |
35 | +++ breezy/bzr/remote.py 2017-06-22 01:54:53 +0000 |
36 | @@ -2658,39 +2658,29 @@ |
37 | yield serializer.read_revision_from_string("".join(chunks)) |
38 | |
39 | @needs_read_lock |
40 | - def get_revisions(self, revision_ids): |
41 | + def iter_revisions(self, revision_ids): |
42 | for rev_id in revision_ids: |
43 | if not rev_id or not isinstance(rev_id, bytes): |
44 | raise errors.InvalidRevisionId( |
45 | revision_id=rev_id, branch=self) |
46 | try: |
47 | missing = set(revision_ids) |
48 | - revs = {} |
49 | for rev in self._iter_revisions_rpc(revision_ids): |
50 | missing.remove(rev.revision_id) |
51 | - revs[rev.revision_id] = rev |
52 | + yield (rev.revision_id, rev) |
53 | + for fallback in self._fallback_repositories: |
54 | + if not missing: |
55 | + break |
56 | + for (revid, rev) in fallback.iter_revisions(missing): |
57 | + if rev is not None: |
58 | + yield (revid, rev) |
59 | + missing.remove(revid) |
60 | + for revid in missing: |
61 | + yield (revid, None) |
62 | except errors.UnknownSmartMethod: |
63 | self._ensure_real() |
64 | - return self._real_repository.get_revisions(revision_ids) |
65 | - for fallback in self._fallback_repositories: |
66 | - if not missing: |
67 | - break |
68 | - for revid in list(missing): |
69 | - # XXX JRV 2011-11-20: It would be nice if there was a |
70 | - # public method on Repository that could be used to query |
71 | - # for revision objects *without* failing completely if one |
72 | - # was missing. There is VersionedFileRepository._iter_revisions, |
73 | - # but unfortunately that's private and not provided by |
74 | - # all repository implementations. |
75 | - try: |
76 | - revs[revid] = fallback.get_revision(revid) |
77 | - except errors.NoSuchRevision: |
78 | - pass |
79 | - else: |
80 | - missing.remove(revid) |
81 | - if missing: |
82 | - raise errors.NoSuchRevision(self, list(missing)[0]) |
83 | - return [revs[revid] for revid in revision_ids] |
84 | + for entry in self._real_repository.iter_revisions(revision_ids): |
85 | + yield entry |
86 | |
87 | def supports_rich_root(self): |
88 | return self._format.rich_root_data |
89 | |
90 | === modified file 'breezy/bzr/vf_repository.py' |
91 | --- breezy/bzr/vf_repository.py 2017-06-22 00:21:13 +0000 |
92 | +++ breezy/bzr/vf_repository.py 2017-06-22 01:54:53 +0000 |
93 | @@ -1102,28 +1102,9 @@ |
94 | be used by reconcile, or reconcile-alike commands that are correcting |
95 | or testing the revision graph. |
96 | """ |
97 | - return self._get_revisions([revision_id])[0] |
98 | - |
99 | - @needs_read_lock |
100 | - def get_revisions(self, revision_ids): |
101 | - """Get many revisions at once. |
102 | - |
103 | - Repositories that need to check data on every revision read should |
104 | - subclass this method. |
105 | - """ |
106 | - return self._get_revisions(revision_ids) |
107 | - |
108 | - @needs_read_lock |
109 | - def _get_revisions(self, revision_ids): |
110 | - """Core work logic to get many revisions without sanity checks.""" |
111 | - revs = {} |
112 | - for revid, rev in self._iter_revisions(revision_ids): |
113 | - if rev is None: |
114 | - raise errors.NoSuchRevision(self, revid) |
115 | - revs[revid] = rev |
116 | - return [revs[revid] for revid in revision_ids] |
117 | - |
118 | - def _iter_revisions(self, revision_ids): |
119 | + return self.get_revisions([revision_id])[0] |
120 | + |
121 | + def iter_revisions(self, revision_ids): |
122 | """Iterate over revision objects. |
123 | |
124 | :param revision_ids: An iterable of revisions to examine. None may be |
125 | @@ -1133,19 +1114,23 @@ |
126 | :return: An iterator of (revid, revision) tuples. Absent revisions ( |
127 | those asked for but not available) are returned as (revid, None). |
128 | """ |
129 | - for rev_id in revision_ids: |
130 | - if not rev_id or not isinstance(rev_id, bytes): |
131 | - raise errors.InvalidRevisionId(revision_id=rev_id, branch=self) |
132 | - keys = [(key,) for key in revision_ids] |
133 | - stream = self.revisions.get_record_stream(keys, 'unordered', True) |
134 | - for record in stream: |
135 | - revid = record.key[0] |
136 | - if record.storage_kind == 'absent': |
137 | - yield (revid, None) |
138 | - else: |
139 | - text = record.get_bytes_as('fulltext') |
140 | - rev = self._serializer.read_revision_from_string(text) |
141 | - yield (revid, rev) |
142 | + self.lock_read() |
143 | + try: |
144 | + for rev_id in revision_ids: |
145 | + if not rev_id or not isinstance(rev_id, bytes): |
146 | + raise errors.InvalidRevisionId(revision_id=rev_id, branch=self) |
147 | + keys = [(key,) for key in revision_ids] |
148 | + stream = self.revisions.get_record_stream(keys, 'unordered', True) |
149 | + for record in stream: |
150 | + revid = record.key[0] |
151 | + if record.storage_kind == 'absent': |
152 | + yield (revid, None) |
153 | + else: |
154 | + text = record.get_bytes_as('fulltext') |
155 | + rev = self._serializer.read_revision_from_string(text) |
156 | + yield (revid, rev) |
157 | + finally: |
158 | + self.unlock() |
159 | |
160 | @needs_write_lock |
161 | def add_signature_text(self, revision_id, signature): |
162 | @@ -1677,7 +1662,7 @@ |
163 | raise AssertionError() |
164 | vf = self.revisions |
165 | if revisions_iterator is None: |
166 | - revisions_iterator = self._iter_revisions(self.all_revision_ids()) |
167 | + revisions_iterator = self.iter_revisions(self.all_revision_ids()) |
168 | for revid, revision in revisions_iterator: |
169 | if revision is None: |
170 | pass |
171 | |
172 | === modified file 'breezy/check.py' |
173 | --- breezy/check.py 2017-06-22 00:21:13 +0000 |
174 | +++ breezy/check.py 2017-06-22 01:54:53 +0000 |
175 | @@ -181,7 +181,7 @@ |
176 | |
177 | def check_revisions(self): |
178 | """Scan revisions, checking data directly available as we go.""" |
179 | - revision_iterator = self.repository._iter_revisions( |
180 | + revision_iterator = self.repository.iter_revisions( |
181 | self.repository.all_revision_ids()) |
182 | revision_iterator = self._check_revisions(revision_iterator) |
183 | # We read the all revisions here: |
184 | |
185 | === modified file 'breezy/log.py' |
186 | --- breezy/log.py 2017-06-05 20:48:31 +0000 |
187 | +++ breezy/log.py 2017-06-22 01:54:53 +0000 |
188 | @@ -409,8 +409,12 @@ |
189 | |
190 | # Find and print the interesting revisions |
191 | generator = self._generator_factory(self.branch, rqst) |
192 | - for lr in generator.iter_log_revisions(): |
193 | - lf.log_revision(lr) |
194 | + try: |
195 | + for lr in generator.iter_log_revisions(): |
196 | + lf.log_revision(lr) |
197 | + except errors.GhostRevisionUnusableHere: |
198 | + raise errors.BzrCommandError( |
199 | + gettext('Further revision history missing.')) |
200 | lf.show_advice() |
201 | |
202 | def _generator_factory(self, branch, rqst): |
203 | @@ -456,6 +460,8 @@ |
204 | continue |
205 | if omit_merges and len(rev.parent_ids) > 1: |
206 | continue |
207 | + if rev is None: |
208 | + raise errors.GhostRevisionUnusableHere(rev_id) |
209 | if diff_type is None: |
210 | diff = None |
211 | else: |
212 | @@ -723,6 +729,7 @@ |
213 | :param exclude_common_ancestry: Whether the start_rev_id should be part of |
214 | the iterated revisions. |
215 | :return: An iterator of (revision_id, dotted_revno, merge_depth) tuples. |
216 | + dotted_revno will be None for ghosts |
217 | :raises _StartNotLinearAncestor: if a start_rev_id is specified but |
218 | is not found walking the left-hand history |
219 | """ |
220 | @@ -731,27 +738,46 @@ |
221 | graph = repo.get_graph() |
222 | if start_rev_id is None and end_rev_id is None: |
223 | cur_revno = br_revno |
224 | - for revision_id in graph.iter_lefthand_ancestry(br_rev_id, |
225 | - (_mod_revision.NULL_REVISION,)): |
226 | - yield revision_id, str(cur_revno), 0 |
227 | - cur_revno -= 1 |
228 | + graph_iter = graph.iter_lefthand_ancestry(br_rev_id, |
229 | + (_mod_revision.NULL_REVISION,)) |
230 | + while True: |
231 | + try: |
232 | + revision_id = graph_iter.next() |
233 | + except StopIteration: |
234 | + raise |
235 | + except errors.RevisionNotPresent as e: |
236 | + # Oops, a ghost. |
237 | + yield e.revision_id, None, None |
238 | + break |
239 | + else: |
240 | + yield revision_id, str(cur_revno), 0 |
241 | + cur_revno -= 1 |
242 | else: |
243 | if end_rev_id is None: |
244 | end_rev_id = br_rev_id |
245 | found_start = start_rev_id is None |
246 | - for revision_id in graph.iter_lefthand_ancestry(end_rev_id, |
247 | - (_mod_revision.NULL_REVISION,)): |
248 | - revno_str = _compute_revno_str(branch, revision_id) |
249 | - if not found_start and revision_id == start_rev_id: |
250 | - if not exclude_common_ancestry: |
251 | + graph_iter = graph.iter_lefthand_ancestry(end_rev_id, |
252 | + (_mod_revision.NULL_REVISION,)) |
253 | + while True: |
254 | + try: |
255 | + revision_id = graph_iter.next() |
256 | + except StopIteration: |
257 | + break |
258 | + except errors.RevisionNotPresent as e: |
259 | + # Oops, a ghost. |
260 | + yield e.revision_id, None, None |
261 | + break |
262 | + else: |
263 | + revno_str = _compute_revno_str(branch, revision_id) |
264 | + if not found_start and revision_id == start_rev_id: |
265 | + if not exclude_common_ancestry: |
266 | + yield revision_id, revno_str, 0 |
267 | + found_start = True |
268 | + break |
269 | + else: |
270 | yield revision_id, revno_str, 0 |
271 | - found_start = True |
272 | - break |
273 | - else: |
274 | - yield revision_id, revno_str, 0 |
275 | - else: |
276 | - if not found_start: |
277 | - raise _StartNotLinearAncestor() |
278 | + if not found_start: |
279 | + raise _StartNotLinearAncestor() |
280 | |
281 | |
282 | def _graph_view_revisions(branch, start_rev_id, end_rev_id, |
283 | @@ -861,7 +887,7 @@ |
284 | :return: An iterator over lists of ((rev_id, revno, merge_depth), rev, |
285 | delta). |
286 | """ |
287 | - if match is None: |
288 | + if not match: |
289 | return log_rev_iterator |
290 | searchRE = [(k, [re.compile(x, re.IGNORECASE) for x in v]) |
291 | for k, v in match.items()] |
292 | @@ -998,10 +1024,8 @@ |
293 | for revs in log_rev_iterator: |
294 | # r = revision_id, n = revno, d = merge depth |
295 | revision_ids = [view[0] for view, _, _ in revs] |
296 | - revisions = repository.get_revisions(revision_ids) |
297 | - revs = [(rev[0], revision, rev[2]) for rev, revision in |
298 | - zip(revs, revisions)] |
299 | - yield revs |
300 | + revisions = dict(repository.iter_revisions(revision_ids)) |
301 | + yield [(rev[0], revisions[rev[0][0]], rev[2]) for rev in revs] |
302 | |
303 | |
304 | def _make_batch_filter(branch, generate_delta, search, log_rev_iterator): |
305 | |
306 | === modified file 'breezy/plugins/stats/cmds.py' |
307 | --- breezy/plugins/stats/cmds.py 2017-06-05 21:24:34 +0000 |
308 | +++ breezy/plugins/stats/cmds.py 2017-06-22 01:54:53 +0000 |
309 | @@ -148,8 +148,8 @@ |
310 | pb = ui.ui_factory.nested_progress_bar() |
311 | try: |
312 | trace.note('getting revisions') |
313 | - revisions = a_repo.get_revisions(revids) |
314 | - for count, rev in enumerate(revisions): |
315 | + revisions = a_repo.iter_revisions(revids) |
316 | + for count, (revid, rev) in enumerate(revisions): |
317 | pb.update('checking', count, len(revids)) |
318 | for author in rev.get_apparent_authors(): |
319 | # XXX: There is a chance sometimes with svn imports that the |
320 | |
321 | === modified file 'breezy/plugins/weave_fmt/repository.py' |
322 | --- breezy/plugins/weave_fmt/repository.py 2017-06-14 23:29:06 +0000 |
323 | +++ breezy/plugins/weave_fmt/repository.py 2017-06-22 01:54:53 +0000 |
324 | @@ -166,11 +166,6 @@ |
325 | self.start_write_group() |
326 | return result |
327 | |
328 | - @needs_read_lock |
329 | - def get_revisions(self, revision_ids): |
330 | - revs = self._get_revisions(revision_ids) |
331 | - return revs |
332 | - |
333 | def _inventory_add_lines(self, revision_id, parents, lines, |
334 | check_content=True): |
335 | """Store lines in inv_vf and return the sha1 of the inventory.""" |
336 | |
337 | === modified file 'breezy/repository.py' |
338 | --- breezy/repository.py 2017-06-20 01:35:59 +0000 |
339 | +++ breezy/repository.py 2017-06-22 01:54:53 +0000 |
340 | @@ -822,11 +822,29 @@ |
341 | |
342 | def get_revisions(self, revision_ids): |
343 | """Get many revisions at once. |
344 | - |
345 | - Repositories that need to check data on every revision read should |
346 | + |
347 | + Repositories that need to check data on every revision read should |
348 | subclass this method. |
349 | """ |
350 | - raise NotImplementedError(self.get_revisions) |
351 | + revs = {} |
352 | + for revid, rev in self.iter_revisions(revision_ids): |
353 | + if rev is None: |
354 | + raise errors.NoSuchRevision(self, revid) |
355 | + revs[revid] = rev |
356 | + return [revs[revid] for revid in revision_ids] |
357 | + |
358 | + def iter_revisions(self, revision_ids): |
359 | + """Iterate over revision objects. |
360 | + |
361 | + :param revision_ids: An iterable of revisions to examine. None may be |
362 | + passed to request all revisions known to the repository. Note that |
363 | + not all repositories can find unreferenced revisions; for those |
364 | + repositories only referenced ones will be returned. |
365 | + :return: An iterator of (revid, revision) tuples. Absent revisions ( |
366 | + those asked for but not available) are returned as (revid, None). |
367 | + N.B.: Revisions are not necessarily yielded in order. |
368 | + """ |
369 | + raise NotImplementedError(self.iter_revisions) |
370 | |
371 | def get_deltas_for_revisions(self, revisions, specific_fileids=None): |
372 | """Produce a generator of revision deltas. |
373 | |
374 | === modified file 'breezy/status.py' |
375 | --- breezy/status.py 2017-05-25 01:35:55 +0000 |
376 | +++ breezy/status.py 2017-06-22 01:54:53 +0000 |
377 | @@ -297,7 +297,7 @@ |
378 | log_formatter = log.LineLogFormatter(to_file) |
379 | for merge in pending: |
380 | try: |
381 | - rev = branch.repository.get_revisions([merge])[0] |
382 | + rev = branch.repository.get_revision(merge) |
383 | except errors.NoSuchRevision: |
384 | # If we are missing a revision, just print out the revision id |
385 | to_file.write(first_prefix + '(ghost) ' + merge + '\n') |
386 | @@ -316,19 +316,7 @@ |
387 | merge_extra.discard(_mod_revision.NULL_REVISION) |
388 | |
389 | # Get a handle to all of the revisions we will need |
390 | - try: |
391 | - revisions = dict((rev.revision_id, rev) for rev in |
392 | - branch.repository.get_revisions(merge_extra)) |
393 | - except errors.NoSuchRevision: |
394 | - # One of the sub nodes is a ghost, check each one |
395 | - revisions = {} |
396 | - for revision_id in merge_extra: |
397 | - try: |
398 | - rev = branch.repository.get_revisions([revision_id])[0] |
399 | - except errors.NoSuchRevision: |
400 | - revisions[revision_id] = None |
401 | - else: |
402 | - revisions[revision_id] = rev |
403 | + revisions = dict(branch.repository.iter_revisions(merge_extra)) |
404 | |
405 | # Display the revisions brought in by this merge. |
406 | rev_id_iterator = _get_sorted_revisions(merge, merge_extra, |
407 | |
408 | === modified file 'breezy/tests/blackbox/test_log.py' |
409 | --- breezy/tests/blackbox/test_log.py 2017-06-10 00:17:06 +0000 |
410 | +++ breezy/tests/blackbox/test_log.py 2017-06-22 01:54:53 +0000 |
411 | @@ -995,16 +995,18 @@ |
412 | self.assertLogRevnos([], ["2", "1"]) |
413 | |
414 | def test_log_range_open_begin(self): |
415 | - self.knownFailure("log with ghosts fails. bug #726466") |
416 | (stdout, stderr) = self.run_bzr(['log', '-r..2'], retcode=3) |
417 | self.assertEqual(["2", "1"], |
418 | [r.revno for r in self.get_captured_revisions()]) |
419 | - self.assertEqual("brz: ERROR: Further revision history missing.", stderr) |
420 | + self.assertEqual("brz: ERROR: Further revision history missing.\n", |
421 | + stderr) |
422 | |
423 | def test_log_range_open_end(self): |
424 | self.assertLogRevnos(["-r1.."], ["2", "1"]) |
425 | |
426 | + |
427 | class TestLogMatch(TestLogWithLogCatcher): |
428 | + |
429 | def prepare_tree(self): |
430 | tree = self.make_branch_and_tree('') |
431 | self.build_tree( |
432 | @@ -1013,7 +1015,7 @@ |
433 | tree.commit(message='message1', committer='committer1', authors=['author1']) |
434 | tree.add('goodbye.txt') |
435 | tree.commit(message='message2', committer='committer2', authors=['author2']) |
436 | - |
437 | + |
438 | def test_message(self): |
439 | self.prepare_tree() |
440 | self.assertLogRevnos(["-m", "message1"], ["1"]) |
441 | |
442 | === modified file 'breezy/tests/per_repository/test_repository.py' |
443 | --- breezy/tests/per_repository/test_repository.py 2017-06-11 14:07:05 +0000 |
444 | +++ breezy/tests/per_repository/test_repository.py 2017-06-22 01:54:53 +0000 |
445 | @@ -424,6 +424,25 @@ |
446 | self.assertEqual(revision.revision_id, revision_id) |
447 | self.assertEqual(revision, repo.get_revision(revision_id)) |
448 | |
449 | + def test_iter_revisions(self): |
450 | + tree = self.make_branch_and_tree('.') |
451 | + tree.commit('initial empty commit', rev_id='a-rev', |
452 | + allow_pointless=True) |
453 | + tree.commit('second empty commit', rev_id='b-rev', |
454 | + allow_pointless=True) |
455 | + tree.commit('third empty commit', rev_id='c-rev', |
456 | + allow_pointless=True) |
457 | + repo = tree.branch.repository |
458 | + revision_ids = ['a-rev', 'c-rev', 'b-rev', 'd-rev'] |
459 | + revid_with_rev = repo.iter_revisions(revision_ids) |
460 | + self.assertEqual( |
461 | + set((revid, rev.revision_id if rev is not None else None) |
462 | + for (revid, rev) in revid_with_rev), |
463 | + {('a-rev', 'a-rev'), |
464 | + ('b-rev', 'b-rev'), |
465 | + ('c-rev', 'c-rev'), |
466 | + ('d-rev', None)}) |
467 | + |
468 | def test_root_entry_has_revision(self): |
469 | tree = self.make_branch_and_tree('.') |
470 | tree.commit('message', rev_id='rev_id') |
471 | |
472 | === modified file 'doc/en/release-notes/brz-3.0.txt' |
473 | --- doc/en/release-notes/brz-3.0.txt 2017-06-22 00:21:13 +0000 |
474 | +++ doc/en/release-notes/brz-3.0.txt 2017-06-22 01:54:53 +0000 |
475 | @@ -104,6 +104,9 @@ |
476 | * Support ``brz commit -x`` in combination with iter_changes. |
477 | (Jelmer Vernooij, #796582, #403811, #694946, #268135, #299879) |
478 | |
479 | +* Print a proper error when encountering ghost revisions in |
480 | + mainline in ``bzr log``. (Jelmer Vernooij, #726466) |
481 | + |
482 | Documentation |
483 | ************* |
484 | |
485 | @@ -151,6 +154,9 @@ |
486 | * ``Repository.get_revisions`` no longer accepts ``None`` as |
487 | argument. (Jelmer Vernooij) |
488 | |
489 | + * A new ``Repository.iter_revisions`` method has been added. |
490 | + (Jelmer Vernooij) |
491 | + |
492 | Internals |
493 | ********* |
494 |