xref: /titanic_44/usr/src/tools/onbld/hgext/cdm.py (revision f6e214c7418f43af38bd8c3a557e3d0a1d311cfa)
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    # We also ignore files with suffixes that tell us that the files
355    # are not mapfiles.
356    MapfileRE = re.compile(r'.*((mapfile[^/]*)|(/map\.+[^/]*)|(\.map))$',
357    	re.IGNORECASE)
358    NotMapSuffixRE = re.compile(r'.*\.[ch]$', re.IGNORECASE)
359
360    ui.write('Mapfile comment check:\n')
361
362    for f, e in filelist:
363        if e and e.is_removed():
364            continue
365        elif (not MapfileRE.match(f)) or NotMapSuffixRE.match(f):
366            continue
367        elif (e or opts.get('honour_nots')) and exclude(f):
368            ui.status('Skipping %s...\n' % f)
369            continue
370
371        fh = open(f, 'r')
372        ret |= Mapfile.mapfilechk(fh, output=ui)
373        fh.close()
374    return ret
375
376
377def cdm_copyright(ui, repo, *args, **opts):
378    '''check active files for valid copyrights
379
380    Check that all active files have a valid copyright containing the
381    current year (and *only* the current year).
382    See http://www.opensolaris.org/os/project/muskoka/on_dev/golden_rules.txt
383    for more info.'''
384
385    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
386    exclude = not_check(repo, 'copyright')
387    ret = 0
388
389    ui.write('Copyright check:\n')
390
391    for f, e in filelist:
392        if e and e.is_removed():
393            continue
394        elif (e or opts.get('honour_nots')) and exclude(f):
395            ui.status('Skipping %s...\n' % f)
396            continue
397
398        fh = open(f, 'r')
399        ret |= Copyright.copyright(fh, output=ui)
400        fh.close()
401    return ret
402
403
404def cdm_hdrchk(ui, repo, *args, **opts):
405    '''check active header files conform to O/N rules'''
406
407    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
408    exclude = not_check(repo, 'hdrchk')
409    ret = 0
410
411    ui.write('Header format check:\n')
412
413    for f, e in filelist:
414        if e and e.is_removed():
415            continue
416        elif not f.endswith('.h'):
417            continue
418        elif (e or opts.get('honour_nots')) and exclude(f):
419            ui.status('Skipping %s...\n' % f)
420            continue
421
422        fh = open(f, 'r')
423        ret |= HdrChk.hdrchk(fh, lenient=True, output=ui)
424        fh.close()
425    return ret
426
427
428def cdm_cstyle(ui, repo, *args, **opts):
429    '''check active C source files conform to the C Style Guide
430
431    See http://opensolaris.org/os/community/documentation/getting_started_docs/cstyle.ms.pdf'''
432
433    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
434    exclude = not_check(repo, 'cstyle')
435    ret = 0
436
437    ui.write('C style check:\n')
438
439    for f, e in filelist:
440        if e and e.is_removed():
441            continue
442        elif not (f.endswith('.c') or f.endswith('.h')):
443            continue
444        elif (e or opts.get('honour_nots')) and exclude(f):
445            ui.status('Skipping %s...\n' % f)
446            continue
447
448        fh = open(f, 'r')
449        ret |= CStyle.cstyle(fh, output=ui,
450                             picky=True, check_posix_types=True,
451                             check_continuation=True)
452        fh.close()
453    return ret
454
455
456def cdm_jstyle(ui, repo, *args, **opts):
457    'check active Java source files for common stylistic errors'
458
459    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
460    exclude = not_check(repo, 'jstyle')
461    ret = 0
462
463    ui.write('Java style check:\n')
464
465    for f, e in filelist:
466        if e and e.is_removed():
467            continue
468        elif not f.endswith('.java'):
469            continue
470        elif (e or opts.get('honour_nots')) and exclude(f):
471            ui.status('Skipping %s...\n' % f)
472            continue
473
474        fh = open(f, 'r')
475        ret |= JStyle.jstyle(fh, output=ui, picky=True)
476        fh.close()
477    return ret
478
479
480def cdm_permchk(ui, repo, *args, **opts):
481    '''check active files permission - warn +x (execute) mode'''
482
483    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
484    exclude = not_check(repo, 'permchk')
485    exeFiles = []
486
487    ui.write('File permission check:\n')
488
489    for f, e in filelist:
490        if e and e.is_removed():
491            continue
492        elif (e or opts.get('honour_nots')) and exclude(f):
493            ui.status('Skipping %s...\n' % f)
494            continue
495
496        mode = stat.S_IMODE(os.stat(f)[stat.ST_MODE])
497        if mode & stat.S_IEXEC:
498            exeFiles.append(f)
499
500    if len(exeFiles) > 0:
501        ui.write('Warning: the following active file(s) have executable mode '
502            '(+x) permission set,\nremove unless intentional:\n')
503        for fname in exeFiles:
504            ui.write("  %s\n" % fname)
505
506    return len(exeFiles) > 0
507
508
509def cdm_tagchk(ui, repo, **opts):
510    '''check if .hgtags is active and issue warning
511
512    Tag sharing among repositories is restricted to gatekeepers'''
513
514    active = wslist[repo].active(opts.get('parent'))
515
516    ui.write('Checking for new tags:\n')
517
518    if ".hgtags" in active:
519        tfile = wslist[repo].filepath('.hgtags')
520        ptip = active.parenttip.rev()
521
522        ui.write('Warning: Workspace contains new non-local tags.\n'
523                 'Only gatekeepers should add or modify such tags.\n'
524                 'Use the following commands to revert these changes:\n'
525                 '  hg revert -r%d %s\n'
526                 '  hg commit %s\n'
527                 'You should also recommit before integration\n' %
528                 (ptip, tfile, tfile))
529
530        return 1
531
532    return 0
533
534
535def cdm_branchchk(ui, repo, **opts):
536    '''check if multiple heads (or branches) are present, or if
537    branch changes are made'''
538
539    ui.write('Checking for multiple heads (or branches):\n')
540
541    heads = set(repo.heads())
542    parents = set([x.node() for x in wslist[repo].workingctx().parents()])
543
544    #
545    # We care if there's more than one head, and those heads aren't
546    # identical to the dirstate parents (if they are identical, it's
547    # an uncommitted merge which mergechk will catch, no need to
548    # complain twice).
549    #
550    if len(heads) > 1 and heads != parents:
551        ui.write('Workspace has multiple heads (or branches):\n')
552        for head in [repo.changectx(head) for head in heads]:
553            ui.write("  %d:%s\t%s\n" %
554                (head.rev(), str(head), head.description().splitlines()[0]))
555        ui.write('You must merge and recommit.\n')
556        return 1
557
558    ui.write('\nChecking for branch changes:\n')
559
560    if repo.dirstate.branch() != 'default':
561        ui.write("Warning: Workspace tip has named branch: '%s'\n"
562                 "Only gatekeepers should push new branches.\n"
563                 "Use the following commands to restore the branch name:\n"
564                 "  hg branch [-f] default\n"
565                 "  hg commit\n"
566                 "You should also recommit before integration\n" %
567                 (repo.dirstate.branch()))
568        return 1
569
570    branches = repo.branchtags().keys()
571    if len(branches) > 1:
572        ui.write('Warning: Workspace has named branches:\n')
573        for t in branches:
574            if t == 'default':
575                continue
576            ui.write("\t%s\n" % t)
577
578        ui.write("Only gatekeepers should push new branches.\n"
579                 "Use the following commands to remove extraneous branches.\n"
580                 "  hg branch [-f] default\n"
581                 "  hg commit"
582                 "You should also recommit before integration\n")
583        return 1
584
585    return 0
586
587
588def cdm_rtichk(ui, repo, **opts):
589    '''check active bug/RFEs for approved RTIs
590
591    Only works on SWAN.'''
592
593    if opts.get('nocheck') or os.path.exists(repo.join('cdm/rtichk.NOT')):
594        ui.status('Skipping RTI checks...\n')
595        return 0
596
597    if not onSWAN():
598        ui.write('RTI checks only work on SWAN, skipping...\n')
599        return 0
600
601    parent = wslist[repo].parent(opts.get('parent'))
602    active = wslist[repo].active(parent)
603
604    ui.write('RTI check:\n')
605
606    bugs = []
607
608    for com in active.comments():
609        match = Comments.isBug(com)
610        if match and match.group(1) not in bugs:
611            bugs.append(match.group(1))
612
613    # RTI normalizes the gate path for us
614    return int(not Rti.rti(bugs, gatePath=parent, output=ui))
615
616
617def cdm_keywords(ui, repo, *args, **opts):
618    '''check source files do not contain SCCS keywords'''
619
620    filelist = buildfilelist(wslist[repo], opts.get('parent'), args)
621    exclude = not_check(repo, 'keywords')
622    ret = 0
623
624    ui.write('Keywords check:\n')
625
626    for f, e in filelist:
627        if e and e.is_removed():
628            continue
629        elif (e or opts.get('honour_nots')) and exclude(f):
630            ui.status('Skipping %s...\n' % f)
631            continue
632
633        fh = open(f, 'r')
634        ret |= Keywords.keywords(fh, output=ui)
635        fh.close()
636    return ret
637
638
639#
640# NB:
641#    There's no reason to hook this up as an invokable command, since
642#    we have 'hg status', but it must accept the same arguments.
643#
644def cdm_outchk(ui, repo, **opts):
645    '''Warn the user if they have uncommitted changes'''
646
647    ui.write('Checking for uncommitted changes:\n')
648
649    st = wslist[repo].modified()
650    if st:
651        ui.write('Warning: the following files have uncommitted changes:\n')
652        for elt in st:
653            ui.write('   %s\n' % elt)
654        return 1
655    return 0
656
657
658def cdm_mergechk(ui, repo, **opts):
659    '''Warn the user if their workspace contains merges'''
660
661    active = wslist[repo].active(opts.get('parent'))
662
663    ui.write('Checking for merges:\n')
664
665    merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1],
666                   active.revs)
667
668    if merges:
669        ui.write('Workspace contains the following merges:\n')
670        for rev in merges:
671            desc = rev.description().splitlines()
672            ui.write('  %s:%s\t%s\n' %
673                     (rev.rev() or "working", str(rev),
674                      desc and desc[0] or "*** uncommitted change ***"))
675        return 1
676    return 0
677
678
679def run_checks(ws, cmds, *args, **opts):
680    '''Run CMDS (with OPTS) over active files in WS'''
681
682    ret = 0
683
684    for cmd in cmds:
685        name = cmd.func_name.split('_')[1]
686        if not ws.ui.configbool('cdm', name, True):
687            ws.ui.status('Skipping %s check...\n' % name)
688        else:
689            ws.ui.pushbuffer()
690            result = cmd(ws.ui, ws.repo, honour_nots=True, *args, **opts)
691            output = ws.ui.popbuffer()
692
693            ret |= result
694
695            if not ws.ui.quiet or result != 0:
696                ws.ui.write(output, '\n')
697    return ret
698
699
700def cdm_nits(ui, repo, *args, **opts):
701    '''check for stylistic nits in active files
702
703    Run cddlchk, copyright, cstyle, hdrchk, jstyle, mapfilechk,
704    permchk, and keywords checks.'''
705
706    cmds = [cdm_cddlchk,
707        cdm_copyright,
708        cdm_cstyle,
709        cdm_hdrchk,
710        cdm_jstyle,
711        cdm_mapfilechk,
712        cdm_permchk,
713        cdm_keywords]
714
715    return run_checks(wslist[repo], cmds, *args, **opts)
716
717
718def cdm_pbchk(ui, repo, **opts):
719    '''pre-putback check all active files
720
721    Run cddlchk, comchk, copyright, cstyle, hdrchk, jstyle, mapfilechk,
722    permchk, tagchk, branchchk, keywords and rtichk checks.  Additionally,
723    warn about uncommitted changes.'''
724
725    #
726    # The current ordering of these is that the commands from cdm_nits
727    # run first in the same order as they would in cdm_nits.  Then the
728    # pbchk specifics run
729    #
730    cmds = [cdm_cddlchk,
731        cdm_copyright,
732        cdm_cstyle,
733        cdm_hdrchk,
734        cdm_jstyle,
735        cdm_mapfilechk,
736        cdm_permchk,
737        cdm_keywords,
738        cdm_comchk,
739        cdm_tagchk,
740        cdm_branchchk,
741        cdm_rtichk,
742        cdm_outchk,
743        cdm_mergechk]
744
745    return run_checks(wslist[repo], cmds, **opts)
746
747
748def cdm_recommit(ui, repo, **opts):
749    '''replace outgoing changesets with a single equivalent changeset
750
751    Replace all outgoing changesets with a single changeset containing
752    equivalent changes.  This removes uninteresting changesets created
753    during development that would only serve as noise in the gate.
754
755    Any changed file that is now identical in content to that in the
756    parent workspace (whether identical in history or otherwise) will
757    not be included in the new changeset.  Any merges information will
758    also be removed.
759
760    If no files are changed in comparison to the parent workspace, the
761    outgoing changesets will be removed, but no new changeset created.
762
763    recommit will refuse to run if the workspace contains more than
764    one outgoing head, even if those heads are on the same branch.  To
765    recommit with only one branch containing outgoing changesets, your
766    workspace must be on that branch and at that branch head.
767
768    recommit will prompt you to take a backup if your workspace has
769    been changed since the last backup was taken.  In almost all
770    cases, you should allow it to take one (the default).
771
772    recommit cannot be run if the workspace contains any uncommitted
773    changes, applied Mq patches, or has multiple outgoing heads (or
774    branches).
775    '''
776
777    ws = wslist[repo]
778
779    if not os.getcwd().startswith(repo.root):
780        raise util.Abort('recommit is not safe to run with -R')
781
782    abort_if_dirty(ws)
783
784    wlock = repo.wlock()
785    lock = repo.lock()
786
787    try:
788        parent = ws.parent(opts['parent'])
789        between = repo.changelog.nodesbetween(ws.findoutgoing(parent))[2]
790        heads = set(between) & set(repo.heads())
791
792        if len(heads) > 1:
793            ui.warn('Workspace has multiple outgoing heads (or branches):\n')
794            for head in sorted(map(repo.changelog.rev, heads), reverse=True):
795                ui.warn('\t%d\n' % head)
796            raise util.Abort('you must merge before recommitting')
797
798        active = ws.active(parent)
799
800        if filter(lambda b: len(b.parents()) > 1, active.bases()):
801            raise util.Abort('Cannot recommit a merge of two non-outgoing '
802                             'changesets')
803
804        if len(active.revs) <= 0:
805            raise util.Abort("no changes to recommit")
806
807        if len(active.files()) <= 0:
808            ui.warn("Recommitting %d active changesets, but no active files\n" %
809                    len(active.revs))
810
811        #
812        # During the course of a recommit, any file bearing a name
813        # matching the source name of any renamed file will be
814        # clobbered by the operation.
815        #
816        # As such, we ask the user before proceeding.
817        #
818        bogosity = [f.parentname for f in active if f.is_renamed() and
819                    os.path.exists(repo.wjoin(f.parentname))]
820        if bogosity:
821            ui.warn("The following file names are the original name of a "
822                    "rename and also present\n"
823                    "in the working directory:\n")
824
825            for fname in bogosity:
826                ui.warn("  %s\n" % fname)
827
828            if not yes_no(ui, "These files will be removed by recommit."
829                          "  Continue?",
830                          False):
831                raise util.Abort("recommit would clobber files")
832
833        user = opts['user'] or ui.username()
834        comments = '\n'.join(active.comments())
835
836        message = cmdutil.logmessage(opts) or ui.edit(comments, user)
837        if not message:
838            raise util.Abort('empty commit message')
839
840        bk = CdmBackup(ui, ws, backup_name(repo.root))
841        if bk.need_backup():
842            if yes_no(ui, 'Do you want to backup files first?', True):
843                bk.backup()
844
845        oldtags = repo.tags()
846        clearedtags = [(name, nd, repo.changelog.rev(nd), local)
847                for name, nd, local in active.tags()]
848
849        ws.squishdeltas(active, message, user=user)
850    finally:
851        lock.release()
852        wlock.release()
853
854    if clearedtags:
855        ui.write("Removed tags:\n")
856        for name, nd, rev, local in sorted(clearedtags,
857                                           key=lambda x: x[0].lower()):
858            ui.write("  %5s:%s:\t%s%s\n" % (rev, node.short(nd),
859                                            name, (local and ' (local)' or '')))
860
861        for ntag, nnode in sorted(repo.tags().items(),
862                                  key=lambda x: x[0].lower()):
863            if ntag in oldtags and ntag != "tip":
864                if oldtags[ntag] != nnode:
865                    ui.write("tag '%s' now refers to revision %d:%s\n" %
866                             (ntag, repo.changelog.rev(nnode),
867                              node.short(nnode)))
868
869
870def do_eval(cmd, files, root, changedir=True):
871    if not changedir:
872        os.chdir(root)
873
874    for path in sorted(files):
875        dirn, base = os.path.split(path)
876
877        if changedir:
878            os.chdir(os.path.join(root, dirn))
879
880        os.putenv('workspace', root)
881        os.putenv('filepath', path)
882        os.putenv('dir', dirn)
883        os.putenv('file', base)
884        os.system(cmd)
885
886
887def cdm_eval(ui, repo, *command, **opts):
888    '''run cmd for each active file
889
890    cmd can refer to:
891      $file      -	active file basename.
892      $dir       -	active file dirname.
893      $filepath  -	path from workspace root to active file.
894      $workspace -	full path to workspace root.
895
896    For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last
897    the 3 log entries for each active file, preceded by its directory.'''
898
899    act = wslist[repo].active(opts['parent'])
900    cmd = ' '.join(command)
901    files = [x.name for x in act if not x.is_removed()]
902
903    do_eval(cmd, files, repo.root, not opts['remain'])
904
905
906def cdm_apply(ui, repo, *command, **opts):
907    '''apply cmd to all active files
908
909    For example 'hg apply wc -l' outputs a line count of active files.'''
910
911    act = wslist[repo].active(opts['parent'])
912
913    if opts['remain']:
914        appnd = ' $filepath'
915    else:
916        appnd = ' $file'
917
918    cmd = ' '.join(command) + appnd
919    files = [x.name for x in act if not x.is_removed()]
920
921    do_eval(cmd, files, repo.root, not opts['remain'])
922
923
924def cdm_reparent_11(ui, repo, parent):
925    '''reparent your workspace
926
927    Updates the 'default' path.'''
928
929    filename = repo.join('hgrc')
930
931    p = ui.expandpath(parent)
932    cp = util.configparser()
933
934    try:
935        cp.read(filename)
936    except ConfigParser.ParsingError, inst:
937        raise util.Abort('failed to parse %s\n%s' % (filename, inst))
938
939    try:
940        fh = open(filename, 'w')
941    except IOError, e:
942        raise util.Abort('Failed to open workspace configuration: %s' % e)
943
944    if not cp.has_section('paths'):
945        cp.add_section('paths')
946    cp.set('paths', 'default', p)
947    cp.write(fh)
948    fh.close()
949
950
951def cdm_reparent_13(ui, repo, parent):
952    '''reparent your workspace
953
954    Updates the 'default' path in this repository's .hg/hgrc.'''
955
956    def append_new_parent(parent):
957        fp = None
958        try:
959            fp = repo.opener('hgrc', 'a', atomictemp=True)
960            if fp.tell() != 0:
961                fp.write('\n')
962            fp.write('[paths]\n'
963                     'default = %s\n\n' % parent)
964            fp.rename()
965        finally:
966            if fp and not fp.closed:
967                fp.close()
968
969    def update_parent(path, line, parent):
970        line = line - 1 # The line number we're passed will be 1-based
971        fp = None
972
973        try:
974            fp = open(path)
975            data = fp.readlines()
976        finally:
977            if fp and not fp.closed:
978                fp.close()
979
980        #
981        # line will be the last line of any continued block, go back
982        # to the first removing the continuation as we go.
983        #
984        while data[line][0].isspace():
985            data.pop(line)
986            line -= 1
987
988        assert data[line].startswith('default')
989
990        data[line] = "default = %s\n" % parent
991        if data[-1] != '\n':
992            data.append('\n')
993
994        try:
995            fp = util.atomictempfile(path, 'w', 0644)
996            fp.writelines(data)
997            fp.rename()
998        finally:
999            if fp and not fp.closed:
1000                fp.close()
1001
1002    from mercurial import config
1003    parent = ui.expandpath(parent)
1004
1005    if not os.path.exists(repo.join('hgrc')):
1006        append_new_parent(parent)
1007        return
1008
1009    cfg = config.config()
1010    cfg.read(repo.join('hgrc'))
1011    source = cfg.source('paths', 'default')
1012
1013    if not source:
1014        append_new_parent(parent)
1015        return
1016    else:
1017        path, target = source.rsplit(':', 1)
1018
1019        if path != repo.join('hgrc'):
1020            raise util.Abort("Cannot edit path specification not in repo hgrc\n"
1021                             "default path is from: %s" % source)
1022
1023        update_parent(path, int(target), parent)
1024
1025if Version.at_least("1.3"):
1026    cdm_reparent = cdm_reparent_13
1027else:
1028    cdm_reparent = cdm_reparent_11
1029
1030
1031def backup_name(fullpath):
1032    '''Create a backup directory name based on the specified path.
1033
1034    In most cases this is the basename of the path specified, but
1035    certain cases are handled specially to create meaningful names'''
1036
1037    special = ['usr/closed']
1038
1039    fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep)
1040
1041    #
1042    # If a path is 'special', we append the basename of the path to
1043    # the path element preceding the constant, special, part.
1044    #
1045    # Such that for instance:
1046    #     /foo/bar/onnv-fixes/usr/closed
1047    #  has a backup name of:
1048    #     onnv-fixes-closed
1049    #
1050    for elt in special:
1051        elt = elt.split(os.path.sep)
1052        pathpos = len(elt)
1053
1054        if fullpath[-pathpos:] == elt:
1055            return "%s-%s" % (fullpath[-pathpos - 1], elt[-1])
1056    else:
1057        return fullpath[-1]
1058
1059
1060def cdm_backup(ui, repo, if_newer=False):
1061    '''make backup copies of all workspace changes
1062
1063    Backups will be stored in ~/cdm.backup/<basename of workspace>.'''
1064
1065    name = backup_name(repo.root)
1066    bk = CdmBackup(ui, wslist[repo], name)
1067
1068    wlock = repo.wlock()
1069    lock = repo.lock()
1070
1071    try:
1072        if if_newer and not bk.need_backup():
1073            ui.status('backup is up-to-date\n')
1074        else:
1075            bk.backup()
1076    finally:
1077        lock.release()
1078        wlock.release()
1079
1080
1081def cdm_restore(ui, repo, backup, **opts):
1082    '''restore workspace from backup
1083
1084    Restores a workspace from the specified backup directory and generation
1085    (which defaults to the latest).'''
1086
1087    if not os.getcwd().startswith(repo.root):
1088        raise util.Abort('restore is not safe to run with -R')
1089
1090    abort_if_dirty(wslist[repo])
1091
1092    if opts['generation']:
1093        gen = int(opts['generation'])
1094    else:
1095        gen = None
1096
1097    if os.path.exists(backup):
1098        backup = os.path.abspath(backup)
1099
1100    wlock = repo.wlock()
1101    lock = repo.lock()
1102
1103    try:
1104        bk = CdmBackup(ui, wslist[repo], backup)
1105        bk.restore(gen)
1106    finally:
1107        lock.release()
1108        wlock.release()
1109
1110
1111def cdm_webrev(ui, repo, **opts):
1112    '''generate webrev and optionally upload it
1113
1114    This command passes all arguments to webrev script'''
1115
1116    webrev_args = ""
1117    for key in opts.keys():
1118        if opts[key]:
1119            if type(opts[key]) == type(True):
1120                webrev_args += '-' + key + ' '
1121            else:
1122                webrev_args += '-' + key + ' ' + opts[key] + ' '
1123
1124    retval = os.system('webrev ' + webrev_args)
1125    if retval != 0:
1126        return retval - 255
1127
1128    return 0
1129
1130
1131cmdtable = {
1132    'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'),
1133                          ('r', 'remain', None, 'do not change directories')],
1134              'hg apply [-p PARENT] [-r] command...'),
1135    'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')],
1136             'hg arcs [-p PARENT]'),
1137    '^backup|bu': (cdm_backup, [('t', 'if-newer', None,
1138                             'only backup if workspace files are newer')],
1139               'hg backup [-t]'),
1140    'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')],
1141                  'hg branchchk [-p PARENT]'),
1142    'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')],
1143             'hg bugs [-p PARENT]'),
1144    'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')],
1145                'hg cddlchk [-p PARENT]'),
1146    'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'),
1147                            ('N', 'nocheck', None,
1148                             'do not compare comments with databases')],
1149               'hg comchk [-p PARENT]'),
1150    'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')],
1151                 'hg comments [-p PARENT]'),
1152    'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')],
1153                  'hg copyright [-p PARENT]'),
1154    'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')],
1155               'hg cstyle [-p PARENT]'),
1156    'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'),
1157                        ('r', 'remain', None, 'do not change directories')],
1158             'hg eval [-p PARENT] [-r] command...'),
1159    'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')],
1160               'hg hdrchk [-p PARENT]'),
1161    'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')],
1162               'hg jstyle [-p PARENT]'),
1163    'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')],
1164                 'hg keywords [-p PARENT]'),
1165    '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'),
1166                                ('r', 'removed', None, 'show removed files'),
1167                                ('a', 'added', None, 'show added files'),
1168                                ('m', 'modified', None, 'show modified files')],
1169                    'hg list [-amrRu] [-p PARENT]'),
1170    'mapfilechk': (cdm_mapfilechk, [('p', 'parent', '', 'parent workspace')],
1171                'hg mapfilechk [-p PARENT]'),
1172    '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')],
1173             'hg nits [-p PARENT]'),
1174    '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'),
1175                           ('N', 'nocheck', None, 'skip RTI check')],
1176              'hg pbchk [-N] [-p PARENT]'),
1177    'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')],
1178                'hg permchk [-p PARENT]'),
1179    '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace'),
1180                             ('a', 'text', None, 'treat all files as text'),
1181                             ('g', 'git', None, 'use extended git diff format'),
1182                             ('w', 'ignore-all-space', None,
1183                              'ignore white space when comparing lines'),
1184                             ('b', 'ignore-space-change', None,
1185                              'ignore changes in the amount of white space'),
1186                             ('B', 'ignore-blank-lines', None,
1187                              'ignore changes whos lines are all blank'),
1188                             ('U', 'unified', 3,
1189                              'number of lines of context to show'),
1190                             ('I', 'include', [],
1191                              'include names matching the given patterns'),
1192                             ('X', 'exclude', [],
1193                              'exclude names matching the given patterns')],
1194               'hg pdiffs [OPTION...] [-p PARENT] [FILE...]'),
1195    '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'),
1196                                      ('f', 'force', None, 'force operation'),
1197                                      ('m', 'message', '',
1198                                       'use <text> as commit message'),
1199                                      ('l', 'logfile', '',
1200                                       'read commit message from file'),
1201                                      ('u', 'user', '',
1202                                       'record user as committer')],
1203                       'hg recommit [-f] [-p PARENT]'),
1204    'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')],
1205                'hg renamed [-p PARENT]'),
1206    'reparent': (cdm_reparent, [], 'hg reparent PARENT'),
1207    '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')],
1208                 'hg restore [-g GENERATION] BACKUP'),
1209    'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'),
1210                            ('N', 'nocheck', None, 'skip RTI check')],
1211               'hg rtichk [-N] [-p PARENT]'),
1212    'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')],
1213               'hg tagchk [-p PARENT]'),
1214    'webrev': (cdm_webrev, [('C', 'C', '', 'ITS priority file'),
1215                            ('D', 'D', '', 'delete remote webrev'),
1216                            ('I', 'I', '', 'ITS configuration file'),
1217                            ('i', 'i', '', 'include file'),
1218                            ('l', 'l', '', 'extract file list from putback -n'),
1219                            ('N', 'N', None, 'supress comments'),
1220                            ('n', 'n', None, 'do not generate webrev'),
1221                            ('O', 'O', None, 'OpenSolaris mode'),
1222                            ('o', 'o', '', 'output directory'),
1223                            ('p', 'p', '', 'use specified parent'),
1224                            ('t', 't', '', 'upload target'),
1225                            ('U', 'U', None, 'upload the webrev'),
1226                            ('w', 'w', '', 'use wx active file')],
1227               'hg webrev [WEBREV_OPTIONS]'),
1228}
1229