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