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