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