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, re, 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 # We are interested in examining any file that has the following 348 # in its final path segment: 349 # - Contains the word 'mapfile' 350 # - Begins with 'map.' 351 # - Ends with '.map' 352 # We don't want to match unless these things occur in final path segment 353 # because directory names with these strings don't indicate a mapfile. 354 # We also ignore files with suffixes that tell us that the files 355 # are not mapfiles. 356 MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$', 357 re.IGNORECASE) 358 NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE) 359 360 ui.write('Mapfile comment check:\n') 361 362 for f, e in filelist: 363 if e and e.is_removed(): 364 continue 365 elif (not MapfileRE.match(f)) or NotMapSuffixRE.match(f): 366 continue 367 elif (e or opts.get('honour_nots')) and exclude(f): 368 ui.status('Skipping %s...\n' % f) 369 continue 370 371 fh = open(f, 'r') 372 ret |= Mapfile.mapfilechk(fh, output=ui) 373 fh.close() 374 return ret 375 376 377def cdm_copyright(ui, repo, *args, **opts): 378 '''check active files for valid copyrights 379 380 Check that all active files have a valid copyright containing the 381 current year (and *only* the current year). 382 See http://www.opensolaris.org/os/project/muskoka/on_dev/golden_rules.txt 383 for more info.''' 384 385 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 386 exclude = not_check(repo, 'copyright') 387 ret = 0 388 389 ui.write('Copyright check:\n') 390 391 for f, e in filelist: 392 if e and e.is_removed(): 393 continue 394 elif (e or opts.get('honour_nots')) and exclude(f): 395 ui.status('Skipping %s...\n' % f) 396 continue 397 398 fh = open(f, 'r') 399 ret |= Copyright.copyright(fh, output=ui) 400 fh.close() 401 return ret 402 403 404def cdm_hdrchk(ui, repo, *args, **opts): 405 '''check active header files conform to O/N rules''' 406 407 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 408 exclude = not_check(repo, 'hdrchk') 409 ret = 0 410 411 ui.write('Header format check:\n') 412 413 for f, e in filelist: 414 if e and e.is_removed(): 415 continue 416 elif not f.endswith('.h'): 417 continue 418 elif (e or opts.get('honour_nots')) and exclude(f): 419 ui.status('Skipping %s...\n' % f) 420 continue 421 422 fh = open(f, 'r') 423 ret |= HdrChk.hdrchk(fh, lenient=True, output=ui) 424 fh.close() 425 return ret 426 427 428def cdm_cstyle(ui, repo, *args, **opts): 429 '''check active C source files conform to the C Style Guide 430 431 See http://opensolaris.org/os/community/documentation/getting_started_docs/cstyle.ms.pdf''' 432 433 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 434 exclude = not_check(repo, 'cstyle') 435 ret = 0 436 437 ui.write('C style check:\n') 438 439 for f, e in filelist: 440 if e and e.is_removed(): 441 continue 442 elif not (f.endswith('.c') or f.endswith('.h')): 443 continue 444 elif (e or opts.get('honour_nots')) and exclude(f): 445 ui.status('Skipping %s...\n' % f) 446 continue 447 448 fh = open(f, 'r') 449 ret |= CStyle.cstyle(fh, output=ui, 450 picky=True, check_posix_types=True, 451 check_continuation=True) 452 fh.close() 453 return ret 454 455 456def cdm_jstyle(ui, repo, *args, **opts): 457 'check active Java source files for common stylistic errors' 458 459 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 460 exclude = not_check(repo, 'jstyle') 461 ret = 0 462 463 ui.write('Java style check:\n') 464 465 for f, e in filelist: 466 if e and e.is_removed(): 467 continue 468 elif not f.endswith('.java'): 469 continue 470 elif (e or opts.get('honour_nots')) and exclude(f): 471 ui.status('Skipping %s...\n' % f) 472 continue 473 474 fh = open(f, 'r') 475 ret |= JStyle.jstyle(fh, output=ui, picky=True) 476 fh.close() 477 return ret 478 479 480def cdm_permchk(ui, repo, *args, **opts): 481 '''check active files permission - warn +x (execute) mode''' 482 483 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 484 exclude = not_check(repo, 'permchk') 485 exeFiles = [] 486 487 ui.write('File permission check:\n') 488 489 for f, e in filelist: 490 if e and e.is_removed(): 491 continue 492 elif (e or opts.get('honour_nots')) and exclude(f): 493 ui.status('Skipping %s...\n' % f) 494 continue 495 496 mode = stat.S_IMODE(os.stat(f)[stat.ST_MODE]) 497 if mode & stat.S_IEXEC: 498 exeFiles.append(f) 499 500 if len(exeFiles) > 0: 501 ui.write('Warning: the following active file(s) have executable mode ' 502 '(+x) permission set,\nremove unless intentional:\n') 503 for fname in exeFiles: 504 ui.write(" %s\n" % fname) 505 506 return len(exeFiles) > 0 507 508 509def cdm_tagchk(ui, repo, **opts): 510 '''check if .hgtags is active and issue warning 511 512 Tag sharing among repositories is restricted to gatekeepers''' 513 514 active = wslist[repo].active(opts.get('parent')) 515 516 ui.write('Checking for new tags:\n') 517 518 if ".hgtags" in active: 519 tfile = wslist[repo].filepath('.hgtags') 520 ptip = active.parenttip.rev() 521 522 ui.write('Warning: Workspace contains new non-local tags.\n' 523 'Only gatekeepers should add or modify such tags.\n' 524 'Use the following commands to revert these changes:\n' 525 ' hg revert -r%d %s\n' 526 ' hg commit %s\n' 527 'You should also recommit before integration\n' % 528 (ptip, tfile, tfile)) 529 530 return 1 531 532 return 0 533 534 535def cdm_branchchk(ui, repo, **opts): 536 '''check if multiple heads (or branches) are present, or if 537 branch changes are made''' 538 539 ui.write('Checking for multiple heads (or branches):\n') 540 541 heads = set(repo.heads()) 542 parents = set([x.node() for x in wslist[repo].workingctx().parents()]) 543 544 # 545 # We care if there's more than one head, and those heads aren't 546 # identical to the dirstate parents (if they are identical, it's 547 # an uncommitted merge which mergechk will catch, no need to 548 # complain twice). 549 # 550 if len(heads) > 1 and heads != parents: 551 ui.write('Workspace has multiple heads (or branches):\n') 552 for head in [repo.changectx(head) for head in heads]: 553 ui.write(" %d:%s\t%s\n" % 554 (head.rev(), str(head), head.description().splitlines()[0])) 555 ui.write('You must merge and recommit.\n') 556 return 1 557 558 ui.write('\nChecking for branch changes:\n') 559 560 if repo.dirstate.branch() != 'default': 561 ui.write("Warning: Workspace tip has named branch: '%s'\n" 562 "Only gatekeepers should push new branches.\n" 563 "Use the following commands to restore the branch name:\n" 564 " hg branch [-f] default\n" 565 " hg commit\n" 566 "You should also recommit before integration\n" % 567 (repo.dirstate.branch())) 568 return 1 569 570 branches = repo.branchtags().keys() 571 if len(branches) > 1: 572 ui.write('Warning: Workspace has named branches:\n') 573 for t in branches: 574 if t == 'default': 575 continue 576 ui.write("\t%s\n" % t) 577 578 ui.write("Only gatekeepers should push new branches.\n" 579 "Use the following commands to remove extraneous branches.\n" 580 " hg branch [-f] default\n" 581 " hg commit" 582 "You should also recommit before integration\n") 583 return 1 584 585 return 0 586 587 588def cdm_rtichk(ui, repo, **opts): 589 '''check active bug/RFEs for approved RTIs 590 591 Only works on SWAN.''' 592 593 if opts.get('nocheck') or os.path.exists(repo.join('cdm/rtichk.NOT')): 594 ui.status('Skipping RTI checks...\n') 595 return 0 596 597 if not onSWAN(): 598 ui.write('RTI checks only work on SWAN, skipping...\n') 599 return 0 600 601 parent = wslist[repo].parent(opts.get('parent')) 602 active = wslist[repo].active(parent) 603 604 ui.write('RTI check:\n') 605 606 bugs = [] 607 608 for com in active.comments(): 609 match = Comments.isBug(com) 610 if match and match.group(1) not in bugs: 611 bugs.append(match.group(1)) 612 613 # RTI normalizes the gate path for us 614 return int(not Rti.rti(bugs, gatePath=parent, output=ui)) 615 616 617def cdm_keywords(ui, repo, *args, **opts): 618 '''check source files do not contain SCCS keywords''' 619 620 filelist = buildfilelist(wslist[repo], opts.get('parent'), args) 621 exclude = not_check(repo, 'keywords') 622 ret = 0 623 624 ui.write('Keywords check:\n') 625 626 for f, e in filelist: 627 if e and e.is_removed(): 628 continue 629 elif (e or opts.get('honour_nots')) and exclude(f): 630 ui.status('Skipping %s...\n' % f) 631 continue 632 633 fh = open(f, 'r') 634 ret |= Keywords.keywords(fh, output=ui) 635 fh.close() 636 return ret 637 638 639# 640# NB: 641# There's no reason to hook this up as an invokable command, since 642# we have 'hg status', but it must accept the same arguments. 643# 644def cdm_outchk(ui, repo, **opts): 645 '''Warn the user if they have uncommitted changes''' 646 647 ui.write('Checking for uncommitted changes:\n') 648 649 st = wslist[repo].modified() 650 if st: 651 ui.write('Warning: the following files have uncommitted changes:\n') 652 for elt in st: 653 ui.write(' %s\n' % elt) 654 return 1 655 return 0 656 657 658def cdm_mergechk(ui, repo, **opts): 659 '''Warn the user if their workspace contains merges''' 660 661 active = wslist[repo].active(opts.get('parent')) 662 663 ui.write('Checking for merges:\n') 664 665 merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1], 666 active.revs) 667 668 if merges: 669 ui.write('Workspace contains the following merges:\n') 670 for rev in merges: 671 desc = rev.description().splitlines() 672 ui.write(' %s:%s\t%s\n' % 673 (rev.rev() or "working", str(rev), 674 desc and desc[0] or "*** uncommitted change ***")) 675 return 1 676 return 0 677 678 679def run_checks(ws, cmds, *args, **opts): 680 '''Run CMDS (with OPTS) over active files in WS''' 681 682 ret = 0 683 684 for cmd in cmds: 685 name = cmd.func_name.split('_')[1] 686 if not ws.ui.configbool('cdm', name, True): 687 ws.ui.status('Skipping %s check...\n' % name) 688 else: 689 ws.ui.pushbuffer() 690 result = cmd(ws.ui, ws.repo, honour_nots=True, *args, **opts) 691 output = ws.ui.popbuffer() 692 693 ret |= result 694 695 if not ws.ui.quiet or result != 0: 696 ws.ui.write(output, '\n') 697 return ret 698 699 700def cdm_nits(ui, repo, *args, **opts): 701 '''check for stylistic nits in active files 702 703 Run cddlchk, copyright, cstyle, hdrchk, jstyle, mapfilechk, 704 permchk, and keywords checks.''' 705 706 cmds = [cdm_cddlchk, 707 cdm_copyright, 708 cdm_cstyle, 709 cdm_hdrchk, 710 cdm_jstyle, 711 cdm_mapfilechk, 712 cdm_permchk, 713 cdm_keywords] 714 715 return run_checks(wslist[repo], cmds, *args, **opts) 716 717 718def cdm_pbchk(ui, repo, **opts): 719 '''pre-putback check all active files 720 721 Run cddlchk, comchk, copyright, cstyle, hdrchk, jstyle, mapfilechk, 722 permchk, tagchk, branchchk, keywords and rtichk checks. Additionally, 723 warn about uncommitted changes.''' 724 725 # 726 # The current ordering of these is that the commands from cdm_nits 727 # run first in the same order as they would in cdm_nits. Then the 728 # pbchk specifics run 729 # 730 cmds = [cdm_cddlchk, 731 cdm_copyright, 732 cdm_cstyle, 733 cdm_hdrchk, 734 cdm_jstyle, 735 cdm_mapfilechk, 736 cdm_permchk, 737 cdm_keywords, 738 cdm_comchk, 739 cdm_tagchk, 740 cdm_branchchk, 741 cdm_rtichk, 742 cdm_outchk, 743 cdm_mergechk] 744 745 return run_checks(wslist[repo], cmds, **opts) 746 747 748def cdm_recommit(ui, repo, **opts): 749 '''replace outgoing changesets with a single equivalent changeset 750 751 Replace all outgoing changesets with a single changeset containing 752 equivalent changes. This removes uninteresting changesets created 753 during development that would only serve as noise in the gate. 754 755 Any changed file that is now identical in content to that in the 756 parent workspace (whether identical in history or otherwise) will 757 not be included in the new changeset. Any merges information will 758 also be removed. 759 760 If no files are changed in comparison to the parent workspace, the 761 outgoing changesets will be removed, but no new changeset created. 762 763 recommit will refuse to run if the workspace contains more than 764 one outgoing head, even if those heads are on the same branch. To 765 recommit with only one branch containing outgoing changesets, your 766 workspace must be on that branch and at that branch head. 767 768 recommit will prompt you to take a backup if your workspace has 769 been changed since the last backup was taken. In almost all 770 cases, you should allow it to take one (the default). 771 772 recommit cannot be run if the workspace contains any uncommitted 773 changes, applied Mq patches, or has multiple outgoing heads (or 774 branches). 775 ''' 776 777 ws = wslist[repo] 778 779 if not os.getcwd().startswith(repo.root): 780 raise util.Abort('recommit is not safe to run with -R') 781 782 abort_if_dirty(ws) 783 784 wlock = repo.wlock() 785 lock = repo.lock() 786 787 try: 788 parent = ws.parent(opts['parent']) 789 between = repo.changelog.nodesbetween(ws.findoutgoing(parent))[2] 790 heads = set(between) & set(repo.heads()) 791 792 if len(heads) > 1: 793 ui.warn('Workspace has multiple outgoing heads (or branches):\n') 794 for head in sorted(map(repo.changelog.rev, heads), reverse=True): 795 ui.warn('\t%d\n' % head) 796 raise util.Abort('you must merge before recommitting') 797 798 active = ws.active(parent) 799 800 if filter(lambda b: len(b.parents()) > 1, active.bases()): 801 raise util.Abort('Cannot recommit a merge of two non-outgoing ' 802 'changesets') 803 804 if len(active.revs) <= 0: 805 raise util.Abort("no changes to recommit") 806 807 if len(active.files()) <= 0: 808 ui.warn("Recommitting %d active changesets, but no active files\n" % 809 len(active.revs)) 810 811 # 812 # During the course of a recommit, any file bearing a name 813 # matching the source name of any renamed file will be 814 # clobbered by the operation. 815 # 816 # As such, we ask the user before proceeding. 817 # 818 bogosity = [f.parentname for f in active if f.is_renamed() and 819 os.path.exists(repo.wjoin(f.parentname))] 820 if bogosity: 821 ui.warn("The following file names are the original name of a " 822 "rename and also present\n" 823 "in the working directory:\n") 824 825 for fname in bogosity: 826 ui.warn(" %s\n" % fname) 827 828 if not yes_no(ui, "These files will be removed by recommit." 829 " Continue?", 830 False): 831 raise util.Abort("recommit would clobber files") 832 833 user = opts['user'] or ui.username() 834 comments = '\n'.join(active.comments()) 835 836 message = cmdutil.logmessage(opts) or ui.edit(comments, user) 837 if not message: 838 raise util.Abort('empty commit message') 839 840 bk = CdmBackup(ui, ws, backup_name(repo.root)) 841 if bk.need_backup(): 842 if yes_no(ui, 'Do you want to backup files first?', True): 843 bk.backup() 844 845 oldtags = repo.tags() 846 clearedtags = [(name, nd, repo.changelog.rev(nd), local) 847 for name, nd, local in active.tags()] 848 849 ws.squishdeltas(active, message, user=user) 850 finally: 851 lock.release() 852 wlock.release() 853 854 if clearedtags: 855 ui.write("Removed tags:\n") 856 for name, nd, rev, local in sorted(clearedtags, 857 key=lambda x: x[0].lower()): 858 ui.write(" %5s:%s:\t%s%s\n" % (rev, node.short(nd), 859 name, (local and ' (local)' or ''))) 860 861 for ntag, nnode in sorted(repo.tags().items(), 862 key=lambda x: x[0].lower()): 863 if ntag in oldtags and ntag != "tip": 864 if oldtags[ntag] != nnode: 865 ui.write("tag '%s' now refers to revision %d:%s\n" % 866 (ntag, repo.changelog.rev(nnode), 867 node.short(nnode))) 868 869 870def do_eval(cmd, files, root, changedir=True): 871 if not changedir: 872 os.chdir(root) 873 874 for path in sorted(files): 875 dirn, base = os.path.split(path) 876 877 if changedir: 878 os.chdir(os.path.join(root, dirn)) 879 880 os.putenv('workspace', root) 881 os.putenv('filepath', path) 882 os.putenv('dir', dirn) 883 os.putenv('file', base) 884 os.system(cmd) 885 886 887def cdm_eval(ui, repo, *command, **opts): 888 '''run cmd for each active file 889 890 cmd can refer to: 891 $file - active file basename. 892 $dir - active file dirname. 893 $filepath - path from workspace root to active file. 894 $workspace - full path to workspace root. 895 896 For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last 897 the 3 log entries for each active file, preceded by its directory.''' 898 899 act = wslist[repo].active(opts['parent']) 900 cmd = ' '.join(command) 901 files = [x.name for x in act if not x.is_removed()] 902 903 do_eval(cmd, files, repo.root, not opts['remain']) 904 905 906def cdm_apply(ui, repo, *command, **opts): 907 '''apply cmd to all active files 908 909 For example 'hg apply wc -l' outputs a line count of active files.''' 910 911 act = wslist[repo].active(opts['parent']) 912 913 if opts['remain']: 914 appnd = ' $filepath' 915 else: 916 appnd = ' $file' 917 918 cmd = ' '.join(command) + appnd 919 files = [x.name for x in act if not x.is_removed()] 920 921 do_eval(cmd, files, repo.root, not opts['remain']) 922 923 924def cdm_reparent_11(ui, repo, parent): 925 '''reparent your workspace 926 927 Updates the 'default' path.''' 928 929 filename = repo.join('hgrc') 930 931 p = ui.expandpath(parent) 932 cp = util.configparser() 933 934 try: 935 cp.read(filename) 936 except ConfigParser.ParsingError, inst: 937 raise util.Abort('failed to parse %s\n%s' % (filename, inst)) 938 939 try: 940 fh = open(filename, 'w') 941 except IOError, e: 942 raise util.Abort('Failed to open workspace configuration: %s' % e) 943 944 if not cp.has_section('paths'): 945 cp.add_section('paths') 946 cp.set('paths', 'default', p) 947 cp.write(fh) 948 fh.close() 949 950 951def cdm_reparent_13(ui, repo, parent): 952 '''reparent your workspace 953 954 Updates the 'default' path in this repository's .hg/hgrc.''' 955 956 def append_new_parent(parent): 957 fp = None 958 try: 959 fp = repo.opener('hgrc', 'a', atomictemp=True) 960 if fp.tell() != 0: 961 fp.write('\n') 962 fp.write('[paths]\n' 963 'default = %s\n\n' % parent) 964 fp.rename() 965 finally: 966 if fp and not fp.closed: 967 fp.close() 968 969 def update_parent(path, line, parent): 970 line = line - 1 # The line number we're passed will be 1-based 971 fp = None 972 973 try: 974 fp = open(path) 975 data = fp.readlines() 976 finally: 977 if fp and not fp.closed: 978 fp.close() 979 980 # 981 # line will be the last line of any continued block, go back 982 # to the first removing the continuation as we go. 983 # 984 while data[line][0].isspace(): 985 data.pop(line) 986 line -= 1 987 988 assert data[line].startswith('default') 989 990 data[line] = "default = %s\n" % parent 991 if data[-1] != '\n': 992 data.append('\n') 993 994 try: 995 fp = util.atomictempfile(path, 'w', 0644) 996 fp.writelines(data) 997 fp.rename() 998 finally: 999 if fp and not fp.closed: 1000 fp.close() 1001 1002 from mercurial import config 1003 parent = ui.expandpath(parent) 1004 1005 if not os.path.exists(repo.join('hgrc')): 1006 append_new_parent(parent) 1007 return 1008 1009 cfg = config.config() 1010 cfg.read(repo.join('hgrc')) 1011 source = cfg.source('paths', 'default') 1012 1013 if not source: 1014 append_new_parent(parent) 1015 return 1016 else: 1017 path, target = source.rsplit(':', 1) 1018 1019 if path != repo.join('hgrc'): 1020 raise util.Abort("Cannot edit path specification not in repo hgrc\n" 1021 "default path is from: %s" % source) 1022 1023 update_parent(path, int(target), parent) 1024 1025if Version.at_least("1.3"): 1026 cdm_reparent = cdm_reparent_13 1027else: 1028 cdm_reparent = cdm_reparent_11 1029 1030 1031def backup_name(fullpath): 1032 '''Create a backup directory name based on the specified path. 1033 1034 In most cases this is the basename of the path specified, but 1035 certain cases are handled specially to create meaningful names''' 1036 1037 special = ['usr/closed'] 1038 1039 fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep) 1040 1041 # 1042 # If a path is 'special', we append the basename of the path to 1043 # the path element preceding the constant, special, part. 1044 # 1045 # Such that for instance: 1046 # /foo/bar/onnv-fixes/usr/closed 1047 # has a backup name of: 1048 # onnv-fixes-closed 1049 # 1050 for elt in special: 1051 elt = elt.split(os.path.sep) 1052 pathpos = len(elt) 1053 1054 if fullpath[-pathpos:] == elt: 1055 return "%s-%s" % (fullpath[-pathpos - 1], elt[-1]) 1056 else: 1057 return fullpath[-1] 1058 1059 1060def cdm_backup(ui, repo, if_newer=False): 1061 '''make backup copies of all workspace changes 1062 1063 Backups will be stored in ~/cdm.backup/<basename of workspace>.''' 1064 1065 name = backup_name(repo.root) 1066 bk = CdmBackup(ui, wslist[repo], name) 1067 1068 wlock = repo.wlock() 1069 lock = repo.lock() 1070 1071 try: 1072 if if_newer and not bk.need_backup(): 1073 ui.status('backup is up-to-date\n') 1074 else: 1075 bk.backup() 1076 finally: 1077 lock.release() 1078 wlock.release() 1079 1080 1081def cdm_restore(ui, repo, backup, **opts): 1082 '''restore workspace from backup 1083 1084 Restores a workspace from the specified backup directory and generation 1085 (which defaults to the latest).''' 1086 1087 if not os.getcwd().startswith(repo.root): 1088 raise util.Abort('restore is not safe to run with -R') 1089 1090 abort_if_dirty(wslist[repo]) 1091 1092 if opts['generation']: 1093 gen = int(opts['generation']) 1094 else: 1095 gen = None 1096 1097 if os.path.exists(backup): 1098 backup = os.path.abspath(backup) 1099 1100 wlock = repo.wlock() 1101 lock = repo.lock() 1102 1103 try: 1104 bk = CdmBackup(ui, wslist[repo], backup) 1105 bk.restore(gen) 1106 finally: 1107 lock.release() 1108 wlock.release() 1109 1110 1111def cdm_webrev(ui, repo, **opts): 1112 '''generate webrev and optionally upload it 1113 1114 This command passes all arguments to webrev script''' 1115 1116 webrev_args = "" 1117 for key in opts.keys(): 1118 if opts[key]: 1119 if type(opts[key]) == type(True): 1120 webrev_args += '-' + key + ' ' 1121 else: 1122 webrev_args += '-' + key + ' ' + opts[key] + ' ' 1123 1124 retval = os.system('webrev ' + webrev_args) 1125 if retval != 0: 1126 return retval - 255 1127 1128 return 0 1129 1130 1131cmdtable = { 1132 'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'), 1133 ('r', 'remain', None, 'do not change directories')], 1134 'hg apply [-p PARENT] [-r] command...'), 1135 'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')], 1136 'hg arcs [-p PARENT]'), 1137 '^backup|bu': (cdm_backup, [('t', 'if-newer', None, 1138 'only backup if workspace files are newer')], 1139 'hg backup [-t]'), 1140 'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')], 1141 'hg branchchk [-p PARENT]'), 1142 'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')], 1143 'hg bugs [-p PARENT]'), 1144 'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')], 1145 'hg cddlchk [-p PARENT]'), 1146 'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'), 1147 ('N', 'nocheck', None, 1148 'do not compare comments with databases')], 1149 'hg comchk [-p PARENT]'), 1150 'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')], 1151 'hg comments [-p PARENT]'), 1152 'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')], 1153 'hg copyright [-p PARENT]'), 1154 'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')], 1155 'hg cstyle [-p PARENT]'), 1156 'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'), 1157 ('r', 'remain', None, 'do not change directories')], 1158 'hg eval [-p PARENT] [-r] command...'), 1159 'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')], 1160 'hg hdrchk [-p PARENT]'), 1161 'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')], 1162 'hg jstyle [-p PARENT]'), 1163 'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')], 1164 'hg keywords [-p PARENT]'), 1165 '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'), 1166 ('r', 'removed', None, 'show removed files'), 1167 ('a', 'added', None, 'show added files'), 1168 ('m', 'modified', None, 'show modified files')], 1169 'hg list [-amrRu] [-p PARENT]'), 1170 'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')], 1171 'hg mapfilechk [-p PARENT]'), 1172 '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')], 1173 'hg nits [-p PARENT]'), 1174 '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'), 1175 ('N', 'nocheck', None, 'skip RTI check')], 1176 'hg pbchk [-N] [-p PARENT]'), 1177 'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')], 1178 'hg permchk [-p PARENT]'), 1179 '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'), 1180 ('a', 'text', None, 'treat all files as text'), 1181 ('g', 'git', None, 'use extended git diff format'), 1182 ('w', 'ignore-all-space', None, 1183 'ignore white space when comparing lines'), 1184 ('b', 'ignore-space-change', None, 1185 'ignore changes in the amount of white space'), 1186 ('B', 'ignore-blank-lines', None, 1187 'ignore changes whos lines are all blank'), 1188 ('U', 'unified', 3, 1189 'number of lines of context to show'), 1190 ('I', 'include', [], 1191 'include names matching the given patterns'), 1192 ('X', 'exclude', [], 1193 'exclude names matching the given patterns')], 1194 'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'), 1195 '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'), 1196 ('f', 'force', None, 'force operation'), 1197 ('m', 'message', '', 1198 'use <text> as commit message'), 1199 ('l', 'logfile', '', 1200 'read commit message from file'), 1201 ('u', 'user', '', 1202 'record user as committer')], 1203 'hg recommit [-f] [-p PARENT]'), 1204 'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')], 1205 'hg renamed [-p PARENT]'), 1206 'reparent': (cdm_reparent, [], 'hg reparent PARENT'), 1207 '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')], 1208 'hg restore [-g GENERATION] BACKUP'), 1209 'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'), 1210 ('N', 'nocheck', None, 'skip RTI check')], 1211 'hg rtichk [-N] [-p PARENT]'), 1212 'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')], 1213 'hg tagchk [-p PARENT]'), 1214 'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'), 1215 ('D', 'D', '', 'delete remote webrev'), 1216 ('I', 'I', '', 'ITS configuration file'), 1217 ('i', 'i', '', 'include file'), 1218 ('l', 'l', '', 'extract file list from putback -n'), 1219 ('N', 'N', None, 'supress comments'), 1220 ('n', 'n', None, 'do not generate webrev'), 1221 ('O', 'O', None, 'OpenSolaris mode'), 1222 ('o', 'o', '', 'output directory'), 1223 ('p', 'p', '', 'use specified parent'), 1224 ('t', 't', '', 'upload target'), 1225 ('U', 'U', None, 'upload the webrev'), 1226 ('w', 'w', '', 'use wx active file')], 1227 'hg webrev [WEBREV_OPTIONS]'), 1228} 1229