101 """Merge another instance into this one. |
101 """Merge another instance into this one. |
102 |
102 |
103 This is used to combine multiple sources of release notes together. |
103 This is used to combine multiple sources of release notes together. |
104 """ |
104 """ |
105 if not fuzz: |
105 if not fuzz: |
106 ui.warn(_("module 'fuzzywuzzy' not found, merging of similar " |
106 ui.warn( |
107 "releasenotes is disabled\n")) |
107 _( |
|
108 "module 'fuzzywuzzy' not found, merging of similar " |
|
109 "releasenotes is disabled\n" |
|
110 ) |
|
111 ) |
108 |
112 |
109 for section in other: |
113 for section in other: |
110 existingnotes = ( |
114 existingnotes = converttitled( |
111 converttitled(self.titledforsection(section)) + |
115 self.titledforsection(section) |
112 convertnontitled(self.nontitledforsection(section))) |
116 ) + convertnontitled(self.nontitledforsection(section)) |
113 for title, paragraphs in other.titledforsection(section): |
117 for title, paragraphs in other.titledforsection(section): |
114 if self.hastitledinsection(section, title): |
118 if self.hastitledinsection(section, title): |
115 # TODO prompt for resolution if different and running in |
119 # TODO prompt for resolution if different and running in |
116 # interactive mode. |
120 # interactive mode. |
117 ui.write(_('%s already exists in %s section; ignoring\n') % |
121 ui.write( |
118 (title, section)) |
122 _('%s already exists in %s section; ignoring\n') |
|
123 % (title, section) |
|
124 ) |
119 continue |
125 continue |
120 |
126 |
121 incoming_str = converttitled([(title, paragraphs)])[0] |
127 incoming_str = converttitled([(title, paragraphs)])[0] |
122 if section == 'fix': |
128 if section == 'fix': |
123 issue = getissuenum(incoming_str) |
129 issue = getissuenum(incoming_str) |
143 |
149 |
144 if similar(ui, existingnotes, incoming_str): |
150 if similar(ui, existingnotes, incoming_str): |
145 continue |
151 continue |
146 |
152 |
147 self.addnontitleditem(section, paragraphs) |
153 self.addnontitleditem(section, paragraphs) |
|
154 |
148 |
155 |
149 class releasenotessections(object): |
156 class releasenotessections(object): |
150 def __init__(self, ui, repo=None): |
157 def __init__(self, ui, repo=None): |
151 if repo: |
158 if repo: |
152 sections = util.sortdict(DEFAULT_SECTIONS) |
159 sections = util.sortdict(DEFAULT_SECTIONS) |
192 for para in paragraphs: |
201 for para in paragraphs: |
193 lines.extend(para) |
202 lines.extend(para) |
194 string_list.append(' '.join(lines)) |
203 string_list.append(' '.join(lines)) |
195 return string_list |
204 return string_list |
196 |
205 |
|
206 |
197 def getissuenum(incoming_str): |
207 def getissuenum(incoming_str): |
198 """ |
208 """ |
199 Returns issue number from the incoming string if it exists |
209 Returns issue number from the incoming string if it exists |
200 """ |
210 """ |
201 issue = re.search(RE_ISSUE, incoming_str, re.IGNORECASE) |
211 issue = re.search(RE_ISSUE, incoming_str, re.IGNORECASE) |
202 if issue: |
212 if issue: |
203 issue = issue.group() |
213 issue = issue.group() |
204 return issue |
214 return issue |
|
215 |
205 |
216 |
206 def findissue(ui, existing, issue): |
217 def findissue(ui, existing, issue): |
207 """ |
218 """ |
208 Returns true if issue number already exists in notes. |
219 Returns true if issue number already exists in notes. |
209 """ |
220 """ |
211 ui.write(_('"%s" already exists in notes; ignoring\n') % issue) |
222 ui.write(_('"%s" already exists in notes; ignoring\n') % issue) |
212 return True |
223 return True |
213 else: |
224 else: |
214 return False |
225 return False |
215 |
226 |
|
227 |
216 def similar(ui, existing, incoming_str): |
228 def similar(ui, existing, incoming_str): |
217 """ |
229 """ |
218 Returns true if similar note found in existing notes. |
230 Returns true if similar note found in existing notes. |
219 """ |
231 """ |
220 if len(incoming_str.split()) > 10: |
232 if len(incoming_str.split()) > 10: |
221 merge = similaritycheck(incoming_str, existing) |
233 merge = similaritycheck(incoming_str, existing) |
222 if not merge: |
234 if not merge: |
223 ui.write(_('"%s" already exists in notes file; ignoring\n') |
235 ui.write( |
224 % incoming_str) |
236 _('"%s" already exists in notes file; ignoring\n') |
|
237 % incoming_str |
|
238 ) |
225 return True |
239 return True |
226 else: |
240 else: |
227 return False |
241 return False |
228 else: |
242 else: |
229 return False |
243 return False |
|
244 |
230 |
245 |
231 def similaritycheck(incoming_str, existingnotes): |
246 def similaritycheck(incoming_str, existingnotes): |
232 """ |
247 """ |
233 Returns false when note fragment can be merged to existing notes. |
248 Returns false when note fragment can be merged to existing notes. |
234 """ |
249 """ |
242 if score > 75: |
257 if score > 75: |
243 merge = False |
258 merge = False |
244 break |
259 break |
245 return merge |
260 return merge |
246 |
261 |
|
262 |
247 def getcustomadmonitions(repo): |
263 def getcustomadmonitions(repo): |
248 ctx = repo['.'] |
264 ctx = repo['.'] |
249 p = config.config() |
265 p = config.config() |
250 |
266 |
251 def read(f, sections=None, remap=None): |
267 def read(f, sections=None, remap=None): |
252 if f in ctx: |
268 if f in ctx: |
253 data = ctx[f].data() |
269 data = ctx[f].data() |
254 p.parse(f, data, sections, remap, read) |
270 p.parse(f, data, sections, remap, read) |
255 else: |
271 else: |
256 raise error.Abort(_(".hgreleasenotes file \'%s\' not found") % |
272 raise error.Abort( |
257 repo.pathto(f)) |
273 _(".hgreleasenotes file \'%s\' not found") % repo.pathto(f) |
|
274 ) |
258 |
275 |
259 if '.hgreleasenotes' in ctx: |
276 if '.hgreleasenotes' in ctx: |
260 read('.hgreleasenotes') |
277 read('.hgreleasenotes') |
261 return p['sections'] |
278 return p['sections'] |
|
279 |
262 |
280 |
263 def checkadmonitions(ui, repo, directives, revs): |
281 def checkadmonitions(ui, repo, directives, revs): |
264 """ |
282 """ |
265 Checks the commit messages for admonitions and their validity. |
283 Checks the commit messages for admonitions and their validity. |
266 |
284 |
278 admonition = re.search(RE_DIRECTIVE, ctx.description()) |
296 admonition = re.search(RE_DIRECTIVE, ctx.description()) |
279 if admonition: |
297 if admonition: |
280 if admonition.group(1) in directives: |
298 if admonition.group(1) in directives: |
281 continue |
299 continue |
282 else: |
300 else: |
283 ui.write(_("Invalid admonition '%s' present in changeset %s" |
301 ui.write( |
284 "\n") % (admonition.group(1), ctx.hex()[:12])) |
302 _("Invalid admonition '%s' present in changeset %s" "\n") |
285 sim = lambda x: difflib.SequenceMatcher(None, |
303 % (admonition.group(1), ctx.hex()[:12]) |
286 admonition.group(1), x).ratio() |
304 ) |
|
305 sim = lambda x: difflib.SequenceMatcher( |
|
306 None, admonition.group(1), x |
|
307 ).ratio() |
287 |
308 |
288 similar = [s for s in directives if sim(s) > 0.6] |
309 similar = [s for s in directives if sim(s) > 0.6] |
289 if len(similar) == 1: |
310 if len(similar) == 1: |
290 ui.write(_("(did you mean %s?)\n") % similar[0]) |
311 ui.write(_("(did you mean %s?)\n") % similar[0]) |
291 elif similar: |
312 elif similar: |
292 ss = ", ".join(sorted(similar)) |
313 ss = ", ".join(sorted(similar)) |
293 ui.write(_("(did you mean one of %s?)\n") % ss) |
314 ui.write(_("(did you mean one of %s?)\n") % ss) |
294 |
315 |
|
316 |
295 def _getadmonitionlist(ui, sections): |
317 def _getadmonitionlist(ui, sections): |
296 for section in sections: |
318 for section in sections: |
297 ui.write("%s: %s\n" % (section[0], section[1])) |
319 ui.write("%s: %s\n" % (section[0], section[1])) |
298 |
320 |
|
321 |
299 def parsenotesfromrevisions(repo, directives, revs): |
322 def parsenotesfromrevisions(repo, directives, revs): |
300 notes = parsedreleasenotes() |
323 notes = parsedreleasenotes() |
301 |
324 |
302 for rev in revs: |
325 for rev in revs: |
303 ctx = repo[rev] |
326 ctx = repo[rev] |
304 |
327 |
305 blocks, pruned = minirst.parse(ctx.description(), |
328 blocks, pruned = minirst.parse( |
306 admonitions=directives) |
329 ctx.description(), admonitions=directives |
|
330 ) |
307 |
331 |
308 for i, block in enumerate(blocks): |
332 for i, block in enumerate(blocks): |
309 if block['type'] != 'admonition': |
333 if block['type'] != 'admonition': |
310 continue |
334 continue |
311 |
335 |
312 directive = block['admonitiontitle'] |
336 directive = block['admonitiontitle'] |
313 title = block['lines'][0].strip() if block['lines'] else None |
337 title = block['lines'][0].strip() if block['lines'] else None |
314 |
338 |
315 if i + 1 == len(blocks): |
339 if i + 1 == len(blocks): |
316 raise error.Abort(_('changeset %s: release notes directive %s ' |
340 raise error.Abort( |
317 'lacks content') % (ctx, directive)) |
341 _( |
|
342 'changeset %s: release notes directive %s ' |
|
343 'lacks content' |
|
344 ) |
|
345 % (ctx, directive) |
|
346 ) |
318 |
347 |
319 # Now search ahead and find all paragraphs attached to this |
348 # Now search ahead and find all paragraphs attached to this |
320 # admonition. |
349 # admonition. |
321 paragraphs = [] |
350 paragraphs = [] |
322 for j in range(i + 1, len(blocks)): |
351 for j in range(i + 1, len(blocks)): |
328 |
357 |
329 if pblock['type'] == 'admonition': |
358 if pblock['type'] == 'admonition': |
330 break |
359 break |
331 |
360 |
332 if pblock['type'] != 'paragraph': |
361 if pblock['type'] != 'paragraph': |
333 repo.ui.warn(_('changeset %s: unexpected block in release ' |
362 repo.ui.warn( |
334 'notes directive %s\n') % (ctx, directive)) |
363 _( |
|
364 'changeset %s: unexpected block in release ' |
|
365 'notes directive %s\n' |
|
366 ) |
|
367 % (ctx, directive) |
|
368 ) |
335 |
369 |
336 if pblock['indent'] > 0: |
370 if pblock['indent'] > 0: |
337 paragraphs.append(pblock['lines']) |
371 paragraphs.append(pblock['lines']) |
338 else: |
372 else: |
339 break |
373 break |
340 |
374 |
341 # TODO consider using title as paragraph for more concise notes. |
375 # TODO consider using title as paragraph for more concise notes. |
342 if not paragraphs: |
376 if not paragraphs: |
343 repo.ui.warn(_("error parsing releasenotes for revision: " |
377 repo.ui.warn( |
344 "'%s'\n") % node.hex(ctx.node())) |
378 _("error parsing releasenotes for revision: " "'%s'\n") |
|
379 % node.hex(ctx.node()) |
|
380 ) |
345 if title: |
381 if title: |
346 notes.addtitleditem(directive, title, paragraphs) |
382 notes.addtitleditem(directive, title, paragraphs) |
347 else: |
383 else: |
348 notes.addnontitleditem(directive, paragraphs) |
384 notes.addnontitleditem(directive, paragraphs) |
349 |
385 |
350 return notes |
386 return notes |
|
387 |
351 |
388 |
352 def parsereleasenotesfile(sections, text): |
389 def parsereleasenotesfile(sections, text): |
353 """Parse text content containing generated release notes.""" |
390 """Parse text content containing generated release notes.""" |
354 notes = parsedreleasenotes() |
391 notes = parsedreleasenotes() |
355 |
392 |
373 notefragment.append(lines) |
410 notefragment.append(lines) |
374 continue |
411 continue |
375 else: |
412 else: |
376 lines = [[l[1:].strip() for l in block['lines']]] |
413 lines = [[l[1:].strip() for l in block['lines']]] |
377 |
414 |
378 for block in blocks[i + 1:]: |
415 for block in blocks[i + 1 :]: |
379 if block['type'] in ('bullet', 'section'): |
416 if block['type'] in ('bullet', 'section'): |
380 break |
417 break |
381 if block['type'] == 'paragraph': |
418 if block['type'] == 'paragraph': |
382 lines.append(block['lines']) |
419 lines.append(block['lines']) |
383 notefragment.append(lines) |
420 notefragment.append(lines) |
384 continue |
421 continue |
385 elif block['type'] != 'paragraph': |
422 elif block['type'] != 'paragraph': |
386 raise error.Abort(_('unexpected block type in release notes: ' |
423 raise error.Abort( |
387 '%s') % block['type']) |
424 _('unexpected block type in release notes: ' '%s') |
|
425 % block['type'] |
|
426 ) |
388 if title: |
427 if title: |
389 notefragment.append(block['lines']) |
428 notefragment.append(block['lines']) |
390 |
429 |
391 return notefragment |
430 return notefragment |
392 |
431 |
400 # TODO the parsing around paragraphs and bullet points needs some |
439 # TODO the parsing around paragraphs and bullet points needs some |
401 # work. |
440 # work. |
402 if block['underline'] == '=': # main section |
441 if block['underline'] == '=': # main section |
403 name = sections.sectionfromtitle(title) |
442 name = sections.sectionfromtitle(title) |
404 if not name: |
443 if not name: |
405 raise error.Abort(_('unknown release notes section: %s') % |
444 raise error.Abort( |
406 title) |
445 _('unknown release notes section: %s') % title |
|
446 ) |
407 |
447 |
408 currentsection = name |
448 currentsection = name |
409 bullet_points = gatherparagraphsbullets(i) |
449 bullet_points = gatherparagraphsbullets(i) |
410 if bullet_points: |
450 if bullet_points: |
411 for para in bullet_points: |
451 for para in bullet_points: |
466 lines.append(BULLET_SECTION) |
508 lines.append(BULLET_SECTION) |
467 lines.append('-' * len(BULLET_SECTION)) |
509 lines.append('-' * len(BULLET_SECTION)) |
468 lines.append('') |
510 lines.append('') |
469 |
511 |
470 for paragraphs in nontitled: |
512 for paragraphs in nontitled: |
471 lines.extend(stringutil.wrap(' '.join(paragraphs[0]), |
513 lines.extend( |
472 width=78, |
514 stringutil.wrap( |
473 initindent='* ', |
515 ' '.join(paragraphs[0]), |
474 hangindent=' ').splitlines()) |
516 width=78, |
|
517 initindent='* ', |
|
518 hangindent=' ', |
|
519 ).splitlines() |
|
520 ) |
475 |
521 |
476 for para in paragraphs[1:]: |
522 for para in paragraphs[1:]: |
477 lines.append('') |
523 lines.append('') |
478 lines.extend(stringutil.wrap(' '.join(para), |
524 lines.extend( |
479 width=78, |
525 stringutil.wrap( |
480 initindent=' ', |
526 ' '.join(para), |
481 hangindent=' ').splitlines()) |
527 width=78, |
|
528 initindent=' ', |
|
529 hangindent=' ', |
|
530 ).splitlines() |
|
531 ) |
482 |
532 |
483 lines.append('') |
533 lines.append('') |
484 |
534 |
485 if lines and lines[-1]: |
535 if lines and lines[-1]: |
486 lines.append('') |
536 lines.append('') |
487 |
537 |
488 return '\n'.join(lines) |
538 return '\n'.join(lines) |
489 |
539 |
490 @command('releasenotes', |
540 |
491 [('r', 'rev', '', _('revisions to process for release notes'), _('REV')), |
541 @command( |
492 ('c', 'check', False, _('checks for validity of admonitions (if any)'), |
542 'releasenotes', |
493 _('REV')), |
543 [ |
494 ('l', 'list', False, _('list the available admonitions with their title'), |
544 ('r', 'rev', '', _('revisions to process for release notes'), _('REV')), |
495 None)], |
545 ( |
|
546 'c', |
|
547 'check', |
|
548 False, |
|
549 _('checks for validity of admonitions (if any)'), |
|
550 _('REV'), |
|
551 ), |
|
552 ( |
|
553 'l', |
|
554 'list', |
|
555 False, |
|
556 _('list the available admonitions with their title'), |
|
557 None, |
|
558 ), |
|
559 ], |
496 _('hg releasenotes [-r REV] [-c] FILE'), |
560 _('hg releasenotes [-r REV] [-c] FILE'), |
497 helpcategory=command.CATEGORY_CHANGE_NAVIGATION) |
561 helpcategory=command.CATEGORY_CHANGE_NAVIGATION, |
|
562 ) |
498 def releasenotes(ui, repo, file_=None, **opts): |
563 def releasenotes(ui, repo, file_=None, **opts): |
499 """parse release notes from commit messages into an output file |
564 """parse release notes from commit messages into an output file |
500 |
565 |
501 Given an output file and set of revisions, this command will parse commit |
566 Given an output file and set of revisions, this command will parse commit |
502 messages for release notes then add them to the output file. |
567 messages for release notes then add them to the output file. |
613 |
678 |
614 notes.merge(ui, incoming) |
679 notes.merge(ui, incoming) |
615 |
680 |
616 with open(file_, 'wb') as fh: |
681 with open(file_, 'wb') as fh: |
617 fh.write(serializenotes(sections, notes)) |
682 fh.write(serializenotes(sections, notes)) |
|
683 |
618 |
684 |
619 @command('debugparsereleasenotes', norepo=True) |
685 @command('debugparsereleasenotes', norepo=True) |
620 def debugparsereleasenotes(ui, path, repo=None): |
686 def debugparsereleasenotes(ui, path, repo=None): |
621 """parse release notes and print resulting data structure""" |
687 """parse release notes and print resulting data structure""" |
622 if path == '-': |
688 if path == '-': |