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