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