diff --git a/hgext3rd/undo.py b/hgext3rd/undo.py --- a/hgext3rd/undo.py +++ b/hgext3rd/undo.py @@ -6,7 +6,6 @@ # GNU General Public License version 2 or any later version. from __future__ import absolute_import -import os from mercurial.i18n import _ @@ -14,11 +13,13 @@ dispatch, error, extensions, + lock as lockmod, registrar, revlog, revset, revsetlang, smartset, + transaction, util, ) @@ -31,6 +32,11 @@ cmdtable = {} command = registrar.command(cmdtable) +# Setup + +def extsetup(ui): + extensions.wrapfunction(dispatch, 'runcommand', _runcommandwrapper) + # Wrappers def _runcommandwrapper(orig, lui, repo, cmd, fullargs, *args): @@ -42,7 +48,7 @@ # Check wether undolog is consistent # ie check wether the undo ext was # off before this command - safelog(repo, "") + safelog(repo, [""]) result = orig(lui, repo, cmd, fullargs, *args) @@ -53,80 +59,127 @@ # Write: Log control def safelog(repo, command): + '''boilerplate for log command + + input: + repo: mercurial.localrepo + command: list of strings, first is string of command run + output: bool + True if changes have been recorded, False otherwise + ''' + changes = False if repo is not None:# some hg commands don't require repo - with repo.lock(): - with repo.transaction("undolog"): - log(repo.filtered('visible'), command) + # undolog specific lock + # allows running command during other commands when + # otherwise legal. Could cause weird undolog states, + # which gap handling generally covers. TODO: edge case + # handling here could be refined. To this end, we should + # log to scuba when getting this lock fails + with lockmod.lock(repo.vfs, "undologlock", + desc="recording undo info"): + # developer config: undo._duringundologlock + if repo.ui.configbool('undo', '_duringundologlock'): + repo.hook("duringundologlock") + tr = lighttransaction(repo) + with tr: + changes = log(repo.filtered('visible'), command, tr) + return changes -def log(repo, command): +def lighttransaction(repo): + # full fledged transactions have two serious issues: + # 1. they may cause infite loops through hooks + # that run commands + # 2. they are really expensive performance wise + # + # ligtthransaction avoids certain hooks from being + # executed, doesn't check repo locks, doesn't check + # abandoned tr's (since we only record info) and doesn't + # do any tag handling + vfsmap = {'plain': repo.vfs} + tr = transaction.transaction(repo.ui.warn, repo.svfs, vfsmap, + "_undojournal", "_undolog") + return tr + +def log(repo, command, tr): + '''logs data neccesary for undo if repo state has changed + + input: + repo: mercurial.localrepo + command: los, first is command to be recorded as run + tr: transaction + output: bool + True if changes recorded + False if no changes to record + ''' newnodes = { - 'bookmarks': _logbookmarks(repo), - 'draftheads': _logdraftheads(repo), - 'workingparent': _logworkingparent(repo), + 'bookmarks': _logbookmarks(repo, tr), + 'draftheads': _logdraftheads(repo, tr), + 'workingparent': _logworkingparent(repo, tr), } try: exsistingnodes = _readindex(repo, 0) except IndexError: exsistingnodes = {} if all(newnodes.get(x) == exsistingnodes.get(x) for x in newnodes.keys()): - return + # no changes to record + return False else: newnodes.update({ - 'date': _logdate(repo), - 'command': _logcommand(repo, command), + 'date': _logdate(repo, tr), + 'command': _logcommand(repo, tr, command), }) - _logindex(repo, newnodes) + _logindex(repo, tr, newnodes) + # changes have been recorded + return True # Write: Logs -def writelog(repo, name, revstring): - assert repo.currenttransaction() is not None - # The transaction code doesn't work with vfs +def writelog(repo, tr, name, revstring): + if tr is None: + raise error.ProgrammingError + # the transaction code doesn't work with vfs # specifically, repo.recover() assumes svfs? repo.svfs.makedirs('undolog') - path = os.path.join('undolog', name) - rlog = revlog.revlog(repo.svfs, path) - tr = repo.currenttransaction() + rlog = _getrevlog(repo, name) node = rlog.addrevision(revstring, tr, 1, nullid, nullid) return hex(node) -def _logdate(repo): +def _logdate(repo, tr): revstring = " ".join(str(x) for x in util.makedate()) - return writelog(repo, "date.i", revstring) + return writelog(repo, tr, "date.i", revstring) -def _logdraftheads(repo): +def _logdraftheads(repo, tr): revs = repo.revs('heads(draft())') tonode = repo.changelog.node hexnodes = [hex(tonode(x)) for x in revs] revstring = "\n".join(sorted(hexnodes)) - return writelog(repo, "draftheads.i", revstring) + return writelog(repo, tr, "draftheads.i", revstring) -def _logcommand(repo, command): +def _logcommand(repo, tr, command): revstring = "\0".join(command) - return writelog(repo, "command.i", revstring) + return writelog(repo, tr, "command.i", revstring) -def _logbookmarks(repo): +def _logbookmarks(repo, tr): revstring = "\n".join(sorted('%s %s' % (name, hex(node)) for name, node in repo._bookmarks.iteritems())) - return writelog(repo, "bookmarks.i", revstring) + return writelog(repo, tr, "bookmarks.i", revstring) -def _logworkingparent(repo): +def _logworkingparent(repo, tr): revstring = repo['.'].hex() - return writelog(repo, "workingparent.i", revstring) + return writelog(repo, tr, "workingparent.i", revstring) -def _logindex(repo, nodes): +def _logindex(repo, tr, nodes): revstring = "\n".join(sorted('%s %s' % (k, v) for k, v in nodes.items())) - return writelog(repo, "index.i", revstring) + return writelog(repo, tr, "index.i", revstring) # Read def _readindex(repo, reverseindex, prefetchedrevlog=None): if prefetchedrevlog is None: - path = os.path.join('undolog', 'index.i') - rlog = revlog.revlog(repo.svfs, path) + rlog = _getrevlog(repo, 'index.i') else: rlog = prefetchedrevlog - index = len(rlog) - reverseindex - 1 + index = _invertindex(rlog, reverseindex) if index < 0 or index > len(rlog) - 1: raise IndexError chunk = rlog.revision(index) @@ -138,8 +191,7 @@ return indexdict def _readnode(repo, filename, hexnode): - path = os.path.join('undolog', filename) - rlog = revlog.revlog(repo.svfs, path) + rlog = _getrevlog(repo, filename) return rlog.revision(bin(hexnode)) # Visualize @@ -154,19 +206,19 @@ ('n', 'index', 0, _("details about specific operation")), ('l', 'list', False, _("list recent undo-able operation")) ]) -def debugundohistory(ui, repo, *args, **kwargs): +def debugundohistory(ui, repo, *args, **opts): """ Print operational history 0 is the most recent operation """ if repo is not None: - if kwargs.get('list'): + if opts.get('list'): if args and args[0].isdigit(): offset = int(args[0]) else: offset = 0 _debugundolist(ui, repo, offset) else: - reverseindex = kwargs.get('index') + reverseindex = opts.get('index') if 0 == reverseindex and args and args[0].isdigit(): reverseindex = int(args[0]) _debugundoindex(ui, repo, reverseindex) @@ -176,9 +228,7 @@ template = "{sub('\0', ' ', undo)}\n" fm = ui.formatter('debugundohistory', {'template': template}) - path = os.path.join('undolog', 'index.i') - - prefetchedrevlog = revlog.revlog(repo.svfs, path) + prefetchedrevlog = _getrevlog(repo, 'index.i') recentrange = min(5, len(prefetchedrevlog) - offset) if 0 == recentrange: fm.startitem() @@ -186,6 +236,8 @@ for i in range(recentrange): nodedict = _readindex(repo, i + offset, prefetchedrevlog) commandstr = _readnode(repo, 'command.i', nodedict['command']) + if "" == commandstr: + commandstr = " -- gap in log -- " fm.startitem() fm.write('undo', '%s', str(i + offset) + ": " + commandstr) fm.end() @@ -211,7 +263,7 @@ try: oldnodes = _readindex(repo, reverseindex + 1) oldheads = _readnode(repo, filename, oldnodes[filename[:-2]]) - except IndexError:# Index is oldest log + except IndexError: # index is oldest log content = rawcontent else: content = "ADDED:\n\t" + "\n\t".join(sorted( @@ -259,7 +311,11 @@ revs = _getolddrafts(repo, reverseindex) return smartset.baseset(revs) -# Setup +# Tools -def extsetup(ui): - extensions.wrapfunction(dispatch, 'runcommand', _runcommandwrapper) +def _invertindex(rlog, indexorreverseindex): + return len(rlog) - 1 - indexorreverseindex + +def _getrevlog(repo, filename): + path = 'undolog/' + filename + return revlog.revlog(repo.svfs, path) diff --git a/tests/test-undo.t b/tests/test-undo.t --- a/tests/test-undo.t +++ b/tests/test-undo.t @@ -1,6 +1,8 @@ $ cat >> $HGRCPATH < [extensions] > undo = $TESTDIR/../hgext3rd/undo.py + > [undo] + > _duringundologlock=1 > EOF Build up a repo @@ -121,7 +123,7 @@ $ touch a5 && hg add a5 && hg ci -ma5 $ hg debugundohistory -l 0: ci -ma5 - 1: + 1: -- gap in log -- 2: commit -m words 3: update master 4: commit --amend @@ -148,7 +150,7 @@ [255] Revset tests - $ hg log -G -r 'draft()' --hidden >> c1 + $ hg log -G -r 'draft()' --hidden > /dev/null $ hg debugundohistory -n 0 command: ci -ma5 @@ -171,3 +173,9 @@ @ 8[tip][master] aa430c8afedf 1970-01-01 00:00 +0000 test | a5 ~ + +Test undolog lock + $ hg log --config hooks.duringundologlock="sleep 1" > /dev/null & + $ sleep 0.1 + $ hg st --time + time: real [1-9]*\..* (re)