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 }