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