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