xref: /titanic_41/usr/src/tools/onbld/hgext/cdm.py (revision a31148363f598def767ac48c5d82e1572e44b935)
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 2010 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    '''replace outgoing changesets with a single equivalent changeset
725
726    Replace all outgoing changesets with a single changeset containing
727    equivalent changes.  This removes uninteresting changesets created
728    during development that would only serve as noise in the gate.
729
730    Any changed file that is now identical in content to that in the
731    parent workspace (whether identical in history or otherwise) will
732    not be included in the new changeset.  Any merges information will
733    also be removed.
734
735    If no files are changed in comparison to the parent workspace, the
736    outgoing changesets will be removed, but no new changeset created.
737
738    recommit will refuse to run if the workspace contains more than
739    one outgoing head, even if those heads are on the same branch.  To
740    recommit with only one branch containing outgoing changesets, your
741    workspace must be on that branch and at that branch head.
742
743    recommit will prompt you to take a backup if your workspace has
744    been changed since the last backup was taken.  In almost all
745    cases, you should allow it to take one (the default).
746
747    recommit cannot be run if the workspace contains any uncommitted
748    changes, applied Mq patches, or has multiple outgoing heads (or
749    branches).
750    '''
751
752    ws = wslist[repo]
753
754    if not os.getcwd().startswith(repo.root):
755        raise util.Abort('recommit is not safe to run with -R')
756
757    abort_if_dirty(ws)
758
759    wlock = repo.wlock()
760    lock = repo.lock()
761
762    try:
763        parent = ws.parent(opts['parent'])
764        between = repo.changelog.nodesbetween(ws.findoutgoing(parent))[2]
765        heads = set(between) & set(repo.heads())
766
767        if len(heads) > 1:
768            ui.warn('Workspace has multiple outgoing heads (or branches):\n')
769            for head in sorted(map(repo.changelog.rev, heads), reverse=True):
770                ui.warn('\t%d\n' % head)
771            raise util.Abort('you must merge before recommitting')
772
773        active = ws.active(parent)
774
775        if filter(lambda b: len(b.parents()) > 1, active.bases()):
776            raise util.Abort('Cannot recommit a merge of two non-outgoing '
777                             'changesets')
778
779        if len(active.revs) <= 0:
780            raise util.Abort("no changes to recommit")
781
782        if len(active.files()) <= 0:
783            ui.warn("Recommitting %d active changesets, but no active files\n" %
784                    len(active.revs))
785
786        #
787        # During the course of a recommit, any file bearing a name
788        # matching the source name of any renamed file will be
789        # clobbered by the operation.
790        #
791        # As such, we ask the user before proceeding.
792        #
793        bogosity = [f.parentname for f in active if f.is_renamed() and
794                    os.path.exists(repo.wjoin(f.parentname))]
795        if bogosity:
796            ui.warn("The following file names are the original name of a "
797                    "rename and also present\n"
798                    "in the working directory:\n")
799
800            for fname in bogosity:
801                ui.warn("  %s\n" % fname)
802
803            if not yes_no(ui, "These files will be removed by recommit."
804                          "  Continue?",
805                          False):
806                raise util.Abort("recommit would clobber files")
807
808        user = opts['user'] or ui.username()
809        comments = '\n'.join(active.comments())
810
811        message = cmdutil.logmessage(opts) or ui.edit(comments, user)
812        if not message:
813            raise util.Abort('empty commit message')
814
815        bk = CdmBackup(ui, ws, backup_name(repo.root))
816        if bk.need_backup():
817            if yes_no(ui, 'Do you want to backup files first?', True):
818                bk.backup()
819
820        oldtags = repo.tags()
821        clearedtags = [(name, nd, repo.changelog.rev(nd), local)
822                for name, nd, local in active.tags()]
823
824        ws.squishdeltas(active, message, user=user)
825    finally:
826        lock.release()
827        wlock.release()
828
829    if clearedtags:
830        ui.write("Removed tags:\n")
831        for name, nd, rev, local in sorted(clearedtags,
832                                           key=lambda x: x[0].lower()):
833            ui.write("  %5s:%s:\t%s%s\n" % (rev, node.short(nd),
834                                            name, (local and ' (local)' or '')))
835
836        for ntag, nnode in sorted(repo.tags().items(),
837                                  key=lambda x: x[0].lower()):
838            if ntag in oldtags and ntag != "tip":
839                if oldtags[ntag] != nnode:
840                    ui.write("tag '%s' now refers to revision %d:%s\n" %
841                             (ntag, repo.changelog.rev(nnode),
842                              node.short(nnode)))
843
844
845def do_eval(cmd, files, root, changedir=True):
846    if not changedir:
847        os.chdir(root)
848
849    for path in sorted(files):
850        dirn, base = os.path.split(path)
851
852        if changedir:
853            os.chdir(os.path.join(root, dirn))
854
855        os.putenv('workspace', root)
856        os.putenv('filepath', path)
857        os.putenv('dir', dirn)
858        os.putenv('file', base)
859        os.system(cmd)
860
861
862def cdm_eval(ui, repo, *command, **opts):
863    '''run cmd for each active file
864
865    cmd can refer to:
866      $file      -	active file basename.
867      $dir       -	active file dirname.
868      $filepath  -	path from workspace root to active file.
869      $workspace -	full path to workspace root.
870
871    For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last
872    the 3 log entries for each active file, preceded by its directory.'''
873
874    act = wslist[repo].active(opts['parent'])
875    cmd = ' '.join(command)
876    files = [x.name for x in act if not x.is_removed()]
877
878    do_eval(cmd, files, repo.root, not opts['remain'])
879
880
881def cdm_apply(ui, repo, *command, **opts):
882    '''apply cmd to all active files
883
884    For example 'hg apply wc -l' outputs a line count of active files.'''
885
886    act = wslist[repo].active(opts['parent'])
887
888    if opts['remain']:
889        appnd = ' $filepath'
890    else:
891        appnd = ' $file'
892
893    cmd = ' '.join(command) + appnd
894    files = [x.name for x in act if not x.is_removed()]
895
896    do_eval(cmd, files, repo.root, not opts['remain'])
897
898
899def cdm_reparent_11(ui, repo, parent):
900    '''reparent your workspace
901
902    Updates the 'default' path.'''
903
904    filename = repo.join('hgrc')
905
906    p = ui.expandpath(parent)
907    cp = util.configparser()
908
909    try:
910        cp.read(filename)
911    except ConfigParser.ParsingError, inst:
912        raise util.Abort('failed to parse %s\n%s' % (filename, inst))
913
914    try:
915        fh = open(filename, 'w')
916    except IOError, e:
917        raise util.Abort('Failed to open workspace configuration: %s' % e)
918
919    if not cp.has_section('paths'):
920        cp.add_section('paths')
921    cp.set('paths', 'default', p)
922    cp.write(fh)
923    fh.close()
924
925
926def cdm_reparent_13(ui, repo, parent):
927    '''reparent your workspace
928
929    Updates the 'default' path in this repository's .hg/hgrc.'''
930
931    def append_new_parent(parent):
932        fp = None
933        try:
934            fp = repo.opener('hgrc', 'a', atomictemp=True)
935            if fp.tell() != 0:
936                fp.write('\n')
937            fp.write('[paths]\n'
938                     'default = %s\n\n' % parent)
939            fp.rename()
940        finally:
941            if fp and not fp.closed:
942                fp.close()
943
944    def update_parent(path, line, parent):
945        line = line - 1 # The line number we're passed will be 1-based
946        fp = None
947
948        try:
949            fp = open(path)
950            data = fp.readlines()
951        finally:
952            if fp and not fp.closed:
953                fp.close()
954
955        #
956        # line will be the last line of any continued block, go back
957        # to the first removing the continuation as we go.
958        #
959        while data[line][0].isspace():
960            data.pop(line)
961            line -= 1
962
963        assert data[line].startswith('default')
964
965        data[line] = "default = %s\n" % parent
966        if data[-1] != '\n':
967            data.append('\n')
968
969        try:
970            fp = util.atomictempfile(path, 'w', 0644)
971            fp.writelines(data)
972            fp.rename()
973        finally:
974            if fp and not fp.closed:
975                fp.close()
976
977    from mercurial import config
978    parent = ui.expandpath(parent)
979
980    if not os.path.exists(repo.join('hgrc')):
981        append_new_parent(parent)
982        return
983
984    cfg = config.config()
985    cfg.read(repo.join('hgrc'))
986    source = cfg.source('paths', 'default')
987
988    if not source:
989        append_new_parent(parent)
990        return
991    else:
992        path, target = source.rsplit(':', 1)
993
994        if path != repo.join('hgrc'):
995            raise util.Abort("Cannot edit path specification not in repo hgrc\n"
996                             "default path is from: %s" % source)
997
998        update_parent(path, int(target), parent)
999
1000if Version.at_least("1.3"):
1001    cdm_reparent = cdm_reparent_13
1002else:
1003    cdm_reparent = cdm_reparent_11
1004
1005
1006def backup_name(fullpath):
1007    '''Create a backup directory name based on the specified path.
1008
1009    In most cases this is the basename of the path specified, but
1010    certain cases are handled specially to create meaningful names'''
1011
1012    special = ['usr/closed']
1013
1014    fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep)
1015
1016    #
1017    # If a path is 'special', we append the basename of the path to
1018    # the path element preceding the constant, special, part.
1019    #
1020    # Such that for instance:
1021    #     /foo/bar/onnv-fixes/usr/closed
1022    #  has a backup name of:
1023    #     onnv-fixes-closed
1024    #
1025    for elt in special:
1026        elt = elt.split(os.path.sep)
1027        pathpos = len(elt)
1028
1029        if fullpath[-pathpos:] == elt:
1030            return "%s-%s" % (fullpath[-pathpos - 1], elt[-1])
1031    else:
1032        return fullpath[-1]
1033
1034
1035def cdm_backup(ui, repo, if_newer=False):
1036    '''make backup copies of all workspace changes
1037
1038    Backups will be stored in ~/cdm.backup/<basename of workspace>.'''
1039
1040    name = backup_name(repo.root)
1041    bk = CdmBackup(ui, wslist[repo], name)
1042
1043    wlock = repo.wlock()
1044    lock = repo.lock()
1045
1046    try:
1047        if if_newer and not bk.need_backup():
1048            ui.status('backup is up-to-date\n')
1049        else:
1050            bk.backup()
1051    finally:
1052        lock.release()
1053        wlock.release()
1054
1055
1056def cdm_restore(ui, repo, backup, **opts):
1057    '''restore workspace from backup
1058
1059    Restores a workspace from the specified backup directory and generation
1060    (which defaults to the latest).'''
1061
1062    if not os.getcwd().startswith(repo.root):
1063        raise util.Abort('restore is not safe to run with -R')
1064
1065    abort_if_dirty(wslist[repo])
1066
1067    if opts['generation']:
1068        gen = int(opts['generation'])
1069    else:
1070        gen = None
1071
1072    if os.path.exists(backup):
1073        backup = os.path.abspath(backup)
1074
1075    wlock = repo.wlock()
1076    lock = repo.lock()
1077
1078    try:
1079        bk = CdmBackup(ui, wslist[repo], backup)
1080        bk.restore(gen)
1081    finally:
1082        lock.release()
1083        wlock.release()
1084
1085
1086def cdm_webrev(ui, repo, **opts):
1087    '''generate webrev and optionally upload it
1088
1089    This command passes all arguments to webrev script'''
1090
1091    webrev_args = ""
1092    for key in opts.keys():
1093        if opts[key]:
1094            if type(opts[key]) == type(True):
1095                webrev_args += '-' + key + ' '
1096            else:
1097                webrev_args += '-' + key + ' ' + opts[key] + ' '
1098
1099    retval = os.system('webrev ' + webrev_args)
1100    if retval != 0:
1101        return retval - 255
1102
1103    return 0
1104
1105
1106cmdtable = {
1107    'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'),
1108                          ('r', 'remain', None, 'do not change directories')],
1109              'hg apply [-p PARENT] [-r] command...'),
1110    'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')],
1111             'hg arcs [-p PARENT]'),
1112    '^backup|bu': (cdm_backup, [('t', 'if-newer', None,
1113                             'only backup if workspace files are newer')],
1114               'hg backup [-t]'),
1115    'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')],
1116                  'hg branchchk [-p PARENT]'),
1117    'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')],
1118             'hg bugs [-p PARENT]'),
1119    'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')],
1120                'hg cddlchk [-p PARENT]'),
1121    'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'),
1122                            ('N', 'nocheck', None,
1123                             'do not compare comments with databases')],
1124               'hg comchk [-p PARENT]'),
1125    'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')],
1126                 'hg comments [-p PARENT]'),
1127    'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')],
1128                  'hg copyright [-p PARENT]'),
1129    'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')],
1130               'hg cstyle [-p PARENT]'),
1131    'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'),
1132                        ('r', 'remain', None, 'do not change directories')],
1133             'hg eval [-p PARENT] [-r] command...'),
1134    'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')],
1135               'hg hdrchk [-p PARENT]'),
1136    'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')],
1137               'hg jstyle [-p PARENT]'),
1138    'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')],
1139                 'hg keywords [-p PARENT]'),
1140    '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'),
1141                                ('r', 'removed', None, 'show removed files'),
1142                                ('a', 'added', None, 'show added files'),
1143                                ('m', 'modified', None, 'show modified files')],
1144                    'hg list [-amrRu] [-p PARENT]'),
1145    'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')],
1146                'hg mapfilechk [-p PARENT]'),
1147    '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')],
1148             'hg nits [-p PARENT]'),
1149    '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'),
1150                           ('N', 'nocheck', None, 'skip RTI check')],
1151              'hg pbchk [-N] [-p PARENT]'),
1152    'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')],
1153                'hg permchk [-p PARENT]'),
1154    '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'),
1155                             ('a', 'text', None, 'treat all files as text'),
1156                             ('g', 'git', None, 'use extended git diff format'),
1157                             ('w', 'ignore-all-space', None,
1158                              'ignore white space when comparing lines'),
1159                             ('b', 'ignore-space-change', None,
1160                              'ignore changes in the amount of white space'),
1161                             ('B', 'ignore-blank-lines', None,
1162                              'ignore changes whos lines are all blank'),
1163                             ('U', 'unified', 3,
1164                              'number of lines of context to show'),
1165                             ('I', 'include', [],
1166                              'include names matching the given patterns'),
1167                             ('X', 'exclude', [],
1168                              'exclude names matching the given patterns')],
1169               'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'),
1170    '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'),
1171                                      ('f', 'force', None, 'force operation'),
1172                                      ('m', 'message', '',
1173                                       'use <text> as commit message'),
1174                                      ('l', 'logfile', '',
1175                                       'read commit message from file'),
1176                                      ('u', 'user', '',
1177                                       'record user as committer')],
1178                       'hg recommit [-f] [-p PARENT]'),
1179    'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')],
1180                'hg renamed [-p PARENT]'),
1181    'reparent': (cdm_reparent, [], 'hg reparent PARENT'),
1182    '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')],
1183                 'hg restore [-g GENERATION] BACKUP'),
1184    'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'),
1185                            ('N', 'nocheck', None, 'skip RTI check')],
1186               'hg rtichk [-N] [-p PARENT]'),
1187    'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')],
1188               'hg tagchk [-p PARENT]'),
1189    'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'),
1190                            ('D', 'D', '', 'delete remote webrev'),
1191                            ('I', 'I', '', 'ITS configuration file'),
1192                            ('i', 'i', '', 'include file'),
1193                            ('l', 'l', '', 'extract file list from putback -n'),
1194                            ('N', 'N', None, 'supress comments'),
1195                            ('n', 'n', None, 'do not generate webrev'),
1196                            ('O', 'O', None, 'OpenSolaris mode'),
1197                            ('o', 'o', '', 'output directory'),
1198                            ('p', 'p', '', 'use specified parent'),
1199                            ('t', 't', '', 'upload target'),
1200                            ('U', 'U', None, 'upload the webrev'),
1201                            ('w', 'w', '', 'use wx active file')],
1202               'hg webrev [WEBREV_OPTIONS]'),
1203}
1204