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