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