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