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