Show More
@@ -0,0 +1,429 | |||||
|
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 | |||||
|
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 | |||||
|
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 | |||||
|
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