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