xref: /titanic_44/usr/src/tools/onbld/hgext/cdm.py (revision b6917abefc343244b784f0cc34bc65b01469c3bf)
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 2008 Sun Microsystems, Inc.  All rights reserved.
18# Use is subject to license terms.
19#
20# ident	"%Z%%M%	%I%	%E% SMI"
21#
22
23'''workspace extensions for mercurial
24
25This extension contains a number of commands to help you work within
26the OpenSolaris consolidations.
27
28Common uses:
29
30Show diffs relative to parent workspace			- pdiffs
31Check source style rules				- nits
32Run pre-putback checks					- pbchk
33Collapse all your changes into a single changeset	- recommit'''
34
35
36#
37# NB: This assumes the normal directory structure, with this
38#     extension 2 levels below .../lib/python.
39#
40#     If you change that, change this
41#
42import sys, os
43sys.path.insert(1, "%s/../../" % os.path.dirname(__file__))
44
45from onbld.Scm import Version
46from mercurial import util
47
48try:
49    Version.check_version()
50except Version.VersionMismatch, badversion:
51    raise util.Abort("Version Mismatch:\n %s\n" % badversion)
52
53import ConfigParser
54from mercurial import cmdutil, node
55
56from onbld.Scm.WorkSpace import WorkSpace, ActiveEntry
57from onbld.Scm.Backup import CdmBackup
58from onbld.Checks import Cddl, Comments, Copyright, CStyle, HdrChk
59from onbld.Checks import JStyle, Keywords, Rti, onSWAN
60
61
62def yes_no(ui, msg, default):
63    if default:
64        prompt = ' [Y/n]:'
65        defanswer = 'y'
66    else:
67        prompt = ' [y/N]:'
68        defanswer = 'n'
69
70    if ui.interactive and sys.stdin.isatty():
71        resp = ui.prompt(msg + prompt, r'([Yy(es)?|[Nn]o?)?',
72                         default=defanswer)
73        if not resp:
74            return default
75        elif resp[0] in ['Y', 'y']:
76            return True
77        else:
78            return False
79    else:
80        return default
81
82#
83# Adding a reference to WorkSpace from a repo causes a circular reference
84# repo <-> WorkSpace.
85#
86# This prevents repo, WorkSpace and members thereof from being garbage
87# collected.  Since transactions are aborted when the transaction object
88# is collected, and localrepo holds a reference to the most recently created
89# transaction, this prevents transactions from cleanly aborting.
90#
91# Instead, we hold the repo->WorkSpace association in a dictionary, breaking
92# that dependence.
93#
94wslist = {}
95
96
97def reposetup(ui, repo):
98    if repo.local() and repo not in wslist:
99        wslist[repo] = WorkSpace(repo)
100
101
102def cdm_pdiffs(ui, repo, parent=None):
103    '''list workspace diffs relative to parent workspace
104
105    The parent tip is taken to be the latest revision shared between
106    us and the parent workspace.'''
107    diffs = wslist[repo].pdiff(parent)
108    if diffs:
109        ui.write(diffs)
110
111
112def cdm_list(ui, repo, **opts):
113    '''list files changed relative to parent workspace
114
115    The parent tip is taken to be the latest revision shared between
116    us and the parent workspace.'''
117
118    wanted = []
119
120    if opts['added']:
121        wanted.append(ActiveEntry.ADDED)
122    if opts['modified']:
123        wanted.append(ActiveEntry.MODIFIED)
124    if opts['removed']:
125        wanted.append(ActiveEntry.REMOVED)
126
127    act = wslist[repo].active(opts['parent'])
128    chngmap = {ActiveEntry.MODIFIED: 'modified',
129               ActiveEntry.ADDED: 'added',
130               ActiveEntry.REMOVED: 'removed'}
131
132    lst = {}
133    for entry in act:
134        if wanted and (entry.change not in wanted):
135            continue
136
137        chngstr = chngmap[entry.change]
138        if chngstr not in lst:
139            lst[chngstr] = []
140        lst[chngstr].append(entry)
141
142    for chng in sorted(lst.keys()):
143        ui.write(chng + ':\n')
144        for elt in sorted(lst[chng]):
145            if elt.is_renamed():
146                ui.write('\t%s (renamed from %s)\n' % (elt.name,
147                                                      elt.parentname))
148            elif elt.is_copied():
149                ui.write('\t%s (copied from %s)\n' % (elt.name,
150                                                      elt.parentname))
151            else:
152                ui.write('\t%s\n' % elt.name)
153
154
155def cdm_arcs(ui, repo, parent=None):
156    'show all ARC cases in checkin comments'
157    act = wslist[repo].active(parent)
158
159    # We take a set of the appropriate comments to eliminate duplicates.
160    for elt in set(filter(Comments.isARC, act.comments())):
161        ui.write(elt + '\n')
162
163
164def cdm_bugs(ui, repo, parent=None):
165    'show all bug IDs in checkin comments'
166    act = wslist[repo].active(parent)
167
168    for elt in set(filter(Comments.isBug, act.comments())):
169        ui.write(elt + '\n')
170
171
172def cdm_comments(ui, repo, parent=None):
173    'show checkin comments for active files'
174    act = wslist[repo].active(parent)
175
176    for elt in act.comments():
177        ui.write(elt + '\n')
178
179
180def cdm_renamed(ui, repo, parent=None):
181    '''show renamed active files
182
183    Renamed files are shown in the format
184
185       newname oldname
186
187    One pair per-line.'''
188
189    act = wslist[repo].active(parent)
190
191    for entry in sorted(filter(lambda x: x.is_renamed(), act)):
192        ui.write('%s %s\n' % (entry.name, entry.parentname))
193
194
195def cdm_comchk(ui, repo, **opts):
196    '''check checkin comments for active files
197
198    Check that checkin comments conform to O/N rules.'''
199
200    active = opts.get('active') or wslist[repo].active(opts['parent'])
201
202    ui.write('Comments check:\n')
203
204    check_db = not opts['nocheck']
205    return Comments.comchk(active.comments(), check_db=check_db, output=ui)
206
207
208def cdm_cddlchk(ui, repo, **opts):
209    '''check for a valid CDDL block in active files
210
211    See http://www.opensolaris.org/os/community/on/devref_toc/devref_7/#7_2_3_nonformatting_considerations
212    for more info.'''
213
214    active = opts.get('active') or wslist[repo].active(opts['parent'])
215
216    ui.write('CDDL block check:\n')
217
218    lenient = True
219    ret = 0
220
221    for entry in sorted(active):
222        if entry.is_removed():
223            continue
224        elif entry.is_added():
225            lenient = False
226        else:
227            lenient = True
228
229        path = wslist[repo].filepath(entry.name)
230        fh = open(path, 'r')
231        ret |= Cddl.cddlchk(fh, lenient=lenient, output=ui)
232        fh.close()
233    return ret
234
235
236def cdm_copyright(ui, repo, **opts):
237    '''check active files for valid copyrights
238
239    Check that all active files have a valid copyright containing the
240    current year (and *only* the current year).
241    See http://www.opensolaris.org/os/project/muskoka/on_dev/golden_rules.txt
242    for more info.'''
243
244    active = opts.get('active') or wslist[repo].active(opts['parent'])
245
246    ui.write('Copyright check:\n')
247
248    ret = 0
249
250    for entry in sorted(active):
251        if entry.is_removed():
252            continue
253
254        path = wslist[repo].filepath(entry.name)
255
256        fh = open(path, 'r')
257        ret |= Copyright.copyright(fh, output=ui)
258        fh.close()
259    return ret
260
261
262def cdm_hdrchk(ui, repo, **opts):
263    '''check active header files conform to O/N rules'''
264
265    active = opts.get('active') or wslist[repo].active(opts['parent'])
266
267    ui.write('Header format check:\n')
268
269    ret = 0
270
271    for entry in sorted(active):
272        if entry.is_removed():
273            continue
274
275        path = wslist[repo].filepath(entry.name)
276
277        if entry.name.endswith('.h'):
278            fh = open(path, 'r')
279            ret |= HdrChk.hdrchk(fh, lenient=True, output=ui)
280            fh.close()
281    return ret
282
283
284def cdm_cstyle(ui, repo, **opts):
285    '''check active C source files conform to the C Style Guide
286
287    See http://opensolaris.org/os/community/documentation/getting_started_docs/cstyle.ms.pdf'''
288
289    active = opts.get('active') or wslist[repo].active(opts['parent'])
290
291    ui.write('C style check:\n')
292
293    ret = 0
294
295    for entry in sorted(active):
296        if entry.is_removed():
297            continue
298
299        path = wslist[repo].filepath(entry.name)
300
301        if entry.name.endswith('.c') or entry.name.endswith('.h'):
302            fh = open(path, 'r')
303            ret |= CStyle.cstyle(fh, output=ui,
304                                 picky=True, check_posix_types=True,
305                                 check_continuation=True)
306            fh.close()
307    return ret
308
309
310def cdm_jstyle(ui, repo, **opts):
311    'check active Java source files for common stylistic errors'
312
313    active = opts.get('active') or wslist[repo].active(opts['parent'])
314
315    ui.write('Java style check:\n')
316
317    ret = 0
318
319    for entry in sorted(active):
320        if entry.is_removed():
321            continue
322
323        path = wslist[repo].filepath(entry.name)
324
325        if entry.name.endswith('.java'):
326            fh = open(path, 'r')
327            ret |= JStyle.jstyle(fh, output=ui, picky=True)
328            fh.close()
329    return ret
330
331
332def cdm_permchk(ui, repo, **opts):
333    '''check active files permission - warn +x (execute) mode'''
334
335    active = opts.get('active') or wslist[repo].active(opts['parent'])
336
337    ui.write('File permission check:\n')
338
339    exeFiles = []
340    for entry in sorted(active):
341        if entry.is_removed():
342            continue
343
344        path = wslist[repo].filepath(entry.name)
345
346        if active.localtip.manifest().execf(path):
347            exeFiles.append(path)
348
349    if len(exeFiles) > 0:
350        ui.write('Warning: the following active file(s) have executable mode '
351            '(+x) permission set,\nremove unless intentional:\n')
352        for fname in exeFiles:
353            ui.write("  %s\n" % fname)
354
355    return len(exeFiles) > 0
356
357
358def cdm_tagchk(ui, repo, **opts):
359    '''check if .hgtags is active and issue warning
360
361    Tag sharing among repositories is restricted to gatekeepers'''
362
363    active = opts.get('active') or wslist[repo].active(opts['parent'])
364
365    ui.write('Checking for new tags:\n')
366
367    if ".hgtags" in active:
368        tfile = wslist[repo].filepath('.hgtags')
369        ptip = active.parenttip.rev()
370
371        ui.write('Warning: Workspace contains new non-local tags.\n'
372                 'Only gatekeepers should add or modify such tags.\n'
373                 'Use the following commands to revert these changes:\n'
374                 '  hg revert -r%d %s\n'
375                 '  hg commit %s\n'
376                 'You should also recommit before integration\n' %
377                 (ptip, tfile, tfile))
378
379        return 1
380
381    return 0
382
383
384def cdm_branchchk(ui, repo, **opts):
385    '''check if multiple heads (or branches) are present, or if
386    branch changes are made'''
387
388    ui.write('Checking for multiple heads (or branches):\n')
389
390    heads = set(repo.heads())
391    parents = set([x.node() for x in repo.workingctx().parents()])
392
393    #
394    # We care if there's more than one head, and those heads aren't
395    # identical to the dirstate parents (if they are identical, it's
396    # an uncommitted merge which mergechk will catch, no need to
397    # complain twice).
398    #
399    if len(heads) > 1 and heads != parents:
400        ui.write('Workspace has multiple heads (or branches):\n')
401        for head in [repo.changectx(head) for head in heads]:
402            ui.write("  %d:%s\t%s\n" %
403                (head.rev(), str(head), head.description().splitlines()[0]))
404        ui.write('You must merge and recommit.\n')
405        return 1
406
407    ui.write('\nChecking for branch changes:\n')
408
409    if repo.dirstate.branch() != 'default':
410        ui.write("Warning: Workspace tip has named branch: '%s'\n"
411                 "Only gatekeepers should push new branches.\n"
412                 "Use the following commands to restore the branch name:\n"
413                 "  hg branch [-f] default\n"
414                 "  hg commit\n"
415                 "You should also recommit before integration\n" %
416                 (repo.dirstate.branch()))
417        return 1
418
419    branches = repo.branchtags().keys()
420    if len(branches) > 1:
421        ui.write('Warning: Workspace has named branches:\n')
422        for t in branches:
423            if t == 'default':
424                continue
425            ui.write("\t%s\n" % t)
426
427        ui.write("Only gatekeepers should push new branches.\n"
428                 "Use the following commands to remove extraneous branches.\n"
429                 "  hg branch [-f] default\n"
430                 "  hg commit"
431                 "You should also recommit before integration\n")
432        return 1
433
434    return 0
435
436
437def cdm_rtichk(ui, repo, **opts):
438    '''check active bug/RFEs for approved RTIs
439
440    Only works on SWAN.'''
441
442    if opts['nocheck']:
443        ui.status('Skipping RTI checks...\n')
444        return 0
445
446    if not onSWAN():
447        ui.write('RTI checks only work on SWAN, skipping...\n')
448        return 0
449
450    active = opts.get('active') or wslist[repo].active(opts['parent'])
451
452    ui.write('RTI check:\n')
453
454    bugs = []
455
456    for com in active.comments():
457        match = Comments.isBug(com)
458        if match and match.group(1) not in bugs:
459            bugs.append(match.group(1))
460
461    # RTI normalizes the gate path for us
462    return int(not Rti.rti(bugs, gatePath=opts['parent'], output=ui))
463
464
465def cdm_keywords(ui, repo, **opts):
466    '''check source files do not contain SCCS keywords'''
467
468    active = opts.get('active') or wslist[repo].active(opts['parent'])
469
470    ui.write('Keywords check:\n')
471
472    ret = 0
473    for entry in sorted(active):
474        if entry.is_removed():
475            continue
476
477        path = wslist[repo].filepath(entry.name)
478        fh = open(path, 'r')
479        ret |= Keywords.keywords(fh, output=ui)
480        fh.close()
481    return ret
482
483
484#
485# NB:
486#    There's no reason to hook this up as an invokable command, since
487#    we have 'hg status', but it must accept the same arguments.
488#
489def cdm_outchk(ui, repo, **opts):
490    '''Warn the user if they have uncommitted changes'''
491
492    ui.write('Checking for uncommitted changes:\n')
493
494    st = wslist[repo].modified()
495    if st:
496        ui.write('Warning: the following files have uncommitted changes:\n')
497        for elt in st:
498            ui.write('   %s\n' % elt)
499        return 1
500    return 0
501
502
503def cdm_mergechk(ui, repo, **opts):
504    '''Warn the user if their workspace contains merges'''
505
506    active = opts.get('active') or wslist[repo].active(opts['parent'])
507
508    ui.write('Checking for merges:\n')
509
510    merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1],
511                   active.revs)
512
513    if merges:
514        ui.write('Workspace contains the following merges:\n')
515        for rev in merges:
516            desc = rev.description().splitlines()
517            ui.write('  %s:%s\t%s\n' %
518                     (rev.rev() or "working", str(rev),
519                      desc and desc[0] or "*** uncommitted change ***"))
520        return 1
521    return 0
522
523
524def run_checks(ws, cmds, **opts):
525    '''Run CMDS (with OPTS) over active files in WS'''
526    active = ws.active(opts['parent'])
527
528    ret = 0
529
530    for cmd in cmds:
531        name = cmd.func_name.split('_')[1]
532        if not ws.ui.configbool('cdm', name, True):
533            ws.ui.status('Skipping %s check...\n' % name)
534        else:
535            ws.ui.pushbuffer()
536
537            result = cmd(ws.ui, ws.repo, active=active, **opts)
538            ret |= result
539
540            output = ws.ui.popbuffer()
541            if not ws.ui.quiet or result != 0:
542                ws.ui.write(output, '\n')
543    return ret
544
545
546def cdm_nits(ui, repo, **opts):
547    '''check for stylistic nits in active files
548
549    Run cddlchk, copyright, cstyle, hdrchk, jstyle, permchk, and
550    keywords checks.'''
551
552    cmds = [cdm_cddlchk,
553        cdm_copyright,
554        cdm_cstyle,
555        cdm_hdrchk,
556        cdm_jstyle,
557        cdm_permchk,
558        cdm_keywords]
559
560    return run_checks(wslist[repo], cmds, **opts)
561
562
563def cdm_pbchk(ui, repo, **opts):
564    '''pre-putback check all active files
565
566    Run cddlchk, comchk, copyright, cstyle, hdrchk, jstyle, permchk, tagchk,
567    branchchk, keywords and rtichk checks.  Additionally, warn about
568    uncommitted changes.'''
569
570    #
571    # The current ordering of these is that the commands from cdm_nits
572    # run first in the same order as they would in cdm_nits.  Then the
573    # pbchk specifics run
574    #
575    cmds = [cdm_cddlchk,
576        cdm_copyright,
577        cdm_cstyle,
578        cdm_hdrchk,
579        cdm_jstyle,
580        cdm_permchk,
581        cdm_keywords,
582        cdm_comchk,
583        cdm_tagchk,
584        cdm_branchchk,
585        cdm_rtichk,
586        cdm_outchk,
587        cdm_mergechk]
588
589    return run_checks(wslist[repo], cmds, **opts)
590
591
592def cdm_recommit(ui, repo, **opts):
593    '''compact outgoing deltas into a single, conglomerate delta'''
594
595    if not os.getcwd().startswith(repo.root):
596        raise util.Abort('recommit is not safe to run with -R')
597
598    if wslist[repo].modified():
599        raise util.Abort('workspace has uncommitted changes')
600
601    if wslist[repo].merged():
602        raise util.Abort('workspace contains uncommitted merge')
603
604    if wslist[repo].branched():
605        raise util.Abort('workspace contains uncommitted branch')
606
607    if wslist[repo].mq_applied():
608        raise util.Abort("workspace has Mq patches applied")
609
610    wlock = repo.wlock()
611    lock = repo.lock()
612
613    heads = repo.heads()
614    if len(heads) > 1:
615        ui.warn('Workspace has multiple heads (or branches):\n')
616        for head in heads:
617            ui.warn('\t%d\n' % repo.changelog.rev(head))
618        raise util.Abort('you must merge before recommitting')
619
620    active = wslist[repo].active(opts['parent'])
621
622    if len(active.revs) <= 0:
623        raise util.Abort("no changes to recommit")
624
625    if len(active.files()) <= 0:
626        ui.warn("Recommitting %d active changesets, but no active files\n" %
627                len(active.revs))
628
629    #
630    # During the course of a recommit, any file bearing a name matching the
631    # source name of any renamed file will be clobbered by the operation.
632    #
633    # As such, we ask the user before proceeding.
634    #
635    bogosity = [f.parentname for f in active if f.is_renamed() and
636                os.path.exists(repo.wjoin(f.parentname))]
637
638    if bogosity:
639        ui.warn("The following file names are the original name of a rename "
640                "and also present\n"
641                "in the working directory:\n")
642        for fname in bogosity:
643            ui.warn("  %s\n" % fname)
644        if not yes_no(ui, "These files will be removed by recommit.  Continue?",
645                      False):
646            raise util.Abort("recommit would clobber files")
647
648    user = opts['user'] or ui.username()
649
650    message = cmdutil.logmessage(opts) or ui.edit('\n'.join(active.comments()),
651                                                  user)
652    if not message:
653        raise util.Abort('empty commit message')
654
655    name = backup_name(repo.root)
656    bk = CdmBackup(ui, wslist[repo], name)
657    if bk.need_backup():
658        if yes_no(ui, 'Do you want to backup files first?', True):
659            bk.backup()
660
661    oldtags = repo.tags()
662    clearedtags = [(name, nd, repo.changelog.rev(nd), local)
663            for name, nd, local in active.tags()]
664
665    wslist[repo].squishdeltas(active, message, user=user)
666
667    if clearedtags:
668        ui.write("Removed tags:\n")
669        for name, nd, rev, local in clearedtags:
670            ui.write("  %s %s:%s%s\n" % (name, rev, node.short(nd),
671                                         (local and ' (local)') or ''))
672
673    for ntag, nnode in repo.tags().items():
674        if ntag in oldtags and ntag != "tip":
675            if oldtags[ntag] != nnode:
676                ui.write("tag %s now refers to revision %d:%s\n" %
677                         (ntag, repo.changelog.rev(nnode), node.short(nnode)))
678
679
680def do_eval(cmd, files, root, changedir=True):
681    if not changedir:
682        os.chdir(root)
683
684    for path in sorted(files):
685        dirn, base = os.path.split(path)
686
687        if changedir:
688            os.chdir(os.path.join(root, dirn))
689
690        os.putenv('workspace', root)
691        os.putenv('filepath', path)
692        os.putenv('dir', dirn)
693        os.putenv('file', base)
694        os.system(cmd)
695
696
697def cdm_eval(ui, repo, *command, **opts):
698    '''run cmd for each active file
699
700    cmd can refer to:
701      $file      -	active file basename.
702      $dir       -	active file dirname.
703      $filepath  -	path from workspace root to active file.
704      $workspace -	full path to workspace root.
705
706    For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last
707    the 3 log entries for each active file, preceded by its directory.'''
708
709    act = wslist[repo].active(opts['parent'])
710    cmd = ' '.join(command)
711    files = [x.name for x in act if not x.is_removed()]
712
713    do_eval(cmd, files, repo.root, not opts['remain'])
714
715
716def cdm_apply(ui, repo, *command, **opts):
717    '''apply cmd to all active files
718
719    For example 'hg apply wc -l' outputs a line count of active files.'''
720
721    act = wslist[repo].active(opts['parent'])
722
723    if opts['remain']:
724        appnd = ' $filepath'
725    else:
726        appnd = ' $file'
727
728    cmd = ' '.join(command) + appnd
729    files = [x.name for x in act if not x.is_removed()]
730
731    do_eval(cmd, files, repo.root, not opts['remain'])
732
733
734def cdm_reparent(ui, repo, parent):
735    '''reparent your workspace
736
737    Updates the 'default' path.'''
738
739    filename = repo.join('hgrc')
740
741    cp = util.configparser()
742    try:
743        cp.read(filename)
744    except ConfigParser.ParsingError, inst:
745        raise util.Abort('failed to parse %s\n%s' % (filename, inst))
746
747    try:
748        fh = open(filename, 'w')
749    except IOError, e:
750        raise util.Abort('Failed to open workspace configuration: %s' % e)
751
752    if not cp.has_section('paths'):
753        cp.add_section('paths')
754    cp.set('paths', 'default', parent)
755    cp.write(fh)
756    fh.close()
757
758
759def backup_name(fullpath):
760    '''Create a backup directory name based on the specified path.
761
762    In most cases this is the basename of the path specified, but
763    certain cases are handled specially to create meaningful names'''
764
765    special = ['usr/closed']
766
767    fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep)
768
769    #
770    # If a path is 'special', we append the basename of the path to
771    # the path element preceding the constant, special, part.
772    #
773    # Such that for instance:
774    #     /foo/bar/onnv-fixes/usr/closed
775    #  has a backup name of:
776    #     onnv-fixes-closed
777    #
778    for elt in special:
779        elt = elt.split(os.path.sep)
780        pathpos = len(elt)
781
782        if fullpath[-pathpos:] == elt:
783            return "%s-%s" % (fullpath[-pathpos - 1], elt[-1])
784    else:
785        return fullpath[-1]
786
787
788def cdm_backup(ui, repo, if_newer=False):
789    '''make backup copies of all workspace changes
790
791    Backups will be stored in ~/cdm.backup/<basename of workspace>.'''
792
793    name = backup_name(repo.root)
794    bk = CdmBackup(ui, wslist[repo], name)
795
796    if if_newer and not bk.need_backup():
797        ui.status('backup is up-to-date\n')
798    else:
799        bk.backup()
800
801
802def cdm_restore(ui, repo, backup, **opts):
803    '''restore workspace from backup
804
805    Restores a workspace from the specified backup directory and generation
806    (which defaults to the latest).'''
807
808    if not os.getcwd().startswith(repo.root):
809        raise util.Abort('restore is not safe to run with -R')
810    if wslist[repo].modified():
811        raise util.Abort('Workspace has uncommitted changes')
812    if wslist[repo].merged():
813        raise util.Abort('Workspace has an uncommitted merge')
814    if wslist[repo].branched():
815        raise util.Abort('Workspace has an uncommitted branch')
816
817    if opts['generation']:
818        gen = int(opts['generation'])
819    else:
820        gen = None
821
822    if os.path.exists(backup):
823        backup = os.path.abspath(backup)
824
825    bk = CdmBackup(ui, wslist[repo], backup)
826    bk.restore(gen)
827
828cmdtable = {
829    'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'),
830                          ('r', 'remain', None, 'do not change directories')],
831              'hg apply [-p PARENT] [-r] command...'),
832    'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')],
833             'hg arcs [-p PARENT]'),
834    '^backup|bu': (cdm_backup, [('t', 'if-newer', None,
835                             'only backup if workspace files are newer')],
836               'hg backup [-t]'),
837    'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')],
838                  'hg branchchk [-p PARENT]'),
839    'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')],
840             'hg bugs [-p PARENT]'),
841    'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')],
842                'hg cddlchk [-p PARENT]'),
843    'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'),
844                            ('N', 'nocheck', None,
845                             'do not compare comments with databases')],
846               'hg comchk [-p PARENT]'),
847    'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')],
848                 'hg comments [-p PARENT]'),
849    'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')],
850                  'hg copyright [-p PARENT]'),
851    'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')],
852               'hg cstyle [-p PARENT]'),
853    'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'),
854                        ('r', 'remain', None, 'do not change directories')],
855             'hg eval [-p PARENT] [-r] command...'),
856    'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')],
857               'hg hdrchk [-p PARENT]'),
858    'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')],
859               'hg jstyle [-p PARENT]'),
860    'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')],
861                 'hg keywords [-p PARENT]'),
862    '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'),
863                                ('r', 'removed', None, 'show removed files'),
864                                ('a', 'added', None, 'show added files'),
865                                ('m', 'modified', None, 'show modified files')],
866                    'hg list [-amrRu] [-p PARENT]'),
867    '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')],
868             'hg nits [-p PARENT]'),
869    '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'),
870                           ('N', 'nocheck', None, 'skip RTI check')],
871              'hg pbchk [-N] [-p PARENT]'),
872    'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')],
873                'hg permchk [-p PARENT]'),
874    '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace')],
875               'hg pdiffs [-p PARENT]'),
876    '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'),
877                                      ('f', 'force', None, 'force operation'),
878                                      ('m', 'message', '',
879                                       'use <text> as commit message'),
880                                      ('l', 'logfile', '',
881                                       'read commit message from file'),
882                                      ('u', 'user', '',
883                                       'record user as committer')],
884                       'hg recommit [-f] [-p PARENT]'),
885    'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')],
886                'hg renamed [-p PARENT]'),
887    'reparent': (cdm_reparent, [], 'hg reparent PARENT'),
888    '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')],
889                 'hg restore [-g GENERATION] BACKUP'),
890    'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'),
891                            ('N', 'nocheck', None, 'skip RTI check')],
892               'hg rtichk [-N] [-p PARENT]'),
893    'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')],
894               'hg tagchk [-p PARENT]'),
895}
896