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