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