comparison hgext/releasenotes.py @ 43076:2372284d9457

formatting: blacken the codebase This is using my patch to black (https://github.com/psf/black/pull/826) so we don't un-wrap collection literals. Done with: hg files 'set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"' | xargs black -S # skip-blame mass-reformatting only # no-check-commit reformats foo_bar functions Differential Revision: https://phab.mercurial-scm.org/D6971
author Augie Fackler <augie@google.com>
date Sun, 06 Oct 2019 09:45:02 -0400
parents aaad36b88298
children 687b865b95ad
comparison
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
26 pycompat, 26 pycompat,
27 registrar, 27 registrar,
28 scmutil, 28 scmutil,
29 util, 29 util,
30 ) 30 )
31 from mercurial.utils import ( 31 from mercurial.utils import stringutil
32 stringutil,
33 )
34 32
35 cmdtable = {} 33 cmdtable = {}
36 command = registrar.command(cmdtable) 34 command = registrar.command(cmdtable)
37 35
38 try: 36 try:
39 import fuzzywuzzy.fuzz as fuzz 37 import fuzzywuzzy.fuzz as fuzz
38
40 fuzz.token_set_ratio 39 fuzz.token_set_ratio
41 except ImportError: 40 except ImportError:
42 fuzz = None 41 fuzz = None
43 42
44 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for 43 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
58 RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$') 57 RE_DIRECTIVE = re.compile(br'^\.\. ([a-zA-Z0-9_]+)::\s*([^$]+)?$')
59 RE_ISSUE = br'\bissue ?[0-9]{4,6}(?![0-9])\b' 58 RE_ISSUE = br'\bissue ?[0-9]{4,6}(?![0-9])\b'
60 59
61 BULLET_SECTION = _('Other Changes') 60 BULLET_SECTION = _('Other Changes')
62 61
62
63 class parsedreleasenotes(object): 63 class parsedreleasenotes(object):
64 def __init__(self): 64 def __init__(self):
65 self.sections = {} 65 self.sections = {}
66 66
67 def __contains__(self, section): 67 def __contains__(self, section):
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)
168 if value == title: 175 if value == title:
169 return name 176 return name
170 177
171 return None 178 return None
172 179
180
173 def converttitled(titledparagraphs): 181 def converttitled(titledparagraphs):
174 """ 182 """
175 Convert titled paragraphs to strings 183 Convert titled paragraphs to strings
176 """ 184 """
177 string_list = [] 185 string_list = []
180 for para in paragraphs: 188 for para in paragraphs:
181 lines.extend(para) 189 lines.extend(para)
182 string_list.append(' '.join(lines)) 190 string_list.append(' '.join(lines))
183 return string_list 191 return string_list
184 192
193
185 def convertnontitled(nontitledparagraphs): 194 def convertnontitled(nontitledparagraphs):
186 """ 195 """
187 Convert non-titled bullets to strings 196 Convert non-titled bullets to strings
188 """ 197 """
189 string_list = [] 198 string_list = []
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:
422 else: 462 else:
423 raise error.Abort(_('unsupported section type for %s') % title) 463 raise error.Abort(_('unsupported section type for %s') % title)
424 464
425 return notes 465 return notes
426 466
467
427 def serializenotes(sections, notes): 468 def serializenotes(sections, notes):
428 """Serialize release notes from parsed fragments and notes. 469 """Serialize release notes from parsed fragments and notes.
429 470
430 This function essentially takes the output of ``parsenotesfromrevisions()`` 471 This function essentially takes the output of ``parsenotesfromrevisions()``
431 and ``parserelnotesfile()`` and produces output combining the 2. 472 and ``parserelnotesfile()`` and produces output combining the 2.
447 lines.append('') 488 lines.append('')
448 489
449 for i, para in enumerate(paragraphs): 490 for i, para in enumerate(paragraphs):
450 if i: 491 if i:
451 lines.append('') 492 lines.append('')
452 lines.extend(stringutil.wrap(' '.join(para), 493 lines.extend(
453 width=78).splitlines()) 494 stringutil.wrap(' '.join(para), width=78).splitlines()
495 )
454 496
455 lines.append('') 497 lines.append('')
456 498
457 # Second pass to emit bullet list items. 499 # Second pass to emit bullet list items.
458 500
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 == '-':