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.
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.
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.
17 # Copyright 2009 Sun Microsystems, Inc. All rights reserved.
18 # Use is subject to license terms.
20 # Copyright 2008, 2011, Richard Lowe
31 dirstate (handled by CdmUncommittedBackup)
32 File containing dirstate nodeid (the changeset we need
33 to update the workspace to after applying the bundle).
34 This is the node to which the working copy changes
35 (see 'diff', below) will be applied if applicable.
37 bundle (handled by CdmCommittedBackup)
38 An Hg bundle containing outgoing committed changes.
40 nodes (handled by CdmCommittedBackup)
41 A text file listing the full (hex) nodeid of all nodes in
42 bundle, used by need_backup.
44 diff (handled by CdmUncommittedBackup)
45 A Git-formatted diff containing uncommitted changes.
47 renames (handled by CdmUncommittedBackup)
48 A list of renames in the working copy that have to be
49 applied manually, rather than by the diff.
51 metadata.tar.gz (handled by CdmMetadataBackup)
54 $SRCTOP/.hg/patches (Mq data)
56 clear.tar.gz (handled by CdmClearBackup)
58 copies of each modified or added file, as it is in
61 ... for each outgoing head
64 copies of each modified or added file in the
68 Newest backup generation.
70 All files in a given backup generation, with the exception of
71 dirstate, are optional.
74 import grp
, os
, pwd
, shutil
, tarfile
, time
, traceback
75 from cStringIO
import StringIO
77 from mercurial
import changegroup
, cmdutil
, error
, node
, patch
, util
78 from onbld
.Scm
import Version
81 class CdmNodeMissing(util
.Abort
):
82 '''a required node is not present in the destination workspace.
84 This may occur both in the case where the bundle contains a
85 changeset which is a child of a node not present in the
86 destination workspace (because the destination workspace is not as
87 up-to-date as the source), or because the source and destination
88 workspace are not related.
90 It may also happen in cases where the uncommitted changes need to
91 be applied onto a node that the workspace does not possess even
92 after application of the bundle (on a branch not present
93 in the bundle or destination workspace, for instance)'''
95 def __init__(self
, msg
, name
):
97 # If e.name is a string 20 characters long, it is
98 # assumed to be a node. (Mercurial makes this
99 # same assumption, when creating a LookupError)
101 if isinstance(name
, str) and len(name
) == 20:
106 util
.Abort
.__init
__(self
, "%s: changeset '%s' is missing\n"
107 "Your workspace is either not "
108 "sufficiently up to date,\n"
109 "or is unrelated to the workspace from "
110 "which the backup was taken.\n" % (msg
, n
))
113 class CdmTarFile(tarfile
.TarFile
):
114 '''Tar file access + simple comparison to the filesystem, and
115 creation addition of files from Mercurial filectx objects.'''
117 def __init__(self
, *args
, **kwargs
):
118 tarfile
.TarFile
.__init
__(self
, *args
, **kwargs
)
121 def members_match_fs(self
, rootpath
):
122 '''Compare the contents of the tar archive to the directory
123 specified by rootpath. Return False if they differ.
125 Every file in the archive must match the equivalent file in
128 The existence, modification time, and size of each file are
129 compared, content is not.'''
131 def _member_matches_fs(member
, rootpath
):
132 '''Compare a single member to its filesystem counterpart'''
133 fpath
= os
.path
.join(rootpath
, member
.name
)
135 if not os
.path
.exists(fpath
):
137 elif ((os
.path
.isfile(fpath
) != member
.isfile()) or
138 (os
.path
.isdir(fpath
) != member
.isdir()) or
139 (os
.path
.islink(fpath
) != member
.issym())):
143 # The filesystem may return a modification time with a
144 # fractional component (as a float), whereas the tar format
145 # only stores it to the whole second, perform the comparison
146 # using integers (truncated, not rounded)
148 elif member
.mtime
!= int(os
.path
.getmtime(fpath
)):
150 elif not member
.isdir() and member
.size
!= os
.path
.getsize(fpath
):
156 if not _member_matches_fs(elt
, rootpath
):
161 def addfilectx(self
, filectx
, path
=None):
162 '''Add a filectx object to the archive.
164 Use the path specified by the filectx object or, if specified,
167 The size, modification time, type and permissions of the tar
168 member are taken from the filectx object, user and group id
169 are those of the invoking user, user and group name are those
170 of the invoking user if information is available, or "unknown"
174 t
= tarfile
.TarInfo(path
or filectx
.path())
175 t
.size
= filectx
.size()
176 t
.mtime
= filectx
.date()[0]
181 t
.uname
= pwd
.getpwuid(t
.uid
).pw_name
186 t
.gname
= grp
.getgrgid(t
.gid
).gr_name
191 # Mercurial versions symlinks by setting a flag and storing
192 # the destination path in place of the file content. The
193 # actual contents (in the tar), should be empty.
195 if 'l' in filectx
.flags():
196 t
.type = tarfile
.SYMTYPE
198 t
.linkname
= filectx
.data()
201 t
.type = tarfile
.REGTYPE
202 t
.mode
= 'x' in filectx
.flags() and 0755 or 0644
203 data
= StringIO(filectx
.data())
205 self
.addfile(t
, data
)
208 class CdmCommittedBackup(object):
209 '''Backup of committed changes'''
211 def __init__(self
, backup
, ws
):
214 self
.files
= ('bundle', 'nodes')
216 def _outgoing_nodes(self
, parent
):
217 '''Return a list of all outgoing nodes in hex format'''
220 outgoing
= self
.ws
.findoutgoing(parent
)
221 nodes
= self
.ws
.repo
.changelog
.nodesbetween(outgoing
)[0]
222 return map(node
.hex, nodes
)
227 '''Backup committed changes'''
228 parent
= self
.ws
.parent()
231 self
.ws
.ui
.warn('Workspace has no parent, committed changes will '
232 'not be backed up\n')
235 out
= self
.ws
.findoutgoing(parent
)
239 cg
= self
.ws
.repo
.changegroup(out
, 'bundle')
240 changegroup
.writebundle(cg
, self
.bu
.backupfile('bundle'), 'HG10BZ')
242 outnodes
= self
._outgoing
_nodes
(parent
)
249 fp
= self
.bu
.open('nodes', 'w')
250 fp
.write('%s\n' % '\n'.join(outnodes
))
251 except EnvironmentError, e
:
252 raise util
.Abort("couldn't store outgoing nodes: %s" % e
)
254 if fp
and not fp
.closed
:
258 '''Restore committed changes from backup'''
260 if not self
.bu
.exists('bundle'):
263 bpath
= self
.bu
.backupfile('bundle')
267 f
= self
.bu
.open('bundle')
268 bundle
= changegroup
.readbundle(f
, bpath
)
269 self
.ws
.repo
.addchangegroup(bundle
, 'strip',
271 except EnvironmentError, e
:
272 raise util
.Abort("couldn't restore committed changes: %s\n"
274 except error
.LookupError, e
:
275 raise CdmNodeMissing("couldn't restore committed changes",
278 if f
and not f
.closed
:
281 def need_backup(self
):
282 '''Compare backup of committed changes to workspace'''
284 if self
.bu
.exists('nodes'):
288 f
= self
.bu
.open('nodes')
289 bnodes
= set(line
.rstrip('\r\n') for line
in f
.readlines())
291 except EnvironmentError, e
:
292 raise util
.Abort("couldn't open backup node list: %s" % e
)
294 if f
and not f
.closed
:
299 outnodes
= set(self
._outgoing
_nodes
(self
.ws
.parent()))
302 # If there are outgoing nodes not in the prior backup we need
303 # to take a new backup; it's fine if there are nodes in the
304 # old backup which are no longer outgoing, however.
306 if not outnodes
<= bnodes
:
312 '''Remove backed up committed changes'''
318 class CdmUncommittedBackup(object):
319 '''Backup of uncommitted changes'''
321 def __init__(self
, backup
, ws
):
324 self
.wctx
= self
.ws
.workingctx(worklist
=True)
326 def _clobbering_renames(self
):
327 '''Return a list of pairs of files representing renames/copies
328 that clobber already versioned files. [(old-name new-name)...]
332 # Note that this doesn't handle uncommitted merges
333 # as CdmUncommittedBackup itself doesn't.
335 parent
= self
.wctx
.parents()[0]
338 for fname
in self
.wctx
.added() + self
.wctx
.modified():
339 rn
= self
.wctx
.filectx(fname
).renamed()
340 if rn
and fname
in parent
:
341 ret
.append((rn
[0], fname
))
345 '''Backup uncommitted changes'''
348 raise util
.Abort("Unable to backup an uncommitted merge.\n"
349 "Please complete your merge and commit")
351 dirstate
= node
.hex(self
.wctx
.parents()[0].node())
356 fp
= self
.bu
.open('dirstate', 'w')
357 fp
.write(dirstate
+ '\n')
359 except EnvironmentError, e
:
360 raise util
.Abort("couldn't save working copy parent: %s" % e
)
363 fp
= self
.bu
.open('renames', 'w')
364 for cons
in self
._clobbering
_renames
():
365 fp
.write("%s %s\n" % cons
)
367 except EnvironmentError, e
:
368 raise util
.Abort("couldn't save clobbering copies: %s" % e
)
371 fp
= self
.bu
.open('diff', 'w')
372 match
= self
.ws
.matcher(files
=self
.wctx
.files())
373 fp
.write(self
.ws
.diff(opts
={'git': True}, match
=match
))
374 except EnvironmentError, e
:
375 raise util
.Abort("couldn't save working copy diff: %s" % e
)
377 if fp
and not fp
.closed
:
381 '''Return the desired working copy node from the backup'''
385 fp
= self
.bu
.open('dirstate')
386 dirstate
= fp
.readline().strip()
387 except EnvironmentError, e
:
388 raise util
.Abort("couldn't read saved parent: %s" % e
)
390 if fp
and not fp
.closed
:
396 '''Restore uncommitted changes'''
397 dirstate
= self
._dirstate
()
400 # Check that the patch's parent changeset exists.
403 n
= node
.bin(dirstate
)
404 self
.ws
.repo
.changelog
.lookup(n
)
405 except error
.LookupError, e
:
406 raise CdmNodeMissing("couldn't restore uncommitted changes",
410 self
.ws
.clean(rev
=dirstate
)
411 except util
.Abort
, e
:
412 raise util
.Abort("couldn't update to saved node: %s" % e
)
414 if not self
.bu
.exists('diff'):
418 # There's a race here whereby if the patch (or part thereof)
419 # is applied within the same second as the clean above (such
420 # that modification time doesn't change) and if the size of
421 # that file does not change, Hg may not see the change.
423 # We sleep a full second to avoid this, as sleeping merely
424 # until the next second begins would require very close clock
425 # synchronization on network filesystems.
431 diff
= self
.bu
.backupfile('diff')
433 fuzz
= patch
.patch(diff
, self
.ws
.ui
, strip
=1,
434 cwd
=self
.ws
.repo
.root
, files
=files
)
436 raise util
.Abort('working copy diff applied with fuzz')
438 raise util
.Abort("couldn't apply working copy diff: %s\n"
441 if Version
.at_least("1.7"):
442 cmdutil
.updatedir(self
.ws
.ui
, self
.ws
.repo
, files
)
444 patch
.updatedir(self
.ws
.ui
, self
.ws
.repo
, files
)
446 if not self
.bu
.exists('renames'):
450 # We need to re-apply name changes where the new name
451 # (rename/copy destination) is an already versioned file, as
452 # Hg would otherwise ignore them.
455 fp
= self
.bu
.open('renames')
457 source
, dest
= line
.strip().split()
458 self
.ws
.copy(source
, dest
)
459 except EnvironmentError, e
:
460 raise util
.Abort('unable to open renames file: %s' % e
)
462 raise util
.Abort('corrupt renames file: %s' %
463 self
.bu
.backupfile('renames'))
465 def need_backup(self
):
466 '''Compare backup of uncommitted changes to workspace'''
467 cnode
= self
.wctx
.parents()[0].node()
468 if self
._dirstate
() != node
.hex(cnode
):
472 match
= self
.ws
.matcher(files
=self
.wctx
.files())
473 curdiff
= self
.ws
.diff(opts
={'git': True}, match
=match
)
476 if self
.bu
.exists('diff'):
478 fd
= self
.bu
.open('diff')
481 except EnvironmentError, e
:
482 raise util
.Abort("couldn't open backup diff %s\n"
483 " %s" % (self
.bu
.backupfile('diff'), e
))
487 if backdiff
!= curdiff
:
490 currrenamed
= self
._clobbering
_renames
()
493 if self
.bu
.exists('renames'):
495 fd
= self
.bu
.open('renames')
496 bakrenamed
= [tuple(line
.strip().split(' ')) for line
in fd
]
498 except EnvironmentError, e
:
499 raise util
.Abort("couldn't open renames file %s: %s\n" %
500 (self
.bu
.backupfile('renames'), e
))
502 if currrenamed
!= bakrenamed
:
505 if fd
and not fd
.closed
:
511 '''Remove backed up uncommitted changes'''
513 for f
in ('dirstate', 'diff', 'renames'):
517 class CdmMetadataBackup(object):
518 '''Backup of workspace metadata'''
520 def __init__(self
, backup
, ws
):
523 self
.files
= ('hgrc', 'localtags', 'patches', 'cdm')
526 '''Backup workspace metadata'''
528 tarpath
= self
.bu
.backupfile('metadata.tar.gz')
531 # Files is a list of tuples (name, path), where name is as in
532 # self.files, and path is the absolute path.
534 files
= filter(lambda (name
, path
): os
.path
.exists(path
),
535 zip(self
.files
, map(self
.ws
.repo
.join
, self
.files
)))
541 tar
= CdmTarFile
.gzopen(tarpath
, 'w')
542 except (EnvironmentError, tarfile
.TarError
), e
:
543 raise util
.Abort("couldn't open %s for writing: %s" %
547 for name
, path
in files
:
550 except (EnvironmentError, tarfile
.TarError
), e
:
552 # tarfile.TarError doesn't include the tar member or file
553 # in question, so we have to do so ourselves.
555 if isinstance(e
, tarfile
.TarError
):
556 errstr
= "%s: %s" % (name
, e
)
560 raise util
.Abort("couldn't backup metadata to %s:\n"
561 " %s" % (tarpath
, errstr
))
565 def old_restore(self
):
566 '''Restore workspace metadata from an pre-tar backup'''
568 for fname
in self
.files
:
569 if self
.bu
.exists(fname
):
570 bfile
= self
.bu
.backupfile(fname
)
571 wfile
= self
.ws
.repo
.join(fname
)
574 shutil
.copy2(bfile
, wfile
)
575 except EnvironmentError, e
:
576 raise util
.Abort("couldn't restore metadata from %s:\n"
579 def tar_restore(self
):
580 '''Restore workspace metadata (from a tar-style backup)'''
582 if not self
.bu
.exists('metadata.tar.gz'):
585 tarpath
= self
.bu
.backupfile('metadata.tar.gz')
588 tar
= CdmTarFile
.gzopen(tarpath
)
589 except (EnvironmentError, tarfile
.TarError
), e
:
590 raise util
.Abort("couldn't open %s: %s" % (tarpath
, e
))
595 tar
.extract(elt
, path
=self
.ws
.repo
.path
)
596 except (EnvironmentError, tarfile
.TarError
), e
:
597 # Make sure the member name is in the exception message.
598 if isinstance(e
, tarfile
.TarError
):
599 errstr
= "%s: %s" % (elt
.name
, e
)
603 raise util
.Abort("couldn't restore metadata from %s:\n"
607 if tar
and not tar
.closed
:
611 '''Restore workspace metadata'''
613 if self
.bu
.exists('hgrc'):
619 '''Yield the repo-relative path to each file we operate on,
620 including each file within any affected directory'''
622 for elt
in self
.files
:
623 path
= self
.ws
.repo
.join(elt
)
625 if not os
.path
.exists(path
):
628 if os
.path
.isdir(path
):
629 for root
, dirs
, files
in os
.walk(path
, topdown
=True):
633 yield os
.path
.join(root
, f
)
637 def need_backup(self
):
638 '''Compare backed up workspace metadata to workspace'''
640 def strip_trailing_pathsep(pathname
):
641 '''Remove a possible trailing path separator from PATHNAME'''
642 return pathname
.endswith('/') and pathname
[:-1] or pathname
644 if self
.bu
.exists('metadata.tar.gz'):
645 tarpath
= self
.bu
.backupfile('metadata.tar.gz')
647 tar
= CdmTarFile
.gzopen(tarpath
)
648 except (EnvironmentError, tarfile
.TarError
), e
:
649 raise util
.Abort("couldn't open metadata tarball: %s\n"
650 " %s" % (tarpath
, e
))
652 if not tar
.members_match_fs(self
.ws
.repo
.path
):
656 tarnames
= map(strip_trailing_pathsep
, tar
.getnames())
661 repopath
= self
.ws
.repo
.path
662 if not repopath
.endswith('/'):
665 for path
in self
._walk
():
666 if path
.replace(repopath
, '', 1) not in tarnames
:
672 '''Remove backed up workspace metadata'''
673 self
.bu
.unlink('metadata.tar.gz')
676 class CdmClearBackup(object):
677 '''A backup (in tar format) of complete source files from every
680 Paths in the tarball are prefixed by the revision and node of the
681 head, or "working" for the working directory.
683 This is done purely for the benefit of the user, and as such takes
684 no part in restore or need_backup checking, restore always
685 succeeds, need_backup always returns False
688 def __init__(self
, backup
, ws
):
692 def _branch_pairs(self
):
693 '''Return a list of tuples (parenttip, localtip) for each
694 outgoing head. If the working copy contains modified files,
695 it is a head, and neither of its parents are.
698 parent
= self
.ws
.parent()
701 outgoing
= self
.ws
.findoutgoing(parent
)
702 outnodes
= set(self
.ws
.repo
.changelog
.nodesbetween(outgoing
)[0])
704 heads
= [self
.ws
.repo
.changectx(n
) for n
in self
.ws
.repo
.heads()
710 wctx
= self
.ws
.workingctx()
711 if wctx
.files(): # We only care about file changes.
712 heads
= filter(lambda x
: x
not in wctx
.parents(), heads
) + [wctx
]
716 if head
.rev() is None:
721 pairs
.append((self
.ws
.parenttip(c
, outnodes
), head
))
725 '''Save a clear copy of each source file modified between each
726 head and that head's parenttip (see WorkSpace.parenttip).
729 tarpath
= self
.bu
.backupfile('clear.tar.gz')
730 branches
= self
._branch
_pairs
()
736 tar
= CdmTarFile
.gzopen(tarpath
, 'w')
737 except (EnvironmentError, tarfile
.TarError
), e
:
738 raise util
.Abort("Could not open %s for writing: %s" %
742 for parent
, child
in branches
:
743 tpath
= child
.node() and node
.short(child
.node()) or "working"
745 for fname
, change
in self
.ws
.status(parent
, child
).iteritems():
746 if change
not in ('added', 'modified'):
750 tar
.addfilectx(child
.filectx(fname
),
751 os
.path
.join(tpath
, fname
))
752 except ValueError, e
:
755 crev
= "working copy"
756 raise util
.Abort("Could not backup clear file %s "
757 "from %s: %s\n" % (fname
, crev
, e
))
762 '''Cleanup a failed Clear backup.
764 Remove the clear tarball from the backup directory.
767 self
.bu
.unlink('clear.tar.gz')
770 '''Clear backups are never restored, do nothing'''
773 def need_backup(self
):
774 '''Clear backups are never compared, return False (no backup needed).
776 Should a backup actually be needed, one of the other
777 implementation classes would notice in any situation we would.
783 class CdmBackup(object):
784 '''A backup of a given workspace'''
786 def __init__(self
, ui
, ws
, name
):
789 self
.backupdir
= self
._find
_backup
_dir
(name
)
792 # The order of instances here controls the order the various operations
795 # There's some inherent dependence, in that on restore we need
796 # to restore committed changes prior to uncommitted changes
797 # (as the parent revision of any uncommitted changes is quite
798 # likely to not exist until committed changes are restored).
799 # Metadata restore can happen at any point, but happens last
800 # as a matter of convention.
802 self
.modules
= [x(self
, ws
) for x
in [CdmCommittedBackup
,
803 CdmUncommittedBackup
,
807 if os
.path
.exists(os
.path
.join(self
.backupdir
, 'latest')):
808 generation
= os
.readlink(os
.path
.join(self
.backupdir
, 'latest'))
809 self
.generation
= int(os
.path
.split(generation
)[1])
813 def _find_backup_dir(self
, name
):
814 '''Find the path to an appropriate backup directory based on NAME'''
816 if os
.path
.isabs(name
):
819 if self
.ui
.config('cdm', 'backupdir'):
820 backupbase
= os
.path
.expanduser(self
.ui
.config('cdm', 'backupdir'))
825 home
= os
.getenv('HOME') or pwd
.getpwuid(os
.getuid()).pw_dir
827 pass # Handled anyway
830 raise util
.Abort('Could not determine your HOME directory to '
833 backupbase
= os
.path
.join(home
, 'cdm.backup')
835 backupdir
= os
.path
.join(backupbase
, name
)
837 # If backupdir exists, it must be a directory.
838 if (os
.path
.exists(backupdir
) and not os
.path
.isdir(backupdir
)):
839 raise util
.Abort('%s exists but is not a directory' % backupdir
)
843 def _update_latest(self
, gen
):
844 '''Update latest symlink to point to the current generation'''
845 linkpath
= os
.path
.join(self
.backupdir
, 'latest')
847 if os
.path
.lexists(linkpath
):
850 os
.symlink(str(gen
), linkpath
)
852 def _create_gen(self
, gen
):
853 '''Create a new backup generation'''
855 os
.makedirs(os
.path
.join(self
.backupdir
, str(gen
)))
856 self
._update
_latest
(gen
)
857 except EnvironmentError, e
:
858 raise util
.Abort("Couldn't create backup generation %s: %s" %
859 (os
.path
.join(self
.backupdir
, str(gen
)), e
))
861 def backupfile(self
, path
):
862 '''return full path to backup file FILE at GEN'''
863 return os
.path
.join(self
.backupdir
, str(self
.generation
), path
)
865 def unlink(self
, name
):
866 '''Unlink the specified path from the backup directory.
867 A no-op if the path does not exist.
870 fpath
= self
.backupfile(name
)
871 if os
.path
.exists(fpath
):
874 def open(self
, name
, mode
='r'):
875 '''Open the specified file in the backup directory'''
876 return open(self
.backupfile(name
), mode
)
878 def exists(self
, name
):
879 '''Return boolean indicating wether a given file exists in the
881 return os
.path
.exists(self
.backupfile(name
))
883 def need_backup(self
):
884 '''Compare backed up changes to workspace'''
886 # If there's no current backup generation, or the last backup was
887 # invalid (lacking the dirstate file), we need a backup regardless
890 if not self
.generation
or not self
.exists('dirstate'):
893 for x
in self
.modules
:
900 '''Take a backup of the current workspace
902 Calling code is expected to hold both the working copy lock
903 and repository lock.'''
905 if not os
.path
.exists(self
.backupdir
):
907 os
.makedirs(self
.backupdir
)
908 except EnvironmentError, e
:
909 raise util
.Abort('Could not create backup directory %s: %s' %
913 self
._create
_gen
(self
.generation
)
916 for x
in self
.modules
:
919 if isinstance(e
, KeyboardInterrupt):
920 self
.ws
.ui
.warn("Interrupted\n")
922 self
.ws
.ui
.warn("Error: %s\n" % e
)
923 show_traceback
= self
.ws
.ui
.configbool('ui', 'traceback',
927 # If it's not a 'normal' error, we want to print a stack
928 # trace now in case the attempt to remove the partial
929 # backup also fails, and raises a second exception.
931 if (not isinstance(e
, (EnvironmentError, util
.Abort
))
933 traceback
.print_exc()
935 for x
in self
.modules
:
938 os
.rmdir(os
.path
.join(self
.backupdir
, str(self
.generation
)))
941 if self
.generation
!= 0:
942 self
._update
_latest
(self
.generation
)
944 os
.unlink(os
.path
.join(self
.backupdir
, 'latest'))
946 raise util
.Abort('Backup failed')
948 def restore(self
, gen
=None):
949 '''Restore workspace from backup
951 Restores from backup generation GEN (defaulting to the latest)
954 Calling code is expected to hold both the working copy lock
955 and repository lock of the destination workspace.'''
957 if not os
.path
.exists(self
.backupdir
):
958 raise util
.Abort('Backup directory does not exist: %s' %
962 if not os
.path
.exists(os
.path
.join(self
.backupdir
, str(gen
))):
963 raise util
.Abort('Backup generation does not exist: %s' %
964 (os
.path
.join(self
.backupdir
, str(gen
))))
965 self
.generation
= int(gen
)
967 if not self
.generation
: # This is OK, 0 is not a valid generation
968 raise util
.Abort('Backup has no generations: %s' % self
.backupdir
)
970 if not self
.exists('dirstate'):
971 raise util
.Abort('Backup %s/%s is incomplete (dirstate missing)' %
972 (self
.backupdir
, self
.generation
))
975 for x
in self
.modules
:
977 except util
.Abort
, e
:
978 raise util
.Abort('Error restoring workspace:\n'
980 'Workspace may be partially restored' % e
)