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