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