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