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