Mercurial > hg-stable
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 == '-': |