##// END OF EJS Templates
releasenotes: mention changeset with warning and abort...
Rishabh Madan -
r36787:3fff6f30 default
parent child Browse files
Show More
@@ -1,636 +1,636 b''
1 1 # Copyright 2017-present Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """generate release notes from commit messages (EXPERIMENTAL)
7 7
8 8 It is common to maintain files detailing changes in a project between
9 9 releases. Maintaining these files can be difficult and time consuming.
10 10 The :hg:`releasenotes` command provided by this extension makes the
11 11 process simpler by automating it.
12 12 """
13 13
14 14 from __future__ import absolute_import
15 15
16 16 import difflib
17 17 import errno
18 18 import re
19 19 import sys
20 20 import textwrap
21 21
22 22 from mercurial.i18n import _
23 23 from mercurial import (
24 24 config,
25 25 error,
26 26 minirst,
27 27 node,
28 28 pycompat,
29 29 registrar,
30 30 scmutil,
31 31 util,
32 32 )
33 33
34 34 cmdtable = {}
35 35 command = registrar.command(cmdtable)
36 36
37 37 try:
38 38 import fuzzywuzzy.fuzz as fuzz
39 39 fuzz.token_set_ratio
40 40 except ImportError:
41 41 fuzz = None
42 42
43 43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
44 44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
45 45 # be specifying the version(s) of Mercurial they are tested with, or
46 46 # leave the attribute unspecified.
47 47 testedwith = 'ships-with-hg-core'
48 48
49 49 DEFAULT_SECTIONS = [
50 50 ('feature', _('New Features')),
51 51 ('bc', _('Backwards Compatibility Changes')),
52 52 ('fix', _('Bug Fixes')),
53 53 ('perf', _('Performance Improvements')),
54 54 ('api', _('API Changes')),
55 55 ]
56 56
57 57 RE_DIRECTIVE = re.compile('^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$')
58 58 RE_ISSUE = r'\bissue ?[0-9]{4,6}(?![0-9])\b'
59 59
60 60 BULLET_SECTION = _('Other Changes')
61 61
62 62 class parsedreleasenotes(object):
63 63 def __init__(self):
64 64 self.sections = {}
65 65
66 66 def __contains__(self, section):
67 67 return section in self.sections
68 68
69 69 def __iter__(self):
70 70 return iter(sorted(self.sections))
71 71
72 72 def addtitleditem(self, section, title, paragraphs):
73 73 """Add a titled release note entry."""
74 74 self.sections.setdefault(section, ([], []))
75 75 self.sections[section][0].append((title, paragraphs))
76 76
77 77 def addnontitleditem(self, section, paragraphs):
78 78 """Adds a non-titled release note entry.
79 79
80 80 Will be rendered as a bullet point.
81 81 """
82 82 self.sections.setdefault(section, ([], []))
83 83 self.sections[section][1].append(paragraphs)
84 84
85 85 def titledforsection(self, section):
86 86 """Returns titled entries in a section.
87 87
88 88 Returns a list of (title, paragraphs) tuples describing sub-sections.
89 89 """
90 90 return self.sections.get(section, ([], []))[0]
91 91
92 92 def nontitledforsection(self, section):
93 93 """Returns non-titled, bulleted paragraphs in a section."""
94 94 return self.sections.get(section, ([], []))[1]
95 95
96 96 def hastitledinsection(self, section, title):
97 97 return any(t[0] == title for t in self.titledforsection(section))
98 98
99 99 def merge(self, ui, other):
100 100 """Merge another instance into this one.
101 101
102 102 This is used to combine multiple sources of release notes together.
103 103 """
104 104 if not fuzz:
105 105 ui.warn(_("module 'fuzzywuzzy' not found, merging of similar "
106 106 "releasenotes is disabled\n"))
107 107
108 108 for section in other:
109 109 existingnotes = converttitled(self.titledforsection(section)) + \
110 110 convertnontitled(self.nontitledforsection(section))
111 111 for title, paragraphs in other.titledforsection(section):
112 112 if self.hastitledinsection(section, title):
113 113 # TODO prompt for resolution if different and running in
114 114 # interactive mode.
115 115 ui.write(_('%s already exists in %s section; ignoring\n') %
116 116 (title, section))
117 117 continue
118 118
119 119 incoming_str = converttitled([(title, paragraphs)])[0]
120 120 if section == 'fix':
121 121 issue = getissuenum(incoming_str)
122 122 if issue:
123 123 if findissue(ui, existingnotes, issue):
124 124 continue
125 125
126 126 if similar(ui, existingnotes, incoming_str):
127 127 continue
128 128
129 129 self.addtitleditem(section, title, paragraphs)
130 130
131 131 for paragraphs in other.nontitledforsection(section):
132 132 if paragraphs in self.nontitledforsection(section):
133 133 continue
134 134
135 135 incoming_str = convertnontitled([paragraphs])[0]
136 136 if section == 'fix':
137 137 issue = getissuenum(incoming_str)
138 138 if issue:
139 139 if findissue(ui, existingnotes, issue):
140 140 continue
141 141
142 142 if similar(ui, existingnotes, incoming_str):
143 143 continue
144 144
145 145 self.addnontitleditem(section, paragraphs)
146 146
147 147 class releasenotessections(object):
148 148 def __init__(self, ui, repo=None):
149 149 if repo:
150 150 sections = util.sortdict(DEFAULT_SECTIONS)
151 151 custom_sections = getcustomadmonitions(repo)
152 152 if custom_sections:
153 153 sections.update(custom_sections)
154 154 self._sections = list(sections.iteritems())
155 155 else:
156 156 self._sections = list(DEFAULT_SECTIONS)
157 157
158 158 def __iter__(self):
159 159 return iter(self._sections)
160 160
161 161 def names(self):
162 162 return [t[0] for t in self._sections]
163 163
164 164 def sectionfromtitle(self, title):
165 165 for name, value in self._sections:
166 166 if value == title:
167 167 return name
168 168
169 169 return None
170 170
171 171 def converttitled(titledparagraphs):
172 172 """
173 173 Convert titled paragraphs to strings
174 174 """
175 175 string_list = []
176 176 for title, paragraphs in titledparagraphs:
177 177 lines = []
178 178 for para in paragraphs:
179 179 lines.extend(para)
180 180 string_list.append(' '.join(lines))
181 181 return string_list
182 182
183 183 def convertnontitled(nontitledparagraphs):
184 184 """
185 185 Convert non-titled bullets to strings
186 186 """
187 187 string_list = []
188 188 for paragraphs in nontitledparagraphs:
189 189 lines = []
190 190 for para in paragraphs:
191 191 lines.extend(para)
192 192 string_list.append(' '.join(lines))
193 193 return string_list
194 194
195 195 def getissuenum(incoming_str):
196 196 """
197 197 Returns issue number from the incoming string if it exists
198 198 """
199 199 issue = re.search(RE_ISSUE, incoming_str, re.IGNORECASE)
200 200 if issue:
201 201 issue = issue.group()
202 202 return issue
203 203
204 204 def findissue(ui, existing, issue):
205 205 """
206 206 Returns true if issue number already exists in notes.
207 207 """
208 208 if any(issue in s for s in existing):
209 209 ui.write(_('"%s" already exists in notes; ignoring\n') % issue)
210 210 return True
211 211 else:
212 212 return False
213 213
214 214 def similar(ui, existing, incoming_str):
215 215 """
216 216 Returns true if similar note found in existing notes.
217 217 """
218 218 if len(incoming_str.split()) > 10:
219 219 merge = similaritycheck(incoming_str, existing)
220 220 if not merge:
221 221 ui.write(_('"%s" already exists in notes file; ignoring\n')
222 222 % incoming_str)
223 223 return True
224 224 else:
225 225 return False
226 226 else:
227 227 return False
228 228
229 229 def similaritycheck(incoming_str, existingnotes):
230 230 """
231 231 Returns false when note fragment can be merged to existing notes.
232 232 """
233 233 # fuzzywuzzy not present
234 234 if not fuzz:
235 235 return True
236 236
237 237 merge = True
238 238 for bullet in existingnotes:
239 239 score = fuzz.token_set_ratio(incoming_str, bullet)
240 240 if score > 75:
241 241 merge = False
242 242 break
243 243 return merge
244 244
245 245 def getcustomadmonitions(repo):
246 246 ctx = repo['.']
247 247 p = config.config()
248 248
249 249 def read(f, sections=None, remap=None):
250 250 if f in ctx:
251 251 data = ctx[f].data()
252 252 p.parse(f, data, sections, remap, read)
253 253 else:
254 254 raise error.Abort(_(".hgreleasenotes file \'%s\' not found") %
255 255 repo.pathto(f))
256 256
257 257 if '.hgreleasenotes' in ctx:
258 258 read('.hgreleasenotes')
259 259 return p['sections']
260 260
261 261 def checkadmonitions(ui, repo, directives, revs):
262 262 """
263 263 Checks the commit messages for admonitions and their validity.
264 264
265 265 .. abcd::
266 266
267 267 First paragraph under this admonition
268 268
269 269 For this commit message, using `hg releasenotes -r . --check`
270 270 returns: Invalid admonition 'abcd' present in changeset 3ea92981e103
271 271
272 272 As admonition 'abcd' is neither present in default nor custom admonitions
273 273 """
274 274 for rev in revs:
275 275 ctx = repo[rev]
276 276 admonition = re.search(RE_DIRECTIVE, ctx.description())
277 277 if admonition:
278 278 if admonition.group(1) in directives:
279 279 continue
280 280 else:
281 281 ui.write(_("Invalid admonition '%s' present in changeset %s"
282 282 "\n") % (admonition.group(1), ctx.hex()[:12]))
283 283 sim = lambda x: difflib.SequenceMatcher(None,
284 284 admonition.group(1), x).ratio()
285 285
286 286 similar = [s for s in directives if sim(s) > 0.6]
287 287 if len(similar) == 1:
288 288 ui.write(_("(did you mean %s?)\n") % similar[0])
289 289 elif similar:
290 290 ss = ", ".join(sorted(similar))
291 291 ui.write(_("(did you mean one of %s?)\n") % ss)
292 292
293 293 def _getadmonitionlist(ui, sections):
294 294 for section in sections:
295 295 ui.write("%s: %s\n" % (section[0], section[1]))
296 296
297 297 def parsenotesfromrevisions(repo, directives, revs):
298 298 notes = parsedreleasenotes()
299 299
300 300 for rev in revs:
301 301 ctx = repo[rev]
302 302
303 303 blocks, pruned = minirst.parse(ctx.description(),
304 304 admonitions=directives)
305 305
306 306 for i, block in enumerate(blocks):
307 307 if block['type'] != 'admonition':
308 308 continue
309 309
310 310 directive = block['admonitiontitle']
311 311 title = block['lines'][0].strip() if block['lines'] else None
312 312
313 313 if i + 1 == len(blocks):
314 raise error.Abort(_('release notes directive %s lacks content')
315 % directive)
314 raise error.Abort(_('changeset %s: release notes directive %s '
315 'lacks content') % (ctx, directive))
316 316
317 317 # Now search ahead and find all paragraphs attached to this
318 318 # admonition.
319 319 paragraphs = []
320 320 for j in range(i + 1, len(blocks)):
321 321 pblock = blocks[j]
322 322
323 323 # Margin blocks may appear between paragraphs. Ignore them.
324 324 if pblock['type'] == 'margin':
325 325 continue
326 326
327 327 if pblock['type'] != 'paragraph':
328 repo.ui.warn(_('unexpected block in release notes '
329 'directive %s\n') % directive)
328 repo.ui.warn(_('changeset %s: unexpected block in release '
329 'notes directive %s\n') % (ctx, directive))
330 330
331 331 if pblock['indent'] > 0:
332 332 paragraphs.append(pblock['lines'])
333 333 else:
334 334 break
335 335
336 336 # TODO consider using title as paragraph for more concise notes.
337 337 if not paragraphs:
338 338 repo.ui.warn(_("error parsing releasenotes for revision: "
339 339 "'%s'\n") % node.hex(ctx.node()))
340 340 if title:
341 341 notes.addtitleditem(directive, title, paragraphs)
342 342 else:
343 343 notes.addnontitleditem(directive, paragraphs)
344 344
345 345 return notes
346 346
347 347 def parsereleasenotesfile(sections, text):
348 348 """Parse text content containing generated release notes."""
349 349 notes = parsedreleasenotes()
350 350
351 351 blocks = minirst.parse(text)[0]
352 352
353 353 def gatherparagraphsbullets(offset, title=False):
354 354 notefragment = []
355 355
356 356 for i in range(offset + 1, len(blocks)):
357 357 block = blocks[i]
358 358
359 359 if block['type'] == 'margin':
360 360 continue
361 361 elif block['type'] == 'section':
362 362 break
363 363 elif block['type'] == 'bullet':
364 364 if block['indent'] != 0:
365 365 raise error.Abort(_('indented bullet lists not supported'))
366 366 if title:
367 367 lines = [l[1:].strip() for l in block['lines']]
368 368 notefragment.append(lines)
369 369 continue
370 370 else:
371 371 lines = [[l[1:].strip() for l in block['lines']]]
372 372
373 373 for block in blocks[i + 1:]:
374 374 if block['type'] in ('bullet', 'section'):
375 375 break
376 376 if block['type'] == 'paragraph':
377 377 lines.append(block['lines'])
378 378 notefragment.append(lines)
379 379 continue
380 380 elif block['type'] != 'paragraph':
381 381 raise error.Abort(_('unexpected block type in release notes: '
382 382 '%s') % block['type'])
383 383 if title:
384 384 notefragment.append(block['lines'])
385 385
386 386 return notefragment
387 387
388 388 currentsection = None
389 389 for i, block in enumerate(blocks):
390 390 if block['type'] != 'section':
391 391 continue
392 392
393 393 title = block['lines'][0]
394 394
395 395 # TODO the parsing around paragraphs and bullet points needs some
396 396 # work.
397 397 if block['underline'] == '=': # main section
398 398 name = sections.sectionfromtitle(title)
399 399 if not name:
400 400 raise error.Abort(_('unknown release notes section: %s') %
401 401 title)
402 402
403 403 currentsection = name
404 404 bullet_points = gatherparagraphsbullets(i)
405 405 if bullet_points:
406 406 for para in bullet_points:
407 407 notes.addnontitleditem(currentsection, para)
408 408
409 409 elif block['underline'] == '-': # sub-section
410 410 if title == BULLET_SECTION:
411 411 bullet_points = gatherparagraphsbullets(i)
412 412 for para in bullet_points:
413 413 notes.addnontitleditem(currentsection, para)
414 414 else:
415 415 paragraphs = gatherparagraphsbullets(i, True)
416 416 notes.addtitleditem(currentsection, title, paragraphs)
417 417 else:
418 418 raise error.Abort(_('unsupported section type for %s') % title)
419 419
420 420 return notes
421 421
422 422 def serializenotes(sections, notes):
423 423 """Serialize release notes from parsed fragments and notes.
424 424
425 425 This function essentially takes the output of ``parsenotesfromrevisions()``
426 426 and ``parserelnotesfile()`` and produces output combining the 2.
427 427 """
428 428 lines = []
429 429
430 430 for sectionname, sectiontitle in sections:
431 431 if sectionname not in notes:
432 432 continue
433 433
434 434 lines.append(sectiontitle)
435 435 lines.append('=' * len(sectiontitle))
436 436 lines.append('')
437 437
438 438 # First pass to emit sub-sections.
439 439 for title, paragraphs in notes.titledforsection(sectionname):
440 440 lines.append(title)
441 441 lines.append('-' * len(title))
442 442 lines.append('')
443 443
444 444 wrapper = textwrap.TextWrapper(width=78)
445 445 for i, para in enumerate(paragraphs):
446 446 if i:
447 447 lines.append('')
448 448 lines.extend(wrapper.wrap(' '.join(para)))
449 449
450 450 lines.append('')
451 451
452 452 # Second pass to emit bullet list items.
453 453
454 454 # If the section has titled and non-titled items, we can't
455 455 # simply emit the bullet list because it would appear to come
456 456 # from the last title/section. So, we emit a new sub-section
457 457 # for the non-titled items.
458 458 nontitled = notes.nontitledforsection(sectionname)
459 459 if notes.titledforsection(sectionname) and nontitled:
460 460 # TODO make configurable.
461 461 lines.append(BULLET_SECTION)
462 462 lines.append('-' * len(BULLET_SECTION))
463 463 lines.append('')
464 464
465 465 for paragraphs in nontitled:
466 466 wrapper = textwrap.TextWrapper(initial_indent='* ',
467 467 subsequent_indent=' ',
468 468 width=78)
469 469 lines.extend(wrapper.wrap(' '.join(paragraphs[0])))
470 470
471 471 wrapper = textwrap.TextWrapper(initial_indent=' ',
472 472 subsequent_indent=' ',
473 473 width=78)
474 474 for para in paragraphs[1:]:
475 475 lines.append('')
476 476 lines.extend(wrapper.wrap(' '.join(para)))
477 477
478 478 lines.append('')
479 479
480 480 if lines and lines[-1]:
481 481 lines.append('')
482 482
483 483 return '\n'.join(lines)
484 484
485 485 @command('releasenotes',
486 486 [('r', 'rev', '', _('revisions to process for release notes'), _('REV')),
487 487 ('c', 'check', False, _('checks for validity of admonitions (if any)'),
488 488 _('REV')),
489 489 ('l', 'list', False, _('list the available admonitions with their title'),
490 490 None)],
491 491 _('hg releasenotes [-r REV] [-c] FILE'))
492 492 def releasenotes(ui, repo, file_=None, **opts):
493 493 """parse release notes from commit messages into an output file
494 494
495 495 Given an output file and set of revisions, this command will parse commit
496 496 messages for release notes then add them to the output file.
497 497
498 498 Release notes are defined in commit messages as ReStructuredText
499 499 directives. These have the form::
500 500
501 501 .. directive:: title
502 502
503 503 content
504 504
505 505 Each ``directive`` maps to an output section in a generated release notes
506 506 file, which itself is ReStructuredText. For example, the ``.. feature::``
507 507 directive would map to a ``New Features`` section.
508 508
509 509 Release note directives can be either short-form or long-form. In short-
510 510 form, ``title`` is omitted and the release note is rendered as a bullet
511 511 list. In long form, a sub-section with the title ``title`` is added to the
512 512 section.
513 513
514 514 The ``FILE`` argument controls the output file to write gathered release
515 515 notes to. The format of the file is::
516 516
517 517 Section 1
518 518 =========
519 519
520 520 ...
521 521
522 522 Section 2
523 523 =========
524 524
525 525 ...
526 526
527 527 Only sections with defined release notes are emitted.
528 528
529 529 If a section only has short-form notes, it will consist of bullet list::
530 530
531 531 Section
532 532 =======
533 533
534 534 * Release note 1
535 535 * Release note 2
536 536
537 537 If a section has long-form notes, sub-sections will be emitted::
538 538
539 539 Section
540 540 =======
541 541
542 542 Note 1 Title
543 543 ------------
544 544
545 545 Description of the first long-form note.
546 546
547 547 Note 2 Title
548 548 ------------
549 549
550 550 Description of the second long-form note.
551 551
552 552 If the ``FILE`` argument points to an existing file, that file will be
553 553 parsed for release notes having the format that would be generated by this
554 554 command. The notes from the processed commit messages will be *merged*
555 555 into this parsed set.
556 556
557 557 During release notes merging:
558 558
559 559 * Duplicate items are automatically ignored
560 560 * Items that are different are automatically ignored if the similarity is
561 561 greater than a threshold.
562 562
563 563 This means that the release notes file can be updated independently from
564 564 this command and changes should not be lost when running this command on
565 565 that file. A particular use case for this is to tweak the wording of a
566 566 release note after it has been added to the release notes file.
567 567
568 568 The -c/--check option checks the commit message for invalid admonitions.
569 569
570 570 The -l/--list option, presents the user with a list of existing available
571 571 admonitions along with their title. This also includes the custom
572 572 admonitions (if any).
573 573 """
574 574
575 575 opts = pycompat.byteskwargs(opts)
576 576 sections = releasenotessections(ui, repo)
577 577
578 578 listflag = opts.get('list')
579 579
580 580 if listflag and opts.get('rev'):
581 581 raise error.Abort(_('cannot use both \'--list\' and \'--rev\''))
582 582 if listflag and opts.get('check'):
583 583 raise error.Abort(_('cannot use both \'--list\' and \'--check\''))
584 584
585 585 if listflag:
586 586 return _getadmonitionlist(ui, sections)
587 587
588 588 rev = opts.get('rev')
589 589 revs = scmutil.revrange(repo, [rev or 'not public()'])
590 590 if opts.get('check'):
591 591 return checkadmonitions(ui, repo, sections.names(), revs)
592 592
593 593 incoming = parsenotesfromrevisions(repo, sections.names(), revs)
594 594
595 595 if file_ is None:
596 596 ui.pager('releasenotes')
597 597 return ui.write(serializenotes(sections, incoming))
598 598
599 599 try:
600 600 with open(file_, 'rb') as fh:
601 601 notes = parsereleasenotesfile(sections, fh.read())
602 602 except IOError as e:
603 603 if e.errno != errno.ENOENT:
604 604 raise
605 605
606 606 notes = parsedreleasenotes()
607 607
608 608 notes.merge(ui, incoming)
609 609
610 610 with open(file_, 'wb') as fh:
611 611 fh.write(serializenotes(sections, notes))
612 612
613 613 @command('debugparsereleasenotes', norepo=True)
614 614 def debugparsereleasenotes(ui, path, repo=None):
615 615 """parse release notes and print resulting data structure"""
616 616 if path == '-':
617 617 text = sys.stdin.read()
618 618 else:
619 619 with open(path, 'rb') as fh:
620 620 text = fh.read()
621 621
622 622 sections = releasenotessections(ui, repo)
623 623
624 624 notes = parsereleasenotesfile(sections, text)
625 625
626 626 for section in notes:
627 627 ui.write(_('section: %s\n') % section)
628 628 for title, paragraphs in notes.titledforsection(section):
629 629 ui.write(_(' subsection: %s\n') % title)
630 630 for para in paragraphs:
631 631 ui.write(_(' paragraph: %s\n') % ' '.join(para))
632 632
633 633 for paragraphs in notes.nontitledforsection(section):
634 634 ui.write(_(' bullet point:\n'))
635 635 for para in paragraphs:
636 636 ui.write(_(' paragraph: %s\n') % ' '.join(para))
@@ -1,202 +1,202 b''
1 1 #require fuzzywuzzy
2 2
3 3 $ cat >> $HGRCPATH << EOF
4 4 > [extensions]
5 5 > releasenotes=
6 6 > EOF
7 7
8 8 Bullet point with a single item spanning a single line
9 9
10 10 $ hg debugparsereleasenotes - << EOF
11 11 > New Features
12 12 > ============
13 13 >
14 14 > * Bullet point item with a single line
15 15 > EOF
16 16 section: feature
17 17 bullet point:
18 18 paragraph: Bullet point item with a single line
19 19
20 20 Bullet point that spans multiple lines.
21 21
22 22 $ hg debugparsereleasenotes - << EOF
23 23 > New Features
24 24 > ============
25 25 >
26 26 > * Bullet point with a paragraph
27 27 > that spans multiple lines.
28 28 > EOF
29 29 section: feature
30 30 bullet point:
31 31 paragraph: Bullet point with a paragraph that spans multiple lines.
32 32
33 33 $ hg debugparsereleasenotes - << EOF
34 34 > New Features
35 35 > ============
36 36 >
37 37 > * Bullet point with a paragraph
38 38 > that spans multiple lines.
39 39 >
40 40 > And has an empty line between lines too.
41 41 > With a line cuddling that.
42 42 > EOF
43 43 section: feature
44 44 bullet point:
45 45 paragraph: Bullet point with a paragraph that spans multiple lines.
46 46 paragraph: And has an empty line between lines too. With a line cuddling that.
47 47
48 48 Multiple bullet points. With some entries being multiple lines.
49 49
50 50 $ hg debugparsereleasenotes - << EOF
51 51 > New Features
52 52 > ============
53 53 >
54 54 > * First bullet point. It has a single line.
55 55 >
56 56 > * Second bullet point.
57 57 > It consists of multiple lines.
58 58 >
59 59 > * Third bullet point. It has a single line.
60 60 > EOF
61 61 section: feature
62 62 bullet point:
63 63 paragraph: First bullet point. It has a single line.
64 64 bullet point:
65 65 paragraph: Second bullet point. It consists of multiple lines.
66 66 bullet point:
67 67 paragraph: Third bullet point. It has a single line.
68 68
69 69 Bullet point without newline between items
70 70
71 71 $ hg debugparsereleasenotes - << EOF
72 72 > New Features
73 73 > ============
74 74 >
75 75 > * First bullet point
76 76 > * Second bullet point
77 77 > And it has multiple lines
78 78 > * Third bullet point
79 79 > * Fourth bullet point
80 80 > EOF
81 81 section: feature
82 82 bullet point:
83 83 paragraph: First bullet point
84 84 bullet point:
85 85 paragraph: Second bullet point And it has multiple lines
86 86 bullet point:
87 87 paragraph: Third bullet point
88 88 bullet point:
89 89 paragraph: Fourth bullet point
90 90
91 91 Sub-section contents are read
92 92
93 93 $ hg debugparsereleasenotes - << EOF
94 94 > New Features
95 95 > ============
96 96 >
97 97 > First Feature
98 98 > -------------
99 99 >
100 100 > This is the first new feature that was implemented.
101 101 >
102 102 > And a second paragraph about it.
103 103 >
104 104 > Second Feature
105 105 > --------------
106 106 >
107 107 > This is the second new feature that was implemented.
108 108 >
109 109 > Paragraph two.
110 110 >
111 111 > Paragraph three.
112 112 > EOF
113 113 section: feature
114 114 subsection: First Feature
115 115 paragraph: This is the first new feature that was implemented.
116 116 paragraph: And a second paragraph about it.
117 117 subsection: Second Feature
118 118 paragraph: This is the second new feature that was implemented.
119 119 paragraph: Paragraph two.
120 120 paragraph: Paragraph three.
121 121
122 122 Multiple sections are read
123 123
124 124 $ hg debugparsereleasenotes - << EOF
125 125 > New Features
126 126 > ============
127 127 >
128 128 > * Feature 1
129 129 > * Feature 2
130 130 >
131 131 > Bug Fixes
132 132 > =========
133 133 >
134 134 > * Fix 1
135 135 > * Fix 2
136 136 > EOF
137 137 section: feature
138 138 bullet point:
139 139 paragraph: Feature 1
140 140 bullet point:
141 141 paragraph: Feature 2
142 142 section: fix
143 143 bullet point:
144 144 paragraph: Fix 1
145 145 bullet point:
146 146 paragraph: Fix 2
147 147
148 148 Mixed sub-sections and bullet list
149 149
150 150 $ hg debugparsereleasenotes - << EOF
151 151 > New Features
152 152 > ============
153 153 >
154 154 > Feature 1
155 155 > ---------
156 156 >
157 157 > Some words about the first feature.
158 158 >
159 159 > Feature 2
160 160 > ---------
161 161 >
162 162 > Some words about the second feature.
163 163 > That span multiple lines.
164 164 >
165 165 > Other Changes
166 166 > -------------
167 167 >
168 168 > * Bullet item 1
169 169 > * Bullet item 2
170 170 > EOF
171 171 section: feature
172 172 subsection: Feature 1
173 173 paragraph: Some words about the first feature.
174 174 subsection: Feature 2
175 175 paragraph: Some words about the second feature. That span multiple lines.
176 176 bullet point:
177 177 paragraph: Bullet item 1
178 178 bullet point:
179 179 paragraph: Bullet item 2
180 180
181 181 Warn user in case of unexpected block while parsing
182 182
183 183 $ hg init relnotes-warn
184 184 $ cd relnotes-warn
185 185 $ touch feature1
186 186 $ hg -q commit -A -l - << EOF
187 187 > commit 1
188 188 >
189 189 > .. feature::
190 190 >
191 191 > new feature added.
192 192 > some words about the feature.
193 193 > EOF
194 194
195 195 $ hg releasenote -r .
196 unexpected block in release notes directive feature
196 changeset a4251905c440: unexpected block in release notes directive feature
197 197 New Features
198 198 ============
199 199
200 200 * new feature added. some words about the feature.
201 201
202 202 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now