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