sapling/infinitepush/__init__.py
Stanislau Hlebik 6086c2f0cb infinitepush fix: pass dest to the orig
Test Plan: Run `test-infinitepush-*`

Reviewers: durham, mitrandir, rmcelroy

Reviewed By: rmcelroy

Subscribers: mjpieters, #sourcecontrol

Differential Revision: https://phabricator.intern.facebook.com/D4081739

Tasks: 12479677

Signature: t1:4081739:1477498757:7e105e400fd99e0a6dcb80165a2ed9d9c6d1a548
2016-10-26 09:29:23 -07:00

723 lines
26 KiB
Python

# Infinite push
#
# Copyright 2016 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
import errno
import logging
import os
import resource
import tempfile
from collections import defaultdict
from mercurial import (
bundle2,
changegroup,
cmdutil,
commands,
discovery,
encoding,
error,
exchange,
extensions,
hg,
localrepo,
util,
pushkey,
revset,
phases,
wireproto,
)
from mercurial.extensions import wrapcommand, wrapfunction
from mercurial.hg import repository
from mercurial.node import bin, hex
from mercurial.i18n import _
from mercurial.peer import batchable, future
from mercurial.wireproto import encodelist, decodelist
cmdtable = {}
command = cmdutil.command(cmdtable)
pushrebaseparttype = 'b2x:rebase'
scratchbranchparttype = 'b2x:infinitepush'
experimental = 'experimental'
configbookmark = 'server-bundlestore-bookmark'
configcreate = 'server-bundlestore-create'
configscratchpush = 'infinitepush-scratchpush'
_scratchbranchmatcher = lambda x: False
def _buildexternalbundlestore(ui):
put_args = ui.configlist('infinitepush', 'put_args', [])
put_binary = ui.config('infinitepush', 'put_binary')
if not put_binary:
raise error.Abort('put binary is not specified')
get_args = ui.configlist('infinitepush', 'get_args', [])
get_binary = ui.config('infinitepush', 'get_binary')
if not get_binary:
raise error.Abort('get binary is not specified')
from . import store
return store.externalbundlestore(put_binary, put_args, get_binary, get_args)
def _buildsqlindex(ui):
sqlhost = ui.config('infinitepush', 'sqlhost')
if not sqlhost:
raise error.Abort(_('please set infinitepush.sqlhost'))
host, port, db, user, password = sqlhost.split(':')
reponame = ui.config('infinitepush', 'reponame')
if not reponame:
raise error.Abort(_('please set infinitepush.reponame'))
logfile = ui.config('infinitepush', 'logfile', '')
from . import sqlindexapi
return sqlindexapi.sqlindexapi(
reponame, host, port, db, user, password,
logfile, _getloglevel(ui))
def _getloglevel(ui):
loglevel = ui.config('infinitepush', 'loglevel', 'DEBUG')
numeric_loglevel = getattr(logging, loglevel.upper(), None)
if not isinstance(numeric_loglevel, int):
raise error.Abort(_('invalid log level %s') % loglevel)
return numeric_loglevel
class bundlestore(object):
def __init__(self, repo):
self._repo = repo
storetype = self._repo.ui.config('infinitepush', 'storetype', '')
if storetype == 'disk':
from . import store
self.store = store.filebundlestore(self._repo.ui, self._repo)
elif storetype == 'external':
self.store = _buildexternalbundlestore(self._repo.ui)
else:
raise error.Abort(
_('unknown infinitepush store type specified %s') % storetype)
indextype = self._repo.ui.config('infinitepush', 'indextype', '')
if indextype == 'disk':
from . import fileindexapi
self.index = fileindexapi.fileindexapi(self._repo)
elif indextype == 'sql':
self.index = _buildsqlindex(self._repo.ui)
else:
raise error.Abort(
_('unknown infinitepush index type specified %s') % indextype)
def _isserver(ui):
return ui.configbool('infinitepush', 'server')
def reposetup(ui, repo):
if _isserver(ui) and repo.local():
repo.bundlestore = bundlestore(repo)
def uisetup(ui):
# remotenames circumvents the default push implementation entirely, so make
# sure we load after it so that we wrap it.
order = extensions._order
order.remove('infinitepush')
order.append('infinitepush')
extensions._order = order
def extsetup(ui):
commonsetup(ui)
if _isserver(ui):
serverextsetup(ui)
else:
clientextsetup(ui)
def commonsetup(ui):
wireproto.commands['listkeyspatterns'] = (
wireprotolistkeyspatterns, 'namespace patterns')
scratchbranchpat = ui.config('infinitepush', 'branchpattern')
if scratchbranchpat:
global _scratchbranchmatcher
kind, pat, _scratchbranchmatcher = util.stringmatcher(scratchbranchpat)
def serverextsetup(ui):
origpushkeyhandler = bundle2.parthandlermapping['pushkey']
def newpushkeyhandler(*args, **kwargs):
bundle2pushkey(origpushkeyhandler, *args, **kwargs)
newpushkeyhandler.params = origpushkeyhandler.params
bundle2.parthandlermapping['pushkey'] = newpushkeyhandler
wrapfunction(localrepo.localrepository, 'listkeys', localrepolistkeys)
wireproto.commands['lookup'] = (
_lookupwrap(wireproto.commands['lookup'][0]), 'key')
wrapfunction(exchange, 'getbundlechunks', getbundlechunks)
def clientextsetup(ui):
entry = wrapcommand(commands.table, 'push', _push)
# Don't add the 'to' arg if it already exists
if not any(a for a in entry[1] if a[1] == 'to'):
entry[1].append(('', 'to', '', _('push revs to this bookmark')))
if not any(a for a in entry[1] if a[1] == 'create'):
entry[1].append(
('', 'create', None, _('create a new remote bookmark')))
entry[1].append(
('', 'bundle-store', None,
_('force push to go to bundle store (EXPERIMENTAL)')))
wrapcommand(commands.table, 'pull', _pull)
wrapfunction(discovery, 'checkheads', _checkheads)
wireproto.wirepeer.listkeyspatterns = listkeyspatterns
# Move infinitepush part before pushrebase part
# to avoid generation of both parts.
partorder = exchange.b2partsgenorder
index = partorder.index('changeset')
if pushrebaseparttype in partorder:
index = min(index, partorder.index(pushrebaseparttype))
partorder.insert(
index, partorder.pop(partorder.index(scratchbranchparttype)))
def _checkheads(orig, pushop):
if pushop.ui.configbool(experimental, configscratchpush, False):
return
return orig(pushop)
def wireprotolistkeyspatterns(repo, proto, namespace, patterns):
patterns = decodelist(patterns)
d = repo.listkeys(encoding.tolocal(namespace), patterns).items()
return pushkey.encodekeys(d)
def localrepolistkeys(orig, self, namespace, patterns=None):
if namespace == 'bookmarks' and patterns:
index = self.bundlestore.index
results = {}
patterns = set(patterns)
# TODO(stash): this function has a limitation:
# patterns are not actually patterns, just simple string comparison
for bookmark in patterns:
if _scratchbranchmatcher(bookmark):
# TODO(stash): use `getbookmarks()` method
node = index.getnode(bookmark)
if node:
results[bookmark] = node
bookmarks = orig(self, namespace)
for bookmark, node in bookmarks.items():
if bookmark in patterns:
results[bookmark] = node
return results
else:
return orig(self, namespace)
@batchable
def listkeyspatterns(self, namespace, patterns):
if not self.capable('pushkey'):
yield {}, None
f = future()
self.ui.debug('preparing listkeys for "%s" with pattern "%s"\n' %
(namespace, patterns))
yield {
'namespace': encoding.fromlocal(namespace),
'patterns': encodelist(patterns)
}, f
d = f.value
self.ui.debug('received listkey for "%s": %i bytes\n'
% (namespace, len(d)))
yield pushkey.decodekeys(d)
def _includefilelogstobundle(bundlecaps, bundlerepo, ui):
'''Tells remotefilelog to include all changed files to the changegroup
By default remotefilelog doesn't include file content to the changegroup.
But we need to include it if we are fetching from bundlestore.
'''
revs = bundlerepo.revs('bundle()')
cl = bundlerepo.changelog
changedfiles = set()
for r in revs:
# [3] means changed files
changedfiles.update(cl.read(r)[3])
if not changedfiles:
return bundlecaps
changedfiles = '\0'.join(changedfiles)
newcaps = []
appended = False
for cap in (bundlecaps or []):
if cap.startswith('excludepattern='):
newcaps.append('\0'.join((cap, changedfiles)))
appended = True
else:
newcaps.append(cap)
if not appended:
# Not found excludepattern cap. Just append it
newcaps.append('excludepattern=' + changedfiles)
return newcaps
def getbundlechunks(orig, repo, source, heads=None, common=None,
bundlecaps=None, **kwargs):
# Check if heads exists, if not, check bundle store
hasscratchnode = False
for head in heads:
if head not in repo.changelog.nodemap:
if hasscratchnode:
raise error.Abort(
'not implemented: not possible to pull more than '
'one scratch branch')
index = repo.bundlestore.index
store = repo.bundlestore.store
bundleid = index.getbundle(hex(head))
bundleraw = store.read(bundleid)
bundlefile = _makebundlefromraw(bundleraw)
bundlepath = "bundle:%s+%s" % (repo.root, bundlefile)
bundlerepo = repository(repo.ui, bundlepath)
repo = bundlerepo
hasscratchnode = True
bundlecaps = _includefilelogstobundle(bundlecaps, bundlerepo,
repo.ui)
return orig(repo, source, heads=heads, common=common,
bundlecaps=bundlecaps, **kwargs)
def _lookupwrap(orig):
def _lookup(repo, proto, key):
localkey = encoding.tolocal(key)
if isinstance(localkey, str) and _scratchbranchmatcher(localkey):
scratchnode = repo.bundlestore.index.getnode(localkey)
if scratchnode:
return "%s %s\n" % (1, scratchnode)
else:
return "%s %s\n" % (0, 'scratch branch %s not found' % localkey)
else:
try:
c = repo[localkey]
r = c.hex()
return "%s %s\n" % (1, r)
except Exception as inst:
if repo.bundlestore.index.getbundle(localkey):
return "%s %s\n" % (1, localkey)
else:
r = str(inst)
return "%s %s\n" % (0, r)
return _lookup
def validaterevset(repo, revset):
"""Abort if the revs to be pushed aren't valid for a scratch branch."""
if not repo.revs(revset):
raise error.Abort(_('nothing to push'))
heads = repo.revs('heads(%r)', revset)
if len(heads) > 1:
raise error.Abort(
_('cannot push more than one head to a scratch branch'))
def getscratchbranchpart(repo, peer, outgoing, force, ui, bookmark, create):
if not outgoing.missing:
raise error.Abort(_('no commits to push'))
if scratchbranchparttype not in bundle2.bundle2caps(peer):
raise error.Abort(_('no server support for %r') % scratchbranchparttype)
validaterevset(repo, revset.formatspec('%ln', outgoing.missing))
cg = changegroup.getlocalchangegroupraw(repo, 'push', outgoing)
params = {}
if bookmark:
params['bookmark'] = bookmark
# 'prevbooknode' is necessary for pushkey reply part
params['bookprevnode'] = ''
if bookmark in repo:
params['bookprevnode'] = repo[bookmark].hex()
if create:
params['create'] = '1'
if force:
params['force'] = '1'
# Do not send pushback bundle2 part with bookmarks if remotenames extension
# is enabled. It will be handled manually in `_push()`
if not _isremotebooksenabled(ui):
params['pushbackbookmarks'] = '1'
# .upper() marks this as a mandatory part: server will abort if there's no
# handler
return bundle2.bundlepart(
scratchbranchparttype.upper(),
advisoryparams=params.items(),
data=cg)
def _pull(orig, ui, repo, source="default", **opts):
# Copy paste from `pull` command
source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
hasscratchbookmarks = False
scratchbookmarks = {}
if opts.get('bookmark'):
bookmarks = []
revs = opts.get('rev') or []
for bookmark in opts.get('bookmark'):
if _scratchbranchmatcher(bookmark):
other = hg.peer(repo, opts, source)
if hasscratchbookmarks:
raise error.Abort('not implemented: not possible to pull '
'more than one scratch branch')
fetchedbookmarks = other.listkeyspatterns('bookmarks',
patterns=[bookmark])
if bookmark not in fetchedbookmarks:
raise error.Abort('remote bookmark %s not found!' %
bookmark)
scratchbookmarks[bookmark] = fetchedbookmarks[bookmark]
revs.append(fetchedbookmarks[bookmark])
hasscratchbookmarks = True
else:
bookmarks.append(bookmark)
opts['bookmark'] = bookmarks
opts['rev'] = revs
if hasscratchbookmarks:
# Set anyincoming to True
oldfindcommonincoming = wrapfunction(discovery,
'findcommonincoming',
_findcommonincoming)
try:
# Remote scratch bookmarks will be deleted because remotenames doesn't
# know about them. Let's save it before pull and restore after
remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, source)
result = orig(ui, repo, source, **opts)
# TODO(stash): race condition is possible
# if scratch bookmarks was updated right after orig.
# But that's unlikely and shouldn't be harmful.
if _isremotebooksenabled(ui):
remotescratchbookmarks.update(scratchbookmarks)
_saveremotebookmarks(repo, remotescratchbookmarks, source)
else:
_savelocalbookmarks(repo, scratchbookmarks)
return result
finally:
if hasscratchbookmarks:
discovery.findcommonincoming = oldfindcommonincoming
def _isremotebooksenabled(ui):
return ('remotenames' in extensions._extensions and
ui.configbool('remotenames', 'bookmarks', True))
def _readscratchremotebookmarks(ui, repo, other):
if _isremotebooksenabled(ui):
remotenamesext = extensions.find('remotenames')
remotepath = remotenamesext.activepath(repo.ui, other)
result = {}
for remotebookmark in repo.names['remotebookmarks'].listnames(repo):
path, bookname = remotenamesext.splitremotename(remotebookmark)
if path == remotepath and _scratchbranchmatcher(bookname):
nodes = repo.names['remotebookmarks'].nodes(repo,
remotebookmark)
result[bookname] = hex(nodes[0])
return result
else:
return {}
def _saveremotebookmarks(repo, newbookmarks, remote):
remotenamesext = extensions.find('remotenames')
remotepath = remotenamesext.activepath(repo.ui, remote)
branches = defaultdict(list)
bookmarks = {}
remotenames = remotenamesext.readremotenames(repo)
for hexnode, nametype, remote, rname in remotenames:
if remote != remotepath:
continue
if nametype == 'bookmarks':
if rname in newbookmarks:
# It's possible if we have a normal bookmark that matches
# scratch branch pattern. In this case just use the current
# bookmark node
del newbookmarks[rname]
bookmarks[rname] = hexnode
elif nametype == 'branches':
# saveremotenames expects 20 byte binary nodes for branches
branches[rname].append(bin(hexnode))
for bookmark, hexnode in newbookmarks.items():
bookmarks[bookmark] = hexnode
remotenamesext.saveremotenames(repo, remotepath, branches, bookmarks)
def _savelocalbookmarks(repo, bookmarks):
with repo.wlock():
with repo.lock():
with repo.transaction('bookmark') as tr:
for scratchbook, node in bookmarks.items():
changectx = repo[node]
repo._bookmarks[scratchbook] = changectx.node()
repo._bookmarks.recordchange(tr)
def _findcommonincoming(orig, *args, **kwargs):
common, inc, remoteheads = orig(*args, **kwargs)
return common, True, remoteheads
def _push(orig, ui, repo, dest=None, *args, **opts):
oldbookmark = ui.backupconfig(experimental, configbookmark)
oldcreate = ui.backupconfig(experimental, configcreate)
oldphasemove = None
try:
bookmark = opts.get('to')
create = opts.get('create') or False
scratchpush = opts.get('bundle_store')
if _scratchbranchmatcher(bookmark):
# Hack to fix interaction with remotenames. Remotenames push
# '--to' bookmark to the server but we don't want to push scratch
# bookmark to the server. Let's delete '--to' and '--create' and
# also set allow_anon to True (because if --to is not set
# remotenames will think that we are pushing anonymoush head)
if 'to' in opts:
del opts['to']
if 'create' in opts:
del opts['create']
opts['allow_anon'] = True
ui.setconfig(experimental, configbookmark, bookmark, '--to')
ui.setconfig(experimental, configcreate, create, '--create')
scratchpush = True
# bundle2 can be sent back after push (for example, bundle2
# containing `pushkey` part to update bookmarks)
ui.setconfig(experimental, 'bundle2.pushback', True)
if scratchpush:
ui.setconfig(experimental, configscratchpush, True)
oldphasemove = wrapfunction(exchange,
'_localphasemove',
_phasemove)
# Copy-paste from `push` command
path = ui.paths.getpath(dest, default=('default-push', 'default'))
destpath = path.pushloc or path.loc
# Remote scratch bookmarks will be deleted because remotenames doesn't
# know about them. Let's save it before push and restore after
remotescratchbookmarks = _readscratchremotebookmarks(ui, repo, destpath)
result = orig(ui, repo, dest, *args, **opts)
if _isremotebooksenabled(ui):
if bookmark and scratchpush:
other = hg.peer(repo, opts, destpath)
fetchedbookmarks = other.listkeyspatterns('bookmarks',
patterns=[bookmark])
remotescratchbookmarks.update(fetchedbookmarks)
_saveremotebookmarks(repo, remotescratchbookmarks, destpath)
finally:
ui.restoreconfig(oldbookmark)
ui.restoreconfig(oldcreate)
if oldphasemove:
exchange._localphasemove = oldphasemove
return result
def _phasemove(orig, pushop, nodes, phase=phases.public):
"""prevent commits from being marked public
Since these are going to a scratch branch, they aren't really being
published."""
if phase != phases.public:
orig(pushop, nodes, phase)
@exchange.b2partsgenerator(scratchbranchparttype)
def partgen(pushop, bundler):
bookmark = pushop.ui.config(experimental, configbookmark)
create = pushop.ui.configbool(experimental, configcreate)
scratchpush = pushop.ui.configbool(experimental, configscratchpush)
if 'changesets' in pushop.stepsdone or not scratchpush:
return
if scratchbranchparttype not in bundle2.bundle2caps(pushop.remote):
return
pushop.stepsdone.add('changesets')
if not pushop.outgoing.missing:
pushop.ui.status(_('no changes found\n'))
pushop.cgresult = 0
return
scratchpart = getscratchbranchpart(pushop.repo,
pushop.remote,
pushop.outgoing,
pushop.force,
pushop.ui,
bookmark,
create)
bundler.addpart(scratchpart)
def handlereply(op):
# server either succeeds or aborts; no code to read
pushop.cgresult = 1
return handlereply
bundle2.capabilities[scratchbranchparttype] = ()
def _makebundlefile(part):
"""constructs a temporary bundle file
part.data should be an uncompressed v1 changegroup"""
fp = None
fd, bundlefile = tempfile.mkstemp()
try: # guards bundlefile
try: # guards fp
fp = os.fdopen(fd, 'wb')
magic = 'HG10UN'
fp.write(magic)
data = part.read(resource.getpagesize() - len(magic))
while data:
fp.write(data)
data = part.read(resource.getpagesize())
finally:
fp.close()
except Exception:
try:
os.unlink(bundlefile)
except Exception:
# we would rather see the original exception
pass
raise
return bundlefile
def _makebundlefromraw(data):
fp = None
fd, bundlefile = tempfile.mkstemp()
try: # guards bundlefile
try: # guards fp
fp = os.fdopen(fd, 'wb')
fp.write(data)
finally:
fp.close()
except Exception:
try:
os.unlink(bundlefile)
except Exception:
# we would rather see the original exception
pass
raise
return bundlefile
def _getrevs(bundle, oldnode, force):
'extracts and validates the revs to be imported'
validaterevset(bundle, 'bundle()')
revs = [bundle[r] for r in bundle.revs('sort(bundle())')]
# new bookmark
if oldnode is None:
return revs
# Fast forward update
if oldnode in bundle and list(bundle.set('bundle() & %s::', oldnode)):
return revs
# Forced non-fast forward update
if force:
return revs
else:
raise error.Abort(_('non-forward push'),
hint=_('use --force to override'))
@bundle2.parthandler(scratchbranchparttype,
('bookmark', 'bookprevnode' 'create', 'force',
'pushbackbookmarks'))
def bundle2scratchbranch(op, part):
'''unbundle a bundle2 part containing a changegroup to store'''
params = part.params
index = op.repo.bundlestore.index
store = op.repo.bundlestore.store
op.records.add(scratchbranchparttype + '_skippushkey', True)
bundlefile = None
try: # guards bundlefile
bundlefile = _makebundlefile(part)
bundlepath = "bundle:%s+%s" % (op.repo.root, bundlefile)
bundle = repository(op.repo.ui, bundlepath)
bookmark = params.get('bookmark')
bookprevnode = params.get('bookprevnode', '')
create = params.get('create')
force = params.get('force')
if bookmark:
oldnode = index.getnode(bookmark)
if not oldnode and not create:
raise error.Abort("unknown bookmark %s" % bookmark,
hint="use --create if you want to create one")
else:
oldnode = None
revs = _getrevs(bundle, oldnode, force)
# Notify the user of what is being pushed
plural = 's' if len(revs) > 1 else ''
op.repo.ui.warn(_("pushing %s commit%s:\n") % (len(revs), plural))
maxoutput = 10
for i in range(0, min(len(revs), maxoutput)):
firstline = bundle[revs[i]].description().split('\n')[0][:50]
op.repo.ui.warn((" %s %s\n") % (revs[i], firstline))
if len(revs) > maxoutput + 1:
op.repo.ui.warn((" ...\n"))
firstline = bundle[revs[-1]].description().split('\n')[0][:50]
op.repo.ui.warn((" %s %s\n") % (revs[-1], firstline))
nodes = [hex(rev.node()) for rev in revs]
newnodes = filter(lambda node: not index.getbundle(node), nodes)
if newnodes:
with open(bundlefile, 'r') as f:
key = store.write(f.read())
if bookmark:
index.addbookmarkandbundle(key, newnodes,
bookmark, newnodes[-1])
if params.get('pushbackbookmarks'):
_addbookmarkpushbackpart(op, bookmark,
newnodes[-1], bookprevnode)
else:
# Push new scratch commits with no bookmark
index.addbundle(key, newnodes)
elif bookmark:
# Push new scratch bookmark to known scratch commits
index.addbookmark(bookmark, nodes[-1])
if params.get('pushbackbookmarks'):
_addbookmarkpushbackpart(op, bookmark, nodes[-1], bookprevnode)
finally:
try:
if bundlefile:
os.unlink(bundlefile)
except OSError as e:
if e.errno != errno.ENOENT:
raise
return 1
def _addbookmarkpushbackpart(op, bookmark, newnode, oldnode):
if op.reply and 'pushback' in op.reply.capabilities:
params = {
'namespace': 'bookmarks',
'key': bookmark,
'new': newnode,
'old': oldnode,
}
op.reply.newpart('pushkey', mandatoryparams=params.items())
def bundle2pushkey(orig, op, part):
if op.records[scratchbranchparttype + '_skippushkey']:
if op.reply is not None:
rpart = op.reply.newpart('reply:pushkey')
rpart.addparam('in-reply-to', str(part.id), mandatory=False)
rpart.addparam('return', '1', mandatory=False)
return 1
return orig(op, part)