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