1 # 2 # This program is free software; you can redistribute it and/or modify 3 # it under the terms of the GNU General Public License version 2 4 # as published by the Free Software Foundation. 5 # 6 # This program is distributed in the hope that it will be useful, 7 # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 # GNU General Public License for more details. 10 # 11 # You should have received a copy of the GNU General Public License 12 # along with this program; if not, write to the Free Software 13 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 14 # 15 16 # 17 # Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. 18 # Copyright 2008, 2011 Richard Lowe 19 # 20 21 '''OpenSolaris extensions to Mercurial 22 23 This extension contains a number of commands to help you work with 24 the OpenSolaris consolidations. It provides commands to check your 25 changes against the various style rules used for OpenSolaris, to 26 backup and restore your changes, to generate code reviews, and to 27 prepare your changes for integration. 28 29 30 The Parent 31 32 To provide a uniform notion of parent workspace regardless of 33 filesystem-based access, Cadmium uses the highest numbered changeset 34 on the current branch that is also in the parent workspace to 35 represent the parent workspace. 36 37 38 The Active List 39 40 Many Cadmium commands operate on the active list, the set of 41 files ('active files') you have changed in this workspace in relation 42 to its parent workspace, and the metadata (commentary, primarily) 43 associated with those changes. 44 45 46 NOT Files 47 48 Many of Cadmium's commands to check that your work obeys the 49 various stylistic rules of the OpenSolaris consolidations (such as 50 those run by 'hg nits') allow files to be excluded from this checking 51 by means of NOT files kept in the .hg/cdm/ directory of the Mercurial 52 repository for one-time exceptions, and in the exception_lists 53 directory at the repository root for permanent exceptions. (For ON, 54 these would mean one in $CODEMGR_WS and one in 55 $CODEMGR_WS/usr/closed). 56 57 These files are in the same format as the Mercurial hgignore 58 file, a description of which is available in the hgignore(5) manual 59 page. 60 61 62 Common Tasks 63 64 - Show diffs relative to parent workspace - pdiffs 65 - Check source style rules - nits 66 - Run pre-integration checks - pbchk 67 - Collapse all your changes into a single changeset - recommit 68 ''' 69 70 import atexit, os, re, sys, stat, termios 71 72 73 # 74 # Adjust the load path based on the location of cdm.py and the version 75 # of python into which it is being loaded. This assumes the normal 76 # onbld directory structure, where cdm.py is in 77 # lib/python(version)?/onbld/hgext/. If that changes so too must 78 # this. 79 # 80 # This and the case below are not equivalent. In this case we may be 81 # loading a cdm.py in python2.X/ via the lib/python/ symlink but need 82 # python2.Y in sys.path. 83 # 84 sys.path.insert(1, os.path.join(os.path.dirname(__file__), "..", "..", "..", 85 "python%d.%d" % sys.version_info[:2])) 86 87 # 88 # Add the relative path from cdm.py to usr/src/tools to the load path, 89 # such that a cdm.py loaded from the source tree uses the modules also 90 # within the source tree. 91 # 92 sys.path.insert(2, os.path.join(os.path.dirname(__file__), "..", "..")) 93 94 from onbld.Scm import Version 95 from mercurial import util 96 97 try: 98 Version.check_version() 99 except Version.VersionMismatch, badversion: 100 raise util.Abort("Version Mismatch:\n %s\n" % badversion) 101 102 from mercurial import cmdutil, ignore, node, patch 103 104 from onbld.Scm.WorkSpace import WorkSpace, WorkList 105 from onbld.Scm.Backup import CdmBackup 106 from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk 107 from onbld.Checks import JStyle, Keywords, Mapfile 108 109 110 def yes_no(ui, msg, default): 111 if default: 112 prompt = ' [Y/n]:' 113 defanswer = 'y' 114 else: 115 prompt = ' [y/N]:' 116 defanswer = 'n' 117 118 if Version.at_least("1.4"): 119 index = ui.promptchoice(msg + prompt, ['&yes', '&no'], 120 default=['y', 'n'].index(defanswer)) 121 resp = ('y', 'n')[index] 122 else: 123 resp = ui.prompt(msg + prompt, ['&yes', '&no'], default=defanswer) 124 125 return resp[0] in ('Y', 'y') 126 127 128 def buildfilelist(ws, parent, files): 129 '''Build a list of files in which we're interested. 130 131 If no files are specified take files from the active list relative 132 to 'parent'. 133 134 Return a list of 2-tuples the first element being a path relative 135 to the current directory and the second an entry from the active 136 list, or None if an explicit file list was given.''' 137 138 if files: 139 return [(path, None) for path in sorted(files)] 140 else: 141 active = ws.active(parent=parent) 142 return [(ws.filepath(e.name), e) for e in sorted(active)] 143 buildfilelist = util.cachefunc(buildfilelist) 144 145 146 def not_check(repo, cmd): 147 '''return a function which returns boolean indicating whether a file 148 should be skipped for CMD.''' 149 150 # 151 # The ignore routines need a canonical path to the file (relative to the 152 # repo root), whereas the check commands get paths relative to the cwd. 153 # 154 # Wrap our argument such that the path is canonified before it is checked. 155 # 156 def canonified_check(ignfunc): 157 def f(path): 158 cpath = util.canonpath(repo.root, repo.getcwd(), path) 159 return ignfunc(cpath) 160 return f 161 162 ignorefiles = [] 163 164 for f in [repo.join('cdm/%s.NOT' % cmd), 165 repo.wjoin('exception_lists/%s' % cmd)]: 166 if os.path.exists(f): 167 ignorefiles.append(f) 168 169 if ignorefiles: 170 ign = ignore.ignore(repo.root, ignorefiles, repo.ui.warn) 171 return canonified_check(ign) 172 else: 173 return util.never 174 175 176 def abort_if_dirty(ws): 177 '''Abort if the workspace has uncommitted changes, merges, 178 branches, or has Mq patches applied''' 179 180 if ws.modified(): 181 raise util.Abort('workspace has uncommitted changes') 182 if ws.merged(): 183 raise util.Abort('workspace contains uncommitted merge') 184 if ws.branched(): 185 raise util.Abort('workspace contains uncommitted branch') 186 if ws.mq_applied(): 187 raise util.Abort('workspace has Mq patches applied') 188 189 190 # 191 # Adding a reference to WorkSpace from a repo causes a circular reference 192 # repo <-> WorkSpace. 193 # 194 # This prevents repo, WorkSpace and members thereof from being garbage 195 # collected. Since transactions are aborted when the transaction object 196 # is collected, and localrepo holds a reference to the most recently created 197 # transaction, this prevents transactions from cleanly aborting. 198 # 199 # Instead, we hold the repo->WorkSpace association in a dictionary, breaking 200 # that dependence. 201 # 202 wslist = {} 203 204 205 def reposetup(ui, repo): 206 if repo.local() and repo not in wslist: 207 wslist[repo] = WorkSpace(repo) 208 209 if ui.interactive() and sys.stdin.isatty(): 210 ui.setconfig('hooks', 'preoutgoing.cdm_pbconfirm', 211 'python:hgext_cdm.pbconfirm') 212 213 214 def pbconfirm(ui, repo, hooktype, source): 215 def wrapper(settings=None): 216 termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, settings) 217 218 if source == 'push': 219 if not yes_no(ui, "Are you sure you wish to push?", False): 220 return 1 221 else: 222 settings = termios.tcgetattr(sys.stdin.fileno()) 223 orig = list(settings) 224 atexit.register(wrapper, orig) 225 settings[3] = settings[3] & (~termios.ISIG) # c_lflag 226 termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, settings) 227 228 229 def cdm_pdiffs(ui, repo, *pats, **opts): 230 '''diff workspace against its parent 231 232 Show differences between this workspace and its parent workspace 233 in the same manner as 'hg diff'. 234 235 For a description of the changeset used to represent the parent 236 workspace, see The Parent in the extension documentation ('hg help 237 cdm'). 238 ''' 239 240 act = wslist[repo].active(opts.get('parent')) 241 if not act.revs: 242 return 243 244 # 245 # If no patterns were specified, either explicitly or via -I or -X 246 # use the active list files to avoid a workspace walk. 247 # 248 if pats or opts.get('include') or opts.get('exclude'): 249 matchfunc = wslist[repo].matcher(pats=pats, opts=opts) 250 else: 251 matchfunc = wslist[repo].matcher(files=act.files()) 252 253 opts = patch.diffopts(ui, opts) 254 diffs = wslist[repo].diff(act.parenttip.node(), act.localtip.node(), 255 match=matchfunc, opts=opts) 256 if diffs: 257 ui.write(diffs) 258 259 260 def cdm_list(ui, repo, **opts): 261 '''list active files (those changed in this workspace) 262 263 Display a list of files changed in this workspace as compared to 264 its parent workspace. 265 266 File names are displayed one-per line, grouped by manner in which 267 they changed (added, modified, removed). Information about 268 renames or copies is output in parentheses following the file 269 name. 270 271 For a description of the changeset used to represent the parent 272 workspace, see The Parent in the extension documentation ('hg help 273 cdm'). 274 275 Output can be filtered by change type with --added, --modified, 276 and --removed. By default, all files are shown. 277 ''' 278 279 act = wslist[repo].active(opts['parent']) 280 wanted = set(x for x in ('added', 'modified', 'removed') if opts[x]) 281 changes = {} 282 283 for entry in act: 284 if wanted and (entry.change not in wanted): 285 continue 286 287 if entry.change not in changes: 288 changes[entry.change] = [] 289 changes[entry.change].append(entry) 290 291 for change in sorted(changes.keys()): 292 ui.write(change + ':\n') 293 294 for entry in sorted(changes[change]): 295 if entry.is_renamed(): 296 ui.write('\t%s (renamed from %s)\n' % (entry.name, 297 entry.parentname)) 298 elif entry.is_copied(): 299 ui.write('\t%s (copied from %s)\n' % (entry.name, 300 entry.parentname)) 301 else: 302 ui.write('\t%s\n' % entry.name) 303 304 305 def cdm_bugs(ui, repo, parent=None): 306 '''show all bug IDs referenced in changeset comments''' 307 308 act = wslist[repo].active(parent) 309 310 for elt in set(filter(Comments.isBug, act.comments())): 311 ui.write(elt + '\n') 312 313 314 def cdm_comments(ui, repo, parent=None): 315 '''show changeset commentary for all active changesets''' 316 act = wslist[repo].active(parent) 317 318 for elt in act.comments(): 319 ui.write(elt + '\n') 320 321 322 def cdm_renamed(ui, repo, parent=None): 323 '''show renamed active files 324 325 Renamed files are shown in the format:: 326 327 new-name old-name 328 329 One pair per-line. 330 ''' 331 332 act = wslist[repo].active(parent) 333 334 for entry in sorted(filter(lambda x: x.is_renamed(), act)): 335 ui.write('%s %s\n' % (entry.name, entry.parentname)) 336 337 338 def cdm_comchk(ui, repo, **opts): 339 '''check active changeset comment formatting 340 341 Check that active changeset comments conform to O/N rules. 342 343 Each comment line must contain either one bug or ARC case ID 344 followed by its synopsis, or credit an external contributor. 345 ''' 346 347 active = wslist[repo].active(opts.get('parent')) 348 349 ui.write('Comments check:\n') 350 351 check_db = not opts.get('nocheck') 352 return Comments.comchk(active.comments(), check_db=check_db, output=ui) 353 354 355 def cdm_cddlchk(ui, repo, *args, **opts): 356 '''check for a valid CDDL header comment in all active files. 357 358 Check active files for a valid Common Development and Distribution 359 License (CDDL) block comment. 360 361 Newly added files are checked for a copy of the CDDL header 362 comment. Modified files are only checked if they contain what 363 appears to be an existing CDDL header comment. 364 365 Files can be excluded from this check using the cddlchk.NOT file. 366 See NOT Files in the extension documentation ('hg help cdm'). 367 ''' 368 369 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 370 exclude = not_check(repo, 'cddlchk') 371 lenient = True 372 ret = 0 373 374 ui.write('CDDL block check:\n') 375 376 for f, e in filelist: 377 if e and e.is_removed(): 378 continue 379 elif (e or opts.get('honour_nots')) and exclude(f): 380 ui.status('Skipping %s...\n' % f) 381 continue 382 elif e and e.is_added(): 383 lenient = False 384 else: 385 lenient = True 386 387 fh = open(f, 'r') 388 ret |= Cddl.cddlchk(fh, lenient=lenient, output=ui) 389 fh.close() 390 return ret 391 392 393 def cdm_mapfilechk(ui, repo, *args, **opts): 394 '''check for a valid mapfile header block in active files 395 396 Check that all link-editor mapfiles contain the standard mapfile 397 header comment directing the reader to the document containing 398 Solaris object versioning rules (README.mapfile). 399 400 Files can be excluded from this check using the mapfilechk.NOT 401 file. See NOT Files in the extension documentation ('hg help 402 cdm'). 403 ''' 404 405 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 406 exclude = not_check(repo, 'mapfilechk') 407 ret = 0 408 409 # We are interested in examining any file that has the following 410 # in its final path segment: 411 # - Contains the word 'mapfile' 412 # - Begins with 'map.' 413 # - Ends with '.map' 414 # We don't want to match unless these things occur in final path segment 415 # because directory names with these strings don't indicate a mapfile. 416 # We also ignore files with suffixes that tell us that the files 417 # are not mapfiles. 418 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$', 419 re.IGNORECASE) 420 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE) 421 422 ui.write('Mapfile comment check:\n') 423 424 for f, e in filelist: 425 if e and e.is_removed(): 426 continue 427 elif (not MapfileRE.match(f)) or NotMapSuffixRE.match(f): 428 continue 429 elif (e or opts.get('honour_nots')) and exclude(f): 430 ui.status('Skipping %s...\n' % f) 431 continue 432 433 fh = open(f, 'r') 434 ret |= Mapfile.mapfilechk(fh, output=ui) 435 fh.close() 436 return ret 437 438 439 def cdm_copyright(ui, repo, *args, **opts): 440 '''check each active file for a current and correct copyright notice 441 442 Check that all active files have a correctly formed copyright 443 notice containing the current year. 444 445 See the Non-Formatting Considerations section of the OpenSolaris 446 Developer's Reference for more info on the correct form of 447 copyright notice. 448 (http://hub.opensolaris.org/bin/view/Community+Group+on/devref_7#H723NonFormattingConsiderations) 449 450 Files can be excluded from this check using the copyright.NOT file. 451 See NOT Files in the extension documentation ('hg help cdm'). 452 ''' 453 454 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 455 exclude = not_check(repo, 'copyright') 456 ret = 0 457 458 ui.write('Copyright check:\n') 459 460 for f, e in filelist: 461 if e and e.is_removed(): 462 continue 463 elif (e or opts.get('honour_nots')) and exclude(f): 464 ui.status('Skipping %s...\n' % f) 465 continue 466 467 fh = open(f, 'r') 468 ret |= Copyright.copyright(fh, output=ui) 469 fh.close() 470 return ret 471 472 473 def cdm_hdrchk(ui, repo, *args, **opts): 474 '''check active C header files conform to the O/N header rules 475 476 Check that any added or modified C header files conform to the O/N 477 header rules. 478 479 See the section 'HEADER STANDARDS' in the hdrchk(1) manual page 480 for more information on the rules for O/N header file formatting. 481 482 Files can be excluded from this check using the hdrchk.NOT file. 483 See NOT Files in the extension documentation ('hg help cdm'). 484 ''' 485 486 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 487 exclude = not_check(repo, 'hdrchk') 488 ret = 0 489 490 ui.write('Header format check:\n') 491 492 for f, e in filelist: 493 if e and e.is_removed(): 494 continue 495 elif not f.endswith('.h'): 496 continue 497 elif (e or opts.get('honour_nots')) and exclude(f): 498 ui.status('Skipping %s...\n' % f) 499 continue 500 501 fh = open(f, 'r') 502 ret |= HdrChk.hdrchk(fh, lenient=True, output=ui) 503 fh.close() 504 return ret 505 506 507 def cdm_cstyle(ui, repo, *args, **opts): 508 '''check active C source files conform to the C Style Guide 509 510 Check that any added or modified C source file conform to the C 511 Style Guide. 512 513 See the C Style Guide for more information about correct C source 514 formatting. 515 (http://hub.opensolaris.org/bin/download/Community+Group+on/WebHome/cstyle.ms.pdf) 516 517 Files can be excluded from this check using the cstyle.NOT file. 518 See NOT Files in the extension documentation ('hg help cdm'). 519 ''' 520 521 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 522 exclude = not_check(repo, 'cstyle') 523 ret = 0 524 525 ui.write('C style check:\n') 526 527 for f, e in filelist: 528 if e and e.is_removed(): 529 continue 530 elif not (f.endswith('.c') or f.endswith('.h')): 531 continue 532 elif (e or opts.get('honour_nots')) and exclude(f): 533 ui.status('Skipping %s...\n' % f) 534 continue 535 536 fh = open(f, 'r') 537 ret |= CStyle.cstyle(fh, output=ui, 538 picky=True, check_posix_types=True, 539 check_continuation=True) 540 fh.close() 541 return ret 542 543 544 def cdm_jstyle(ui, repo, *args, **opts): 545 '''check active Java source files for common stylistic errors 546 547 Files can be excluded from this check using the jstyle.NOT file. 548 See NOT Files in the extension documentation ('hg help cdm'). 549 ''' 550 551 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 552 exclude = not_check(repo, 'jstyle') 553 ret = 0 554 555 ui.write('Java style check:\n') 556 557 for f, e in filelist: 558 if e and e.is_removed(): 559 continue 560 elif not f.endswith('.java'): 561 continue 562 elif (e or opts.get('honour_nots')) and exclude(f): 563 ui.status('Skipping %s...\n' % f) 564 continue 565 566 fh = open(f, 'r') 567 ret |= JStyle.jstyle(fh, output=ui, picky=True) 568 fh.close() 569 return ret 570 571 572 def cdm_permchk(ui, repo, *args, **opts): 573 '''check the permissions of each active file 574 575 Check that the file permissions of each added or modified file do not 576 contain the executable bit. 577 578 Files can be excluded from this check using the permchk.NOT file. 579 See NOT Files in the extension documentation ('hg help cdm'). 580 ''' 581 582 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 583 exclude = not_check(repo, 'permchk') 584 exeFiles = [] 585 586 ui.write('File permission check:\n') 587 588 for f, e in filelist: 589 if e and e.is_removed(): 590 continue 591 elif (e or opts.get('honour_nots')) and exclude(f): 592 ui.status('Skipping %s...\n' % f) 593 continue 594 595 mode = stat.S_IMODE(os.stat(f)[stat.ST_MODE]) 596 if mode & stat.S_IEXEC: 597 exeFiles.append(f) 598 599 if len(exeFiles) > 0: 600 ui.write('Warning: the following active file(s) have executable mode ' 601 '(+x) permission set,\nremove unless intentional:\n') 602 for fname in exeFiles: 603 ui.write(" %s\n" % fname) 604 605 return len(exeFiles) > 0 606 607 608 def cdm_tagchk(ui, repo, **opts): 609 '''check modification of workspace tags 610 611 Check for any modification of the repository's .hgtags file. 612 613 With the exception of the gatekeepers, nobody should introduce or 614 modify a repository's tags. 615 ''' 616 617 active = wslist[repo].active(opts.get('parent')) 618 619 ui.write('Checking for new tags:\n') 620 621 if ".hgtags" in active: 622 tfile = wslist[repo].filepath('.hgtags') 623 ptip = active.parenttip.rev() 624 625 ui.write('Warning: Workspace contains new non-local tags.\n' 626 'Only gatekeepers should add or modify such tags.\n' 627 'Use the following commands to revert these changes:\n' 628 ' hg revert -r%d %s\n' 629 ' hg commit %s\n' 630 'You should also recommit before integration\n' % 631 (ptip, tfile, tfile)) 632 633 return 1 634 635 return 0 636 637 638 def cdm_branchchk(ui, repo, **opts): 639 '''check for changes in number or name of branches 640 641 Check that the workspace contains only a single head, that it is 642 on the branch 'default', and that no new branches have been 643 introduced. 644 ''' 645 646 ui.write('Checking for multiple heads (or branches):\n') 647 648 heads = set(repo.heads()) 649 parents = set([x.node() for x in wslist[repo].workingctx().parents()]) 650 651 # 652 # We care if there's more than one head, and those heads aren't 653 # identical to the dirstate parents (if they are identical, it's 654 # an uncommitted merge which mergechk will catch, no need to 655 # complain twice). 656 # 657 if len(heads) > 1 and heads != parents: 658 ui.write('Workspace has multiple heads (or branches):\n') 659 for head in [repo.changectx(head) for head in heads]: 660 ui.write(" %d:%s\t%s\n" % 661 (head.rev(), str(head), head.description().splitlines()[0])) 662 ui.write('You must merge and recommit.\n') 663 return 1 664 665 ui.write('\nChecking for branch changes:\n') 666 667 if repo.dirstate.branch() != 'default': 668 ui.write("Warning: Workspace tip has named branch: '%s'\n" 669 "Only gatekeepers should push new branches.\n" 670 "Use the following commands to restore the branch name:\n" 671 " hg branch [-f] default\n" 672 " hg commit\n" 673 "You should also recommit before integration\n" % 674 (repo.dirstate.branch())) 675 return 1 676 677 branches = repo.branchtags().keys() 678 if len(branches) > 1: 679 ui.write('Warning: Workspace has named branches:\n') 680 for t in branches: 681 if t == 'default': 682 continue 683 ui.write("\t%s\n" % t) 684 685 ui.write("Only gatekeepers should push new branches.\n" 686 "Use the following commands to remove extraneous branches.\n" 687 " hg branch [-f] default\n" 688 " hg commit" 689 "You should also recommit before integration\n") 690 return 1 691 692 return 0 693 694 695 def cdm_keywords(ui, repo, *args, **opts): 696 '''check active files for SCCS keywords 697 698 Check that any added or modified files do not contain SCCS keywords 699 (#ident lines, etc.). 700 701 Files can be excluded from this check using the keywords.NOT file. 702 See NOT Files in the extension documentation ('hg help cdm'). 703 ''' 704 705 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 706 exclude = not_check(repo, 'keywords') 707 ret = 0 708 709 ui.write('Keywords check:\n') 710 711 for f, e in filelist: 712 if e and e.is_removed(): 713 continue 714 elif (e or opts.get('honour_nots')) and exclude(f): 715 ui.status('Skipping %s...\n' % f) 716 continue 717 718 fh = open(f, 'r') 719 ret |= Keywords.keywords(fh, output=ui) 720 fh.close() 721 return ret 722 723 724 # 725 # NB: 726 # There's no reason to hook this up as an invokable command, since 727 # we have 'hg status', but it must accept the same arguments. 728 # 729 def cdm_outchk(ui, repo, **opts): 730 '''Warn the user if they have uncommitted changes''' 731 732 ui.write('Checking for uncommitted changes:\n') 733 734 st = wslist[repo].modified() 735 if st: 736 ui.write('Warning: the following files have uncommitted changes:\n') 737 for elt in st: 738 ui.write(' %s\n' % elt) 739 return 1 740 return 0 741 742 743 def cdm_mergechk(ui, repo, **opts): 744 '''Warn the user if their workspace contains merges''' 745 746 active = wslist[repo].active(opts.get('parent')) 747 748 ui.write('Checking for merges:\n') 749 750 merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1], 751 active.revs) 752 753 if merges: 754 ui.write('Workspace contains the following merges:\n') 755 for rev in merges: 756 desc = rev.description().splitlines() 757 ui.write(' %s:%s\t%s\n' % 758 (rev.rev() or "working", str(rev), 759 desc and desc[0] or "*** uncommitted change ***")) 760 return 1 761 return 0 762 763 764 def run_checks(ws, cmds, *args, **opts): 765 '''Run CMDS (with OPTS) over active files in WS''' 766 767 ret = 0 768 769 for cmd in cmds: 770 name = cmd.func_name.split('_')[1] 771 if not ws.ui.configbool('cdm', name, True): 772 ws.ui.status('Skipping %s check...\n' % name) 773 else: 774 ws.ui.pushbuffer() 775 result = cmd(ws.ui, ws.repo, honour_nots=True, *args, **opts) 776 output = ws.ui.popbuffer() 777 778 ret |= result 779 780 if not ws.ui.quiet or result != 0: 781 ws.ui.write(output, '\n') 782 return ret 783 784 785 def cdm_nits(ui, repo, *args, **opts): 786 '''check for stylistic nits in active files 787 788 Check each active file for basic stylistic errors. 789 790 The following checks are run over each active file (see 'hg help 791 <check>' for more information about each): 792 793 - copyright (copyright statements) 794 - cstyle (C source style) 795 - hdrchk (C header style) 796 - jstyle (java source style) 797 - mapfilechk (link-editor mapfiles) 798 - permchk (file permissions) 799 - keywords (SCCS keywords) 800 801 With the global -q/--quiet option, only provide output for those 802 checks which fail. 803 ''' 804 805 cmds = [cdm_copyright, 806 cdm_cstyle, 807 cdm_hdrchk, 808 cdm_jstyle, 809 cdm_mapfilechk, 810 cdm_permchk, 811 cdm_keywords] 812 813 return run_checks(wslist[repo], cmds, *args, **opts) 814 815 816 def cdm_pbchk(ui, repo, **opts): 817 '''run pre-integration checks on this workspace 818 819 Check this workspace for common errors prior to integration. 820 821 The following checks are run over the active list (see 'hg help 822 <check>' for more information about each): 823 824 - branchchk (addition/modification of branches) 825 - comchk (changeset descriptions) 826 - copyright (copyright statements) 827 - cstyle (C source style) 828 - hdrchk (C header style) 829 - jstyle (java source style) 830 - keywords (SCCS keywords) 831 - mapfilechk (link-editor mapfiles) 832 - permchk (file permissions) 833 - tagchk (addition/modification of tags) 834 835 Additionally, the workspace is checked for outgoing merges (which 836 should be removed with 'hg recommit'), and uncommitted changes. 837 838 With the global -q/--quiet option, only provide output for those 839 checks which fail. 840 ''' 841 842 # 843 # The current ordering of these is that the commands from cdm_nits 844 # run first in the same order as they would in cdm_nits, then the 845 # pbchk specifics are run. 846 # 847 cmds = [cdm_copyright, 848 cdm_cstyle, 849 cdm_hdrchk, 850 cdm_jstyle, 851 cdm_mapfilechk, 852 cdm_permchk, 853 cdm_keywords, 854 cdm_comchk, 855 cdm_tagchk, 856 cdm_branchchk, 857 cdm_outchk, 858 cdm_mergechk] 859 860 return run_checks(wslist[repo], cmds, **opts) 861 862 863 def cdm_recommit(ui, repo, **opts): 864 '''replace outgoing changesets with a single equivalent changeset 865 866 Replace all outgoing changesets with a single changeset containing 867 equivalent changes. This removes uninteresting changesets created 868 during development that would only serve as noise in the gate. 869 870 Any changed file that is now identical in content to that in the 871 parent workspace (whether identical in history or otherwise) will 872 not be included in the new changeset. Any merges information will 873 also be removed. 874 875 If no files are changed in comparison to the parent workspace, the 876 outgoing changesets will be removed, but no new changeset created. 877 878 recommit will refuse to run if the workspace contains more than 879 one outgoing head, even if those heads are on the same branch. To 880 recommit with only one branch containing outgoing changesets, your 881 workspace must be on that branch and at that branch head. 882 883 recommit will prompt you to take a backup if your workspace has 884 been changed since the last backup was taken. In almost all 885 cases, you should allow it to take one (the default). 886 887 recommit cannot be run if the workspace contains any uncommitted 888 changes, applied Mq patches, or has multiple outgoing heads (or 889 branches). 890 ''' 891 892 ws = wslist[repo] 893 894 if not os.getcwd().startswith(repo.root): 895 raise util.Abort('recommit is not safe to run with -R') 896 897 abort_if_dirty(ws) 898 899 wlock = repo.wlock() 900 lock = repo.lock() 901 902 try: 903 parent = ws.parent(opts['parent']) 904 between = repo.changelog.nodesbetween(ws.findoutgoing(parent))[2] 905 heads = set(between) & set(repo.heads()) 906 907 if len(heads) > 1: 908 ui.warn('Workspace has multiple outgoing heads (or branches):\n') 909 for head in sorted(map(repo.changelog.rev, heads), reverse=True): 910 ui.warn('\t%d\n' % head) 911 raise util.Abort('you must merge before recommitting') 912 913 # 914 # We can safely use the worklist here, as we know (from the 915 # abort_if_dirty() check above) that the working copy has not been 916 # modified. 917 # 918 active = ws.active(parent) 919 920 if filter(lambda b: len(b.parents()) > 1, active.bases()): 921 raise util.Abort('Cannot recommit a merge of two non-outgoing ' 922 'changesets') 923 924 if len(active.revs) <= 0: 925 raise util.Abort("no changes to recommit") 926 927 if len(active.files()) <= 0: 928 ui.warn("Recommitting %d active changesets, but no active files\n" % 929 len(active.revs)) 930 931 # 932 # During the course of a recommit, any file bearing a name 933 # matching the source name of any renamed file will be 934 # clobbered by the operation. 935 # 936 # As such, we ask the user before proceeding. 937 # 938 bogosity = [f.parentname for f in active if f.is_renamed() and 939 os.path.exists(repo.wjoin(f.parentname))] 940 if bogosity: 941 ui.warn("The following file names are the original name of a " 942 "rename and also present\n" 943 "in the working directory:\n") 944 945 for fname in bogosity: 946 ui.warn(" %s\n" % fname) 947 948 if not yes_no(ui, "These files will be removed by recommit." 949 " Continue?", 950 False): 951 raise util.Abort("recommit would clobber files") 952 953 user = opts['user'] or ui.username() 954 comments = '\n'.join(active.comments()) 955 956 message = cmdutil.logmessage(opts) or ui.edit(comments, user) 957 if not message: 958 raise util.Abort('empty commit message') 959 960 bk = CdmBackup(ui, ws, backup_name(repo.root)) 961 if bk.need_backup(): 962 if yes_no(ui, 'Do you want to backup files first?', True): 963 bk.backup() 964 965 oldtags = repo.tags() 966 clearedtags = [(name, nd, repo.changelog.rev(nd), local) 967 for name, nd, local in active.tags()] 968 969 ws.squishdeltas(active, message, user=user) 970 finally: 971 lock.release() 972 wlock.release() 973 974 if clearedtags: 975 ui.write("Removed tags:\n") 976 for name, nd, rev, local in sorted(clearedtags, 977 key=lambda x: x[0].lower()): 978 ui.write(" %5s:%s:\t%s%s\n" % (rev, node.short(nd), 979 name, (local and ' (local)' or ''))) 980 981 for ntag, nnode in sorted(repo.tags().items(), 982 key=lambda x: x[0].lower()): 983 if ntag in oldtags and ntag != "tip": 984 if oldtags[ntag] != nnode: 985 ui.write("tag '%s' now refers to revision %d:%s\n" % 986 (ntag, repo.changelog.rev(nnode), 987 node.short(nnode))) 988 989 990 def do_eval(cmd, files, root, changedir=True): 991 if not changedir: 992 os.chdir(root) 993 994 for path in sorted(files): 995 dirn, base = os.path.split(path) 996 997 if changedir: 998 os.chdir(os.path.join(root, dirn)) 999 1000 os.putenv('workspace', root) 1001 os.putenv('filepath', path) 1002 os.putenv('dir', dirn) 1003 os.putenv('file', base) 1004 os.system(cmd) 1005 1006 1007 def cdm_eval(ui, repo, *command, **opts): 1008 '''run specified command for each active file 1009 1010 Run the command specified on the command line for each active 1011 file, with the following variables present in the environment: 1012 1013 :$file: - active file basename. 1014 :$dir: - active file dirname. 1015 :$filepath: - path from workspace root to active file. 1016 :$workspace: - full path to workspace root. 1017 1018 For example: 1019 1020 hg eval 'echo $dir; hg log -l3 $file' 1021 1022 will show the last the 3 log entries for each active file, 1023 preceded by its directory. 1024 ''' 1025 1026 act = wslist[repo].active(opts['parent']) 1027 cmd = ' '.join(command) 1028 files = [x.name for x in act if not x.is_removed()] 1029 1030 do_eval(cmd, files, repo.root, not opts['remain']) 1031 1032 1033 def cdm_apply(ui, repo, *command, **opts): 1034 '''apply specified command to all active files 1035 1036 Run the command specified on the command line over each active 1037 file. 1038 1039 For example 'hg apply "wc -l"' will output a count of the lines in 1040 each active file. 1041 ''' 1042 1043 act = wslist[repo].active(opts['parent']) 1044 1045 if opts['remain']: 1046 appnd = ' $filepath' 1047 else: 1048 appnd = ' $file' 1049 1050 cmd = ' '.join(command) + appnd 1051 files = [x.name for x in act if not x.is_removed()] 1052 1053 do_eval(cmd, files, repo.root, not opts['remain']) 1054 1055 1056 def cdm_reparent(ui, repo, parent): 1057 '''reparent your workspace 1058 1059 Update the 'default' path alias that is used as the default source 1060 for 'hg pull' and the default destination for 'hg push' (unless 1061 there is a 'default-push' alias). This is also the path all 1062 Cadmium commands treat as your parent workspace. 1063 ''' 1064 1065 def append_new_parent(parent): 1066 fp = None 1067 try: 1068 fp = repo.opener('hgrc', 'a', atomictemp=True) 1069 if fp.tell() != 0: 1070 fp.write('\n') 1071 fp.write('[paths]\n' 1072 'default = %s\n\n' % parent) 1073 fp.rename() 1074 finally: 1075 if fp and not fp.closed: 1076 fp.close() 1077 1078 def update_parent(path, line, parent): 1079 line = line - 1 # The line number we're passed will be 1-based 1080 fp = None 1081 1082 try: 1083 fp = open(path) 1084 data = fp.readlines() 1085 finally: 1086 if fp and not fp.closed: 1087 fp.close() 1088 1089 # 1090 # line will be the last line of any continued block, go back 1091 # to the first removing the continuation as we go. 1092 # 1093 while data[line][0].isspace(): 1094 data.pop(line) 1095 line -= 1 1096 1097 assert data[line].startswith('default') 1098 1099 data[line] = "default = %s\n" % parent 1100 if data[-1] != '\n': 1101 data.append('\n') 1102 1103 try: 1104 fp = util.atomictempfile(path, 'w', 0644) 1105 fp.writelines(data) 1106 fp.rename() 1107 finally: 1108 if fp and not fp.closed: 1109 fp.close() 1110 1111 from mercurial import config 1112 parent = ui.expandpath(parent) 1113 1114 if not os.path.exists(repo.join('hgrc')): 1115 append_new_parent(parent) 1116 return 1117 1118 cfg = config.config() 1119 cfg.read(repo.join('hgrc')) 1120 source = cfg.source('paths', 'default') 1121 1122 if not source: 1123 append_new_parent(parent) 1124 return 1125 else: 1126 path, target = source.rsplit(':', 1) 1127 1128 if path != repo.join('hgrc'): 1129 raise util.Abort("Cannot edit path specification not in repo hgrc\n" 1130 "default path is from: %s" % source) 1131 1132 update_parent(path, int(target), parent) 1133 1134 1135 def backup_name(fullpath): 1136 '''Create a backup directory name based on the specified path. 1137 1138 In most cases this is the basename of the path specified, but 1139 certain cases are handled specially to create meaningful names''' 1140 1141 special = ['usr/closed'] 1142 1143 fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep) 1144 1145 # 1146 # If a path is 'special', we append the basename of the path to 1147 # the path element preceding the constant, special, part. 1148 # 1149 # Such that for instance: 1150 # /foo/bar/onnv-fixes/usr/closed 1151 # has a backup name of: 1152 # onnv-fixes-closed 1153 # 1154 for elt in special: 1155 elt = elt.split(os.path.sep) 1156 pathpos = len(elt) 1157 1158 if fullpath[-pathpos:] == elt: 1159 return "%s-%s" % (fullpath[-pathpos - 1], elt[-1]) 1160 else: 1161 return fullpath[-1] 1162 1163 1164 def cdm_backup(ui, repo, if_newer=False): 1165 '''backup workspace changes and metadata 1166 1167 Create a backup copy of changes made in this workspace as compared 1168 to its parent workspace, as well as important metadata of this 1169 workspace. 1170 1171 NOTE: Only changes as compared to the parent workspace are backed 1172 up. If you lose this workspace and its parent, you will not be 1173 able to restore a backup into a clone of the grandparent 1174 workspace. 1175 1176 By default, backups are stored in the cdm.backup/ directory in 1177 your home directory. This is configurable using the cdm.backupdir 1178 configuration variable, for example: 1179 1180 hg backup --config cdm.backupdir=/net/foo/backups 1181 1182 or place the following in an appropriate hgrc file:: 1183 1184 [cdm] 1185 backupdir = /net/foo/backups 1186 1187 Backups have the same name as the workspace in which they were 1188 taken, with '-closed' appended in the case of O/N's usr/closed. 1189 ''' 1190 1191 name = backup_name(repo.root) 1192 bk = CdmBackup(ui, wslist[repo], name) 1193 1194 wlock = repo.wlock() 1195 lock = repo.lock() 1196 1197 try: 1198 if if_newer and not bk.need_backup(): 1199 ui.status('backup is up-to-date\n') 1200 else: 1201 bk.backup() 1202 finally: 1203 lock.release() 1204 wlock.release() 1205 1206 1207 def cdm_restore(ui, repo, backup, **opts): 1208 '''restore workspace from backup 1209 1210 Restore this workspace from a backup (taken by 'hg backup'). 1211 1212 If the specified backup directory does not exist, it is assumed to 1213 be relative to the cadmium backup directory (~/cdm.backup/ by 1214 default). 1215 1216 For example:: 1217 1218 % hg restore on-rfe - Restore the latest backup of ~/cdm.backup/on-rfe 1219 % hg restore -g3 on-rfe - Restore the 3rd backup of ~/cdm.backup/on-rfe 1220 % hg restore /net/foo/backup/on-rfe - Restore from an explicit path 1221 ''' 1222 1223 if not os.getcwd().startswith(repo.root): 1224 raise util.Abort('restore is not safe to run with -R') 1225 1226 abort_if_dirty(wslist[repo]) 1227 1228 if opts['generation']: 1229 gen = int(opts['generation']) 1230 else: 1231 gen = None 1232 1233 if os.path.exists(backup): 1234 backup = os.path.abspath(backup) 1235 1236 wlock = repo.wlock() 1237 lock = repo.lock() 1238 1239 try: 1240 bk = CdmBackup(ui, wslist[repo], backup) 1241 bk.restore(gen) 1242 finally: 1243 lock.release() 1244 wlock.release() 1245 1246 1247 def cdm_webrev(ui, repo, **opts): 1248 '''generate web-based code review and optionally upload it 1249 1250 Generate a web-based code review using webrev(1) and optionally 1251 upload it. All known arguments are passed through to webrev(1). 1252 ''' 1253 1254 webrev_args = "" 1255 for key in opts.keys(): 1256 if opts[key]: 1257 if type(opts[key]) == type(True): 1258 webrev_args += '-' + key + ' ' 1259 else: 1260 webrev_args += '-' + key + ' ' + opts[key] + ' ' 1261 1262 retval = os.system('webrev ' + webrev_args) 1263 if retval != 0: 1264 return retval - 255 1265 1266 return 0 1267 1268 1269 def cdm_debugcdmal(ui, repo, *pats, **opts): 1270 '''dump the active list for the sake of debugging/testing''' 1271 1272 ui.write(wslist[repo].active(opts['parent']).as_text(pats)) 1273 1274 1275 def cdm_changed(ui, repo, *pats, **opts): 1276 '''mark a file as changed in the working copy 1277 1278 Maintain a list of files checked for modification in the working 1279 copy. If the list exists, most cadmium commands will only check 1280 the working copy for changes to those files, rather than checking 1281 the whole workspace (this does not apply to committed changes, 1282 which are always seen). 1283 1284 Since this list functions only as a hint as to where in the 1285 working copy to look for changes, entries that have not actually 1286 been modified (in the working copy, or in general) are not 1287 problematic. 1288 1289 1290 Note: If such a list exists, it must be kept up-to-date. 1291 1292 1293 Renamed files can be added with reference only to their new name: 1294 $ hg mv foo bar 1295 $ hg changed bar 1296 1297 Without arguments, 'hg changed' will list all files recorded as 1298 altered, such that, for instance: 1299 $ hg status $(hg changed) 1300 $ hg diff $(hg changed) 1301 Become useful (generally faster than their unadorned counterparts) 1302 1303 To create an initially empty list: 1304 $ hg changed -i 1305 Until files are added to the list it is equivalent to saying 1306 "Nothing has been changed" 1307 1308 Update the list based on the current active list: 1309 $ hg changed -u 1310 The old list is emptied, and replaced with paths from the 1311 current active list. 1312 1313 Remove the list entirely: 1314 $ hg changed -d 1315 ''' 1316 1317 def modded_files(repo, parent): 1318 out = wslist[repo].findoutgoing(wslist[repo].parent(parent)) 1319 outnodes = repo.changelog.nodesbetween(out)[0] 1320 1321 files = set() 1322 for n in outnodes: 1323 files.update(repo.changectx(n).files()) 1324 1325 files.update(wslist[repo].status().keys()) 1326 return files 1327 1328 # 1329 # specced_pats is convenient to treat as a boolean indicating 1330 # whether any file patterns or paths were specified. 1331 # 1332 specced_pats = pats or opts['include'] or opts['exclude'] 1333 if len(filter(None, [opts['delete'], opts['update'], opts['init'], 1334 specced_pats])) > 1: 1335 raise util.Abort("-d, -u, -i and patterns are mutually exclusive") 1336 1337 wl = WorkList(wslist[repo]) 1338 1339 if (not wl and specced_pats) or opts['init']: 1340 wl.delete() 1341 if yes_no(ui, "Create a list based on your changes thus far?", True): 1342 map(wl.add, modded_files(repo, opts.get('parent'))) 1343 1344 if opts['delete']: 1345 wl.delete() 1346 elif opts['update']: 1347 wl.delete() 1348 map(wl.add, modded_files(repo, opts.get('parent'))) 1349 wl.write() 1350 elif opts['init']: # Any possible old list was deleted above 1351 wl.write() 1352 elif specced_pats: 1353 sources = [] 1354 1355 match = wslist[repo].matcher(pats=pats, opts=opts) 1356 for abso in repo.walk(match): 1357 if abso in repo.dirstate: 1358 wl.add(abso) 1359 # 1360 # Store the source name of any copy. We use this so 1361 # both the add and delete of a rename can be entered 1362 # into the WorkList with only the destination name 1363 # explicitly being mentioned. 1364 # 1365 fctx = wslist[repo].workingctx().filectx(abso) 1366 rn = fctx.renamed() 1367 if rn: 1368 sources.append(rn[0]) 1369 else: 1370 ui.warn("%s is not version controlled -- skipping\n" % 1371 match.rel(abso)) 1372 1373 if sources: 1374 for fname, chng in wslist[repo].status(files=sources).iteritems(): 1375 if chng == 'removed': 1376 wl.add(fname) 1377 wl.write() 1378 else: 1379 for elt in sorted(wl.list()): 1380 ui.write("%s\n" % wslist[repo].filepath(elt)) 1381 1382 1383 cmdtable = { 1384 'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'), 1385 ('r', 'remain', None, 'do not change directory')], 1386 'hg apply [-p PARENT] [-r] command...'), 1387 '^backup|bu': (cdm_backup, [('t', 'if-newer', None, 1388 'only backup if workspace files are newer')], 1389 'hg backup [-t]'), 1390 'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')], 1391 'hg branchchk [-p PARENT]'), 1392 'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')], 1393 'hg bugs [-p PARENT]'), 1394 'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')], 1395 'hg cddlchk [-p PARENT]'), 1396 'changed': (cdm_changed, [('d', 'delete', None, 'delete the file list'), 1397 ('u', 'update', None, 'mark all changed files'), 1398 ('i', 'init', None, 'create an empty file list'), 1399 ('p', 'parent', '', 'parent workspace'), 1400 ('I', 'include', [], 1401 'include names matching the given patterns'), 1402 ('X', 'exclude', [], 1403 'exclude names matching the given patterns')], 1404 'hg changed -d\n' 1405 'hg changed -u\n' 1406 'hg changed -i\n' 1407 'hg changed [-I PATTERN...] [-X PATTERN...] [FILE...]'), 1408 'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'), 1409 ('N', 'nocheck', None, 1410 'do not compare comments with databases')], 1411 'hg comchk [-p PARENT]'), 1412 'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')], 1413 'hg comments [-p PARENT]'), 1414 'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')], 1415 'hg copyright [-p PARENT]'), 1416 'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')], 1417 'hg cstyle [-p PARENT]'), 1418 'debugcdmal': (cdm_debugcdmal, [('p', 'parent', '', 'parent workspace')], 1419 'hg debugcdmal [-p PARENT] [FILE...]'), 1420 'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'), 1421 ('r', 'remain', None, 'do not change directory')], 1422 'hg eval [-p PARENT] [-r] command...'), 1423 'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')], 1424 'hg hdrchk [-p PARENT]'), 1425 'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')], 1426 'hg jstyle [-p PARENT]'), 1427 'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')], 1428 'hg keywords [-p PARENT]'), 1429 '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'), 1430 ('a', 'added', None, 'show added files'), 1431 ('m', 'modified', None, 'show modified files'), 1432 ('r', 'removed', None, 'show removed files')], 1433 'hg list [-amrRu] [-p PARENT]'), 1434 'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')], 1435 'hg mapfilechk [-p PARENT]'), 1436 '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')], 1437 'hg nits [-p PARENT]'), 1438 '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'), 1439 ('N', 'nocheck', None, 'skip database checks')], 1440 'hg pbchk [-N] [-p PARENT]'), 1441 'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')], 1442 'hg permchk [-p PARENT]'), 1443 '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'), 1444 ('a', 'text', None, 'treat all files as text'), 1445 ('g', 'git', None, 'use extended git diff format'), 1446 ('w', 'ignore-all-space', None, 1447 'ignore white space when comparing lines'), 1448 ('b', 'ignore-space-change', None, 1449 'ignore changes in the amount of white space'), 1450 ('B', 'ignore-blank-lines', None, 1451 'ignore changes whose lines are all blank'), 1452 ('U', 'unified', 3, 1453 'number of lines of context to show'), 1454 ('I', 'include', [], 1455 'include names matching the given patterns'), 1456 ('X', 'exclude', [], 1457 'exclude names matching the given patterns')], 1458 'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'), 1459 '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'), 1460 ('m', 'message', '', 1461 'use <text> as commit message'), 1462 ('l', 'logfile', '', 1463 'read commit message from file'), 1464 ('u', 'user', '', 1465 'record user as committer')], 1466 'hg recommit [-m TEXT] [-l FILE] [-u USER] [-p PARENT]'), 1467 'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')], 1468 'hg renamed [-p PARENT]'), 1469 'reparent': (cdm_reparent, [], 'hg reparent PARENT'), 1470 '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')], 1471 'hg restore [-g GENERATION] BACKUP'), 1472 'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')], 1473 'hg tagchk [-p PARENT]'), 1474 'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'), 1475 ('D', 'D', '', 'delete remote webrev'), 1476 ('I', 'I', '', 'ITS configuration file'), 1477 ('i', 'i', '', 'include file'), 1478 ('N', 'N', None, 'suppress comments'), 1479 ('n', 'n', None, 'do not generate webrev'), 1480 ('O', 'O', None, 'OpenSolaris mode'), 1481 ('o', 'o', '', 'output directory'), 1482 ('p', 'p', '', 'use specified parent'), 1483 ('t', 't', '', 'upload target'), 1484 ('U', 'U', None, 'upload the webrev'), 1485 ('w', 'w', '', 'use wx active file')], 1486 'hg webrev [WEBREV_OPTIONS]'), 1487 }