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 2010 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 '''replace outgoing changesets with a single equivalent changeset 725 726 Replace all outgoing changesets with a single changeset containing 727 equivalent changes. This removes uninteresting changesets created 728 during development that would only serve as noise in the gate. 729 730 Any changed file that is now identical in content to that in the 731 parent workspace (whether identical in history or otherwise) will 732 not be included in the new changeset. Any merges information will 733 also be removed. 734 735 If no files are changed in comparison to the parent workspace, the 736 outgoing changesets will be removed, but no new changeset created. 737 738 recommit will refuse to run if the workspace contains more than 739 one outgoing head, even if those heads are on the same branch. To 740 recommit with only one branch containing outgoing changesets, your 741 workspace must be on that branch and at that branch head. 742 743 recommit will prompt you to take a backup if your workspace has 744 been changed since the last backup was taken. In almost all 745 cases, you should allow it to take one (the default). 746 747 recommit cannot be run if the workspace contains any uncommitted 748 changes, applied Mq patches, or has multiple outgoing heads (or 749 branches). 750 ''' 751 752 ws = wslist[repo] 753 754 if not os.getcwd().startswith(repo.root): 755 raise util.Abort('recommit is not safe to run with -R') 756 757 abort_if_dirty(ws) 758 759 wlock = repo.wlock() 760 lock = repo.lock() 761 762 try: 763 parent = ws.parent(opts['parent']) 764 between = repo.changelog.nodesbetween(ws.findoutgoing(parent))[2] 765 heads = set(between) & set(repo.heads()) 766 767 if len(heads) > 1: 768 ui.warn('Workspace has multiple outgoing heads (or branches):\n') 769 for head in sorted(map(repo.changelog.rev, heads), reverse=True): 770 ui.warn('\t%d\n' % head) 771 raise util.Abort('you must merge before recommitting') 772 773 active = ws.active(parent) 774 775 if filter(lambda b: len(b.parents()) > 1, active.bases()): 776 raise util.Abort('Cannot recommit a merge of two non-outgoing ' 777 'changesets') 778 779 if len(active.revs) <= 0: 780 raise util.Abort("no changes to recommit") 781 782 if len(active.files()) <= 0: 783 ui.warn("Recommitting %d active changesets, but no active files\n" % 784 len(active.revs)) 785 786 # 787 # During the course of a recommit, any file bearing a name 788 # matching the source name of any renamed file will be 789 # clobbered by the operation. 790 # 791 # As such, we ask the user before proceeding. 792 # 793 bogosity = [f.parentname for f in active if f.is_renamed() and 794 os.path.exists(repo.wjoin(f.parentname))] 795 if bogosity: 796 ui.warn("The following file names are the original name of a " 797 "rename and also present\n" 798 "in the working directory:\n") 799 800 for fname in bogosity: 801 ui.warn(" %s\n" % fname) 802 803 if not yes_no(ui, "These files will be removed by recommit." 804 " Continue?", 805 False): 806 raise util.Abort("recommit would clobber files") 807 808 user = opts['user'] or ui.username() 809 comments = '\n'.join(active.comments()) 810 811 message = cmdutil.logmessage(opts) or ui.edit(comments, user) 812 if not message: 813 raise util.Abort('empty commit message') 814 815 bk = CdmBackup(ui, ws, backup_name(repo.root)) 816 if bk.need_backup(): 817 if yes_no(ui, 'Do you want to backup files first?', True): 818 bk.backup() 819 820 oldtags = repo.tags() 821 clearedtags = [(name, nd, repo.changelog.rev(nd), local) 822 for name, nd, local in active.tags()] 823 824 ws.squishdeltas(active, message, user=user) 825 finally: 826 lock.release() 827 wlock.release() 828 829 if clearedtags: 830 ui.write("Removed tags:\n") 831 for name, nd, rev, local in sorted(clearedtags, 832 key=lambda x: x[0].lower()): 833 ui.write(" %5s:%s:\t%s%s\n" % (rev, node.short(nd), 834 name, (local and ' (local)' or ''))) 835 836 for ntag, nnode in sorted(repo.tags().items(), 837 key=lambda x: x[0].lower()): 838 if ntag in oldtags and ntag != "tip": 839 if oldtags[ntag] != nnode: 840 ui.write("tag '%s' now refers to revision %d:%s\n" % 841 (ntag, repo.changelog.rev(nnode), 842 node.short(nnode))) 843 844 845def do_eval(cmd, files, root, changedir=True): 846 if not changedir: 847 os.chdir(root) 848 849 for path in sorted(files): 850 dirn, base = os.path.split(path) 851 852 if changedir: 853 os.chdir(os.path.join(root, dirn)) 854 855 os.putenv('workspace', root) 856 os.putenv('filepath', path) 857 os.putenv('dir', dirn) 858 os.putenv('file', base) 859 os.system(cmd) 860 861 862def cdm_eval(ui, repo, *command, **opts): 863 '''run cmd for each active file 864 865 cmd can refer to: 866 $file - active file basename. 867 $dir - active file dirname. 868 $filepath - path from workspace root to active file. 869 $workspace - full path to workspace root. 870 871 For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last 872 the 3 log entries for each active file, preceded by its directory.''' 873 874 act = wslist[repo].active(opts['parent']) 875 cmd = ' '.join(command) 876 files = [x.name for x in act if not x.is_removed()] 877 878 do_eval(cmd, files, repo.root, not opts['remain']) 879 880 881def cdm_apply(ui, repo, *command, **opts): 882 '''apply cmd to all active files 883 884 For example 'hg apply wc -l' outputs a line count of active files.''' 885 886 act = wslist[repo].active(opts['parent']) 887 888 if opts['remain']: 889 appnd = ' $filepath' 890 else: 891 appnd = ' $file' 892 893 cmd = ' '.join(command) + appnd 894 files = [x.name for x in act if not x.is_removed()] 895 896 do_eval(cmd, files, repo.root, not opts['remain']) 897 898 899def cdm_reparent_11(ui, repo, parent): 900 '''reparent your workspace 901 902 Updates the 'default' path.''' 903 904 filename = repo.join('hgrc') 905 906 p = ui.expandpath(parent) 907 cp = util.configparser() 908 909 try: 910 cp.read(filename) 911 except ConfigParser.ParsingError, inst: 912 raise util.Abort('failed to parse %s\n%s' % (filename, inst)) 913 914 try: 915 fh = open(filename, 'w') 916 except IOError, e: 917 raise util.Abort('Failed to open workspace configuration: %s' % e) 918 919 if not cp.has_section('paths'): 920 cp.add_section('paths') 921 cp.set('paths', 'default', p) 922 cp.write(fh) 923 fh.close() 924 925 926def cdm_reparent_13(ui, repo, parent): 927 '''reparent your workspace 928 929 Updates the 'default' path in this repository's .hg/hgrc.''' 930 931 def append_new_parent(parent): 932 fp = None 933 try: 934 fp = repo.opener('hgrc', 'a', atomictemp=True) 935 if fp.tell() != 0: 936 fp.write('\n') 937 fp.write('[paths]\n' 938 'default = %s\n\n' % parent) 939 fp.rename() 940 finally: 941 if fp and not fp.closed: 942 fp.close() 943 944 def update_parent(path, line, parent): 945 line = line - 1 # The line number we're passed will be 1-based 946 fp = None 947 948 try: 949 fp = open(path) 950 data = fp.readlines() 951 finally: 952 if fp and not fp.closed: 953 fp.close() 954 955 # 956 # line will be the last line of any continued block, go back 957 # to the first removing the continuation as we go. 958 # 959 while data[line][0].isspace(): 960 data.pop(line) 961 line -= 1 962 963 assert data[line].startswith('default') 964 965 data[line] = "default = %s\n" % parent 966 if data[-1] != '\n': 967 data.append('\n') 968 969 try: 970 fp = util.atomictempfile(path, 'w', 0644) 971 fp.writelines(data) 972 fp.rename() 973 finally: 974 if fp and not fp.closed: 975 fp.close() 976 977 from mercurial import config 978 parent = ui.expandpath(parent) 979 980 if not os.path.exists(repo.join('hgrc')): 981 append_new_parent(parent) 982 return 983 984 cfg = config.config() 985 cfg.read(repo.join('hgrc')) 986 source = cfg.source('paths', 'default') 987 988 if not source: 989 append_new_parent(parent) 990 return 991 else: 992 path, target = source.rsplit(':', 1) 993 994 if path != repo.join('hgrc'): 995 raise util.Abort("Cannot edit path specification not in repo hgrc\n" 996 "default path is from: %s" % source) 997 998 update_parent(path, int(target), parent) 999 1000if Version.at_least("1.3"): 1001 cdm_reparent = cdm_reparent_13 1002else: 1003 cdm_reparent = cdm_reparent_11 1004 1005 1006def backup_name(fullpath): 1007 '''Create a backup directory name based on the specified path. 1008 1009 In most cases this is the basename of the path specified, but 1010 certain cases are handled specially to create meaningful names''' 1011 1012 special = ['usr/closed'] 1013 1014 fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep) 1015 1016 # 1017 # If a path is 'special', we append the basename of the path to 1018 # the path element preceding the constant, special, part. 1019 # 1020 # Such that for instance: 1021 # /foo/bar/onnv-fixes/usr/closed 1022 # has a backup name of: 1023 # onnv-fixes-closed 1024 # 1025 for elt in special: 1026 elt = elt.split(os.path.sep) 1027 pathpos = len(elt) 1028 1029 if fullpath[-pathpos:] == elt: 1030 return "%s-%s" % (fullpath[-pathpos - 1], elt[-1]) 1031 else: 1032 return fullpath[-1] 1033 1034 1035def cdm_backup(ui, repo, if_newer=False): 1036 '''make backup copies of all workspace changes 1037 1038 Backups will be stored in ~/cdm.backup/<basename of workspace>.''' 1039 1040 name = backup_name(repo.root) 1041 bk = CdmBackup(ui, wslist[repo], name) 1042 1043 wlock = repo.wlock() 1044 lock = repo.lock() 1045 1046 try: 1047 if if_newer and not bk.need_backup(): 1048 ui.status('backup is up-to-date\n') 1049 else: 1050 bk.backup() 1051 finally: 1052 lock.release() 1053 wlock.release() 1054 1055 1056def cdm_restore(ui, repo, backup, **opts): 1057 '''restore workspace from backup 1058 1059 Restores a workspace from the specified backup directory and generation 1060 (which defaults to the latest).''' 1061 1062 if not os.getcwd().startswith(repo.root): 1063 raise util.Abort('restore is not safe to run with -R') 1064 1065 abort_if_dirty(wslist[repo]) 1066 1067 if opts['generation']: 1068 gen = int(opts['generation']) 1069 else: 1070 gen = None 1071 1072 if os.path.exists(backup): 1073 backup = os.path.abspath(backup) 1074 1075 wlock = repo.wlock() 1076 lock = repo.lock() 1077 1078 try: 1079 bk = CdmBackup(ui, wslist[repo], backup) 1080 bk.restore(gen) 1081 finally: 1082 lock.release() 1083 wlock.release() 1084 1085 1086def cdm_webrev(ui, repo, **opts): 1087 '''generate webrev and optionally upload it 1088 1089 This command passes all arguments to webrev script''' 1090 1091 webrev_args = "" 1092 for key in opts.keys(): 1093 if opts[key]: 1094 if type(opts[key]) == type(True): 1095 webrev_args += '-' + key + ' ' 1096 else: 1097 webrev_args += '-' + key + ' ' + opts[key] + ' ' 1098 1099 retval = os.system('webrev ' + webrev_args) 1100 if retval != 0: 1101 return retval - 255 1102 1103 return 0 1104 1105 1106cmdtable = { 1107 'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'), 1108 ('r', 'remain', None, 'do not change directories')], 1109 'hg apply [-p PARENT] [-r] command...'), 1110 'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')], 1111 'hg arcs [-p PARENT]'), 1112 '^backup|bu': (cdm_backup, [('t', 'if-newer', None, 1113 'only backup if workspace files are newer')], 1114 'hg backup [-t]'), 1115 'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')], 1116 'hg branchchk [-p PARENT]'), 1117 'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')], 1118 'hg bugs [-p PARENT]'), 1119 'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')], 1120 'hg cddlchk [-p PARENT]'), 1121 'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'), 1122 ('N', 'nocheck', None, 1123 'do not compare comments with databases')], 1124 'hg comchk [-p PARENT]'), 1125 'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')], 1126 'hg comments [-p PARENT]'), 1127 'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')], 1128 'hg copyright [-p PARENT]'), 1129 'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')], 1130 'hg cstyle [-p PARENT]'), 1131 'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'), 1132 ('r', 'remain', None, 'do not change directories')], 1133 'hg eval [-p PARENT] [-r] command...'), 1134 'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')], 1135 'hg hdrchk [-p PARENT]'), 1136 'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')], 1137 'hg jstyle [-p PARENT]'), 1138 'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')], 1139 'hg keywords [-p PARENT]'), 1140 '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'), 1141 ('r', 'removed', None, 'show removed files'), 1142 ('a', 'added', None, 'show added files'), 1143 ('m', 'modified', None, 'show modified files')], 1144 'hg list [-amrRu] [-p PARENT]'), 1145 'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')], 1146 'hg mapfilechk [-p PARENT]'), 1147 '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')], 1148 'hg nits [-p PARENT]'), 1149 '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'), 1150 ('N', 'nocheck', None, 'skip RTI check')], 1151 'hg pbchk [-N] [-p PARENT]'), 1152 'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')], 1153 'hg permchk [-p PARENT]'), 1154 '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'), 1155 ('a', 'text', None, 'treat all files as text'), 1156 ('g', 'git', None, 'use extended git diff format'), 1157 ('w', 'ignore-all-space', None, 1158 'ignore white space when comparing lines'), 1159 ('b', 'ignore-space-change', None, 1160 'ignore changes in the amount of white space'), 1161 ('B', 'ignore-blank-lines', None, 1162 'ignore changes whos lines are all blank'), 1163 ('U', 'unified', 3, 1164 'number of lines of context to show'), 1165 ('I', 'include', [], 1166 'include names matching the given patterns'), 1167 ('X', 'exclude', [], 1168 'exclude names matching the given patterns')], 1169 'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'), 1170 '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'), 1171 ('f', 'force', None, 'force operation'), 1172 ('m', 'message', '', 1173 'use <text> as commit message'), 1174 ('l', 'logfile', '', 1175 'read commit message from file'), 1176 ('u', 'user', '', 1177 'record user as committer')], 1178 'hg recommit [-f] [-p PARENT]'), 1179 'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')], 1180 'hg renamed [-p PARENT]'), 1181 'reparent': (cdm_reparent, [], 'hg reparent PARENT'), 1182 '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')], 1183 'hg restore [-g GENERATION] BACKUP'), 1184 'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'), 1185 ('N', 'nocheck', None, 'skip RTI check')], 1186 'hg rtichk [-N] [-p PARENT]'), 1187 'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')], 1188 'hg tagchk [-p PARENT]'), 1189 'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'), 1190 ('D', 'D', '', 'delete remote webrev'), 1191 ('I', 'I', '', 'ITS configuration file'), 1192 ('i', 'i', '', 'include file'), 1193 ('l', 'l', '', 'extract file list from putback -n'), 1194 ('N', 'N', None, 'supress comments'), 1195 ('n', 'n', None, 'do not generate webrev'), 1196 ('O', 'O', None, 'OpenSolaris mode'), 1197 ('o', 'o', '', 'output directory'), 1198 ('p', 'p', '', 'use specified parent'), 1199 ('t', 't', '', 'upload target'), 1200 ('U', 'U', None, 'upload the webrev'), 1201 ('w', 'w', '', 'use wx active file')], 1202 'hg webrev [WEBREV_OPTIONS]'), 1203} 1204