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