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