mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 01:07:15 +03:00
683f74bc3b
Adds undo command that changes working copy parent, bookmarks and visible draft commits to a previous repo state. Differential Revision: https://phab.mercurial-scm.org/D51
406 lines
13 KiB
Python
406 lines
13 KiB
Python
# undo.py: records data in revlog for future undo functionality
|
|
#
|
|
# Copyright 2017 Facebook, Inc.
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
from __future__ import absolute_import
|
|
|
|
from mercurial.i18n import _
|
|
|
|
from mercurial import (
|
|
cmdutil,
|
|
dispatch,
|
|
error,
|
|
extensions,
|
|
localrepo,
|
|
lock as lockmod,
|
|
obsolete,
|
|
registrar,
|
|
revlog,
|
|
revset,
|
|
revsetlang,
|
|
smartset,
|
|
transaction,
|
|
util,
|
|
)
|
|
|
|
from mercurial.node import (
|
|
bin,
|
|
hex,
|
|
nullid,
|
|
)
|
|
|
|
cmdtable = {}
|
|
command = registrar.command(cmdtable)
|
|
|
|
# Setup
|
|
|
|
def extsetup(ui):
|
|
extensions.wrapfunction(dispatch, 'runcommand', _runcommandwrapper)
|
|
|
|
# undo has its own locking, whitelist itself to bypass repo lock audit
|
|
localrepo.localrepository._wlockfreeprefix.add('undolog/')
|
|
|
|
# Wrappers
|
|
|
|
def _runcommandwrapper(orig, lui, repo, cmd, fullargs, *args):
|
|
# This wrapper executes whenever a command is run.
|
|
# Some commands (eg hg sl) don't actually modify anything
|
|
# ie can't be undone, but the command doesn't know this.
|
|
command = fullargs
|
|
|
|
# Check wether undolog is consistent
|
|
# ie check wether the undo ext was
|
|
# off before this command
|
|
safelog(repo, [""])
|
|
|
|
result = orig(lui, repo, cmd, fullargs, *args)
|
|
|
|
# record changes to repo
|
|
safelog(repo, command)
|
|
return result
|
|
|
|
# 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
|
|
# undolog specific lock
|
|
# allows running command during other commands when
|
|
# otherwise legal. Could cause weird undolog states,
|
|
# which gap handling generally covers.
|
|
repo.vfs.makedirs('undolog')
|
|
with lockmod.lock(repo.vfs, "undolog/lock", desc="undolog"):
|
|
# 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 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.vfs, vfsmap,
|
|
"undolog/tr.journal", "undolog/tr.undo")
|
|
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, 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()):
|
|
# no changes to record
|
|
return False
|
|
else:
|
|
newnodes.update({
|
|
'date': _logdate(repo, tr),
|
|
'command': _logcommand(repo, tr, command),
|
|
})
|
|
_logindex(repo, tr, newnodes)
|
|
# changes have been recorded
|
|
return True
|
|
|
|
# Write: Logs
|
|
|
|
def writelog(repo, tr, name, revstring):
|
|
if tr is None:
|
|
raise error.ProgrammingError
|
|
rlog = _getrevlog(repo, name)
|
|
node = rlog.addrevision(revstring, tr, 1, nullid, nullid)
|
|
return hex(node)
|
|
|
|
def _logdate(repo, tr):
|
|
revstring = " ".join(str(x) for x in util.makedate())
|
|
return writelog(repo, tr, "date.i", revstring)
|
|
|
|
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, tr, "draftheads.i", revstring)
|
|
|
|
def _logcommand(repo, tr, command):
|
|
revstring = "\0".join(command)
|
|
return writelog(repo, tr, "command.i", revstring)
|
|
|
|
def _logbookmarks(repo, tr):
|
|
revstring = "\n".join(sorted('%s %s' % (name, hex(node))
|
|
for name, node in repo._bookmarks.iteritems()))
|
|
return writelog(repo, tr, "bookmarks.i", revstring)
|
|
|
|
def _logworkingparent(repo, tr):
|
|
revstring = repo['.'].hex()
|
|
return writelog(repo, tr, "workingparent.i", revstring)
|
|
|
|
def _logindex(repo, tr, nodes):
|
|
revstring = "\n".join(sorted('%s %s' % (k, v) for k, v in nodes.items()))
|
|
return writelog(repo, tr, "index.i", revstring)
|
|
|
|
# Read
|
|
|
|
def _readindex(repo, reverseindex, prefetchedrevlog=None):
|
|
if prefetchedrevlog is None:
|
|
rlog = _getrevlog(repo, 'index.i')
|
|
else:
|
|
rlog = prefetchedrevlog
|
|
index = _invertindex(rlog, reverseindex)
|
|
if index < 0 or index > len(rlog) - 1:
|
|
raise IndexError
|
|
chunk = rlog.revision(index)
|
|
indexdict = {}
|
|
for row in chunk.split("\n"):
|
|
kvpair = row.split(' ', 1)
|
|
if kvpair[0]:
|
|
indexdict[kvpair[0]] = kvpair[1]
|
|
return indexdict
|
|
|
|
def _readnode(repo, filename, hexnode):
|
|
rlog = _getrevlog(repo, filename)
|
|
return rlog.revision(bin(hexnode))
|
|
|
|
# Visualize
|
|
|
|
"""debug commands and instrumentation for the undo extension
|
|
|
|
Adds the `debugundohistory` and `debugundosmartlog` commands to visualize
|
|
operational history and to give a preview of how undo will behave.
|
|
"""
|
|
|
|
@command('debugundohistory', [
|
|
('n', 'index', 0, _("details about specific operation")),
|
|
('l', 'list', False, _("list recent undo-able operation"))
|
|
])
|
|
def debugundohistory(ui, repo, *args, **opts):
|
|
""" Print operational history
|
|
0 is the most recent operation
|
|
"""
|
|
if repo is not None:
|
|
if opts.get('list'):
|
|
if args and args[0].isdigit():
|
|
offset = int(args[0])
|
|
else:
|
|
offset = 0
|
|
_debugundolist(ui, repo, offset)
|
|
else:
|
|
reverseindex = opts.get('index')
|
|
if 0 == reverseindex and args and args[0].isdigit():
|
|
reverseindex = int(args[0])
|
|
_debugundoindex(ui, repo, reverseindex)
|
|
|
|
def _debugundolist(ui, repo, offset):
|
|
offset = abs(offset)
|
|
|
|
template = "{sub('\0', ' ', undo)}\n"
|
|
fm = ui.formatter('debugundohistory', {'template': template})
|
|
prefetchedrevlog = _getrevlog(repo, 'index.i')
|
|
recentrange = min(5, len(prefetchedrevlog) - offset)
|
|
if 0 == recentrange:
|
|
fm.startitem()
|
|
fm.write('undo', '%s', "None")
|
|
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()
|
|
|
|
def _debugundoindex(ui, repo, reverseindex):
|
|
try:
|
|
nodedict = _readindex(repo, reverseindex)
|
|
except IndexError:
|
|
raise error.Abort(_("index out of bounds"))
|
|
return
|
|
template = "{tabindent(sub('\0', ' ', content))}\n"
|
|
fm = ui.formatter('debugundohistory', {'template': template})
|
|
cabinet = ('command.i', 'bookmarks.i', 'date.i',
|
|
'draftheads.i', 'workingparent.i')
|
|
for filename in cabinet:
|
|
header = filename[:-2] + ":\n"
|
|
rawcontent = _readnode(repo, filename, nodedict[filename[:-2]])
|
|
if "date.i" == filename:
|
|
splitdate = rawcontent.split(" ")
|
|
datetuple = (float(splitdate[0]), int(splitdate[1]))
|
|
content = util.datestr(datetuple)
|
|
elif "draftheads.i" == filename:
|
|
try:
|
|
oldnodes = _readindex(repo, reverseindex + 1)
|
|
oldheads = _readnode(repo, filename, oldnodes[filename[:-2]])
|
|
except IndexError: # index is oldest log
|
|
content = rawcontent
|
|
else:
|
|
content = "ADDED:\n\t" + "\n\t".join(sorted(
|
|
set(rawcontent.split("\n"))
|
|
- set(oldheads.split("\n"))
|
|
))
|
|
content += "\nREMOVED:\n\t" + "\n\t".join(sorted(
|
|
set(oldheads.split("\n"))
|
|
- set(rawcontent.split("\n"))
|
|
))
|
|
elif "command.i" == filename and "" == rawcontent:
|
|
content = "unkown command(s) run, gap in log"
|
|
else:
|
|
content = rawcontent
|
|
fm.startitem()
|
|
fm.write('content', '%s', header + content)
|
|
fm.end()
|
|
|
|
# Revset logic
|
|
|
|
def _getolddrafts(repo, reverseindex):
|
|
nodedict = _readindex(repo, reverseindex)
|
|
olddraftheads = _readnode(repo, "draftheads.i", nodedict["draftheads"])
|
|
oldheadslist = olddraftheads.split("\n")
|
|
oldlogrevstring = revsetlang.formatspec('draft() & ancestors(%ls)',
|
|
oldheadslist)
|
|
urepo = repo.unfiltered()
|
|
return urepo.revs(oldlogrevstring)
|
|
|
|
revsetpredicate = registrar.revsetpredicate()
|
|
|
|
@revsetpredicate('olddraft')
|
|
def _olddraft(repo, subset, x):
|
|
"""``olddraft([index])``
|
|
previous draft commits
|
|
|
|
'index' is how many undoable commands you want to look back
|
|
an undoable command is one that changed draft heads, bookmarks
|
|
and or working copy parent
|
|
Note: this revset may include hidden commits
|
|
"""
|
|
args = revset.getargsdict(x, 'olddraftrevset', 'reverseindex')
|
|
reverseindex = revsetlang.getinteger(args.get('reverseindex'),
|
|
_('index must be a positive integer'), 1)
|
|
revs = _getolddrafts(repo, reverseindex)
|
|
return smartset.baseset(revs)
|
|
|
|
# Undo:
|
|
|
|
@command('undo', [
|
|
('n', 'index', 1, _("how many steps to undo back")),
|
|
])
|
|
def undo(ui, repo, *args, **opts):
|
|
"""perform an undo
|
|
|
|
Undoes an undoable command. An undoable command is one that changed at
|
|
least one of the following three: bookmarks, working copy parent or
|
|
changesets. Note that this specifically does not include commands like log.
|
|
It will include update if update changes the working copy parent (you update
|
|
to a changeset that isn't the current one). Note that commands that edit
|
|
public repos can't be undone (specifically push).
|
|
|
|
Undo does not preserve the working copy changes.
|
|
"""
|
|
|
|
reverseindex = opts.get("index")
|
|
|
|
with repo.wlock(), repo.lock(), repo.transaction("undo"):
|
|
cmdutil.checkunfinished(repo)
|
|
cmdutil.bailifchanged(repo)
|
|
repo = repo.unfiltered()
|
|
_undoto(ui, repo, reverseindex)
|
|
|
|
def _undoto(ui, repo, reverseindex):
|
|
# undo to specific reverseindex
|
|
# requires inhibit extension
|
|
if repo != repo.unfilterd:
|
|
raise error.ProgrammingError(_("_undoto expects unfilterd repo"))
|
|
try:
|
|
nodedict = _readindex(repo, reverseindex)
|
|
except IndexError:
|
|
raise error.Abort(_("index out of bounds"))
|
|
|
|
# bookmarks
|
|
bookstring = _readnode(repo, "bookmarks.i", nodedict["bookmarks"])
|
|
booklist = bookstring.split("\n")
|
|
# copy implementation for bookmarks
|
|
itercopy = []
|
|
for mark in repo._bookmarks.iteritems():
|
|
itercopy.append(mark)
|
|
bmchanges = [(mark[0], None) for mark in itercopy]
|
|
repo._bookmarks.applychanges(repo, repo.currenttransaction(), bmchanges)
|
|
bmchanges = []
|
|
for mark in booklist:
|
|
if mark:
|
|
kv = mark.rsplit(" ", 1)
|
|
bmchanges.append((kv[0], bin(kv[1])))
|
|
repo._bookmarks.applychanges(repo, repo.currenttransaction(), bmchanges)
|
|
|
|
# working copy parent
|
|
workingcopyparent = _readnode(repo, "workingparent.i",
|
|
nodedict["workingparent"])
|
|
revealcommits(repo, workingcopyparent)
|
|
commands.update(ui, repo, rev=workingcopyparent)
|
|
|
|
# visible changesets
|
|
addedrevs = revsetlang.formatspec('olddraft(0) - olddraft(%d)',
|
|
reverseindex)
|
|
hidecommits(repo, addedrevs)
|
|
|
|
removedrevs = revsetlang.formatspec('olddraft(%d) - olddraft(0)',
|
|
reverseindex)
|
|
revealcommits(repo, removedrevs)
|
|
|
|
# hide and reveal commits
|
|
|
|
def hidecommits(repo, rev):
|
|
ctxts = repo.set(rev)
|
|
for commit in ctxts:
|
|
obsolete.createmarkers(repo, [[commit,[]]])
|
|
|
|
def revealcommits(repo, rev):
|
|
try:
|
|
inhibit = extensions.find('inhibit')
|
|
except KeyError:
|
|
raise error.Abort(_('undo requires inhibit to work properly'))
|
|
else:
|
|
ctxts = repo.set(rev)
|
|
inhibit.revive(ctxts)
|
|
|
|
# Tools
|
|
|
|
def _invertindex(rlog, indexorreverseindex):
|
|
return len(rlog) - 1 - indexorreverseindex
|
|
|
|
def _getrevlog(repo, filename):
|
|
path = 'undolog/' + filename
|
|
return revlog.revlog(repo.vfs, path)
|