xref: /titanic_50/usr/src/tools/onbld/hgext/cdm.py (revision 65b180883d21463716cc4a2c6f5708a76eb0731e)
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    active = opts.get('active') or wslist[repo].active(opts['parent'])
389
390    ui.write('Checking for multiple heads (or branches):\n')
391
392    heads = set(repo.heads())
393    parents = set([x.node() for x in repo.workingctx().parents()])
394
395    #
396    # We care if there's more than one head, and those heads aren't
397    # identical to the dirstate parents (if they are identical, it's
398    # an uncommitted merge which mergechk will catch, no need to
399    # complain twice).
400    #
401    if len(heads) > 1 and heads != parents:
402        ui.write('Workspace has multiple heads (or branches):\n')
403        for head in [repo.changectx(head) for head in heads]:
404            ui.write("  %d:%s\t%s\n" %
405                (head.rev(), str(head), head.description().splitlines()[0]))
406        ui.write('You must merge and recommit.\n')
407        return 1
408
409    ui.write('\nChecking for branch changes:\n')
410
411    if active.localtip.branch() != 'default':
412        ui.write("Warning: Workspace tip has named branch: '%s'\n"
413                 "Only gatekeepers should push new branches.\n"
414                 "Use the following commands to restore the branch name:\n"
415                 "  hg branch [-f] default\n"
416                 "  hg commit\n"
417                 "You should also recommit before integration\n" %
418                 (active.localtip.branch()))
419        return 1
420
421    branches = repo.branchtags().keys()
422    if len(branches) > 1:
423        ui.write('Warning: Workspace has named branches:\n')
424        for t in branches:
425            if t == 'default':
426                continue
427            ui.write("\t%s\n" % t)
428
429        ui.write("Only gatekeepers should push new branches.\n"
430                 "Use the following commands to remove extraneous branches.\n"
431                 "  hg branch [-f] default\n"
432                 "  hg commit"
433                 "You should also recommit before integration\n")
434        return 1
435
436    return 0
437
438
439def cdm_rtichk(ui, repo, **opts):
440    '''check active bug/RFEs for approved RTIs
441
442    Only works on SWAN.'''
443
444    if opts['nocheck']:
445        ui.status('Skipping RTI checks...\n')
446        return 0
447
448    if not onSWAN():
449        ui.write('RTI checks only work on SWAN, skipping...\n')
450        return 0
451
452    active = opts.get('active') or wslist[repo].active(opts['parent'])
453
454    ui.write('RTI check:\n')
455
456    bugs = []
457
458    for com in active.comments():
459        match = Comments.isBug(com)
460        if match and match.group(1) not in bugs:
461            bugs.append(match.group(1))
462
463    # RTI normalizes the gate path for us
464    return int(not Rti.rti(bugs, gatePath=opts['parent'], output=ui))
465
466
467def cdm_keywords(ui, repo, **opts):
468    '''check source files do not contain SCCS keywords'''
469
470    active = opts.get('active') or wslist[repo].active(opts['parent'])
471
472    ui.write('Keywords check:\n')
473
474    ret = 0
475    for entry in sorted(active):
476        if entry.is_removed():
477            continue
478
479        path = wslist[repo].filepath(entry.name)
480        fh = open(path, 'r')
481        ret |= Keywords.keywords(fh, output=ui)
482        fh.close()
483    return ret
484
485
486#
487# NB:
488#    There's no reason to hook this up as an invokable command, since
489#    we have 'hg status', but it must accept the same arguments.
490#
491def cdm_outchk(ui, repo, **opts):
492    '''Warn the user if they have uncommitted changes'''
493
494    ui.write('Checking for uncommitted changes:\n')
495
496    st = wslist[repo].modified()
497    if st:
498        ui.write('Warning: the following files have uncommitted changes:\n')
499        for elt in st:
500            ui.write('   %s\n' % elt)
501        return 1
502    return 0
503
504
505def cdm_mergechk(ui, repo, **opts):
506    '''Warn the user if their workspace contains merges'''
507
508    active = opts.get('active') or wslist[repo].active(opts['parent'])
509
510    ui.write('Checking for merges:\n')
511
512    merges = filter(lambda x: len(x.parents()) == 2 and x.parents()[1],
513                   active.revs)
514
515    if merges:
516        ui.write('Workspace contains the following merges:\n')
517        for rev in merges:
518            desc = rev.description().splitlines()
519            ui.write('  %s:%s\t%s\n' %
520                     (rev.rev() or "working", str(rev),
521                      desc and desc[0] or "*** uncommitted change ***"))
522        return 1
523    return 0
524
525
526def run_checks(ws, cmds, **opts):
527    '''Run CMDS (with OPTS) over active files in WS'''
528    active = ws.active(opts['parent'])
529
530    ret = 0
531
532    for cmd in cmds:
533        name = cmd.func_name.split('_')[1]
534        if not ws.ui.configbool('cdm', name, True):
535            ws.ui.status('Skipping %s check...\n' % name)
536        else:
537            ws.ui.pushbuffer()
538
539            result = cmd(ws.ui, ws.repo, active=active, **opts)
540            ret |= result
541
542            output = ws.ui.popbuffer()
543            if not ws.ui.quiet or result != 0:
544                ws.ui.write(output, '\n')
545    return ret
546
547
548def cdm_nits(ui, repo, **opts):
549    '''check for stylistic nits in active files
550
551    Run cddlchk, copyright, cstyle, hdrchk, jstyle, permchk, and
552    keywords checks.'''
553
554    cmds = [cdm_cddlchk,
555        cdm_copyright,
556        cdm_cstyle,
557        cdm_hdrchk,
558        cdm_jstyle,
559        cdm_permchk,
560        cdm_keywords]
561
562    return run_checks(wslist[repo], cmds, **opts)
563
564
565def cdm_pbchk(ui, repo, **opts):
566    '''pre-putback check all active files
567
568    Run cddlchk, comchk, copyright, cstyle, hdrchk, jstyle, permchk, tagchk,
569    branchchk, keywords and rtichk checks.  Additionally, warn about
570    uncommitted changes.'''
571
572    #
573    # The current ordering of these is that the commands from cdm_nits
574    # run first in the same order as they would in cdm_nits.  Then the
575    # pbchk specifics run
576    #
577    cmds = [cdm_cddlchk,
578        cdm_copyright,
579        cdm_cstyle,
580        cdm_hdrchk,
581        cdm_jstyle,
582        cdm_permchk,
583        cdm_keywords,
584        cdm_comchk,
585        cdm_tagchk,
586        cdm_branchchk,
587        cdm_rtichk,
588        cdm_outchk,
589        cdm_mergechk]
590
591    return run_checks(wslist[repo], cmds, **opts)
592
593
594def cdm_recommit(ui, repo, **opts):
595    '''compact outgoing deltas into a single, conglomerate delta'''
596
597    if not os.getcwd().startswith(repo.root):
598        raise util.Abort('recommit is not safe to run with -R')
599
600    if wslist[repo].modified():
601        raise util.Abort('workspace has uncommitted changes')
602
603    if wslist[repo].merged():
604        raise util.Abort('workspace contains uncommitted merge')
605
606    if wslist[repo].branched():
607        raise util.Abort('workspace contains uncommitted branch')
608
609    if wslist[repo].mq_applied():
610        raise util.Abort("workspace has Mq patches applied")
611
612    wlock = repo.wlock()
613    lock = repo.lock()
614
615    heads = repo.heads()
616    if len(heads) > 1:
617        ui.warn('Workspace has multiple heads (or branches):\n')
618        for head in heads:
619            ui.warn('\t%d\n' % repo.changelog.rev(head))
620        raise util.Abort('you must merge before recommitting')
621
622    active = wslist[repo].active(opts['parent'])
623
624    if len(active.revs) <= 0:
625        raise util.Abort("no changes to recommit")
626
627    if len(active.files()) <= 0:
628        ui.warn("Recommitting %d active changesets, but no active files\n" %
629                len(active.revs))
630
631    #
632    # During the course of a recommit, any file bearing a name matching the
633    # source name of any renamed file will be clobbered by the operation.
634    #
635    # As such, we ask the user before proceeding.
636    #
637    bogosity = [f.parentname for f in active if f.is_renamed() and
638                os.path.exists(repo.wjoin(f.parentname))]
639
640    if bogosity:
641        ui.warn("The following file names are the original name of a rename "
642                "and also present\n"
643                "in the working directory:\n")
644        for fname in bogosity:
645            ui.warn("  %s\n" % fname)
646        if not yes_no(ui, "These files will be removed by recommit.  Continue?",
647                      False):
648            raise util.Abort("recommit would clobber files")
649
650    user = opts['user'] or ui.username()
651
652    message = cmdutil.logmessage(opts) or ui.edit('\n'.join(active.comments()),
653                                                  user)
654    if not message:
655        raise util.Abort('empty commit message')
656
657    name = backup_name(repo.root)
658    bk = CdmBackup(ui, wslist[repo], name)
659    if bk.need_backup():
660        if yes_no(ui, 'Do you want to backup files first?', True):
661            bk.backup()
662
663    oldtags = repo.tags()
664    clearedtags = [(name, nd, repo.changelog.rev(nd), local)
665            for name, nd, local in active.tags()]
666
667    wslist[repo].squishdeltas(active, message, user=user)
668
669    if clearedtags:
670        ui.write("Removed tags:\n")
671        for name, nd, rev, local in clearedtags:
672            ui.write("  %s %s:%s%s\n" % (name, rev, node.short(nd),
673                                         (local and ' (local)') or ''))
674
675    for ntag, nnode in repo.tags().items():
676        if ntag in oldtags and ntag != "tip":
677            if oldtags[ntag] != nnode:
678                ui.write("tag %s now refers to revision %d:%s\n" %
679                         (ntag, repo.changelog.rev(nnode), node.short(nnode)))
680
681
682def do_eval(cmd, files, root, changedir=True):
683    if not changedir:
684        os.chdir(root)
685
686    for path in sorted(files):
687        dirn, base = os.path.split(path)
688
689        if changedir:
690            os.chdir(os.path.join(root, dirn))
691
692        os.putenv('workspace', root)
693        os.putenv('filepath', path)
694        os.putenv('dir', dirn)
695        os.putenv('file', base)
696        os.system(cmd)
697
698
699def cdm_eval(ui, repo, *command, **opts):
700    '''run cmd for each active file
701
702    cmd can refer to:
703      $file      -	active file basename.
704      $dir       -	active file dirname.
705      $filepath  -	path from workspace root to active file.
706      $workspace -	full path to workspace root.
707
708    For example "hg eval 'echo $dir; hg log -l3 $file'" will show the last
709    the 3 log entries for each active file, preceded by its directory.'''
710
711    act = wslist[repo].active(opts['parent'])
712    cmd = ' '.join(command)
713    files = [x.name for x in act if not x.is_removed()]
714
715    do_eval(cmd, files, repo.root, not opts['remain'])
716
717
718def cdm_apply(ui, repo, *command, **opts):
719    '''apply cmd to all active files
720
721    For example 'hg apply wc -l' outputs a line count of active files.'''
722
723    act = wslist[repo].active(opts['parent'])
724
725    if opts['remain']:
726        appnd = ' $filepath'
727    else:
728        appnd = ' $file'
729
730    cmd = ' '.join(command) + appnd
731    files = [x.name for x in act if not x.is_removed()]
732
733    do_eval(cmd, files, repo.root, not opts['remain'])
734
735
736def cdm_reparent(ui, repo, parent):
737    '''reparent your workspace
738
739    Updates the 'default' path.'''
740
741    filename = repo.join('hgrc')
742
743    cp = util.configparser()
744    try:
745        cp.read(filename)
746    except ConfigParser.ParsingError, inst:
747        raise util.Abort('failed to parse %s\n%s' % (filename, inst))
748
749    try:
750        fh = open(filename, 'w')
751    except IOError, e:
752        raise util.Abort('Failed to open workspace configuration: %s' % e)
753
754    if not cp.has_section('paths'):
755        cp.add_section('paths')
756    cp.set('paths', 'default', parent)
757    cp.write(fh)
758    fh.close()
759
760
761def backup_name(fullpath):
762    '''Create a backup directory name based on the specified path.
763
764    In most cases this is the basename of the path specified, but
765    certain cases are handled specially to create meaningful names'''
766
767    special = ['usr/closed']
768
769    fullpath = fullpath.rstrip(os.path.sep).split(os.path.sep)
770
771    #
772    # If a path is 'special', we append the basename of the path to
773    # the path element preceding the constant, special, part.
774    #
775    # Such that for instance:
776    #     /foo/bar/onnv-fixes/usr/closed
777    #  has a backup name of:
778    #     onnv-fixes-closed
779    #
780    for elt in special:
781        elt = elt.split(os.path.sep)
782        pathpos = len(elt)
783
784        if fullpath[-pathpos:] == elt:
785            return "%s-%s" % (fullpath[-pathpos - 1], elt[-1])
786    else:
787        return fullpath[-1]
788
789
790def cdm_backup(ui, repo, if_newer=False):
791    '''make backup copies of all workspace changes
792
793    Backups will be stored in ~/cdm.backup/<basename of workspace>.'''
794
795    name = backup_name(repo.root)
796    bk = CdmBackup(ui, wslist[repo], name)
797
798    if if_newer and not bk.need_backup():
799        ui.status('backup is up-to-date\n')
800    else:
801        bk.backup()
802
803
804def cdm_restore(ui, repo, backup, **opts):
805    '''restore workspace from backup
806
807    Restores a workspace from the specified backup directory and generation
808    (which defaults to the latest).'''
809
810    if not os.getcwd().startswith(repo.root):
811        raise util.Abort('restore is not safe to run with -R')
812    if wslist[repo].modified():
813        raise util.Abort('Workspace has uncommitted changes')
814    if wslist[repo].merged():
815        raise util.Abort('Workspace has an uncommitted merge')
816    if wslist[repo].branched():
817        raise util.Abort('Workspace has an uncommitted branch')
818
819    if opts['generation']:
820        gen = int(opts['generation'])
821    else:
822        gen = None
823
824    if os.path.exists(backup):
825        backup = os.path.abspath(backup)
826
827    bk = CdmBackup(ui, wslist[repo], backup)
828    bk.restore(gen)
829
830cmdtable = {
831    'apply': (cdm_apply, [('p', 'parent', '', 'parent workspace'),
832                          ('r', 'remain', None, 'do not change directories')],
833              'hg apply [-p PARENT] [-r] command...'),
834    'arcs': (cdm_arcs, [('p', 'parent', '', 'parent workspace')],
835             'hg arcs [-p PARENT]'),
836    '^backup|bu': (cdm_backup, [('t', 'if-newer', None,
837                             'only backup if workspace files are newer')],
838               'hg backup [-t]'),
839    'branchchk': (cdm_branchchk, [('p', 'parent', '', 'parent workspace')],
840                  'hg branchchk [-p PARENT]'),
841    'bugs': (cdm_bugs, [('p', 'parent', '', 'parent workspace')],
842             'hg bugs [-p PARENT]'),
843    'cddlchk': (cdm_cddlchk, [('p', 'parent', '', 'parent workspace')],
844                'hg cddlchk [-p PARENT]'),
845    'comchk': (cdm_comchk, [('p', 'parent', '', 'parent workspace'),
846                            ('N', 'nocheck', None,
847                             'do not compare comments with databases')],
848               'hg comchk [-p PARENT]'),
849    'comments': (cdm_comments, [('p', 'parent', '', 'parent workspace')],
850                 'hg comments [-p PARENT]'),
851    'copyright': (cdm_copyright, [('p', 'parent', '', 'parent workspace')],
852                  'hg copyright [-p PARENT]'),
853    'cstyle': (cdm_cstyle, [('p', 'parent', '', 'parent workspace')],
854               'hg cstyle [-p PARENT]'),
855    'eval': (cdm_eval, [('p', 'parent', '', 'parent workspace'),
856                        ('r', 'remain', None, 'do not change directories')],
857             'hg eval [-p PARENT] [-r] command...'),
858    'hdrchk': (cdm_hdrchk, [('p', 'parent', '', 'parent workspace')],
859               'hg hdrchk [-p PARENT]'),
860    'jstyle': (cdm_jstyle, [('p', 'parent', '', 'parent workspace')],
861               'hg jstyle [-p PARENT]'),
862    'keywords': (cdm_keywords, [('p', 'parent', '', 'parent workspace')],
863                 'hg keywords [-p PARENT]'),
864    '^list|active': (cdm_list, [('p', 'parent', '', 'parent workspace'),
865                                ('r', 'removed', None, 'show removed files'),
866                                ('a', 'added', None, 'show added files'),
867                                ('m', 'modified', None, 'show modified files')],
868                    'hg list [-amrRu] [-p PARENT]'),
869    '^nits': (cdm_nits, [('p', 'parent', '', 'parent workspace')],
870             'hg nits [-p PARENT]'),
871    '^pbchk': (cdm_pbchk, [('p', 'parent', '', 'parent workspace'),
872                           ('N', 'nocheck', None, 'skip RTI check')],
873              'hg pbchk [-N] [-p PARENT]'),
874    'permchk': (cdm_permchk, [('p', 'parent', '', 'parent workspace')],
875                'hg permchk [-p PARENT]'),
876    '^pdiffs': (cdm_pdiffs, [('p', 'parent', '', 'parent workspace')],
877               'hg pdiffs [-p PARENT]'),
878    '^recommit|reci': (cdm_recommit, [('p', 'parent', '', 'parent workspace'),
879                                      ('f', 'force', None, 'force operation'),
880                                      ('m', 'message', '',
881                                       'use <text> as commit message'),
882                                      ('l', 'logfile', '',
883                                       'read commit message from file'),
884                                      ('u', 'user', '',
885                                       'record user as committer')],
886                       'hg recommit [-f] [-p PARENT]'),
887    'renamed': (cdm_renamed, [('p', 'parent', '', 'parent workspace')],
888                'hg renamed [-p PARENT]'),
889    'reparent': (cdm_reparent, [], 'hg reparent PARENT'),
890    '^restore': (cdm_restore, [('g', 'generation', '', 'generation number')],
891                 'hg restore [-g GENERATION] BACKUP'),
892    'rtichk': (cdm_rtichk, [('p', 'parent', '', 'parent workspace'),
893                            ('N', 'nocheck', None, 'skip RTI check')],
894               'hg rtichk [-N] [-p PARENT]'),
895    'tagchk': (cdm_tagchk, [('p', 'parent', '', 'parent workspace')],
896               'hg tagchk [-p PARENT]'),
897}
898