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