xref: /titanic_41/usr/src/tools/onbld/hgext/cdm.py (revision f6911211edbabe97da98b0229c5e476991bbc73b)
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
26the OpenSolaris consolidations.  It provides commands to check your
27changes against the various style rules used for OpenSolaris, to
28backup and restore your changes, to generate code reviews, and to
29prepare your changes for integration.
30
31
32The Parent
33
34    To provide a uniform notion of parent workspace regardless of
35filesystem-based access, Cadmium uses the highest numbered changeset
36on the current branch that is also in the parent workspace to
37represent the parent workspace.
38
39
40The Active List
41
42    Many Cadmium commands operate on the active list, the set of
43files ('active files') you have changed in this workspace in relation
44to its parent workspace, and the metadata (commentary, primarily)
45associated with those changes.
46
47
48NOT Files
49
50    Many of Cadmium's commands to check that your work obeys the
51various stylistic rules of the OpenSolaris consolidations (such as
52those run by 'hg nits') allow files to be excluded from this checking
53by means of NOT files kept in the .hg/cdm/ directory of the Mercurial
54repository for one-time exceptions, and in the exception_lists
55directory at the repository root for permanent exceptions.  (For ON,
56these 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
60file, a description of which is available in the hgignore(5) manual
61page.
62
63
64Common 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
72import 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#
86sys.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#
94sys.path.insert(2, os.path.join(os.path.dirname(__file__), "..", ".."))
95
96from onbld.Scm import Version
97from onbld.Scm import Ignore
98from mercurial import util
99
100try:
101    Version.check_version()
102except Version.VersionMismatch, badversion:
103    raise util.Abort("Version Mismatch:\n %s\n" % badversion)
104
105from mercurial import cmdutil, node, patch
106
107from onbld.Scm.WorkSpace import WorkSpace, WorkList
108from onbld.Scm.Backup import CdmBackup
109from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk
110from onbld.Checks import JStyle, Keywords, ManLint, Mapfile
111
112
113def 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
131def 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)]
146buildfilelist = util.cachefunc(buildfilelist)
147
148
149def 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
179def 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#
205wslist = {}
206
207
208def 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
217def 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
232def 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
263def 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
308def 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
317def 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
325def 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
341def 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
358def 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
396def 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
430def 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
476def 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
510def 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
544def 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
581def 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
609def 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
645def 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
675def 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
732def 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#
766def 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
780def 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
801def 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
822def 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
855def 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
904def 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
1031def 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
1048def 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
1074def 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
1097def 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
1176def 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
1205def 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
1248def 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
1288def 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
1310def 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
1316def 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
1424cmdtable = {
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}
1531