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