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