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