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 2009 Sun Microsystems, Inc. All rights reserved. 18# Use is subject to license terms. 19# 20 21'''OpenSolaris workspace extensions for mercurial 22 23This extension contains a number of commands to help you work within 24the OpenSolaris consolidations. 25 26Common uses: 27 28Show diffs relative to parent workspace - pdiffs 29Check source style rules - nits 30Run pre-putback checks - pbchk 31Collapse all your changes into a single changeset - recommit''' 32 33 34# 35# NB: This assumes the normal directory structure, with this 36# extension 2 levels below .../lib/python. 37# 38# If you change that, change this 39# 40import atexit, os, stat, sys, termios 41 42sys.path.insert(1, "%s/../../" % os.path.dirname(__file__)) 43 44from onbld.Scm import Version 45from mercurial import util 46 47try: 48 Version.check_version() 49except Version.VersionMismatch, badversion: 50 raise util.Abort("Version Mismatch:\n %s\n" % badversion) 51 52import ConfigParser 53from mercurial import cmdutil, ignore, node 54 55from onbld.Scm.WorkSpace import ActiveEntry, WorkSpace 56from onbld.Scm.Backup import CdmBackup 57from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk 58from onbld.Checks import JStyle, Keywords, Mapfile, Rti, onSWAN 59 60 61def yes_no(ui, msg, default): 62 if default: 63 prompt = ' [Y/n]:' 64 defanswer = 'y' 65 else: 66 prompt = ' [y/N]:' 67 defanswer = 'n' 68 69 if Version.at_least("1.3.0"): 70 resp = ui.prompt(msg + prompt, ['&yes', '&no'], default=defanswer) 71 else: 72 resp = ui.prompt(msg + prompt, r'([Yy(es)?|[Nn]o?)?', 73 default=defanswer) 74 75 return resp[0] in ('Y', 'y') 76 77 78def buildfilelist(ws, parent, files): 79 '''Build a list of files in which we're interested. 80 81 If no files are specified take files from the active list relative 82 to 'parent'. 83 84 Return a list of 2-tuples the first element being a path relative 85 to the current directory and the second an entry from the active 86 list, or None if an explicit file list was given.''' 87 88 if files: 89 return [(path, None) for path in sorted(files)] 90 else: 91 active = ws.active(parent=parent) 92 return [(ws.filepath(e.name), e) for e in sorted(active)] 93buildfilelist = util.cachefunc(buildfilelist) 94 95 96def not_check(repo, cmd): 97 '''return a function which returns boolean indicating whether a file 98 should be skipped for CMD.''' 99 100 # 101 # The ignore routines need a canonical path to the file (relative to the 102 # repo root), whereas the check commands get paths relative to the cwd. 103 # 104 # Wrap our argument such that the path is canonified before it is checked. 105 # 106 def canonified_check(ignfunc): 107 def f(path): 108 cpath = util.canonpath(repo.root, repo.getcwd(), path) 109 return ignfunc(cpath) 110 return f 111 112 ignorefiles = [] 113 114 for f in [repo.join('cdm/%s.NOT' % cmd), 115 repo.wjoin('exception_lists/%s' % cmd)]: 116 if os.path.exists(f): 117 ignorefiles.append(f) 118 119 if ignorefiles: 120 ign = ignore.ignore(repo.root, ignorefiles, repo.ui.warn) 121 return canonified_check(ign) 122 else: 123 return util.never 124 125 126def abort_if_dirty(ws): 127 '''Abort if the workspace has uncommitted changes, merges, 128 branches, or has Mq patches applied''' 129 130 if ws.modified(): 131 raise util.Abort('workspace has uncommitted changes') 132 if ws.merged(): 133 raise util.Abort('workspace contains uncommitted merge') 134 if ws.branched(): 135 raise util.Abort('workspace contains uncommitted branch') 136 if ws.mq_applied(): 137 raise util.Abort('workspace has Mq patches applied') 138 139 140# 141# Adding a reference to WorkSpace from a repo causes a circular reference 142# repo <-> WorkSpace. 143# 144# This prevents repo, WorkSpace and members thereof from being garbage 145# collected. Since transactions are aborted when the transaction object 146# is collected, and localrepo holds a reference to the most recently created 147# transaction, this prevents transactions from cleanly aborting. 148# 149# Instead, we hold the repo->WorkSpace association in a dictionary, breaking 150# that dependence. 151# 152wslist = {} 153 154 155def reposetup(ui, repo): 156 if repo.local() and repo not in wslist: 157 wslist[repo] = WorkSpace(repo) 158 159 if Version.at_least("1.3"): 160 interactive = ui.interactive() 161 else: 162 interactive = ui.interactive 163 164 if interactive and sys.stdin.isatty(): 165 ui.setconfig('hooks', 'preoutgoing.cdm_pbconfirm', 166 'python:hgext_cdm.pbconfirm') 167 168 169def pbconfirm(ui, repo, hooktype, source): 170 def wrapper(settings=None): 171 termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, settings) 172 173 if source == 'push': 174 if not yes_no(ui, "Are you sure you wish to push?", False): 175 return 1 176 else: 177 settings = termios.tcgetattr(sys.stdin.fileno()) 178 orig = list(settings) 179 atexit.register(wrapper, orig) 180 settings[3] = settings[3] & (~termios.ISIG) # c_lflag 181 termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, settings) 182 183 184def cdm_pdiffs(ui, repo, *pats, **opts): 185 '''list workspace diffs relative to parent workspace 186 187 The parent tip is taken to be the latest revision shared between 188 us and the parent workspace.''' 189 190 parent = opts['parent'] 191 192 diffs = wslist[repo].pdiff(pats, opts, parent=parent) 193 if diffs: 194 ui.write(diffs) 195 196 197def cdm_list(ui, repo, **opts): 198 '''list files changed relative to parent workspace 199 200 The parent tip is taken to be the latest revision shared between 201 us and the parent workspace.''' 202 203 wanted = [] 204 205 if opts['added']: 206 wanted.append(ActiveEntry.ADDED) 207 if opts['modified']: 208 wanted.append(ActiveEntry.MODIFIED) 209 if opts['removed']: 210 wanted.append(ActiveEntry.REMOVED) 211 212 act = wslist[repo].active(opts['parent']) 213 chngmap = {ActiveEntry.MODIFIED: 'modified', 214 ActiveEntry.ADDED: 'added', 215 ActiveEntry.REMOVED: 'removed'} 216 217 lst = {} 218 for entry in act: 219 if wanted and (entry.change not in wanted): 220 continue 221 222 chngstr = chngmap[entry.change] 223 if chngstr not in lst: 224 lst[chngstr] = [] 225 lst[chngstr].append(entry) 226 227 for chng in sorted(lst.keys()): 228 ui.write(chng + ':\n') 229 for elt in sorted(lst[chng]): 230 if elt.is_renamed(): 231 ui.write('\t%s (renamed from %s)\n' % (elt.name, 232 elt.parentname)) 233 elif elt.is_copied(): 234 ui.write('\t%s (copied from %s)\n' % (elt.name, 235 elt.parentname)) 236 else: 237 ui.write('\t%s\n' % elt.name) 238 239 240def cdm_arcs(ui, repo, parent=None): 241 'show all ARC cases in checkin comments' 242 act = wslist[repo].active(parent) 243 244 # We take a set of the appropriate comments to eliminate duplicates. 245 for elt in set(filter(Comments.isARC, act.comments())): 246 ui.write(elt + '\n') 247 248 249def cdm_bugs(ui, repo, parent=None): 250 'show all bug IDs in checkin comments' 251 act = wslist[repo].active(parent) 252 253 for elt in set(filter(Comments.isBug, act.comments())): 254 ui.write(elt + '\n') 255 256 257def cdm_comments(ui, repo, parent=None): 258 'show checkin comments for active files' 259 act = wslist[repo].active(parent) 260 261 for elt in act.comments(): 262 ui.write(elt + '\n') 263 264 265def cdm_renamed(ui, repo, parent=None): 266 '''show renamed active files 267 268 Renamed files are shown in the format 269 270 newname oldname 271 272 One pair per-line.''' 273 274 act = wslist[repo].active(parent) 275 276 for entry in sorted(filter(lambda x: x.is_renamed(), act)): 277 ui.write('%s %s\n' % (entry.name, entry.parentname)) 278 279 280def cdm_comchk(ui, repo, **opts): 281 '''check checkin comments for active files 282 283 Check that checkin comments conform to O/N rules.''' 284 285 active = wslist[repo].active(opts.get('parent')) 286 287 ui.write('Comments check:\n') 288 289 check_db = not opts.get('nocheck') 290 return Comments.comchk(active.comments(), check_db=check_db, output=ui, 291 arcPath=ui.config('cdm', 'arcpath', None)) 292 293 294def cdm_cddlchk(ui, repo, *args, **opts): 295 '''check for a valid CDDL block in active files 296 297 See http://www.opensolaris.org/os/community/on/devref_toc/devref_7/#7_2_3_nonformatting_considerations 298 for more info.''' 299 300 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 301 exclude = not_check(repo, 'cddlchk') 302 lenient = True 303 ret = 0 304 305 ui.write('CDDL block check:\n') 306 307 for f, e in filelist: 308 if e and e.is_removed(): 309 continue 310 elif (e or opts.get('honour_nots')) and exclude(f): 311 ui.status('Skipping %s...\n' % f) 312 continue 313 elif e and e.is_added(): 314 lenient = False 315 else: 316 lenient = True 317 318 fh = open(f, 'r') 319 ret |= Cddl.cddlchk(fh, lenient=lenient, output=ui) 320 fh.close() 321 return ret 322 323 324def cdm_mapfilechk(ui, repo, *args, **opts): 325 '''check for a valid MAPFILE header block in active files 326 327 Check that all link-editor mapfiles contain the standard mapfile 328 header comment directing the reader to the document containing 329 Solaris object versioning rules (README.mapfile).''' 330 331 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 332 exclude = not_check(repo, 'mapfilechk') 333 ret = 0 334 335 ui.write('Mapfile comment check:\n') 336 337 for f, e in filelist: 338 if e and e.is_removed(): 339 continue 340 elif f.find('mapfile') == -1: 341 continue 342 elif (e or opts.get('honour_nots')) and exclude(f): 343 ui.status('Skipping %s...\n' % f) 344 continue 345 346 fh = open(f, 'r') 347 ret |= Mapfile.mapfilechk(fh, output=ui) 348 fh.close() 349 return ret 350 351 352def cdm_copyright(ui, repo, *args, **opts): 353 '''check active files for valid copyrights 354 355 Check that all active files have a valid copyright containing the 356 current year (and *only* the current year). 357 See http://www.opensolaris.org/os/project/muskoka/on_dev/golden_rules.txt 358 for more info.''' 359 360 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 361 exclude = not_check(repo, 'copyright') 362 ret = 0 363 364 ui.write('Copyright check:\n') 365 366 for f, e in filelist: 367 if e and e.is_removed(): 368 continue 369 elif (e or opts.get('honour_nots')) and exclude(f): 370 ui.status('Skipping %s...\n' % f) 371 continue 372 373 fh = open(f, 'r') 374 ret |= Copyright.copyright(fh, output=ui) 375 fh.close() 376 return ret 377 378 379def cdm_hdrchk(ui, repo, *args, **opts): 380 '''check active header files conform to O/N rules''' 381 382 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 383 exclude = not_check(repo, 'hdrchk') 384 ret = 0 385 386 ui.write('Header format check:\n') 387 388 for f, e in filelist: 389 if e and e.is_removed(): 390 continue 391 elif not f.endswith('.h'): 392 continue 393 elif (e or opts.get('honour_nots')) and exclude(f): 394 ui.status('Skipping %s...\n' % f) 395 continue 396 397 fh = open(f, 'r') 398 ret |= HdrChk.hdrchk(fh, lenient=True, output=ui) 399 fh.close() 400 return ret 401 402 403def cdm_cstyle(ui, repo, *args, **opts): 404 '''check active C source files conform to the C Style Guide 405 406 See http://opensolaris.org/os/community/documentation/getting_started_docs/cstyle.ms.pdf''' 407 408 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 409 exclude = not_check(repo, 'cstyle') 410 ret = 0 411 412 ui.write('C style check:\n') 413 414 for f, e in filelist: 415 if e and e.is_removed(): 416 continue 417 elif not (f.endswith('.c') or f.endswith('.h')): 418 continue 419 elif (e or opts.get('honour_nots')) and exclude(f): 420 ui.status('Skipping %s...\n' % f) 421 continue 422 423 fh = open(f, 'r') 424 ret |= CStyle.cstyle(fh, output=ui, 425 picky=True, check_posix_types=True, 426 check_continuation=True) 427 fh.close() 428 return ret 429 430 431def cdm_jstyle(ui, repo, *args, **opts): 432 'check active Java source files for common stylistic errors' 433 434 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 435 exclude = not_check(repo, 'jstyle') 436 ret = 0 437 438 ui.write('Java style check:\n') 439 440 for f, e in filelist: 441 if e and e.is_removed(): 442 continue 443 elif not f.endswith('.java'): 444 continue 445 elif (e or opts.get('honour_nots')) and exclude(f): 446 ui.status('Skipping %s...\n' % f) 447 continue 448 449 fh = open(f, 'r') 450 ret |= JStyle.jstyle(fh, output=ui, picky=True) 451 fh.close() 452 return ret 453 454 455def cdm_permchk(ui, repo, *args, **opts): 456 '''check active files permission - warn +x (execute) mode''' 457 458 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 459 exclude = not_check(repo, 'permchk') 460 exeFiles = [] 461 462 ui.write('File permission check:\n') 463 464 for f, e in filelist: 465 if e and e.is_removed(): 466 continue 467 elif (e or opts.get('honour_nots')) and exclude(f): 468 ui.status('Skipping %s...\n' % f) 469 continue 470 471 mode = stat.S_IMODE(os.stat(f)[stat.ST_MODE]) 472 if mode & stat.S_IEXEC: 473 exeFiles.append(f) 474 475 if len(exeFiles) > 0: 476 ui.write('Warning: the following active file(s) have executable mode ' 477 '(+x) permission set,\nremove unless intentional:\n') 478 for fname in exeFiles: 479 ui.write(" %s\n" % fname) 480 481 return len(exeFiles) > 0 482 483 484def cdm_tagchk(ui, repo, **opts): 485 '''check if .hgtags is active and issue warning 486 487 Tag sharing among repositories is restricted to gatekeepers''' 488 489 active = wslist[repo].active(opts.get('parent')) 490 491 ui.write('Checking for new tags:\n') 492 493 if ".hgtags" in active: 494 tfile = wslist[repo].filepath('.hgtags') 495 ptip = active.parenttip.rev() 496 497 ui.write('Warning: Workspace contains new non-local tags.\n' 498 'Only gatekeepers should add or modify such tags.\n' 499 'Use the following commands to revert these changes:\n' 500 ' hg revert -r%d %s\n' 501 ' hg commit %s\n' 502 'You should also recommit before integration\n' % 503 (ptip, tfile, tfile)) 504 505 return 1 506 507 return 0 508 509 510def cdm_branchchk(ui, repo, **opts): 511 '''check if multiple heads (or branches) are present, or if 512 branch changes are made''' 513 514 ui.write('Checking for multiple heads (or branches):\n') 515 516 heads = set(repo.heads()) 517 parents = set([x.node() for x in wslist[repo].workingctx().parents()]) 518 519 # 520 # We care if there's more than one head, and those heads aren't 521 # identical to the dirstate parents (if they are identical, it's 522 # an uncommitted merge which mergechk will catch, no need to 523 # complain twice). 524 # 525 if len(heads) > 1 and heads != parents: 526 ui.write('Workspace has multiple heads (or branches):\n') 527 for head in [repo.changectx(head) for head in heads]: 528 ui.write(" %d:%s\t%s\n" % 529 (head.rev(), str(head), head.description().splitlines()[0])) 530 ui.write('You must merge and recommit.\n') 531 return 1 532 533 ui.write('\nChecking for branch changes:\n') 534 535 if repo.dirstate.branch() != 'default': 536 ui.write("Warning: Workspace tip has named branch: '%s'\n" 537 "Only gatekeepers should push new branches.\n" 538 "Use the following commands to restore the branch name:\n" 539 " hg branch [-f] default\n" 540 " hg commit\n" 541 "You should also recommit before integration\n" % 542 (repo.dirstate.branch())) 543 return 1 544 545 branches = repo.branchtags().keys() 546 if len(branches) > 1: 547 ui.write('Warning: Workspace has named branches:\n') 548 for t in branches: 549 if t == 'default': 550 continue 551 ui.write("\t%s\n" % t) 552 553 ui.write("Only gatekeepers should push new branches.\n" 554 "Use the following commands to remove extraneous branches.\n" 555 " hg branch [-f] default\n" 556 " hg commit" 557 "You should also recommit before integration\n") 558 return 1 559 560 return 0 561 562 563def cdm_rtichk(ui, repo, **opts): 564 '''check active bug/RFEs for approved RTIs 565 566 Only works on SWAN.''' 567 568 if opts.get('nocheck') or os.path.exists(repo.join('cdm/rtichk.NOT')): 569 ui.status('Skipping RTI checks...\n') 570 return 0 571 572 if not onSWAN(): 573 ui.write('RTI checks only work on SWAN, skipping...\n') 574 return 0 575 576 parent = wslist[repo].parent(opts.get('parent')) 577 active = wslist[repo].active(parent) 578 579 ui.write('RTI check:\n') 580 581 bugs = [] 582 583 for com in active.comments(): 584 match = Comments.isBug(com) 585 if match and match.group(1) not in bugs: 586 bugs.append(match.group(1)) 587 588 # RTI normalizes the gate path for us 589 return int(not Rti.rti(bugs, gatePath=parent, output=ui)) 590 591 592def cdm_keywords(ui, repo, *args, **opts): 593 '''check source files do not contain SCCS keywords''' 594 595 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 596 exclude = not_check(repo, 'keywords') 597 ret = 0 598 599 ui.write('Keywords check:\n') 600 601 for f, e in filelist: 602 if e and e.is_removed(): 603 continue 604 elif (e or opts.get('honour_nots')) and exclude(f): 605 ui.status('Skipping %s...\n' % f) 606 continue 607 608 fh = open(f, 'r') 609 ret |= Keywords.keywords(fh, output=ui) 610 fh.close() 611 return ret 612 613 614# 615# NB: 616# There's no reason to hook this up as an invokable command, since 617# we have 'hg status', but it must accept the same arguments. 618# 619def cdm_outchk(ui, repo, **opts): 620 '''Warn the user if they have uncommitted changes''' 621 622 ui.write('Checking for uncommitted changes:\n') 623 624 st = wslist[repo].modified() 625 if st: 626 ui.write('Warning: the following files have uncommitted changes:\n') 627 for elt in st: 628 ui.write(' %s\n' % elt) 629 return 1 630 return 0 631 632 633def cdm_mergechk(ui, repo, **opts): 634 '''Warn the user if their workspace contains merges''' 635 636 active = wslist[repo].active(opts.get('parent')) 637 638 ui.write('Checking for merges:\n') 639 640 merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1], 641 active.revs) 642 643 if merges: 644 ui.write('Workspace contains the following merges:\n') 645 for rev in merges: 646 desc = rev.description().splitlines() 647 ui.write(' %s:%s\t%s\n' % 648 (rev.rev() or "working", str(rev), 649 desc and desc[0] or "*** uncommitted change ***")) 650 return 1 651 return 0 652 653 654def run_checks(ws, cmds, *args, **opts): 655 '''Run CMDS (with OPTS) over active files in WS''' 656 657 ret = 0 658 659 for cmd in cmds: 660 name = cmd.func_name.split('_')[1] 661 if not ws.ui.configbool('cdm', name, True): 662 ws.ui.status('Skipping %s check...\n' % name) 663 else: 664 ws.ui.pushbuffer() 665 result = cmd(ws.ui, ws.repo, honour_nots=True, *args, **opts) 666 output = ws.ui.popbuffer() 667 668 ret |= result 669 670 if not ws.ui.quiet or result != 0: 671 ws.ui.write(output, '\n') 672 return ret 673 674 675def cdm_nits(ui, repo, *args, **opts): 676 '''check for stylistic nits in active files 677 678 Run cddlchk, copyright, cstyle, hdrchk, jstyle, mapfilechk, 679 permchk, and keywords checks.''' 680 681 cmds = [cdm_cddlchk, 682 cdm_copyright, 683 cdm_cstyle, 684 cdm_hdrchk, 685 cdm_jstyle, 686 cdm_mapfilechk, 687 cdm_permchk, 688 cdm_keywords] 689 690 return run_checks(wslist[repo], cmds, *args, **opts) 691 692 693def cdm_pbchk(ui, repo, **opts): 694 '''pre-putback check all active files 695 696 Run cddlchk, comchk, copyright, cstyle, hdrchk, jstyle, mapfilechk, 697 permchk, tagchk, branchchk, keywords and rtichk checks. Additionally, 698 warn about uncommitted changes.''' 699 700 # 701 # The current ordering of these is that the commands from cdm_nits 702 # run first in the same order as they would in cdm_nits. Then the 703 # pbchk specifics run 704 # 705 cmds = [cdm_cddlchk, 706 cdm_copyright, 707 cdm_cstyle, 708 cdm_hdrchk, 709 cdm_jstyle, 710 cdm_mapfilechk, 711 cdm_permchk, 712 cdm_keywords, 713 cdm_comchk, 714 cdm_tagchk, 715 cdm_branchchk, 716 cdm_rtichk, 717 cdm_outchk, 718 cdm_mergechk] 719 720 return run_checks(wslist[repo], cmds, **opts) 721 722 723def cdm_recommit(ui, repo, **opts): 724 '''compact outgoing deltas into a single, conglomerate, delta''' 725 726 if not os.getcwd().startswith(repo.root): 727 raise util.Abort('recommit is not safe to run with -R') 728 729 abort_if_dirty(wslist[repo]) 730 731 heads = repo.heads() 732 if len(heads) > 1: 733 ui.warn('Workspace has multiple heads (or branches):\n') 734 for head in heads: 735 ui.warn('\t%d\n' % repo.changelog.rev(head)) 736 raise util.Abort('you must merge before recommitting') 737 738 wlock = repo.wlock() 739 lock = repo.lock() 740 741 try: 742 active = wslist[repo].active(opts['parent']) 743 744 if len(active.revs) <= 0: 745 raise util.Abort("no changes to recommit") 746 747 if len(active.files()) <= 0: 748 ui.warn("Recommitting %d active changesets, but no active files\n" % 749 len(active.revs)) 750 751 # 752 # During the course of a recommit, any file bearing a name 753 # matching the source name of any renamed file will be 754 # clobbered by the operation. 755 # 756 # As such, we ask the user before proceeding. 757 # 758 bogosity = [f.parentname for f in active if f.is_renamed() and 759 os.path.exists(repo.wjoin(f.parentname))] 760 if bogosity: 761 ui.warn("The following file names are the original name of a " 762 "rename and also present\n" 763 "in the working directory:\n") 764 765 for fname in bogosity: 766 ui.warn(" %s\n" % fname) 767 768 if not yes_no(ui, "These files will be removed by recommit." 769 " Continue?", 770 False): 771 raise util.Abort("recommit would clobber files") 772 773 user = opts['user'] or ui.username() 774 comments = '\n'.join(active.comments()) 775 776 message = cmdutil.logmessage(opts) or ui.edit(comments, user) 777 if not message: 778 raise util.Abort('empty commit message') 779 780 bk = CdmBackup(ui, wslist[repo], backup_name(repo.root)) 781 if bk.need_backup(): 782 if yes_no(ui, 'Do you want to backup files first?', True): 783 bk.backup() 784 785 oldtags = repo.tags() 786 clearedtags = [(name, nd, repo.changelog.rev(nd), local) 787 for name, nd, local in active.tags()] 788 789 wslist[repo].squishdeltas(active, message, user=user) 790 finally: 791 lock.release() 792 wlock.release() 793 794 if clearedtags: 795 ui.write("Removed tags:\n") 796 for name, nd, rev, local in sorted(clearedtags, 797 key=lambda x: x[0].lower()): 798 ui.write(" %5s:%s:\t%s%s\n" % (rev, node.short(nd), 799 name, (local and ' (local)' or ''))) 800 801 for ntag, nnode in sorted(repo.tags().items(), 802 key=lambda x: x[0].lower()): 803 if ntag in oldtags and ntag != "tip": 804 if oldtags[ntag] != nnode: 805 ui.write("tag '%s' now refers to revision %d:%s\n" % 806 (ntag, repo.changelog.rev(nnode), 807 node.short(nnode))) 808 809 810def do_eval(cmd, files, root, changedir=True): 811 if not changedir: 812 os.chdir(root) 813 814 for path in sorted(files): 815 dirn, base = os.path.split(path) 816 817 if changedir: 818 os.chdir(os.path.join(root, dirn)) 819 820 os.putenv('workspace', root) 821 os.putenv('filepath', path) 822 os.putenv('dir', dirn) 823 os.putenv('file', base) 824 os.system(cmd) 825 826 827def cdm_eval(ui, repo, *command, **opts): 828 '''run cmd for each active file 829 830 cmd can refer to: 831 $file - active file basename. 832 $dir - active file dirname. 833 $filepath - path from workspace root to active file. 834 $workspace - full path to workspace root. 835 836 For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last 837 the 3 log entries for each active file, preceded by its directory.''' 838 839 act = wslist[repo].active(opts['parent']) 840 cmd = ' '.join(command) 841 files = [x.name for x in act if not x.is_removed()] 842 843 do_eval(cmd, files, repo.root, not opts['remain']) 844 845 846def cdm_apply(ui, repo, *command, **opts): 847 '''apply cmd to all active files 848 849 For example 'hg apply wc -l' outputs a line count of active files.''' 850 851 act = wslist[repo].active(opts['parent']) 852 853 if opts['remain']: 854 appnd = ' $filepath' 855 else: 856 appnd = ' $file' 857 858 cmd = ' '.join(command) + appnd 859 files = [x.name for x in act if not x.is_removed()] 860 861 do_eval(cmd, files, repo.root, not opts['remain']) 862 863 864def cdm_reparent_11(ui, repo, parent): 865 '''reparent your workspace 866 867 Updates the 'default' path.''' 868 869 filename = repo.join('hgrc') 870 871 p = ui.expandpath(parent) 872 cp = util.configparser() 873 874 try: 875 cp.read(filename) 876 except ConfigParser.ParsingError, inst: 877 raise util.Abort('failed to parse %s\n%s' % (filename, inst)) 878 879 try: 880 fh = open(filename, 'w') 881 except IOError, e: 882 raise util.Abort('Failed to open workspace configuration: %s' % e) 883 884 if not cp.has_section('paths'): 885 cp.add_section('paths') 886 cp.set('paths', 'default', p) 887 cp.write(fh) 888 fh.close() 889 890 891def cdm_reparent_13(ui, repo, parent): 892 '''reparent your workspace 893 894 Updates the 'default' path in this repository's .hg/hgrc.''' 895 896 def append_new_parent(parent): 897 fp = None 898 try: 899 fp = repo.opener('hgrc', 'a', atomictemp=True) 900 if fp.tell() != 0: 901 fp.write('\n') 902 fp.write('[paths]\n' 903 'default = %s\n\n' % parent) 904 fp.rename() 905 finally: 906 if fp and not fp.closed: 907 fp.close() 908 909 def update_parent(path, line, parent): 910 line = line - 1 # The line number we're passed will be 1-based 911 fp = None 912 913 try: 914 fp = open(path) 915 data = fp.readlines() 916 finally: 917 if fp and not fp.closed: 918 fp.close() 919 920 # 921 # line will be the last line of any continued block, go back 922 # to the first removing the continuation as we go. 923 # 924 while data[line][0].isspace(): 925 data.pop(line) 926 line -= 1 927 928 assert data[line].startswith('default') 929 930 data[line] = "default = %s\n" % parent 931 if data[-1] != '\n': 932 data.append('\n') 933 934 try: 935 fp = util.atomictempfile(path, 'w', 0644) 936 fp.writelines(data) 937 fp.rename() 938 finally: 939 if fp and not fp.closed: 940 fp.close() 941 942 from mercurial import config 943 parent = ui.expandpath(parent) 944 945 if not os.path.exists(repo.join('hgrc')): 946 append_new_parent(parent) 947 return 948 949 cfg = config.config() 950 cfg.read(repo.join('hgrc')) 951 source = cfg.source('paths', 'default') 952 953 if not source: 954 append_new_parent(parent) 955 return 956 else: 957 path, target = source.rsplit(':', 1) 958 959 if path != repo.join('hgrc'): 960 raise util.Abort("Cannot edit path specification not in repo hgrc\n" 961 "default path is from: %s" % source) 962 963 update_parent(path, int(target), parent) 964 965if Version.at_least("1.3"): 966 cdm_reparent = cdm_reparent_13 967else: 968 cdm_reparent = cdm_reparent_11 969 970 971def backup_name(fullpath): 972 '''Create a backup directory name based on the specified path. 973 974 In most cases this is the basename of the path specified, but 975 certain cases are handled specially to create meaningful names''' 976 977 special = ['usr/closed'] 978 979 fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep) 980 981 # 982 # If a path is 'special', we append the basename of the path to 983 # the path element preceding the constant, special, part. 984 # 985 # Such that for instance: 986 # /foo/bar/onnv-fixes/usr/closed 987 # has a backup name of: 988 # onnv-fixes-closed 989 # 990 for elt in special: 991 elt = elt.split(os.path.sep) 992 pathpos = len(elt) 993 994 if fullpath[-pathpos:] == elt: 995 return "%s-%s" % (fullpath[-pathpos - 1], elt[-1]) 996 else: 997 return fullpath[-1] 998 999 1000def cdm_backup(ui, repo, if_newer=False): 1001 '''make backup copies of all workspace changes 1002 1003 Backups will be stored in ~/cdm.backup/<basename of workspace>.''' 1004 1005 name = backup_name(repo.root) 1006 bk = CdmBackup(ui, wslist[repo], name) 1007 1008 wlock = repo.wlock() 1009 lock = repo.lock() 1010 1011 try: 1012 if if_newer and not bk.need_backup(): 1013 ui.status('backup is up-to-date\n') 1014 else: 1015 bk.backup() 1016 finally: 1017 lock.release() 1018 wlock.release() 1019 1020 1021def cdm_restore(ui, repo, backup, **opts): 1022 '''restore workspace from backup 1023 1024 Restores a workspace from the specified backup directory and generation 1025 (which defaults to the latest).''' 1026 1027 if not os.getcwd().startswith(repo.root): 1028 raise util.Abort('restore is not safe to run with -R') 1029 1030 abort_if_dirty(wslist[repo]) 1031 1032 if opts['generation']: 1033 gen = int(opts['generation']) 1034 else: 1035 gen = None 1036 1037 if os.path.exists(backup): 1038 backup = os.path.abspath(backup) 1039 1040 wlock = repo.wlock() 1041 lock = repo.lock() 1042 1043 try: 1044 bk = CdmBackup(ui, wslist[repo], backup) 1045 bk.restore(gen) 1046 finally: 1047 lock.release() 1048 wlock.release() 1049 1050 1051def cdm_webrev(ui, repo, **opts): 1052 '''generate webrev and optionally upload it 1053 1054 This command passes all arguments to webrev script''' 1055 1056 webrev_args = "" 1057 for key in opts.keys(): 1058 if opts[key]: 1059 if type(opts[key]) == type(True): 1060 webrev_args += '-' + key + ' ' 1061 else: 1062 webrev_args += '-' + key + ' ' + opts[key] + ' ' 1063 1064 retval = os.system('webrev ' + webrev_args) 1065 if retval != 0: 1066 return retval - 255 1067 1068 return 0 1069 1070 1071cmdtable = { 1072 'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'), 1073 ('r', 'remain', None, 'do not change directories')], 1074 'hg apply [-p PARENT] [-r] command...'), 1075 'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')], 1076 'hg arcs [-p PARENT]'), 1077 '^backup|bu': (cdm_backup, [('t', 'if-newer', None, 1078 'only backup if workspace files are newer')], 1079 'hg backup [-t]'), 1080 'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')], 1081 'hg branchchk [-p PARENT]'), 1082 'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')], 1083 'hg bugs [-p PARENT]'), 1084 'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')], 1085 'hg cddlchk [-p PARENT]'), 1086 'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'), 1087 ('N', 'nocheck', None, 1088 'do not compare comments with databases')], 1089 'hg comchk [-p PARENT]'), 1090 'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')], 1091 'hg comments [-p PARENT]'), 1092 'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')], 1093 'hg copyright [-p PARENT]'), 1094 'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')], 1095 'hg cstyle [-p PARENT]'), 1096 'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'), 1097 ('r', 'remain', None, 'do not change directories')], 1098 'hg eval [-p PARENT] [-r] command...'), 1099 'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')], 1100 'hg hdrchk [-p PARENT]'), 1101 'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')], 1102 'hg jstyle [-p PARENT]'), 1103 'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')], 1104 'hg keywords [-p PARENT]'), 1105 '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'), 1106 ('r', 'removed', None, 'show removed files'), 1107 ('a', 'added', None, 'show added files'), 1108 ('m', 'modified', None, 'show modified files')], 1109 'hg list [-amrRu] [-p PARENT]'), 1110 'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')], 1111 'hg mapfilechk [-p PARENT]'), 1112 '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')], 1113 'hg nits [-p PARENT]'), 1114 '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'), 1115 ('N', 'nocheck', None, 'skip RTI check')], 1116 'hg pbchk [-N] [-p PARENT]'), 1117 'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')], 1118 'hg permchk [-p PARENT]'), 1119 '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'), 1120 ('a', 'text', None, 'treat all files as text'), 1121 ('g', 'git', None, 'use extended git diff format'), 1122 ('w', 'ignore-all-space', None, 1123 'ignore white space when comparing lines'), 1124 ('b', 'ignore-space-change', None, 1125 'ignore changes in the amount of white space'), 1126 ('B', 'ignore-blank-lines', None, 1127 'ignore changes whos lines are all blank'), 1128 ('U', 'unified', 3, 1129 'number of lines of context to show'), 1130 ('I', 'include', [], 1131 'include names matching the given patterns'), 1132 ('X', 'exclude', [], 1133 'exclude names matching the given patterns')], 1134 'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'), 1135 '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'), 1136 ('f', 'force', None, 'force operation'), 1137 ('m', 'message', '', 1138 'use <text> as commit message'), 1139 ('l', 'logfile', '', 1140 'read commit message from file'), 1141 ('u', 'user', '', 1142 'record user as committer')], 1143 'hg recommit [-f] [-p PARENT]'), 1144 'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')], 1145 'hg renamed [-p PARENT]'), 1146 'reparent': (cdm_reparent, [], 'hg reparent PARENT'), 1147 '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')], 1148 'hg restore [-g GENERATION] BACKUP'), 1149 'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'), 1150 ('N', 'nocheck', None, 'skip RTI check')], 1151 'hg rtichk [-N] [-p PARENT]'), 1152 'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')], 1153 'hg tagchk [-p PARENT]'), 1154 'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'), 1155 ('D', 'D', '', 'delete remote webrev'), 1156 ('I', 'I', '', 'ITS configuration file'), 1157 ('i', 'i', '', 'include file'), 1158 ('l', 'l', '', 'extract file list from putback -n'), 1159 ('N', 'N', None, 'supress comments'), 1160 ('n', 'n', None, 'do not generate webrev'), 1161 ('O', 'O', None, 'OpenSolaris mode'), 1162 ('o', 'o', '', 'output directory'), 1163 ('p', 'p', '', 'use specified parent'), 1164 ('t', 't', '', 'upload target'), 1165 ('U', 'U', None, 'upload the webrev'), 1166 ('w', 'w', '', 'use wx active file')], 1167 'hg webrev [WEBREV_OPTIONS]'), 1168} 1169