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