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