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