##// END OF EJS Templates
comments: fix minor spelling issues found with spell checker
Mads Kiilerich -
r20549:2025315c default
parent child Browse files
Show More
@@ -1,706 +1,706 b''
1 1 # minirst.py - minimal reStructuredText parser
2 2 #
3 3 # Copyright 2009, 2010 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """simplified reStructuredText parser.
9 9
10 10 This parser knows just enough about reStructuredText to parse the
11 11 Mercurial docstrings.
12 12
13 13 It cheats in a major way: nested blocks are not really nested. They
14 14 are just indented blocks that look like they are nested. This relies
15 15 on the user to keep the right indentation for the blocks.
16 16
17 17 Remember to update http://mercurial.selenic.com/wiki/HelpStyleGuide
18 18 when adding support for new constructs.
19 19 """
20 20
21 21 import re
22 22 import util, encoding
23 23 from i18n import _
24 24
25 25 import cgi
26 26
27 27 def section(s):
28 28 return "%s\n%s\n\n" % (s, "\"" * encoding.colwidth(s))
29 29
30 30 def subsection(s):
31 31 return "%s\n%s\n\n" % (s, '=' * encoding.colwidth(s))
32 32
33 33 def subsubsection(s):
34 34 return "%s\n%s\n\n" % (s, "-" * encoding.colwidth(s))
35 35
36 36 def subsubsubsection(s):
37 37 return "%s\n%s\n\n" % (s, "." * encoding.colwidth(s))
38 38
39 39 def replace(text, substs):
40 40 '''
41 41 Apply a list of (find, replace) pairs to a text.
42 42
43 43 >>> replace("foo bar", [('f', 'F'), ('b', 'B')])
44 44 'Foo Bar'
45 45 >>> encoding.encoding = 'latin1'
46 46 >>> replace('\\x81\\\\', [('\\\\', '/')])
47 47 '\\x81/'
48 48 >>> encoding.encoding = 'shiftjis'
49 49 >>> replace('\\x81\\\\', [('\\\\', '/')])
50 50 '\\x81\\\\'
51 51 '''
52 52
53 53 # some character encodings (cp932 for Japanese, at least) use
54 54 # ASCII characters other than control/alphabet/digit as a part of
55 55 # multi-bytes characters, so direct replacing with such characters
56 56 # on strings in local encoding causes invalid byte sequences.
57 57 utext = text.decode(encoding.encoding)
58 58 for f, t in substs:
59 59 utext = utext.replace(f, t)
60 60 return utext.encode(encoding.encoding)
61 61
62 62 _blockre = re.compile(r"\n(?:\s*\n)+")
63 63
64 64 def findblocks(text):
65 65 """Find continuous blocks of lines in text.
66 66
67 67 Returns a list of dictionaries representing the blocks. Each block
68 68 has an 'indent' field and a 'lines' field.
69 69 """
70 70 blocks = []
71 71 for b in _blockre.split(text.lstrip('\n').rstrip()):
72 72 lines = b.splitlines()
73 73 if lines:
74 74 indent = min((len(l) - len(l.lstrip())) for l in lines)
75 75 lines = [l[indent:] for l in lines]
76 76 blocks.append(dict(indent=indent, lines=lines))
77 77 return blocks
78 78
79 79 def findliteralblocks(blocks):
80 80 """Finds literal blocks and adds a 'type' field to the blocks.
81 81
82 82 Literal blocks are given the type 'literal', all other blocks are
83 83 given type the 'paragraph'.
84 84 """
85 85 i = 0
86 86 while i < len(blocks):
87 87 # Searching for a block that looks like this:
88 88 #
89 89 # +------------------------------+
90 90 # | paragraph |
91 91 # | (ends with "::") |
92 92 # +------------------------------+
93 93 # +---------------------------+
94 94 # | indented literal block |
95 95 # +---------------------------+
96 96 blocks[i]['type'] = 'paragraph'
97 97 if blocks[i]['lines'][-1].endswith('::') and i + 1 < len(blocks):
98 98 indent = blocks[i]['indent']
99 99 adjustment = blocks[i + 1]['indent'] - indent
100 100
101 101 if blocks[i]['lines'] == ['::']:
102 102 # Expanded form: remove block
103 103 del blocks[i]
104 104 i -= 1
105 105 elif blocks[i]['lines'][-1].endswith(' ::'):
106 106 # Partially minimized form: remove space and both
107 107 # colons.
108 108 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-3]
109 109 elif len(blocks[i]['lines']) == 1 and \
110 110 blocks[i]['lines'][0].lstrip(' ').startswith('.. ') and \
111 111 blocks[i]['lines'][0].find(' ', 3) == -1:
112 # directive on its onw line, not a literal block
112 # directive on its own line, not a literal block
113 113 i += 1
114 114 continue
115 115 else:
116 116 # Fully minimized form: remove just one colon.
117 117 blocks[i]['lines'][-1] = blocks[i]['lines'][-1][:-1]
118 118
119 119 # List items are formatted with a hanging indent. We must
120 120 # correct for this here while we still have the original
121 121 # information on the indentation of the subsequent literal
122 122 # blocks available.
123 123 m = _bulletre.match(blocks[i]['lines'][0])
124 124 if m:
125 125 indent += m.end()
126 126 adjustment -= m.end()
127 127
128 128 # Mark the following indented blocks.
129 129 while i + 1 < len(blocks) and blocks[i + 1]['indent'] > indent:
130 130 blocks[i + 1]['type'] = 'literal'
131 131 blocks[i + 1]['indent'] -= adjustment
132 132 i += 1
133 133 i += 1
134 134 return blocks
135 135
136 136 _bulletre = re.compile(r'(-|[0-9A-Za-z]+\.|\(?[0-9A-Za-z]+\)|\|) ')
137 137 _optionre = re.compile(r'^(-([a-zA-Z0-9]), )?(--[a-z0-9-]+)'
138 138 r'((.*) +)(.*)$')
139 139 _fieldre = re.compile(r':(?![: ])([^:]*)(?<! ):[ ]+(.*)')
140 140 _definitionre = re.compile(r'[^ ]')
141 141 _tablere = re.compile(r'(=+\s+)*=+')
142 142
143 143 def splitparagraphs(blocks):
144 144 """Split paragraphs into lists."""
145 145 # Tuples with (list type, item regexp, single line items?). Order
146 146 # matters: definition lists has the least specific regexp and must
147 147 # come last.
148 148 listtypes = [('bullet', _bulletre, True),
149 149 ('option', _optionre, True),
150 150 ('field', _fieldre, True),
151 151 ('definition', _definitionre, False)]
152 152
153 153 def match(lines, i, itemre, singleline):
154 154 """Does itemre match an item at line i?
155 155
156 156 A list item can be followed by an indented line or another list
157 157 item (but only if singleline is True).
158 158 """
159 159 line1 = lines[i]
160 160 line2 = i + 1 < len(lines) and lines[i + 1] or ''
161 161 if not itemre.match(line1):
162 162 return False
163 163 if singleline:
164 164 return line2 == '' or line2[0] == ' ' or itemre.match(line2)
165 165 else:
166 166 return line2.startswith(' ')
167 167
168 168 i = 0
169 169 while i < len(blocks):
170 170 if blocks[i]['type'] == 'paragraph':
171 171 lines = blocks[i]['lines']
172 172 for type, itemre, singleline in listtypes:
173 173 if match(lines, 0, itemre, singleline):
174 174 items = []
175 175 for j, line in enumerate(lines):
176 176 if match(lines, j, itemre, singleline):
177 177 items.append(dict(type=type, lines=[],
178 178 indent=blocks[i]['indent']))
179 179 items[-1]['lines'].append(line)
180 180 blocks[i:i + 1] = items
181 181 break
182 182 i += 1
183 183 return blocks
184 184
185 185 _fieldwidth = 14
186 186
187 187 def updatefieldlists(blocks):
188 188 """Find key for field lists."""
189 189 i = 0
190 190 while i < len(blocks):
191 191 if blocks[i]['type'] != 'field':
192 192 i += 1
193 193 continue
194 194
195 195 j = i
196 196 while j < len(blocks) and blocks[j]['type'] == 'field':
197 197 m = _fieldre.match(blocks[j]['lines'][0])
198 198 key, rest = m.groups()
199 199 blocks[j]['lines'][0] = rest
200 200 blocks[j]['key'] = key
201 201 j += 1
202 202
203 203 i = j + 1
204 204
205 205 return blocks
206 206
207 207 def updateoptionlists(blocks):
208 208 i = 0
209 209 while i < len(blocks):
210 210 if blocks[i]['type'] != 'option':
211 211 i += 1
212 212 continue
213 213
214 214 optstrwidth = 0
215 215 j = i
216 216 while j < len(blocks) and blocks[j]['type'] == 'option':
217 217 m = _optionre.match(blocks[j]['lines'][0])
218 218
219 219 shortoption = m.group(2)
220 220 group3 = m.group(3)
221 221 longoption = group3[2:].strip()
222 222 desc = m.group(6).strip()
223 223 longoptionarg = m.group(5).strip()
224 224 blocks[j]['lines'][0] = desc
225 225
226 226 noshortop = ''
227 227 if not shortoption:
228 228 noshortop = ' '
229 229
230 230 opt = "%s%s" % (shortoption and "-%s " % shortoption or '',
231 231 ("%s--%s %s") % (noshortop, longoption,
232 232 longoptionarg))
233 233 opt = opt.rstrip()
234 234 blocks[j]['optstr'] = opt
235 235 optstrwidth = max(optstrwidth, encoding.colwidth(opt))
236 236 j += 1
237 237
238 238 for block in blocks[i:j]:
239 239 block['optstrwidth'] = optstrwidth
240 240 i = j + 1
241 241 return blocks
242 242
243 243 def prunecontainers(blocks, keep):
244 244 """Prune unwanted containers.
245 245
246 246 The blocks must have a 'type' field, i.e., they should have been
247 247 run through findliteralblocks first.
248 248 """
249 249 pruned = []
250 250 i = 0
251 251 while i + 1 < len(blocks):
252 252 # Searching for a block that looks like this:
253 253 #
254 254 # +-------+---------------------------+
255 255 # | ".. container ::" type |
256 256 # +---+ |
257 257 # | blocks |
258 258 # +-------------------------------+
259 259 if (blocks[i]['type'] == 'paragraph' and
260 260 blocks[i]['lines'][0].startswith('.. container::')):
261 261 indent = blocks[i]['indent']
262 262 adjustment = blocks[i + 1]['indent'] - indent
263 263 containertype = blocks[i]['lines'][0][15:]
264 264 prune = containertype not in keep
265 265 if prune:
266 266 pruned.append(containertype)
267 267
268 268 # Always delete "..container:: type" block
269 269 del blocks[i]
270 270 j = i
271 271 i -= 1
272 272 while j < len(blocks) and blocks[j]['indent'] > indent:
273 273 if prune:
274 274 del blocks[j]
275 275 else:
276 276 blocks[j]['indent'] -= adjustment
277 277 j += 1
278 278 i += 1
279 279 return blocks, pruned
280 280
281 281 _sectionre = re.compile(r"""^([-=`:.'"~^_*+#])\1+$""")
282 282
283 283 def findtables(blocks):
284 284 '''Find simple tables
285 285
286 286 Only simple one-line table elements are supported
287 287 '''
288 288
289 289 for block in blocks:
290 290 # Searching for a block that looks like this:
291 291 #
292 292 # === ==== ===
293 293 # A B C
294 294 # === ==== === <- optional
295 295 # 1 2 3
296 296 # x y z
297 297 # === ==== ===
298 298 if (block['type'] == 'paragraph' and
299 299 len(block['lines']) > 2 and
300 300 _tablere.match(block['lines'][0]) and
301 301 block['lines'][0] == block['lines'][-1]):
302 302 block['type'] = 'table'
303 303 block['header'] = False
304 304 div = block['lines'][0]
305 305
306 306 # column markers are ASCII so we can calculate column
307 307 # position in bytes
308 308 columns = [x for x in xrange(len(div))
309 309 if div[x] == '=' and (x == 0 or div[x - 1] == ' ')]
310 310 rows = []
311 311 for l in block['lines'][1:-1]:
312 312 if l == div:
313 313 block['header'] = True
314 314 continue
315 315 row = []
316 316 # we measure columns not in bytes or characters but in
317 317 # colwidth which makes things tricky
318 318 pos = columns[0] # leading whitespace is bytes
319 319 for n, start in enumerate(columns):
320 320 if n + 1 < len(columns):
321 321 width = columns[n + 1] - start
322 322 v = encoding.getcols(l, pos, width) # gather columns
323 323 pos += len(v) # calculate byte position of end
324 324 row.append(v.strip())
325 325 else:
326 326 row.append(l[pos:].strip())
327 327 rows.append(row)
328 328
329 329 block['table'] = rows
330 330
331 331 return blocks
332 332
333 333 def findsections(blocks):
334 334 """Finds sections.
335 335
336 336 The blocks must have a 'type' field, i.e., they should have been
337 337 run through findliteralblocks first.
338 338 """
339 339 for block in blocks:
340 340 # Searching for a block that looks like this:
341 341 #
342 342 # +------------------------------+
343 343 # | Section title |
344 344 # | ------------- |
345 345 # +------------------------------+
346 346 if (block['type'] == 'paragraph' and
347 347 len(block['lines']) == 2 and
348 348 encoding.colwidth(block['lines'][0]) == len(block['lines'][1]) and
349 349 _sectionre.match(block['lines'][1])):
350 350 block['underline'] = block['lines'][1][0]
351 351 block['type'] = 'section'
352 352 del block['lines'][1]
353 353 return blocks
354 354
355 355 def inlineliterals(blocks):
356 356 substs = [('``', '"')]
357 357 for b in blocks:
358 358 if b['type'] in ('paragraph', 'section'):
359 359 b['lines'] = [replace(l, substs) for l in b['lines']]
360 360 return blocks
361 361
362 362 def hgrole(blocks):
363 363 substs = [(':hg:`', '"hg '), ('`', '"')]
364 364 for b in blocks:
365 365 if b['type'] in ('paragraph', 'section'):
366 366 # Turn :hg:`command` into "hg command". This also works
367 367 # when there is a line break in the command and relies on
368 368 # the fact that we have no stray back-quotes in the input
369 369 # (run the blocks through inlineliterals first).
370 370 b['lines'] = [replace(l, substs) for l in b['lines']]
371 371 return blocks
372 372
373 373 def addmargins(blocks):
374 374 """Adds empty blocks for vertical spacing.
375 375
376 376 This groups bullets, options, and definitions together with no vertical
377 377 space between them, and adds an empty block between all other blocks.
378 378 """
379 379 i = 1
380 380 while i < len(blocks):
381 381 if (blocks[i]['type'] == blocks[i - 1]['type'] and
382 382 blocks[i]['type'] in ('bullet', 'option', 'field')):
383 383 i += 1
384 384 elif not blocks[i - 1]['lines']:
385 # no lines in previous block, do not seperate
385 # no lines in previous block, do not separate
386 386 i += 1
387 387 else:
388 388 blocks.insert(i, dict(lines=[''], indent=0, type='margin'))
389 389 i += 2
390 390 return blocks
391 391
392 392 def prunecomments(blocks):
393 393 """Remove comments."""
394 394 i = 0
395 395 while i < len(blocks):
396 396 b = blocks[i]
397 397 if b['type'] == 'paragraph' and (b['lines'][0].startswith('.. ') or
398 398 b['lines'] == ['..']):
399 399 del blocks[i]
400 400 if i < len(blocks) and blocks[i]['type'] == 'margin':
401 401 del blocks[i]
402 402 else:
403 403 i += 1
404 404 return blocks
405 405
406 406 _admonitionre = re.compile(r"\.\. (admonition|attention|caution|danger|"
407 407 r"error|hint|important|note|tip|warning)::",
408 408 flags=re.IGNORECASE)
409 409
410 410 def findadmonitions(blocks):
411 411 """
412 412 Makes the type of the block an admonition block if
413 413 the first line is an admonition directive
414 414 """
415 415 i = 0
416 416 while i < len(blocks):
417 417 m = _admonitionre.match(blocks[i]['lines'][0])
418 418 if m:
419 419 blocks[i]['type'] = 'admonition'
420 420 admonitiontitle = blocks[i]['lines'][0][3:m.end() - 2].lower()
421 421
422 422 firstline = blocks[i]['lines'][0][m.end() + 1:]
423 423 if firstline:
424 424 blocks[i]['lines'].insert(1, ' ' + firstline)
425 425
426 426 blocks[i]['admonitiontitle'] = admonitiontitle
427 427 del blocks[i]['lines'][0]
428 428 i = i + 1
429 429 return blocks
430 430
431 431 _admonitiontitles = {'attention': _('Attention:'),
432 432 'caution': _('Caution:'),
433 433 'danger': _('!Danger!') ,
434 434 'error': _('Error:'),
435 435 'hint': _('Hint:'),
436 436 'important': _('Important:'),
437 437 'note': _('Note:'),
438 438 'tip': _('Tip:'),
439 439 'warning': _('Warning!')}
440 440
441 441 def formatoption(block, width):
442 442 desc = ' '.join(map(str.strip, block['lines']))
443 443 colwidth = encoding.colwidth(block['optstr'])
444 444 usablewidth = width - 1
445 445 hanging = block['optstrwidth']
446 446 initindent = '%s%s ' % (block['optstr'], ' ' * ((hanging - colwidth)))
447 447 hangindent = ' ' * (encoding.colwidth(initindent) + 1)
448 448 return ' %s\n' % (util.wrap(desc, usablewidth,
449 449 initindent=initindent,
450 450 hangindent=hangindent))
451 451
452 452 def formatblock(block, width):
453 453 """Format a block according to width."""
454 454 if width <= 0:
455 455 width = 78
456 456 indent = ' ' * block['indent']
457 457 if block['type'] == 'admonition':
458 458 admonition = _admonitiontitles[block['admonitiontitle']]
459 459 if not block['lines']:
460 460 return indent + admonition + '\n'
461 461 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
462 462
463 463 defindent = indent + hang * ' '
464 464 text = ' '.join(map(str.strip, block['lines']))
465 465 return '%s\n%s\n' % (indent + admonition,
466 466 util.wrap(text, width=width,
467 467 initindent=defindent,
468 468 hangindent=defindent))
469 469 if block['type'] == 'margin':
470 470 return '\n'
471 471 if block['type'] == 'literal':
472 472 indent += ' '
473 473 return indent + ('\n' + indent).join(block['lines']) + '\n'
474 474 if block['type'] == 'section':
475 475 underline = encoding.colwidth(block['lines'][0]) * block['underline']
476 476 return "%s%s\n%s%s\n" % (indent, block['lines'][0],indent, underline)
477 477 if block['type'] == 'table':
478 478 table = block['table']
479 479 # compute column widths
480 480 widths = [max([encoding.colwidth(e) for e in c]) for c in zip(*table)]
481 481 text = ''
482 482 span = sum(widths) + len(widths) - 1
483 483 indent = ' ' * block['indent']
484 484 hang = ' ' * (len(indent) + span - widths[-1])
485 485
486 486 for row in table:
487 487 l = []
488 488 for w, v in zip(widths, row):
489 489 pad = ' ' * (w - encoding.colwidth(v))
490 490 l.append(v + pad)
491 491 l = ' '.join(l)
492 492 l = util.wrap(l, width=width, initindent=indent, hangindent=hang)
493 493 if not text and block['header']:
494 494 text = l + '\n' + indent + '-' * (min(width, span)) + '\n'
495 495 else:
496 496 text += l + "\n"
497 497 return text
498 498 if block['type'] == 'definition':
499 499 term = indent + block['lines'][0]
500 500 hang = len(block['lines'][-1]) - len(block['lines'][-1].lstrip())
501 501 defindent = indent + hang * ' '
502 502 text = ' '.join(map(str.strip, block['lines'][1:]))
503 503 return '%s\n%s\n' % (term, util.wrap(text, width=width,
504 504 initindent=defindent,
505 505 hangindent=defindent))
506 506 subindent = indent
507 507 if block['type'] == 'bullet':
508 508 if block['lines'][0].startswith('| '):
509 509 # Remove bullet for line blocks and add no extra
510 510 # indention.
511 511 block['lines'][0] = block['lines'][0][2:]
512 512 else:
513 513 m = _bulletre.match(block['lines'][0])
514 514 subindent = indent + m.end() * ' '
515 515 elif block['type'] == 'field':
516 516 key = block['key']
517 517 subindent = indent + _fieldwidth * ' '
518 518 if len(key) + 2 > _fieldwidth:
519 519 # key too large, use full line width
520 520 key = key.ljust(width)
521 521 else:
522 522 # key fits within field width
523 523 key = key.ljust(_fieldwidth)
524 524 block['lines'][0] = key + block['lines'][0]
525 525 elif block['type'] == 'option':
526 526 return formatoption(block, width)
527 527
528 528 text = ' '.join(map(str.strip, block['lines']))
529 529 return util.wrap(text, width=width,
530 530 initindent=indent,
531 531 hangindent=subindent) + '\n'
532 532
533 533 def formathtml(blocks):
534 534 """Format RST blocks as HTML"""
535 535
536 536 out = []
537 537 headernest = ''
538 538 listnest = []
539 539
540 540 def escape(s):
541 541 return cgi.escape(s, True)
542 542
543 543 def openlist(start, level):
544 544 if not listnest or listnest[-1][0] != start:
545 545 listnest.append((start, level))
546 546 out.append('<%s>\n' % start)
547 547
548 548 blocks = [b for b in blocks if b['type'] != 'margin']
549 549
550 550 for pos, b in enumerate(blocks):
551 551 btype = b['type']
552 552 level = b['indent']
553 553 lines = b['lines']
554 554
555 555 if btype == 'admonition':
556 556 admonition = escape(_admonitiontitles[b['admonitiontitle']])
557 557 text = escape(' '.join(map(str.strip, lines)))
558 558 out.append('<p>\n<b>%s</b> %s\n</p>\n' % (admonition, text))
559 559 elif btype == 'paragraph':
560 560 out.append('<p>\n%s\n</p>\n' % escape('\n'.join(lines)))
561 561 elif btype == 'margin':
562 562 pass
563 563 elif btype == 'literal':
564 564 out.append('<pre>\n%s\n</pre>\n' % escape('\n'.join(lines)))
565 565 elif btype == 'section':
566 566 i = b['underline']
567 567 if i not in headernest:
568 568 headernest += i
569 569 level = headernest.index(i) + 1
570 570 out.append('<h%d>%s</h%d>\n' % (level, escape(lines[0]), level))
571 571 elif btype == 'table':
572 572 table = b['table']
573 573 out.append('<table>\n')
574 574 for row in table:
575 575 out.append('<tr>')
576 576 for v in row:
577 577 out.append('<td>')
578 578 out.append(escape(v))
579 579 out.append('</td>')
580 580 out.append('\n')
581 581 out.pop()
582 582 out.append('</tr>\n')
583 583 out.append('</table>\n')
584 584 elif btype == 'definition':
585 585 openlist('dl', level)
586 586 term = escape(lines[0])
587 587 text = escape(' '.join(map(str.strip, lines[1:])))
588 588 out.append(' <dt>%s\n <dd>%s\n' % (term, text))
589 589 elif btype == 'bullet':
590 590 bullet, head = lines[0].split(' ', 1)
591 591 if bullet == '-':
592 592 openlist('ul', level)
593 593 else:
594 594 openlist('ol', level)
595 595 out.append(' <li> %s\n' % escape(' '.join([head] + lines[1:])))
596 596 elif btype == 'field':
597 597 openlist('dl', level)
598 598 key = escape(b['key'])
599 599 text = escape(' '.join(map(str.strip, lines)))
600 600 out.append(' <dt>%s\n <dd>%s\n' % (key, text))
601 601 elif btype == 'option':
602 602 openlist('dl', level)
603 603 opt = escape(b['optstr'])
604 604 desc = escape(' '.join(map(str.strip, lines)))
605 605 out.append(' <dt>%s\n <dd>%s\n' % (opt, desc))
606 606
607 607 # close lists if indent level of next block is lower
608 608 if listnest:
609 609 start, level = listnest[-1]
610 610 if pos == len(blocks) - 1:
611 611 out.append('</%s>\n' % start)
612 612 listnest.pop()
613 613 else:
614 614 nb = blocks[pos + 1]
615 615 ni = nb['indent']
616 616 if (ni < level or
617 617 (ni == level and
618 618 nb['type'] not in 'definition bullet field option')):
619 619 out.append('</%s>\n' % start)
620 620 listnest.pop()
621 621
622 622 return ''.join(out)
623 623
624 624 def parse(text, indent=0, keep=None):
625 625 """Parse text into a list of blocks"""
626 626 pruned = []
627 627 blocks = findblocks(text)
628 628 for b in blocks:
629 629 b['indent'] += indent
630 630 blocks = findliteralblocks(blocks)
631 631 blocks = findtables(blocks)
632 632 blocks, pruned = prunecontainers(blocks, keep or [])
633 633 blocks = findsections(blocks)
634 634 blocks = inlineliterals(blocks)
635 635 blocks = hgrole(blocks)
636 636 blocks = splitparagraphs(blocks)
637 637 blocks = updatefieldlists(blocks)
638 638 blocks = updateoptionlists(blocks)
639 639 blocks = findadmonitions(blocks)
640 640 blocks = addmargins(blocks)
641 641 blocks = prunecomments(blocks)
642 642 return blocks, pruned
643 643
644 644 def formatblocks(blocks, width):
645 645 text = ''.join(formatblock(b, width) for b in blocks)
646 646 return text
647 647
648 648 def format(text, width=80, indent=0, keep=None, style='plain'):
649 649 """Parse and format the text according to width."""
650 650 blocks, pruned = parse(text, indent, keep or [])
651 651 if style == 'html':
652 652 text = formathtml(blocks)
653 653 else:
654 654 text = ''.join(formatblock(b, width) for b in blocks)
655 655 if keep is None:
656 656 return text
657 657 else:
658 658 return text, pruned
659 659
660 660 def getsections(blocks):
661 661 '''return a list of (section name, nesting level, blocks) tuples'''
662 662 nest = ""
663 663 level = 0
664 664 secs = []
665 665 for b in blocks:
666 666 if b['type'] == 'section':
667 667 i = b['underline']
668 668 if i not in nest:
669 669 nest += i
670 670 level = nest.index(i) + 1
671 671 nest = nest[:level]
672 672 secs.append((b['lines'][0], level, [b]))
673 673 else:
674 674 if not secs:
675 675 # add an initial empty section
676 676 secs = [('', 0, [])]
677 677 secs[-1][2].append(b)
678 678 return secs
679 679
680 680 def decorateblocks(blocks, width):
681 681 '''generate a list of (section name, line text) pairs for search'''
682 682 lines = []
683 683 for s in getsections(blocks):
684 684 section = s[0]
685 685 text = formatblocks(s[2], width)
686 686 lines.append([(section, l) for l in text.splitlines(True)])
687 687 return lines
688 688
689 689 def maketable(data, indent=0, header=False):
690 690 '''Generate an RST table for the given table data as a list of lines'''
691 691
692 692 widths = [max(encoding.colwidth(e) for e in c) for c in zip(*data)]
693 693 indent = ' ' * indent
694 694 div = indent + ' '.join('=' * w for w in widths) + '\n'
695 695
696 696 out = [div]
697 697 for row in data:
698 698 l = []
699 699 for w, v in zip(widths, row):
700 700 pad = ' ' * (w - encoding.colwidth(v))
701 701 l.append(v + pad)
702 702 out.append(indent + ' '.join(l) + "\n")
703 703 if header and len(data) > 1:
704 704 out.insert(2, div)
705 705 out.append(div)
706 706 return out
@@ -1,854 +1,854 b''
1 1 # obsolete.py - obsolete markers handling
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 """Obsolete markers handling
10 10
11 11 An obsolete marker maps an old changeset to a list of new
12 12 changesets. If the list of new changesets is empty, the old changeset
13 13 is said to be "killed". Otherwise, the old changeset is being
14 14 "replaced" by the new changesets.
15 15
16 16 Obsolete markers can be used to record and distribute changeset graph
17 17 transformations performed by history rewriting operations, and help
18 18 building new tools to reconciliate conflicting rewriting actions. To
19 19 facilitate conflicts resolution, markers include various annotations
20 20 besides old and news changeset identifiers, such as creation date or
21 21 author name.
22 22
23 23 The old obsoleted changeset is called "precursor" and possible replacements are
24 24 called "successors". Markers that used changeset X as a precursors are called
25 25 "successor markers of X" because they hold information about the successors of
26 26 X. Markers that use changeset Y as a successors are call "precursor markers of
27 27 Y" because they hold information about the precursors of Y.
28 28
29 29 Examples:
30 30
31 31 - When changeset A is replacement by a changeset A', one marker is stored:
32 32
33 33 (A, (A'))
34 34
35 35 - When changesets A and B are folded into a new changeset C two markers are
36 36 stored:
37 37
38 38 (A, (C,)) and (B, (C,))
39 39
40 40 - When changeset A is simply "pruned" from the graph, a marker in create:
41 41
42 42 (A, ())
43 43
44 44 - When changeset A is split into B and C, a single marker are used:
45 45
46 46 (A, (C, C))
47 47
48 48 We use a single marker to distinct the "split" case from the "divergence"
49 49 case. If two independents operation rewrite the same changeset A in to A' and
50 50 A'' when have an error case: divergent rewriting. We can detect it because
51 51 two markers will be created independently:
52 52
53 53 (A, (B,)) and (A, (C,))
54 54
55 55 Format
56 56 ------
57 57
58 58 Markers are stored in an append-only file stored in
59 59 '.hg/store/obsstore'.
60 60
61 61 The file starts with a version header:
62 62
63 63 - 1 unsigned byte: version number, starting at zero.
64 64
65 65
66 66 The header is followed by the markers. Each marker is made of:
67 67
68 68 - 1 unsigned byte: number of new changesets "R", could be zero.
69 69
70 70 - 1 unsigned 32-bits integer: metadata size "M" in bytes.
71 71
72 72 - 1 byte: a bit field. It is reserved for flags used in obsolete
73 73 markers common operations, to avoid repeated decoding of metadata
74 74 entries.
75 75
76 76 - 20 bytes: obsoleted changeset identifier.
77 77
78 78 - N*20 bytes: new changesets identifiers.
79 79
80 80 - M bytes: metadata as a sequence of nul-terminated strings. Each
81 81 string contains a key and a value, separated by a color ':', without
82 82 additional encoding. Keys cannot contain '\0' or ':' and values
83 83 cannot contain '\0'.
84 84 """
85 85 import struct
86 86 import util, base85, node
87 87 import phases
88 88 from i18n import _
89 89
90 90 _pack = struct.pack
91 91 _unpack = struct.unpack
92 92
93 93 _SEEK_END = 2 # os.SEEK_END was introduced in Python 2.5
94 94
95 95 # the obsolete feature is not mature enough to be enabled by default.
96 96 # you have to rely on third party extension extension to enable this.
97 97 _enabled = False
98 98
99 99 # data used for parsing and writing
100 100 _fmversion = 0
101 101 _fmfixed = '>BIB20s'
102 102 _fmnode = '20s'
103 103 _fmfsize = struct.calcsize(_fmfixed)
104 104 _fnodesize = struct.calcsize(_fmnode)
105 105
106 106 ### obsolescence marker flag
107 107
108 108 ## bumpedfix flag
109 109 #
110 110 # When a changeset A' succeed to a changeset A which became public, we call A'
111 111 # "bumped" because it's a successors of a public changesets
112 112 #
113 113 # o A' (bumped)
114 114 # |`:
115 115 # | o A
116 116 # |/
117 117 # o Z
118 118 #
119 119 # The way to solve this situation is to create a new changeset Ad as children
120 120 # of A. This changeset have the same content than A'. So the diff from A to A'
121 121 # is the same than the diff from A to Ad. Ad is marked as a successors of A'
122 122 #
123 123 # o Ad
124 124 # |`:
125 125 # | x A'
126 126 # |'|
127 127 # o | A
128 128 # |/
129 129 # o Z
130 130 #
131 131 # But by transitivity Ad is also a successors of A. To avoid having Ad marked
132 132 # as bumped too, we add the `bumpedfix` flag to the marker. <A', (Ad,)>.
133 133 # This flag mean that the successors express the changes between the public and
134 134 # bumped version and fix the situation, breaking the transitivity of
135 135 # "bumped" here.
136 136 bumpedfix = 1
137 137
138 138 def _readmarkers(data):
139 139 """Read and enumerate markers from raw data"""
140 140 off = 0
141 141 diskversion = _unpack('>B', data[off:off + 1])[0]
142 142 off += 1
143 143 if diskversion != _fmversion:
144 144 raise util.Abort(_('parsing obsolete marker: unknown version %r')
145 145 % diskversion)
146 146
147 147 # Loop on markers
148 148 l = len(data)
149 149 while off + _fmfsize <= l:
150 150 # read fixed part
151 151 cur = data[off:off + _fmfsize]
152 152 off += _fmfsize
153 153 nbsuc, mdsize, flags, pre = _unpack(_fmfixed, cur)
154 154 # read replacement
155 155 sucs = ()
156 156 if nbsuc:
157 157 s = (_fnodesize * nbsuc)
158 158 cur = data[off:off + s]
159 159 sucs = _unpack(_fmnode * nbsuc, cur)
160 160 off += s
161 161 # read metadata
162 162 # (metadata will be decoded on demand)
163 163 metadata = data[off:off + mdsize]
164 164 if len(metadata) != mdsize:
165 165 raise util.Abort(_('parsing obsolete marker: metadata is too '
166 166 'short, %d bytes expected, got %d')
167 167 % (mdsize, len(metadata)))
168 168 off += mdsize
169 169 yield (pre, sucs, flags, metadata)
170 170
171 171 def encodemeta(meta):
172 172 """Return encoded metadata string to string mapping.
173 173
174 174 Assume no ':' in key and no '\0' in both key and value."""
175 175 for key, value in meta.iteritems():
176 176 if ':' in key or '\0' in key:
177 177 raise ValueError("':' and '\0' are forbidden in metadata key'")
178 178 if '\0' in value:
179 179 raise ValueError("':' are forbidden in metadata value'")
180 180 return '\0'.join(['%s:%s' % (k, meta[k]) for k in sorted(meta)])
181 181
182 182 def decodemeta(data):
183 183 """Return string to string dictionary from encoded version."""
184 184 d = {}
185 185 for l in data.split('\0'):
186 186 if l:
187 187 key, value = l.split(':')
188 188 d[key] = value
189 189 return d
190 190
191 191 class marker(object):
192 192 """Wrap obsolete marker raw data"""
193 193
194 194 def __init__(self, repo, data):
195 195 # the repo argument will be used to create changectx in later version
196 196 self._repo = repo
197 197 self._data = data
198 198 self._decodedmeta = None
199 199
200 200 def __hash__(self):
201 201 return hash(self._data)
202 202
203 203 def __eq__(self, other):
204 204 if type(other) != type(self):
205 205 return False
206 206 return self._data == other._data
207 207
208 208 def precnode(self):
209 209 """Precursor changeset node identifier"""
210 210 return self._data[0]
211 211
212 212 def succnodes(self):
213 213 """List of successor changesets node identifiers"""
214 214 return self._data[1]
215 215
216 216 def metadata(self):
217 217 """Decoded metadata dictionary"""
218 218 if self._decodedmeta is None:
219 219 self._decodedmeta = decodemeta(self._data[3])
220 220 return self._decodedmeta
221 221
222 222 def date(self):
223 223 """Creation date as (unixtime, offset)"""
224 224 parts = self.metadata()['date'].split(' ')
225 225 return (float(parts[0]), int(parts[1]))
226 226
227 227 class obsstore(object):
228 228 """Store obsolete markers
229 229
230 230 Markers can be accessed with two mappings:
231 231 - precursors[x] -> set(markers on precursors edges of x)
232 232 - successors[x] -> set(markers on successors edges of x)
233 233 """
234 234
235 235 def __init__(self, sopener):
236 236 # caches for various obsolescence related cache
237 237 self.caches = {}
238 238 self._all = []
239 239 # new markers to serialize
240 240 self.precursors = {}
241 241 self.successors = {}
242 242 self.sopener = sopener
243 243 data = sopener.tryread('obsstore')
244 244 if data:
245 245 self._load(_readmarkers(data))
246 246
247 247 def __iter__(self):
248 248 return iter(self._all)
249 249
250 250 def __nonzero__(self):
251 251 return bool(self._all)
252 252
253 253 def create(self, transaction, prec, succs=(), flag=0, metadata=None):
254 254 """obsolete: add a new obsolete marker
255 255
256 256 * ensuring it is hashable
257 257 * check mandatory metadata
258 258 * encode metadata
259 259
260 260 If you are a human writing code creating marker you want to use the
261 261 `createmarkers` function in this module instead.
262 262 """
263 263 if metadata is None:
264 264 metadata = {}
265 265 if 'date' not in metadata:
266 266 metadata['date'] = "%d %d" % util.makedate()
267 267 if len(prec) != 20:
268 268 raise ValueError(prec)
269 269 for succ in succs:
270 270 if len(succ) != 20:
271 271 raise ValueError(succ)
272 272 marker = (str(prec), tuple(succs), int(flag), encodemeta(metadata))
273 273 self.add(transaction, [marker])
274 274
275 275 def add(self, transaction, markers):
276 276 """Add new markers to the store
277 277
278 278 Take care of filtering duplicate.
279 279 Return the number of new marker."""
280 280 if not _enabled:
281 281 raise util.Abort('obsolete feature is not enabled on this repo')
282 282 known = set(self._all)
283 283 new = []
284 284 for m in markers:
285 285 if m not in known:
286 286 known.add(m)
287 287 new.append(m)
288 288 if new:
289 289 f = self.sopener('obsstore', 'ab')
290 290 try:
291 291 # Whether the file's current position is at the begin or at
292 292 # the end after opening a file for appending is implementation
293 293 # defined. So we must seek to the end before calling tell(),
294 294 # or we may get a zero offset for non-zero sized files on
295 295 # some platforms (issue3543).
296 296 f.seek(0, _SEEK_END)
297 297 offset = f.tell()
298 298 transaction.add('obsstore', offset)
299 299 # offset == 0: new file - add the version header
300 300 for bytes in _encodemarkers(new, offset == 0):
301 301 f.write(bytes)
302 302 finally:
303 303 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
304 304 # call 'filecacheentry.refresh()' here
305 305 f.close()
306 306 self._load(new)
307 307 # new marker *may* have changed several set. invalidate the cache.
308 308 self.caches.clear()
309 309 return len(new)
310 310
311 311 def mergemarkers(self, transaction, data):
312 312 markers = _readmarkers(data)
313 313 self.add(transaction, markers)
314 314
315 315 def _load(self, markers):
316 316 for mark in markers:
317 317 self._all.append(mark)
318 318 pre, sucs = mark[:2]
319 319 self.successors.setdefault(pre, set()).add(mark)
320 320 for suc in sucs:
321 321 self.precursors.setdefault(suc, set()).add(mark)
322 322 if node.nullid in self.precursors:
323 323 raise util.Abort(_('bad obsolescence marker detected: '
324 324 'invalid successors nullid'))
325 325
326 326 def _encodemarkers(markers, addheader=False):
327 327 # Kept separate from flushmarkers(), it will be reused for
328 328 # markers exchange.
329 329 if addheader:
330 330 yield _pack('>B', _fmversion)
331 331 for marker in markers:
332 332 yield _encodeonemarker(marker)
333 333
334 334
335 335 def _encodeonemarker(marker):
336 336 pre, sucs, flags, metadata = marker
337 337 nbsuc = len(sucs)
338 338 format = _fmfixed + (_fmnode * nbsuc)
339 339 data = [nbsuc, len(metadata), flags, pre]
340 340 data.extend(sucs)
341 341 return _pack(format, *data) + metadata
342 342
343 343 # arbitrary picked to fit into 8K limit from HTTP server
344 344 # you have to take in account:
345 345 # - the version header
346 346 # - the base85 encoding
347 347 _maxpayload = 5300
348 348
349 349 def listmarkers(repo):
350 350 """List markers over pushkey"""
351 351 if not repo.obsstore:
352 352 return {}
353 353 keys = {}
354 354 parts = []
355 355 currentlen = _maxpayload * 2 # ensure we create a new part
356 356 for marker in repo.obsstore:
357 357 nextdata = _encodeonemarker(marker)
358 358 if (len(nextdata) + currentlen > _maxpayload):
359 359 currentpart = []
360 360 currentlen = 0
361 361 parts.append(currentpart)
362 362 currentpart.append(nextdata)
363 363 currentlen += len(nextdata)
364 364 for idx, part in enumerate(reversed(parts)):
365 365 data = ''.join([_pack('>B', _fmversion)] + part)
366 366 keys['dump%i' % idx] = base85.b85encode(data)
367 367 return keys
368 368
369 369 def pushmarker(repo, key, old, new):
370 370 """Push markers over pushkey"""
371 371 if not key.startswith('dump'):
372 372 repo.ui.warn(_('unknown key: %r') % key)
373 373 return 0
374 374 if old:
375 375 repo.ui.warn(_('unexpected old value') % key)
376 376 return 0
377 377 data = base85.b85decode(new)
378 378 lock = repo.lock()
379 379 try:
380 380 tr = repo.transaction('pushkey: obsolete markers')
381 381 try:
382 382 repo.obsstore.mergemarkers(tr, data)
383 383 tr.close()
384 384 return 1
385 385 finally:
386 386 tr.release()
387 387 finally:
388 388 lock.release()
389 389
390 390 def allmarkers(repo):
391 391 """all obsolete markers known in a repository"""
392 392 for markerdata in repo.obsstore:
393 393 yield marker(repo, markerdata)
394 394
395 395 def precursormarkers(ctx):
396 396 """obsolete marker marking this changeset as a successors"""
397 397 for data in ctx._repo.obsstore.precursors.get(ctx.node(), ()):
398 398 yield marker(ctx._repo, data)
399 399
400 400 def successormarkers(ctx):
401 401 """obsolete marker making this changeset obsolete"""
402 402 for data in ctx._repo.obsstore.successors.get(ctx.node(), ()):
403 403 yield marker(ctx._repo, data)
404 404
405 405 def allsuccessors(obsstore, nodes, ignoreflags=0):
406 406 """Yield node for every successor of <nodes>.
407 407
408 408 Some successors may be unknown locally.
409 409
410 410 This is a linear yield unsuited to detecting split changesets. It includes
411 411 initial nodes too."""
412 412 remaining = set(nodes)
413 413 seen = set(remaining)
414 414 while remaining:
415 415 current = remaining.pop()
416 416 yield current
417 417 for mark in obsstore.successors.get(current, ()):
418 418 # ignore marker flagged with specified flag
419 419 if mark[2] & ignoreflags:
420 420 continue
421 421 for suc in mark[1]:
422 422 if suc not in seen:
423 423 seen.add(suc)
424 424 remaining.add(suc)
425 425
426 426 def allprecursors(obsstore, nodes, ignoreflags=0):
427 427 """Yield node for every precursors of <nodes>.
428 428
429 429 Some precursors may be unknown locally.
430 430
431 431 This is a linear yield unsuited to detecting folded changesets. It includes
432 432 initial nodes too."""
433 433
434 434 remaining = set(nodes)
435 435 seen = set(remaining)
436 436 while remaining:
437 437 current = remaining.pop()
438 438 yield current
439 439 for mark in obsstore.precursors.get(current, ()):
440 440 # ignore marker flagged with specified flag
441 441 if mark[2] & ignoreflags:
442 442 continue
443 443 suc = mark[0]
444 444 if suc not in seen:
445 445 seen.add(suc)
446 446 remaining.add(suc)
447 447
448 448 def foreground(repo, nodes):
449 449 """return all nodes in the "foreground" of other node
450 450
451 451 The foreground of a revision is anything reachable using parent -> children
452 452 or precursor -> successor relation. It is very similar to "descendant" but
453 453 augmented with obsolescence information.
454 454
455 455 Beware that possible obsolescence cycle may result if complex situation.
456 456 """
457 457 repo = repo.unfiltered()
458 458 foreground = set(repo.set('%ln::', nodes))
459 459 if repo.obsstore:
460 460 # We only need this complicated logic if there is obsolescence
461 461 # XXX will probably deserve an optimised revset.
462 462 nm = repo.changelog.nodemap
463 463 plen = -1
464 464 # compute the whole set of successors or descendants
465 465 while len(foreground) != plen:
466 466 plen = len(foreground)
467 467 succs = set(c.node() for c in foreground)
468 468 mutable = [c.node() for c in foreground if c.mutable()]
469 469 succs.update(allsuccessors(repo.obsstore, mutable))
470 470 known = (n for n in succs if n in nm)
471 471 foreground = set(repo.set('%ln::', known))
472 472 return set(c.node() for c in foreground)
473 473
474 474
475 475 def successorssets(repo, initialnode, cache=None):
476 476 """Return all set of successors of initial nodes
477 477
478 478 The successors set of a changeset A are a group of revisions that succeed
479 479 A. It succeeds A as a consistent whole, each revision being only a partial
480 480 replacement. The successors set contains non-obsolete changesets only.
481 481
482 482 This function returns the full list of successor sets which is why it
483 483 returns a list of tuples and not just a single tuple. Each tuple is a valid
484 484 successors set. Not that (A,) may be a valid successors set for changeset A
485 485 (see below).
486 486
487 487 In most cases, a changeset A will have a single element (e.g. the changeset
488 488 A is replaced by A') in its successors set. Though, it is also common for a
489 489 changeset A to have no elements in its successor set (e.g. the changeset
490 490 has been pruned). Therefore, the returned list of successors sets will be
491 491 [(A',)] or [], respectively.
492 492
493 493 When a changeset A is split into A' and B', however, it will result in a
494 494 successors set containing more than a single element, i.e. [(A',B')].
495 495 Divergent changesets will result in multiple successors sets, i.e. [(A',),
496 496 (A'')].
497 497
498 498 If a changeset A is not obsolete, then it will conceptually have no
499 499 successors set. To distinguish this from a pruned changeset, the successor
500 500 set will only contain itself, i.e. [(A,)].
501 501
502 502 Finally, successors unknown locally are considered to be pruned (obsoleted
503 503 without any successors).
504 504
505 505 The optional `cache` parameter is a dictionary that may contain precomputed
506 506 successors sets. It is meant to reuse the computation of a previous call to
507 507 `successorssets` when multiple calls are made at the same time. The cache
508 508 dictionary is updated in place. The caller is responsible for its live
509 509 spawn. Code that makes multiple calls to `successorssets` *must* use this
510 510 cache mechanism or suffer terrible performances.
511 511
512 512 """
513 513
514 514 succmarkers = repo.obsstore.successors
515 515
516 516 # Stack of nodes we search successors sets for
517 517 toproceed = [initialnode]
518 518 # set version of above list for fast loop detection
519 519 # element added to "toproceed" must be added here
520 520 stackedset = set(toproceed)
521 521 if cache is None:
522 522 cache = {}
523 523
524 524 # This while loop is the flattened version of a recursive search for
525 525 # successors sets
526 526 #
527 527 # def successorssets(x):
528 528 # successors = directsuccessors(x)
529 529 # ss = [[]]
530 530 # for succ in directsuccessors(x):
531 531 # # product as in itertools cartesian product
532 532 # ss = product(ss, successorssets(succ))
533 533 # return ss
534 534 #
535 535 # But we can not use plain recursive calls here:
536 536 # - that would blow the python call stack
537 537 # - obsolescence markers may have cycles, we need to handle them.
538 538 #
539 539 # The `toproceed` list act as our call stack. Every node we search
540 540 # successors set for are stacked there.
541 541 #
542 542 # The `stackedset` is set version of this stack used to check if a node is
543 543 # already stacked. This check is used to detect cycles and prevent infinite
544 544 # loop.
545 545 #
546 546 # successors set of all nodes are stored in the `cache` dictionary.
547 547 #
548 548 # After this while loop ends we use the cache to return the successors sets
549 549 # for the node requested by the caller.
550 550 while toproceed:
551 551 # Every iteration tries to compute the successors sets of the topmost
552 552 # node of the stack: CURRENT.
553 553 #
554 554 # There are four possible outcomes:
555 555 #
556 556 # 1) We already know the successors sets of CURRENT:
557 557 # -> mission accomplished, pop it from the stack.
558 558 # 2) Node is not obsolete:
559 559 # -> the node is its own successors sets. Add it to the cache.
560 560 # 3) We do not know successors set of direct successors of CURRENT:
561 561 # -> We add those successors to the stack.
562 562 # 4) We know successors sets of all direct successors of CURRENT:
563 563 # -> We can compute CURRENT successors set and add it to the
564 564 # cache.
565 565 #
566 566 current = toproceed[-1]
567 567 if current in cache:
568 568 # case (1): We already know the successors sets
569 569 stackedset.remove(toproceed.pop())
570 570 elif current not in succmarkers:
571 571 # case (2): The node is not obsolete.
572 572 if current in repo:
573 573 # We have a valid last successors.
574 574 cache[current] = [(current,)]
575 575 else:
576 576 # Final obsolete version is unknown locally.
577 577 # Do not count that as a valid successors
578 578 cache[current] = []
579 579 else:
580 580 # cases (3) and (4)
581 581 #
582 582 # We proceed in two phases. Phase 1 aims to distinguish case (3)
583 583 # from case (4):
584 584 #
585 585 # For each direct successors of CURRENT, we check whether its
586 586 # successors sets are known. If they are not, we stack the
587 587 # unknown node and proceed to the next iteration of the while
588 588 # loop. (case 3)
589 589 #
590 590 # During this step, we may detect obsolescence cycles: a node
591 591 # with unknown successors sets but already in the call stack.
592 592 # In such a situation, we arbitrary set the successors sets of
593 593 # the node to nothing (node pruned) to break the cycle.
594 594 #
595 595 # If no break was encountered we proceed to phase 2.
596 596 #
597 597 # Phase 2 computes successors sets of CURRENT (case 4); see details
598 598 # in phase 2 itself.
599 599 #
600 600 # Note the two levels of iteration in each phase.
601 601 # - The first one handles obsolescence markers using CURRENT as
602 602 # precursor (successors markers of CURRENT).
603 603 #
604 604 # Having multiple entry here means divergence.
605 605 #
606 606 # - The second one handles successors defined in each marker.
607 607 #
608 608 # Having none means pruned node, multiple successors means split,
609 609 # single successors are standard replacement.
610 610 #
611 611 for mark in sorted(succmarkers[current]):
612 612 for suc in mark[1]:
613 613 if suc not in cache:
614 614 if suc in stackedset:
615 615 # cycle breaking
616 616 cache[suc] = []
617 617 else:
618 618 # case (3) If we have not computed successors sets
619 619 # of one of those successors we add it to the
620 620 # `toproceed` stack and stop all work for this
621 621 # iteration.
622 622 toproceed.append(suc)
623 623 stackedset.add(suc)
624 624 break
625 625 else:
626 626 continue
627 627 break
628 628 else:
629 629 # case (4): we know all successors sets of all direct
630 630 # successors
631 631 #
632 632 # Successors set contributed by each marker depends on the
633 633 # successors sets of all its "successors" node.
634 634 #
635 635 # Each different marker is a divergence in the obsolescence
636 636 # history. It contributes successors sets distinct from other
637 637 # markers.
638 638 #
639 639 # Within a marker, a successor may have divergent successors
640 640 # sets. In such a case, the marker will contribute multiple
641 641 # divergent successors sets. If multiple successors have
642 642 # divergent successors sets, a cartesian product is used.
643 643 #
644 644 # At the end we post-process successors sets to remove
645 645 # duplicated entry and successors set that are strict subset of
646 646 # another one.
647 647 succssets = []
648 648 for mark in sorted(succmarkers[current]):
649 649 # successors sets contributed by this marker
650 650 markss = [[]]
651 651 for suc in mark[1]:
652 652 # cardinal product with previous successors
653 653 productresult = []
654 654 for prefix in markss:
655 655 for suffix in cache[suc]:
656 656 newss = list(prefix)
657 657 for part in suffix:
658 658 # do not duplicated entry in successors set
659 659 # first entry wins.
660 660 if part not in newss:
661 661 newss.append(part)
662 662 productresult.append(newss)
663 663 markss = productresult
664 664 succssets.extend(markss)
665 665 # remove duplicated and subset
666 666 seen = []
667 667 final = []
668 668 candidate = sorted(((set(s), s) for s in succssets if s),
669 669 key=lambda x: len(x[1]), reverse=True)
670 670 for setversion, listversion in candidate:
671 671 for seenset in seen:
672 672 if setversion.issubset(seenset):
673 673 break
674 674 else:
675 675 final.append(listversion)
676 676 seen.append(setversion)
677 677 final.reverse() # put small successors set first
678 678 cache[current] = final
679 679 return cache[initialnode]
680 680
681 681 def _knownrevs(repo, nodes):
682 682 """yield revision numbers of known nodes passed in parameters
683 683
684 684 Unknown revisions are silently ignored."""
685 685 torev = repo.changelog.nodemap.get
686 686 for n in nodes:
687 687 rev = torev(n)
688 688 if rev is not None:
689 689 yield rev
690 690
691 691 # mapping of 'set-name' -> <function to compute this set>
692 692 cachefuncs = {}
693 693 def cachefor(name):
694 694 """Decorator to register a function as computing the cache for a set"""
695 695 def decorator(func):
696 696 assert name not in cachefuncs
697 697 cachefuncs[name] = func
698 698 return func
699 699 return decorator
700 700
701 701 def getrevs(repo, name):
702 702 """Return the set of revision that belong to the <name> set
703 703
704 704 Such access may compute the set and cache it for future use"""
705 705 repo = repo.unfiltered()
706 706 if not repo.obsstore:
707 707 return ()
708 708 if name not in repo.obsstore.caches:
709 709 repo.obsstore.caches[name] = cachefuncs[name](repo)
710 710 return repo.obsstore.caches[name]
711 711
712 712 # To be simple we need to invalidate obsolescence cache when:
713 713 #
714 714 # - new changeset is added:
715 715 # - public phase is changed
716 716 # - obsolescence marker are added
717 717 # - strip is used a repo
718 718 def clearobscaches(repo):
719 719 """Remove all obsolescence related cache from a repo
720 720
721 721 This remove all cache in obsstore is the obsstore already exist on the
722 722 repo.
723 723
724 724 (We could be smarter here given the exact event that trigger the cache
725 725 clearing)"""
726 726 # only clear cache is there is obsstore data in this repo
727 727 if 'obsstore' in repo._filecache:
728 728 repo.obsstore.caches.clear()
729 729
730 730 @cachefor('obsolete')
731 731 def _computeobsoleteset(repo):
732 732 """the set of obsolete revisions"""
733 733 obs = set()
734 734 getrev = repo.changelog.nodemap.get
735 735 getphase = repo._phasecache.phase
736 736 for node in repo.obsstore.successors:
737 737 rev = getrev(node)
738 738 if rev is not None and getphase(repo, rev):
739 739 obs.add(rev)
740 740 return obs
741 741
742 742 @cachefor('unstable')
743 743 def _computeunstableset(repo):
744 744 """the set of non obsolete revisions with obsolete parents"""
745 745 # revset is not efficient enough here
746 746 # we do (obsolete()::) - obsolete() by hand
747 747 obs = getrevs(repo, 'obsolete')
748 748 if not obs:
749 749 return set()
750 750 cl = repo.changelog
751 751 return set(r for r in cl.descendants(obs) if r not in obs)
752 752
753 753 @cachefor('suspended')
754 754 def _computesuspendedset(repo):
755 755 """the set of obsolete parents with non obsolete descendants"""
756 756 suspended = repo.changelog.ancestors(getrevs(repo, 'unstable'))
757 757 return set(r for r in getrevs(repo, 'obsolete') if r in suspended)
758 758
759 759 @cachefor('extinct')
760 760 def _computeextinctset(repo):
761 761 """the set of obsolete parents without non obsolete descendants"""
762 762 return getrevs(repo, 'obsolete') - getrevs(repo, 'suspended')
763 763
764 764
765 765 @cachefor('bumped')
766 766 def _computebumpedset(repo):
767 767 """the set of revs trying to obsolete public revisions"""
768 768 bumped = set()
769 # utils function (avoid attribut lookup in the loop)
769 # utils function (avoid attribute lookup in the loop)
770 770 phase = repo._phasecache.phase # would be faster to grab the full list
771 771 public = phases.public
772 772 cl = repo.changelog
773 773 torev = cl.nodemap.get
774 774 obs = getrevs(repo, 'obsolete')
775 775 for rev in repo:
776 776 # We only evaluate mutable, non-obsolete revision
777 777 if (public < phase(repo, rev)) and (rev not in obs):
778 778 node = cl.node(rev)
779 779 # (future) A cache of precursors may worth if split is very common
780 780 for pnode in allprecursors(repo.obsstore, [node],
781 781 ignoreflags=bumpedfix):
782 782 prev = torev(pnode) # unfiltered! but so is phasecache
783 783 if (prev is not None) and (phase(repo, prev) <= public):
784 784 # we have a public precursors
785 785 bumped.add(rev)
786 786 break # Next draft!
787 787 return bumped
788 788
789 789 @cachefor('divergent')
790 790 def _computedivergentset(repo):
791 791 """the set of rev that compete to be the final successors of some revision.
792 792 """
793 793 divergent = set()
794 794 obsstore = repo.obsstore
795 795 newermap = {}
796 796 for ctx in repo.set('(not public()) - obsolete()'):
797 797 mark = obsstore.precursors.get(ctx.node(), ())
798 798 toprocess = set(mark)
799 799 while toprocess:
800 800 prec = toprocess.pop()[0]
801 801 if prec not in newermap:
802 802 successorssets(repo, prec, newermap)
803 803 newer = [n for n in newermap[prec] if n]
804 804 if len(newer) > 1:
805 805 divergent.add(ctx.rev())
806 806 break
807 807 toprocess.update(obsstore.precursors.get(prec, ()))
808 808 return divergent
809 809
810 810
811 811 def createmarkers(repo, relations, flag=0, metadata=None):
812 812 """Add obsolete markers between changesets in a repo
813 813
814 814 <relations> must be an iterable of (<old>, (<new>, ...)[,{metadata}])
815 815 tuple. `old` and `news` are changectx. metadata is an optional dictionnary
816 816 containing metadata for this marker only. It is merged with the global
817 817 metadata specified through the `metadata` argument of this function,
818 818
819 819 Trying to obsolete a public changeset will raise an exception.
820 820
821 821 Current user and date are used except if specified otherwise in the
822 822 metadata attribute.
823 823
824 824 This function operates within a transaction of its own, but does
825 825 not take any lock on the repo.
826 826 """
827 827 # prepare metadata
828 828 if metadata is None:
829 829 metadata = {}
830 830 if 'date' not in metadata:
831 831 metadata['date'] = '%i %i' % util.makedate()
832 832 if 'user' not in metadata:
833 833 metadata['user'] = repo.ui.username()
834 834 tr = repo.transaction('add-obsolescence-marker')
835 835 try:
836 836 for rel in relations:
837 837 prec = rel[0]
838 838 sucs = rel[1]
839 839 localmetadata = metadata.copy()
840 840 if 2 < len(rel):
841 841 localmetadata.update(rel[2])
842 842
843 843 if not prec.mutable():
844 844 raise util.Abort("cannot obsolete immutable changeset: %s"
845 845 % prec)
846 846 nprec = prec.node()
847 847 nsucs = tuple(s.node() for s in sucs)
848 848 if nprec in nsucs:
849 849 raise util.Abort("changeset %s cannot obsolete itself" % prec)
850 850 repo.obsstore.create(tr, nprec, nsucs, flag, localmetadata)
851 851 repo.filteredrevcache.clear()
852 852 tr.close()
853 853 finally:
854 854 tr.release()
@@ -1,213 +1,213 b''
1 1 # repoview.py - Filtered view of a localrepo object
2 2 #
3 3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 4 # Logilab SA <contact@logilab.fr>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import copy
10 10 import phases
11 11 import util
12 12 import obsolete
13 13
14 14
15 15 def hideablerevs(repo):
16 16 """Revisions candidates to be hidden
17 17
18 18 This is a standalone function to help extensions to wrap it."""
19 19 return obsolete.getrevs(repo, 'obsolete')
20 20
21 21 def computehidden(repo):
22 22 """compute the set of hidden revision to filter
23 23
24 24 During most operation hidden should be filtered."""
25 25 assert not repo.changelog.filteredrevs
26 26 hideable = hideablerevs(repo)
27 27 if hideable:
28 28 cl = repo.changelog
29 29 firsthideable = min(hideable)
30 30 revs = cl.revs(start=firsthideable)
31 31 tofilter = repo.revs(
32 32 '(%ld) and children(%ld)', list(revs), list(hideable))
33 33 blockers = [r for r in tofilter if r not in hideable]
34 34 for par in repo[None].parents():
35 35 blockers.append(par.rev())
36 36 for bm in repo._bookmarks.values():
37 37 blockers.append(repo[bm].rev())
38 38 blocked = cl.ancestors(blockers, inclusive=True)
39 39 return frozenset(r for r in hideable if r not in blocked)
40 40 return frozenset()
41 41
42 42 def computeunserved(repo):
43 43 """compute the set of revision that should be filtered when used a server
44 44
45 45 Secret and hidden changeset should not pretend to be here."""
46 46 assert not repo.changelog.filteredrevs
47 47 # fast path in simple case to avoid impact of non optimised code
48 48 hiddens = filterrevs(repo, 'visible')
49 49 if phases.hassecret(repo):
50 50 cl = repo.changelog
51 51 secret = phases.secret
52 52 getphase = repo._phasecache.phase
53 53 first = min(cl.rev(n) for n in repo._phasecache.phaseroots[secret])
54 54 revs = cl.revs(start=first)
55 55 secrets = set(r for r in revs if getphase(repo, r) >= secret)
56 56 return frozenset(hiddens | secrets)
57 57 else:
58 58 return hiddens
59 59
60 60 def computemutable(repo):
61 61 """compute the set of revision that should be filtered when used a server
62 62
63 63 Secret and hidden changeset should not pretend to be here."""
64 64 assert not repo.changelog.filteredrevs
65 65 # fast check to avoid revset call on huge repo
66 66 if util.any(repo._phasecache.phaseroots[1:]):
67 67 getphase = repo._phasecache.phase
68 68 maymutable = filterrevs(repo, 'base')
69 69 return frozenset(r for r in maymutable if getphase(repo, r))
70 70 return frozenset()
71 71
72 72 def computeimpactable(repo):
73 73 """Everything impactable by mutable revision
74 74
75 75 The immutable filter still have some chance to get invalidated. This will
76 76 happen when:
77 77
78 78 - you garbage collect hidden changeset,
79 79 - public phase is moved backward,
80 80 - something is changed in the filtering (this could be fixed)
81 81
82 82 This filter out any mutable changeset and any public changeset that may be
83 83 impacted by something happening to a mutable revision.
84 84
85 85 This is achieved by filtered everything with a revision number egal or
86 86 higher than the first mutable changeset is filtered."""
87 87 assert not repo.changelog.filteredrevs
88 88 cl = repo.changelog
89 89 firstmutable = len(cl)
90 90 for roots in repo._phasecache.phaseroots[1:]:
91 91 if roots:
92 92 firstmutable = min(firstmutable, min(cl.rev(r) for r in roots))
93 93 # protect from nullrev root
94 94 firstmutable = max(0, firstmutable)
95 95 return frozenset(xrange(firstmutable, len(cl)))
96 96
97 97 # function to compute filtered set
98 98 #
99 # When addding a new filter you MUST update the table at:
99 # When adding a new filter you MUST update the table at:
100 100 # mercurial.branchmap.subsettable
101 101 # Otherwise your filter will have to recompute all its branches cache
102 102 # from scratch (very slow).
103 103 filtertable = {'visible': computehidden,
104 104 'served': computeunserved,
105 105 'immutable': computemutable,
106 106 'base': computeimpactable}
107 107
108 108 def filterrevs(repo, filtername):
109 109 """returns set of filtered revision for this filter name"""
110 110 if filtername not in repo.filteredrevcache:
111 111 func = filtertable[filtername]
112 112 repo.filteredrevcache[filtername] = func(repo.unfiltered())
113 113 return repo.filteredrevcache[filtername]
114 114
115 115 class repoview(object):
116 116 """Provide a read/write view of a repo through a filtered changelog
117 117
118 118 This object is used to access a filtered version of a repository without
119 119 altering the original repository object itself. We can not alter the
120 120 original object for two main reasons:
121 121 - It prevents the use of a repo with multiple filters at the same time. In
122 122 particular when multiple threads are involved.
123 123 - It makes scope of the filtering harder to control.
124 124
125 125 This object behaves very closely to the original repository. All attribute
126 126 operations are done on the original repository:
127 127 - An access to `repoview.someattr` actually returns `repo.someattr`,
128 128 - A write to `repoview.someattr` actually sets value of `repo.someattr`,
129 129 - A deletion of `repoview.someattr` actually drops `someattr`
130 130 from `repo.__dict__`.
131 131
132 132 The only exception is the `changelog` property. It is overridden to return
133 133 a (surface) copy of `repo.changelog` with some revisions filtered. The
134 134 `filtername` attribute of the view control the revisions that need to be
135 135 filtered. (the fact the changelog is copied is an implementation detail).
136 136
137 137 Unlike attributes, this object intercepts all method calls. This means that
138 138 all methods are run on the `repoview` object with the filtered `changelog`
139 139 property. For this purpose the simple `repoview` class must be mixed with
140 140 the actual class of the repository. This ensures that the resulting
141 141 `repoview` object have the very same methods than the repo object. This
142 142 leads to the property below.
143 143
144 144 repoview.method() --> repo.__class__.method(repoview)
145 145
146 146 The inheritance has to be done dynamically because `repo` can be of any
147 147 subclasses of `localrepo`. Eg: `bundlerepo` or `statichttprepo`.
148 148 """
149 149
150 150 def __init__(self, repo, filtername):
151 151 object.__setattr__(self, '_unfilteredrepo', repo)
152 152 object.__setattr__(self, 'filtername', filtername)
153 153 object.__setattr__(self, '_clcachekey', None)
154 154 object.__setattr__(self, '_clcache', None)
155 155
156 156 # not a propertycache on purpose we shall implement a proper cache later
157 157 @property
158 158 def changelog(self):
159 159 """return a filtered version of the changeset
160 160
161 161 this changelog must not be used for writing"""
162 162 # some cache may be implemented later
163 163 unfi = self._unfilteredrepo
164 164 unfichangelog = unfi.changelog
165 165 revs = filterrevs(unfi, self.filtername)
166 166 cl = self._clcache
167 167 newkey = (len(unfichangelog), unfichangelog.tip(), hash(revs))
168 168 if cl is not None:
169 169 # we need to check curkey too for some obscure reason.
170 170 # MQ test show a corruption of the underlying repo (in _clcache)
171 171 # without change in the cachekey.
172 172 oldfilter = cl.filteredrevs
173 173 try:
174 174 cl.filterrevs = () # disable filtering for tip
175 175 curkey = (len(cl), cl.tip(), hash(oldfilter))
176 176 finally:
177 177 cl.filteredrevs = oldfilter
178 178 if newkey != self._clcachekey or newkey != curkey:
179 179 cl = None
180 180 # could have been made None by the previous if
181 181 if cl is None:
182 182 cl = copy.copy(unfichangelog)
183 183 cl.filteredrevs = revs
184 184 object.__setattr__(self, '_clcache', cl)
185 185 object.__setattr__(self, '_clcachekey', newkey)
186 186 return cl
187 187
188 188 def unfiltered(self):
189 189 """Return an unfiltered version of a repo"""
190 190 return self._unfilteredrepo
191 191
192 192 def filtered(self, name):
193 193 """Return a filtered version of a repository"""
194 194 if name == self.filtername:
195 195 return self
196 196 return self.unfiltered().filtered(name)
197 197
198 198 # everything access are forwarded to the proxied repo
199 199 def __getattr__(self, attr):
200 200 return getattr(self._unfilteredrepo, attr)
201 201
202 202 def __setattr__(self, attr, value):
203 203 return setattr(self._unfilteredrepo, attr, value)
204 204
205 205 def __delattr__(self, attr):
206 206 return delattr(self._unfilteredrepo, attr)
207 207
208 208 # The `requirements` attribute is initialized during __init__. But
209 209 # __getattr__ won't be called as it also exists on the class. We need
210 210 # explicit forwarding to main repo here
211 211 @property
212 212 def requirements(self):
213 213 return self._unfilteredrepo.requirements
General Comments 0
You need to be logged in to leave comments. Login now