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
24 the OpenSolaris consolidations. It provides commands to check your
25 changes against the various style rules used for OpenSolaris, to
26 backup and restore your changes, to generate code reviews, and to
27 prepare your changes for integration.
28
29
30 The Parent
31
32 To provide a uniform notion of parent workspace regardless of
33 filesystem-based access, Cadmium uses the highest numbered changeset
34 on the current branch that is also in the parent workspace to
35 represent the parent workspace.
36
37
38 The Active List
39
40 Many Cadmium commands operate on the active list, the set of
41 files ('active files') you have changed in this workspace in relation
42 to its parent workspace, and the metadata (commentary, primarily)
43 associated with those changes.
44
45
46 NOT Files
47
48 Many of Cadmium's commands to check that your work obeys the
49 various stylistic rules of the OpenSolaris consolidations (such as
50 those run by 'hg nits') allow files to be excluded from this checking
51 by means of NOT files kept in the .hg/cdm/ directory of the Mercurial
52 repository for one-time exceptions, and in the exception_lists
53 directory at the repository root for permanent exceptions. (For ON,
54 these 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
58 file, a description of which is available in the hgignore(5) manual
59 page.
60
61
62 Common 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
70 import 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 #
84 sys.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 #
92 sys.path.insert(2, os.path.join(os.path.dirname(__file__), "..", ".."))
93
94 from onbld.Scm import Version
95 from mercurial import util
96
97 try:
98 Version.check_version()
99 except Version.VersionMismatch, badversion:
100 raise util.Abort("Version Mismatch:\n %s\n" % badversion)
101
102 from mercurial import cmdutil, ignore, node, patch
103
104 from onbld.Scm.WorkSpace import WorkSpace, WorkList
105 from onbld.Scm.Backup import CdmBackup
106 from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk
107 from onbld.Checks import JStyle, Keywords, Mapfile
108
109
110 def 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
128 def 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)]
143 buildfilelist = util.cachefunc(buildfilelist)
144
145
146 def 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
176 def 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 #
202 wslist = {}
203
204
205 def 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
214 def 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
229 def 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
260 def 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
305 def 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
314 def 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
322 def 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
338 def 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
355 def 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
393 def 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
439 def 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
473 def 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
507 def 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
544 def 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
572 def 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
608 def 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
638 def 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
695 def 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 #
729 def 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
743 def 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
764 def 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
785 def 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
816 def 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
863 def 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
990 def 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
1007 def 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
1033 def 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
1056 def 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
1135 def 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
1164 def 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
1207 def 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
1247 def 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
1269 def 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
1275 def 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
1383 cmdtable = {
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 }