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