diff --git a/mercurial/transaction.py b/mercurial/transaction.py --- a/mercurial/transaction.py +++ b/mercurial/transaction.py @@ -12,8 +12,8 @@ # GNU General Public License version 2 or any later version. from i18n import _ -import errno -import error +import errno, os +import error, util def active(func): def _active(self, *args, **kwds): @@ -23,7 +23,7 @@ def active(func): return func(self, *args, **kwds) return _active -def _playback(journal, report, opener, entries, unlink=True): +def _playback(journal, report, opener, entries, backupentries, unlink=True): for f, o, ignore in entries: if o or not unlink: try: @@ -39,7 +39,24 @@ def _playback(journal, report, opener, e except (IOError, OSError), inst: if inst.errno != errno.ENOENT: raise + + backupfiles = [] + for f, b, ignore in backupentries: + filepath = opener.join(f) + backuppath = opener.join(b) + try: + util.copyfile(backuppath, filepath) + backupfiles.append(b) + except IOError: + report(_("failed to recover %s\n") % f) + raise + opener.unlink(journal) + backuppath = "%s.backupfiles" % journal + if opener.exists(backuppath): + opener.unlink(backuppath) + for f in backupfiles: + opener.unlink(f) class transaction(object): def __init__(self, report, opener, journal, after=None, createmode=None, @@ -64,13 +81,18 @@ class transaction(object): self.onclose = onclose self.onabort = onabort self.entries = [] + self.backupentries = [] self.map = {} + self.backupmap = {} self.journal = journal self._queue = [] + self.backupjournal = "%s.backupfiles" % journal self.file = opener.open(self.journal, "w") + self.backupsfile = opener.open(self.backupjournal, 'w') if createmode is not None: opener.chmod(self.journal, createmode & 0666) + opener.chmod(self.backupjournal, createmode & 0666) def __del__(self): if self.journal: @@ -78,22 +100,36 @@ class transaction(object): @active def startgroup(self): - self._queue.append([]) + self._queue.append(([], [])) @active def endgroup(self): q = self._queue.pop() - d = ''.join(['%s\0%d\n' % (x[0], x[1]) for x in q]) - self.entries.extend(q) + self.entries.extend(q[0]) + self.backupentries.extend(q[1]) + + offsets = [] + backups = [] + for f, o, _ in q[0]: + offsets.append((f, o)) + + for f, b, _ in q[1]: + backups.append((f, b)) + + d = ''.join(['%s\0%d\n' % (f, o) for f, o in offsets]) self.file.write(d) self.file.flush() + d = ''.join(['%s\0%s\0' % (f, b) for f, b in backups]) + self.backupsfile.write(d) + self.backupsfile.flush() + @active def add(self, file, offset, data=None): - if file in self.map: + if file in self.map or file in self.backupmap: return if self._queue: - self._queue[-1].append((file, offset, data)) + self._queue[-1][0].append((file, offset, data)) return self.entries.append((file, offset, data)) @@ -103,9 +139,43 @@ class transaction(object): self.file.flush() @active + def addbackup(self, file, hardlink=True): + """Adds a backup of the file to the transaction + + Calling addbackup() creates a hardlink backup of the specified file + that is used to recover the file in the event of the transaction + aborting. + + * `file`: the file path, relative to .hg/store + * `hardlink`: use a hardlink to quickly create the backup + """ + + if file in self.map or file in self.backupmap: + return + backupfile = "journal.%s" % file + if self.opener.exists(file): + filepath = self.opener.join(file) + backuppath = self.opener.join(backupfile) + util.copyfiles(filepath, backuppath, hardlink=hardlink) + else: + self.add(file, 0) + return + + if self._queue: + self._queue[-1][1].append((file, backupfile)) + return + + self.backupentries.append((file, backupfile, None)) + self.backupmap[file] = len(self.backupentries) - 1 + self.backupsfile.write("%s\0%s\0" % (file, backupfile)) + self.backupsfile.flush() + + @active def find(self, file): if file in self.map: return self.entries[self.map[file]] + if file in self.backupmap: + return self.backupentries[self.backupmap[file]] return None @active @@ -153,6 +223,11 @@ class transaction(object): self.after() if self.opener.isfile(self.journal): self.opener.unlink(self.journal) + if self.opener.isfile(self.backupjournal): + self.opener.unlink(self.backupjournal) + for f, b, _ in self.backupentries: + self.opener.unlink(b) + self.backupentries = [] self.journal = None @active @@ -171,16 +246,18 @@ class transaction(object): self.onabort() try: - if not self.entries: + if not self.entries and not self.backupentries: if self.journal: self.opener.unlink(self.journal) + if self.backupjournal: + self.opener.unlink(self.backupjournal) return self.report(_("transaction abort!\n")) try: _playback(self.journal, self.report, self.opener, - self.entries, False) + self.entries, self.backupentries, False) self.report(_("rollback completed\n")) except Exception: self.report(_("rollback failed - please run hg recover\n")) @@ -189,7 +266,19 @@ class transaction(object): def rollback(opener, file, report): + """Rolls back the transaction contained in the given file + + Reads the entries in the specified file, and the corresponding + '*.backupfiles' file, to recover from an incomplete transaction. + + * `file`: a file containing a list of entries, specifying where + to truncate each file. The file should contain a list of + file\0offset pairs, delimited by newlines. The corresponding + '*.backupfiles' file should contain a list of file\0backupfile + pairs, delimited by \0. + """ entries = [] + backupentries = [] fp = opener.open(file) lines = fp.readlines() @@ -201,4 +290,14 @@ def rollback(opener, file, report): except ValueError: report(_("couldn't read journal entry %r!\n") % l) - _playback(file, report, opener, entries) + backupjournal = "%s.backupfiles" % file + if opener.exists(backupjournal): + fp = opener.open(backupjournal) + data = fp.read() + if len(data) > 0: + parts = data.split('\0') + for i in xrange(0, len(parts), 2): + f, b = parts[i:i + 1] + backupentries.append((f, b, None)) + + _playback(file, report, opener, entries, backupentries)