mirror of
https://github.com/facebook/sapling.git
synced 2024-10-11 17:27:53 +03:00
ac7e07dbdf
Summary: At a recent team meeting we've decided to remove the command prefix matching behavior, as it can be really annoying for the Rust parser (since it needs to know all the names, but it wants to avoid spinning up Python). It's even more annoying for subcommand support. FWIW git does not have prefix matching. This diff adds various aliases to "roughly" keep the command prefix matching behavior. The list of aliases are obtained by this script in `hg dbsh`: def unique(prefix, names): m = __import__('edenscm.mercurial').mercurial try: return m.cmdutil.findcmd(prefix, m.commands.table, False)[0][0] in names except: return False nameslist=sorted([i.replace('^','') for i in m.commands.table]) aliases = {} for names in nameslist: names = names.split('|') for name in names: if name.startswith('debug'): continue for prefix in [name[:i] for i in xrange(1, len(name))]: if unique(prefix, names): aliases.setdefault(name, []).append(prefix) Debug commands, and commands that are rarely used are not changed, including: 'backfillmanifestrevlog': ['backfillm', 'backfillma', 'backfillman', 'backfillmani', 'backfillmanif', 'backfillmanife', 'backfillmanifes', 'backfillmanifest', 'backfillmanifestr', 'backfillmanifestre', 'backfillmanifestrev', 'backfillmanifestrevl', 'backfillmanifestrevlo'], 'backfilltree': ['backfillt', 'backfilltr', 'backfilltre']} 'blackbox': ['blac', 'black', 'blackb', 'blackbo'], 'cachemanifest': ['cac', 'cach', 'cache', 'cachem', 'cachema', 'cacheman', 'cachemani', 'cachemanif', 'cachemanife', 'cachemanifes'], 'chistedit': ['chi', 'chis', 'chist', 'chiste', 'chisted', 'chistedi'], 'clone': ['clon'], 'cloud': ['clou'], 'convert': ['conv', 'conve', 'conver'], 'copy': ['cop'], 'fastannotate': ['fa', 'fas', 'fast', 'fasta', 'fastan', 'fastann', 'fastanno', 'fastannot', 'fastannota', 'fastannotat'], 'fold': ['fol'], 'githelp': ['gi', 'git', 'gith', 'githe', 'githel'], 'histgrep': ['histg', 'histgr', 'histgre'], 'incoming': ['in', 'inc', 'inco', 'incom', 'incomi', 'incomin'], 'isbackedup': ['is', 'isb', 'isba', 'isbac', 'isback', 'isbacke', 'isbacked', 'isbackedu'], 'manifest': ['ma', 'man', 'mani', 'manif', 'manife', 'manifes'], 'outgoing': ['o', 'ou', 'out', 'outg', 'outgo', 'outgoi', 'outgoin'], 'prefetch': ['pref', 'prefe', 'prefet', 'prefetc'], 'prune': ['pru', 'prun'], 'pushbackup': ['pushb', 'pushba', 'pushbac', 'pushback', 'pushbacku'], 'rage': ['ra', 'rag'], 'record': ['recor'], 'recover': ['recov', 'recove'], 'redo': ['red'], 'repack': ['rep', 'repa', 'repac'], 'reset': ['rese'], 'rollback': ['rol', 'roll', 'rollb', 'rollba', 'rollbac'], 'root': ['roo'], 'serve': ['se', 'ser', 'serv'], 'share': ['sha', 'shar'], 'sparse': ['spa', 'spar', 'spars'], 'svn': ['sv'], 'undo': ['und'], 'unshare': ['unsha', 'unshar'], 'verifyremotefilelog': ['verifyr', 'verifyre', 'verifyrem', 'verifyremo', 'verifyremot', 'verifyremote', 'verifyremotef', 'verifyremotefi', 'verifyremotefil', 'verifyremotefile', 'verifyremotefilel', 'verifyremotefilelo'], Reviewed By: sfilipco Differential Revision: D17644676 fbshipit-source-id: f60f5e6810279b52f9a4a1e048eeb529a96bd735
523 lines
17 KiB
Python
523 lines
17 KiB
Python
# globalrevs.py
|
|
#
|
|
# Copyright 2018 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.
|
|
#
|
|
# no-check-code
|
|
|
|
"""extension for providing strictly increasing revision numbers
|
|
|
|
With this extension enabled, Mercurial starts adding a strictly increasing
|
|
revision number to each commit which is accessible through the 'globalrev'
|
|
template.
|
|
|
|
::
|
|
|
|
[format]
|
|
# support strictly increasing revision numbers for new repositories.
|
|
useglobalrevs = True
|
|
|
|
[globalrevs]
|
|
# Allow new commits through only pushrebase.
|
|
onlypushrebase = True
|
|
|
|
# In this configuration, `globalrevs` extension can only be used to query
|
|
# strictly increasing global revision numbers already embedded in the
|
|
# commits. In particular, `globalrevs` won't embed any data in the commits.
|
|
readonly = True
|
|
|
|
# Repository name to be used as key for storing global revisions data in the
|
|
# database. If not specified, name specified through the configuration
|
|
# `hqsql.reponame` will be used.
|
|
reponame = customname
|
|
|
|
# The starting global revision for a repository. We will only consider the
|
|
# global revisions greater than equal to this value as valid global revision
|
|
# numbers. Note that this implies there maybe commits with global revision
|
|
# number less than this value but there is no guarantee associated those
|
|
# numbers. Therefore, relying on global revision numbers below this value is
|
|
# undefined behaviour.
|
|
startrev = 0
|
|
|
|
# If this configuration is true, we use a cached mapping from `globalrev ->
|
|
# hash` to enable fast lookup of commits based on the globalrev. This
|
|
# mapping can be built using the `updateglobalrevmeta` command.
|
|
fastlookup = False
|
|
|
|
# If this configuration is true, we use ScmQuery to lookup the mapping from
|
|
# `globalrev->hash` to enable fast lookup of the commits based on the
|
|
# globalrev. This configuration is only effective on the clients. For
|
|
# speedup on the servers, the `fastlookup` configuration should be used.
|
|
scmquerylookup = False
|
|
"""
|
|
from __future__ import absolute_import
|
|
|
|
import struct
|
|
|
|
from bindings import nodemap as nodemapmod
|
|
from edenscm.mercurial import (
|
|
error,
|
|
extensions,
|
|
localrepo,
|
|
namespaces,
|
|
phases,
|
|
progress,
|
|
registrar,
|
|
revset,
|
|
smartset,
|
|
)
|
|
from edenscm.mercurial.i18n import _
|
|
from edenscm.mercurial.node import bin, hex, nullid
|
|
|
|
from .hgsql import CorruptionException, executewithsql, ishgsqlbypassed, issqlrepo
|
|
from .pushrebase import isnonpushrebaseblocked
|
|
|
|
|
|
configtable = {}
|
|
configitem = registrar.configitem(configtable)
|
|
configitem("format", "useglobalrevs", default=False)
|
|
configitem("globalrevs", "fastlookup", default=False)
|
|
configitem("globalrevs", "onlypushrebase", default=True)
|
|
configitem("globalrevs", "readonly", default=False)
|
|
configitem("globalrevs", "reponame", default=None)
|
|
configitem("globalrevs", "scmquerylookup ", default=False)
|
|
configitem("globalrevs", "startrev", default=0)
|
|
|
|
cmdtable = {}
|
|
command = registrar.command(cmdtable)
|
|
namespacepredicate = registrar.namespacepredicate()
|
|
revsetpredicate = registrar.revsetpredicate()
|
|
templatekeyword = registrar.templatekeyword()
|
|
|
|
EXTRASCONVERTKEY = "convert_revision"
|
|
EXTRASGLOBALREVKEY = "global_rev"
|
|
LASTREVFILE = "globalrev-nodemap/last-rev"
|
|
MAPFILE = "globalrev-nodemap"
|
|
|
|
|
|
@templatekeyword("globalrev")
|
|
def globalrevkw(repo, ctx, **kwargs):
|
|
return _getglobalrev(repo.ui, ctx.extra())
|
|
|
|
|
|
def _newreporequirementswrapper(orig, repo):
|
|
reqs = orig(repo)
|
|
if repo.ui.configbool("format", "useglobalrevs"):
|
|
reqs.add("globalrevs")
|
|
return reqs
|
|
|
|
|
|
def uisetup(ui):
|
|
extensions.wrapfunction(
|
|
localrepo, "newreporequirements", _newreporequirementswrapper
|
|
)
|
|
|
|
def _hgsqlwrapper(loaded):
|
|
if loaded:
|
|
hgsqlmod = extensions.find("hgsql")
|
|
extensions.wrapfunction(hgsqlmod, "wraprepo", _sqllocalrepowrapper)
|
|
|
|
def _hgsubversionwrapper(loaded):
|
|
if loaded:
|
|
hgsubversionmod = extensions.find("hgsubversion")
|
|
extensions.wrapfunction(
|
|
hgsubversionmod.util, "lookuprev", _lookupsvnrevwrapper
|
|
)
|
|
extensions.wrapfunction(hgsubversionmod, "_svnrevkw", _svnrevkwwrapper)
|
|
else:
|
|
|
|
@templatekeyword("svnrev")
|
|
def svnrevkw(repo, ctx, **kwargs):
|
|
return globalrevkw(repo, ctx, **kwargs)
|
|
|
|
@revsetpredicate("svnrev(number)", safe=True, weight=10)
|
|
def _revsetsvnrev(repo, subset, x):
|
|
"""Changesets with given Subversion revision number.
|
|
"""
|
|
args = revset.getargs(x, 1, 1, "svnrev takes one argument")
|
|
svnrev = revset.getinteger(
|
|
args[0], "the argument to svnrev() must be a number"
|
|
)
|
|
|
|
torev = repo.changelog.rev
|
|
revs = revset.baseset(torev(n) for n in _lookupglobalrev(repo, svnrev))
|
|
return subset & revs
|
|
|
|
extensions.afterloaded("hgsubversion", _hgsubversionwrapper)
|
|
|
|
# We only wrap `hgsql` extension for embedding strictly increasing global
|
|
# revision number in commits if the repository has `hgsql` enabled and it is
|
|
# also configured to write data to the commits. Therefore, do not wrap the
|
|
# extension if that is not the case.
|
|
if not ui.configbool("globalrevs", "readonly") and not ishgsqlbypassed(ui):
|
|
extensions.afterloaded("hgsql", _hgsqlwrapper)
|
|
|
|
cls = localrepo.localrepository
|
|
for reqs in ["_basesupported", "supportedformats"]:
|
|
getattr(cls, reqs).add("globalrevs")
|
|
|
|
|
|
def reposetup(ui, repo):
|
|
# Only need the extra functionality on the servers.
|
|
if issqlrepo(repo):
|
|
_validateextensions(["hgsql", "pushrebase"])
|
|
_validaterepo(repo)
|
|
|
|
|
|
def _validateextensions(extensionlist):
|
|
for extension in extensionlist:
|
|
try:
|
|
extensions.find(extension)
|
|
except Exception:
|
|
raise error.Abort(_("%s extension is not enabled") % extension)
|
|
|
|
|
|
def _validaterepo(repo):
|
|
ui = repo.ui
|
|
|
|
allowonlypushrebase = ui.configbool("globalrevs", "onlypushrebase")
|
|
if allowonlypushrebase and not isnonpushrebaseblocked(repo):
|
|
raise error.Abort(_("pushrebase using incorrect configuration"))
|
|
|
|
|
|
def _sqllocalrepowrapper(orig, repo):
|
|
# This ensures that the repo is of type `sqllocalrepo` which is defined in
|
|
# hgsql extension.
|
|
orig(repo)
|
|
|
|
if not extensions.isenabled(repo.ui, "globalrevs"):
|
|
return
|
|
|
|
# This class will effectively extend the `sqllocalrepo` class.
|
|
class globalrevsrepo(repo.__class__):
|
|
def commitctx(self, ctx, error=False):
|
|
# Assign global revs automatically
|
|
extra = dict(ctx.extra())
|
|
extra[EXTRASGLOBALREVKEY] = self.nextrevisionnumber()
|
|
ctx.extra = lambda: extra
|
|
return super(globalrevsrepo, self).commitctx(ctx, error)
|
|
|
|
def revisionnumberfromdb(self):
|
|
# This must be executed while the SQL lock is taken
|
|
if not self.hassqlwritelock():
|
|
raise error.ProgrammingError("acquiring globalrev needs SQL write lock")
|
|
|
|
reponame = self._globalrevsreponame
|
|
cursor = self.sqlcursor
|
|
|
|
cursor.execute(
|
|
"SELECT value FROM revision_references "
|
|
+ "WHERE repo = %s AND "
|
|
+ "namespace = 'counter' AND "
|
|
+ "name='commit' ",
|
|
(reponame,),
|
|
)
|
|
|
|
counterresults = cursor.fetchall()
|
|
if len(counterresults) == 1:
|
|
return int(counterresults[0][0])
|
|
elif len(counterresults) == 0:
|
|
raise error.Abort(
|
|
CorruptionException(
|
|
_("no commit counters for %s in database") % reponame
|
|
)
|
|
)
|
|
else:
|
|
raise error.Abort(
|
|
CorruptionException(
|
|
_("multiple commit counters for %s in database") % reponame
|
|
)
|
|
)
|
|
|
|
def nextrevisionnumber(self):
|
|
""" get the next strictly increasing revision number for this
|
|
repository.
|
|
"""
|
|
|
|
if self._nextrevisionnumber is None:
|
|
self._nextrevisionnumber = self.revisionnumberfromdb()
|
|
|
|
nextrev = self._nextrevisionnumber
|
|
self._nextrevisionnumber += 1
|
|
return nextrev
|
|
|
|
def transaction(self, *args, **kwargs):
|
|
tr = super(globalrevsrepo, self).transaction(*args, **kwargs)
|
|
if tr.count > 1:
|
|
return tr
|
|
|
|
def transactionabort(orig):
|
|
self._nextrevisionnumber = None
|
|
return orig()
|
|
|
|
extensions.wrapfunction(tr, "_abort", transactionabort)
|
|
return tr
|
|
|
|
def _updaterevisionreferences(self, *args, **kwargs):
|
|
super(globalrevsrepo, self)._updaterevisionreferences(*args, **kwargs)
|
|
|
|
newcount = self._nextrevisionnumber
|
|
|
|
# Only write to database if the global revision number actually
|
|
# changed.
|
|
if newcount is not None:
|
|
reponame = self._globalrevsreponame
|
|
cursor = self.sqlcursor
|
|
|
|
cursor.execute(
|
|
"UPDATE revision_references "
|
|
+ "SET value=%s "
|
|
+ "WHERE repo=%s AND namespace='counter' AND name='commit'",
|
|
(newcount, reponame),
|
|
)
|
|
|
|
repo._globalrevsreponame = (
|
|
repo.ui.config("globalrevs", "reponame") or repo.sqlreponame
|
|
)
|
|
repo._nextrevisionnumber = None
|
|
repo.__class__ = globalrevsrepo
|
|
|
|
|
|
def _lookupsvnrevwrapper(orig, repo, rev):
|
|
return _lookupglobalrev(repo, rev)
|
|
|
|
|
|
def _svnrevkwwrapper(orig, repo, ctx, **kwargs):
|
|
return globalrevkw(repo, ctx, **kwargs)
|
|
|
|
|
|
_u64lestruct = struct.Struct("<Q")
|
|
_bin2u64le = _u64lestruct.unpack
|
|
_u64le2bin = _u64lestruct.pack
|
|
|
|
|
|
class _globalrevmap(object):
|
|
def __init__(self, repo):
|
|
self.lastrev = int(repo.sharedvfs.tryread(LASTREVFILE) or "0")
|
|
self.map = nodemapmod.nodemap(repo.sharedvfs.join(MAPFILE))
|
|
self.repo = repo
|
|
|
|
@staticmethod
|
|
def _globalrevtonode(grev):
|
|
return _u64le2bin(grev).ljust(20, "\0")
|
|
|
|
@staticmethod
|
|
def _nodetoglobalrev(grevnode):
|
|
return _bin2u64le(grevnode[:8])
|
|
|
|
def add(self, grev, hgnode):
|
|
self.map.add(self._globalrevtonode(grev), hgnode)
|
|
|
|
def gethgnode(self, grev):
|
|
return self.map.lookupbyfirst(self._globalrevtonode(grev))
|
|
|
|
def getglobalrev(self, hgnode):
|
|
grevnode = self.map.lookupbysecond(hgnode)
|
|
return self._nodetoglobalrev(grevnode) if grevnode is not None else None
|
|
|
|
def save(self):
|
|
self.map.flush()
|
|
self.repo.sharedvfs.write(LASTREVFILE, "%s" % self.lastrev)
|
|
|
|
|
|
def _lookupglobalrev(repo, grev):
|
|
# A `globalrev` < 0 will never resolve to any commit.
|
|
if grev < 0:
|
|
return []
|
|
|
|
cl = repo.changelog
|
|
changelogrevision = cl.changelogrevision
|
|
tonode = cl.node
|
|
ui = repo.ui
|
|
|
|
def matchglobalrev(rev):
|
|
commitextra = changelogrevision(rev).extra
|
|
globalrev = _getglobalrev(ui, commitextra)
|
|
svnrev = _getsvnrev(commitextra)
|
|
|
|
def isequal(strrev, rev):
|
|
return strrev is not None and int(strrev) == rev
|
|
|
|
return isequal(globalrev, grev) or isequal(svnrev, grev)
|
|
|
|
usefastlookup = ui.configbool("globalrevs", "fastlookup")
|
|
if usefastlookup:
|
|
globalrevmap = _globalrevmap(repo)
|
|
lastrev = globalrevmap.lastrev
|
|
hgnode = globalrevmap.gethgnode(grev)
|
|
if hgnode:
|
|
return [hgnode]
|
|
|
|
matchedrevs = []
|
|
for rev in repo.revs("reverse(all())"):
|
|
# While using fast lookup, we have already searched the indexed commits
|
|
# upto lastrev and therefore, we can safely say that there is no commit
|
|
# which has the specified globalrev if we are looking at a revision
|
|
# before the lastrev.
|
|
if usefastlookup and rev < lastrev:
|
|
break
|
|
|
|
if matchglobalrev(rev):
|
|
matchedrevs.append(tonode(rev))
|
|
break
|
|
|
|
return matchedrevs
|
|
|
|
|
|
def _lookupname(repo, name):
|
|
if (name.startswith("m") or name.startswith("r")) and name[1:].isdigit():
|
|
return _lookupglobalrev(repo, int(name[1:]))
|
|
|
|
|
|
@namespacepredicate("globalrevs", priority=75)
|
|
def _getnamespace(_repo):
|
|
return namespaces.namespace(
|
|
listnames=lambda repo: [], namemap=_lookupname, nodemap=lambda repo, node: []
|
|
)
|
|
|
|
|
|
@revsetpredicate("globalrev(number)", safe=True, weight=10)
|
|
def _revsetglobalrev(repo, subset, x):
|
|
"""Changesets with given global revision number.
|
|
"""
|
|
args = revset.getargs(x, 1, 1, "globalrev takes one argument")
|
|
globalrev = revset.getinteger(
|
|
args[0], "the argument to globalrev() must be a number"
|
|
)
|
|
|
|
torev = repo.changelog.rev
|
|
revs = revset.baseset(torev(n) for n in _lookupglobalrev(repo, globalrev))
|
|
return subset & revs
|
|
|
|
|
|
def getglobalrev(ui, ctx, defval=None):
|
|
"""Wrapper around _getglobalrev. See _getglobalrev for more detail."""
|
|
grev = _getglobalrev(ui, ctx.extra())
|
|
if grev:
|
|
return grev
|
|
return defval
|
|
|
|
|
|
def _getglobalrev(ui, commitextra):
|
|
grev = commitextra.get(EXTRASGLOBALREVKEY)
|
|
|
|
# If we did not find `globalrev` in the commit extras, lets also look for
|
|
# the `svnrev` in the commit extras before we give up. Also, do not return
|
|
# the `globalrev` if it is before the supported starting revision.
|
|
return (
|
|
_getsvnrev(commitextra)
|
|
if not grev or ui.configint("globalrevs", "startrev") > int(grev)
|
|
else grev
|
|
)
|
|
|
|
|
|
def _getsvnrev(commitextra):
|
|
convertrev = commitextra.get(EXTRASCONVERTKEY)
|
|
if convertrev:
|
|
# ex. svn:uuid/path@1234
|
|
return convertrev.rsplit("@", 1)[-1]
|
|
|
|
|
|
@command("updateglobalrevmeta", [], _("hg updateglobalrevmeta"))
|
|
def updateglobalrevmeta(ui, repo, *args, **opts):
|
|
"""Reads globalrevs from the latest hg commits and adds them to the
|
|
globalrev-hg mapping."""
|
|
with repo.wlock(), repo.lock():
|
|
unfi = repo.unfiltered()
|
|
clnode = unfi.changelog.node
|
|
clrevision = unfi.changelog.changelogrevision
|
|
globalrevmap = _globalrevmap(unfi)
|
|
|
|
lastrev = globalrevmap.lastrev
|
|
repolen = len(unfi)
|
|
with progress.bar(ui, _("indexing"), _("revs"), repolen - lastrev) as prog:
|
|
|
|
def addtoglobalrevmap(grev, node):
|
|
if grev:
|
|
globalrevmap.add(int(grev), node)
|
|
|
|
for rev in range(lastrev, repolen): # noqa: F821
|
|
hgnode = clnode(rev)
|
|
commitdata = clrevision(rev)
|
|
extra = commitdata.extra
|
|
|
|
svnrev = _getsvnrev(extra)
|
|
addtoglobalrevmap(svnrev, hgnode)
|
|
|
|
globalrev = _getglobalrev(ui, extra)
|
|
if globalrev != svnrev:
|
|
addtoglobalrevmap(globalrev, hgnode)
|
|
|
|
prog.value += 1
|
|
|
|
globalrevmap.lastrev = repolen
|
|
globalrevmap.save()
|
|
|
|
|
|
@command("globalrev", [], _("hg globalrev"))
|
|
def globalrev(ui, repo, *args, **opts):
|
|
"""prints out the next global revision number for a particular repository by
|
|
reading it from the database.
|
|
"""
|
|
|
|
if not issqlrepo(repo):
|
|
raise error.Abort(_("this repository is not a sql backed repository"))
|
|
|
|
def _printnextglobalrev():
|
|
ui.status(_("%s\n") % repo.revisionnumberfromdb())
|
|
|
|
executewithsql(repo, _printnextglobalrev, sqllock=True)
|
|
|
|
|
|
@command(
|
|
"initglobalrev",
|
|
[
|
|
(
|
|
"",
|
|
"i-know-what-i-am-doing",
|
|
None,
|
|
_("only run initglobalrev if you know exactly what you're doing"),
|
|
)
|
|
],
|
|
_("hg initglobalrev START"),
|
|
)
|
|
def initglobalrev(ui, repo, start, *args, **opts):
|
|
""" initializes the global revision number for a particular repository by
|
|
writing it to the database.
|
|
"""
|
|
|
|
if not issqlrepo(repo):
|
|
raise error.Abort(_("this repository is not a sql backed repository"))
|
|
|
|
if not opts.get("i_know_what_i_am_doing"):
|
|
raise error.Abort(
|
|
_(
|
|
"You must pass --i-know-what-i-am-doing to run this command. "
|
|
+ "Only the Mercurial server admins should ever run this."
|
|
)
|
|
)
|
|
|
|
try:
|
|
startrev = int(start)
|
|
except ValueError:
|
|
raise error.Abort(_("start must be an integer."))
|
|
|
|
def _initglobalrev():
|
|
cursor = repo.sqlcursor
|
|
reponame = repo._globalrevsreponame
|
|
|
|
# Our schemas are setup such that this query will fail if we try to
|
|
# update an existing row which is exactly what we desire here.
|
|
cursor.execute(
|
|
"INSERT INTO "
|
|
+ "revision_references(repo, namespace, name, value) "
|
|
+ "VALUES(%s, 'counter', 'commit', %s)",
|
|
(reponame, startrev),
|
|
)
|
|
|
|
repo.sqlconn.commit()
|
|
|
|
executewithsql(repo, _initglobalrev, sqllock=True)
|