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